Skip to main content

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:

  1. Create a Card Class in app/Services/Cards that extends Qoraiche\Peak\Services\Cards\BaseCard.
  2. Inject any required services into the constructor.
  3. 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
]);
}