Admin Cards
Admin Cards are customizable UI widgets that appear on the admin dashboard to display key metrics or recent data — such as new user registrations, recent subscriptions, or the latest reports.
You can easily create, register, unregister, and extend these cards to tailor the dashboard experience based on your application needs.
Creating a New Card
To create a new admin card, follow these steps:
- Create a Card Class in
app/Services/Cards
that extendsQoraiche\Peak\Services\Cards\BaseCard
. - Inject any required services into the constructor.
- Return data from the
data()
method.
Example: RecentBlogPostsCard.php
<?php
namespace App\Services\Cards;
use Inertia\Inertia;
use Qoraiche\Peak\Services\Admin\AdminOverviewService;
use Qoraiche\Peak\Services\Cards\BaseCard;
class RecentBlogPostsCard extends BaseCard
{
public function __construct(AdminOverviewService $adminOverviewService)
{
parent::__construct($adminOverviewService);
}
public function data(): array
{
[$adminOverviewService] = $this->services;
return [
'recentBlogPosts' => Inertia::defer(
fn() => $adminOverviewService->getRecentBlogPosts(),
'recentCards'
)
];
}
}
Registering the Card
You can register your card in a service provider such as AppServiceProvider
:
use Qoraiche\Peak\Facades\Card;
use App\Services\Cards\RecentBlogPostsCard;
public function boot()
{
$card = [
'slug' => 'recent-blog-posts-card',
'title' => __('Latest Blog Posts'),
'description' => __('Stay updated with the most recently published posts.'),
'component' => RecentBlogPostsCard::class,
'viewMoreRouteName' => 'admin.blog.articles.index'
];
Card::register($card);
}
Tip: Cards are referenced by their slug. You can unregister any card using Card::unregister('your-slug-here');
Dashboard Component
Create a matching component in resources/js/Layouts/Admin/Cards/RecentBlogPostsCard.vue
, using the @peak/Components/Admin/Card.vue
component
<script>
import Card from "@peak/Components/Admin/Card.vue";
import { inject } from "vue";
import { Link } from "@inertiajs/vue3";
import CardEmptyState from "@peak/Components/Admin/CardEmptyState.vue";
import { Eye, List, SquarePen } from "lucide-vue-next";
defineProps({
title: String,
lazy: { type: Boolean, default: true },
description: String,
recentBlogPosts: { type: [Array, Object], default: [] },
viewMoreRouteName: String,
collapsible: { type: Boolean, default: true },
});
const dayjs = inject("dayJS");
</script>
<template>
<Card :collapsible="collapsible" :deferred="lazy" :has-content-padding="false" :has-shadow="false"
deferred-data="recentBlogPosts">
<template #header>
{{ __(title) }}
</template>
<template v-if="description" #description>
{{ __(description) }}
</template>
<template #actions>
<Link v-if="viewMoreRouteName" :href="route(viewMoreRouteName)" v-tooltip="__('View more')"
class="rounded-full size-8 bg-gray-50 flex items-center justify-center text-gray-500 hover:bg-gray-100 ring-blue-600 focus:ring-blue-600">
<List class="size-4"/>
</Link>
</template>
<ul v-if="recentBlogPosts.length" class="divide-y divide-gray-100 px-5" role="list">
<li v-for="post in recentBlogPosts" :key="post.id" class="flex items-center justify-between gap-x-6 py-5">
<div class="flex gap-x-4 items-center">
<img v-if="post.locale_image" :src="post.locale_image" :alt="post.title"
class="size-12 rounded-md object-cover bg-gray-50" />
<div class="min-w-0">
<Link :href="route('admin.blog.articles.edit', post.id)"
class="hover:underline text-sm font-semibold text-gray-900 line-clamp-1">
{{ post.title }}
</Link>
<div class="mt-1 text-xs text-gray-500 flex items-center gap-x-2">
<p>{{ __('Published on') }}
<time :datetime="post.published_at ?? post.created_at">
{{ post.published_at ?? post.created_at }}
</time>
</p>
<svg class="size-0.5 fill-current" viewBox="0 0 2 2"><circle cx="1" cy="1" r="1"/></svg>
<p>
{{ __('Created by') }}
<Link :href="route('admin.user-management.users.edit', post.user.id)" class="hover:underline">
{{ post.user.name }}
</Link>
</p>
</div>
</div>
</div>
<div class="flex items-center gap-x-4">
<Link :href="route('admin.blog.articles.edit', post.id)">
<SquarePen class="w-4 h-4 text-gray-400 hover:text-gray-600"/>
</Link>
<a :href="route('blog.show', post.slug)" target="_blank">
<Eye class="w-4 h-4 text-gray-400 hover:text-gray-600"/>
</a>
</div>
</li>
</ul>
<CardEmptyState v-else />
</Card>
</template>
Registering the Component in Loader
Edit your resources/js/Layouts/Admin/Cards/loader.js
to register the card component:
export const cardComponents = {
RecentBlogPostsCard: () => import('@/Layouts/Admin/Cards/RecentBlogPostsCard.vue'),
// more cards
};
Custom UI
You are free to use the built-in Card component provided by the platform:
import Card from "@peak/Components/Admin/Card.vue";
Or, you can use your own card structure if you need a custom layout or visual style.
Card Manager
The CardManager class allows you to register and manage dashboard cards dynamically in your application. You interact with it using the Card facade:
use Qoraiche\Peak\Facades\Card;
Registering a Single Card
Card::register(
slug: 'stats',
title: 'Site Stats',
component: \App\Cards\StatsCard::class,
description: 'Displays website statistics',
collapsible: true,
viewMoreRouteName: 'stats.index',
order: 1,
lazy: true,
roles: ['admin'],
permissions: ['view-stats']
);
Registering Multiple Cards
Card::registerMany([
[
'slug' => 'traffic',
'title' => 'Traffic Overview',
'component' => \App\Cards\TrafficCard::class,
'description' => 'Traffic insights',
'order' => 2,
'roles' => ['admin', 'analyst'],
],
[
'slug' => 'sales',
'title' => 'Sales Data',
'component' => \App\Cards\SalesCard::class,
'order' => 3,
'permissions' => ['view-sales'],
],
]);
Adding Dynamic Hooks
Use hooks to dynamically register cards based on runtime conditions:
Card::hook(function ($manager) {
if (app()->environment('local')) {
$manager->register(
'debug',
'Debug Info',
\App\Cards\DebugCard::class
);
}
});
Unregistering a Card
Card::unregister('sales');
Roles and Permissions
Cards can be conditionally shown using:
- roles — User must have one of the roles.
- permissions — User must have one of the permissions.
If the user doesn't meet the conditions, the card will not be returned by Card::all()
.
Registering Cards in a Service Provider
Best practice is to register cards in a service provider like AppServiceProvider
:
use Qoraiche\Peak\Facades\Card;
public function boot()
{
Card::registerMany([
[
'slug' => 'user-overview',
'title' => 'User Overview',
'component' => \App\Cards\UserCard::class,
'order' => 1,
'roles' => ['admin'],
],
// Add more cards here
]);
}