ui-fixes
This commit is contained in:
parent
c7447f68ad
commit
e35defe48a
2
.env
2
.env
|
|
@ -1,2 +1,2 @@
|
||||||
VITE_API_BASE_URL= https://api.yimaru.yaltopia.com/
|
VITE_API_BASE_URL= https://api.yimaru.yaltopia.com/api/v1
|
||||||
VITE_GOOGLE_CLIENT_ID=google_client_id
|
VITE_GOOGLE_CLIENT_ID=google_client_id
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import {
|
||||||
} from "recharts"
|
} from "recharts"
|
||||||
import { StatCard } from "../components/dashboard/StatCard"
|
import { StatCard } from "../components/dashboard/StatCard"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"
|
||||||
// import { cn } from "../lib/utils"
|
import { cn } from "../lib/utils"
|
||||||
import { getTeamMemberById } from "../api/team.api"
|
import { getTeamMemberById } from "../api/team.api"
|
||||||
import { getDashboard } from "../api/analytics.api"
|
import { getDashboard } from "../api/analytics.api"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
@ -44,6 +44,7 @@ export function DashboardPage() {
|
||||||
const [userFirstName, setUserFirstName] = useState<string>("")
|
const [userFirstName, setUserFirstName] = useState<string>("")
|
||||||
const [dashboard, setDashboard] = useState<DashboardData | null>(null)
|
const [dashboard, setDashboard] = useState<DashboardData | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [activeStatTab, setActiveStatTab] = useState<"primary" | "secondary">("primary")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
|
|
@ -115,8 +116,45 @@ export function DashboardPage() {
|
||||||
<div className="flex items-center justify-center py-20 text-destructive">Failed to load dashboard data.</div>
|
<div className="flex items-center justify-center py-20 text-destructive">Failed to load dashboard data.</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* Stat tabs */}
|
||||||
|
<div className="mb-3 border-b border-grayScale-200">
|
||||||
|
<div className="-mb-px flex gap-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveStatTab("primary")}
|
||||||
|
className={cn(
|
||||||
|
"relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
|
||||||
|
activeStatTab === "primary"
|
||||||
|
? "text-brand-600"
|
||||||
|
: "text-grayScale-400 hover:text-grayScale-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Overview
|
||||||
|
{activeStatTab === "primary" && (
|
||||||
|
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveStatTab("secondary")}
|
||||||
|
className={cn(
|
||||||
|
"relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
|
||||||
|
activeStatTab === "secondary"
|
||||||
|
? "text-brand-600"
|
||||||
|
: "text-grayScale-400 hover:text-grayScale-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
More metrics
|
||||||
|
{activeStatTab === "secondary" && (
|
||||||
|
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Stat Cards */}
|
{/* Stat Cards */}
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
{activeStatTab === "primary" && (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={Users}
|
icon={Users}
|
||||||
label="Total Users"
|
label="Total Users"
|
||||||
|
|
@ -146,9 +184,11 @@ export function DashboardPage() {
|
||||||
deltaPositive={dashboard.issues.resolution_rate > 0.5}
|
deltaPositive={dashboard.issues.resolution_rate > 0.5}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Secondary Stats */}
|
{/* Secondary Stats */}
|
||||||
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
{activeStatTab === "secondary" && (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={BookOpen}
|
icon={BookOpen}
|
||||||
label="Courses"
|
label="Courses"
|
||||||
|
|
@ -178,6 +218,7 @@ export function DashboardPage() {
|
||||||
deltaPositive
|
deltaPositive
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* User Registrations Chart */}
|
{/* User Registrations Chart */}
|
||||||
<div className="mt-5 grid gap-4">
|
<div className="mt-5 grid gap-4">
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ function formatDateTime(dateStr: string | null | undefined): string {
|
||||||
|
|
||||||
function LoadingSkeleton() {
|
function LoadingSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl space-y-8 px-4 py-10 sm:px-6">
|
<div className="mx-auto w-full max-w-6xl space-y-8 px-4 py-10 sm:px-6">
|
||||||
<div className="animate-pulse space-y-8">
|
<div className="animate-pulse space-y-8">
|
||||||
{/* Hero skeleton */}
|
{/* Hero skeleton */}
|
||||||
<div className="overflow-hidden rounded-2xl border border-grayScale-100">
|
<div className="overflow-hidden rounded-2xl border border-grayScale-100">
|
||||||
|
|
@ -93,15 +93,15 @@ function InfoRow({
|
||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="group flex items-center justify-between rounded-lg px-3 py-3 transition-colors hover:bg-grayScale-100/60">
|
<div className="group flex flex-col gap-1 rounded-lg px-3 py-3 transition-colors hover:bg-grayScale-100/60 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3 text-sm text-grayScale-400">
|
<div className="flex items-center gap-3 text-sm text-grayScale-400">
|
||||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-grayScale-100 text-grayScale-400 transition-colors group-hover:bg-brand-100 group-hover:text-brand-500">
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-grayScale-100 text-grayScale-400 transition-colors group-hover:bg-brand-100 group-hover:text-brand-500">
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium">{label}</span>
|
<span className="font-medium">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-grayScale-600">
|
<div className="flex items-center gap-2 text-sm font-medium text-grayScale-600 sm:justify-end min-w-0">
|
||||||
<span className="text-right">{value || "—"}</span>
|
<span className="truncate text-right sm:text-left">{value || "—"}</span>
|
||||||
{extra}
|
{extra}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -121,13 +121,13 @@ function VerifiedIcon({ verified }: { verified: boolean }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProgressRing({ percent }: { percent: number }) {
|
function ProgressRing({ percent }: { percent: number }) {
|
||||||
const radius = 18;
|
const radius = 14;
|
||||||
const circumference = 2 * Math.PI * radius;
|
const circumference = 2 * Math.PI * radius;
|
||||||
const offset = circumference - (percent / 100) * circumference;
|
const offset = circumference - (percent / 100) * circumference;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative inline-flex items-center justify-center">
|
<div className="relative inline-flex items-center justify-center">
|
||||||
<svg className="h-11 w-11 -rotate-90" viewBox="0 0 44 44">
|
<svg className="h-8 w-8 -rotate-90" viewBox="0 0 44 44">
|
||||||
<circle
|
<circle
|
||||||
cx="22"
|
cx="22"
|
||||||
cy="22"
|
cy="22"
|
||||||
|
|
@ -150,7 +150,7 @@ function ProgressRing({ percent }: { percent: number }) {
|
||||||
className="text-brand-500 transition-all duration-700"
|
className="text-brand-500 transition-all duration-700"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="absolute text-[10px] font-bold text-brand-600">{percent}%</span>
|
<span className="absolute text-[9px] font-bold text-brand-600">{percent}%</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +179,7 @@ export function ProfilePage() {
|
||||||
|
|
||||||
if (error || !profile) {
|
if (error || !profile) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl px-4 py-16 sm:px-6">
|
<div className="mx-auto w-full max-w-6xl px-4 py-16 sm:px-6">
|
||||||
<Card className="border-dashed">
|
<Card className="border-dashed">
|
||||||
<CardContent className="flex flex-col items-center gap-5 p-12">
|
<CardContent className="flex flex-col items-center gap-5 p-12">
|
||||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-grayScale-100">
|
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-grayScale-100">
|
||||||
|
|
@ -203,228 +203,311 @@ export function ProfilePage() {
|
||||||
const initials = `${profile.first_name?.[0] ?? ""}${profile.last_name?.[0] ?? ""}`.toUpperCase();
|
const initials = `${profile.first_name?.[0] ?? ""}${profile.last_name?.[0] ?? ""}`.toUpperCase();
|
||||||
const completionPct = profile.profile_completion_percentage ?? 0;
|
const completionPct = profile.profile_completion_percentage ?? 0;
|
||||||
|
|
||||||
const sectionCardIcons: Record<string, { icon: typeof User; color: string }> = {
|
|
||||||
personal: { icon: User, color: "from-brand-500 to-brand-600" },
|
|
||||||
contact: { icon: Mail, color: "from-brand-400 to-brand-500" },
|
|
||||||
account: { icon: Shield, color: "from-brand-600 to-brand-500" },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl space-y-8 px-4 py-8 sm:px-6">
|
<div className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6">
|
||||||
{/* Hero Card */}
|
{/* Page header (no tabs) */}
|
||||||
<Card className="overflow-hidden border-0 shadow-lg">
|
<div className="mb-5">
|
||||||
{/* Banner gradient */}
|
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">My Info</p>
|
||||||
<div className="relative h-36 bg-gradient-to-br from-brand-600 via-brand-500 to-brand-400 sm:h-40">
|
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-grayScale-800">Profile</h1>
|
||||||
{/* Decorative pattern overlay */}
|
|
||||||
<div className="absolute inset-0 opacity-10">
|
|
||||||
<div
|
|
||||||
className="h-full w-full"
|
|
||||||
style={{
|
|
||||||
backgroundImage:
|
|
||||||
"radial-gradient(circle at 25% 50%, white 1px, transparent 1px), radial-gradient(circle at 75% 50%, white 1px, transparent 1px)",
|
|
||||||
backgroundSize: "40px 40px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Bottom fade */}
|
|
||||||
<div className="absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-white/20 to-transparent" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardContent className="-mt-16 px-6 pb-8 pt-0 sm:px-10">
|
{/* Main profile layout card */}
|
||||||
<div className="flex flex-col items-center text-center">
|
<div className="rounded-2xl border border-grayScale-100 bg-white shadow-sm">
|
||||||
{/* Avatar */}
|
{/* Header strip */}
|
||||||
<Avatar className="h-28 w-28 ring-4 ring-white shadow-lg">
|
<div className="border-b border-grayScale-100 px-6 py-4 sm:px-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">Overview</p>
|
||||||
|
<p className="mt-1 text-sm text-grayScale-500">
|
||||||
|
Personal, job and account details for this team member.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-6 sm:px-8 sm:py-7">
|
||||||
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1.8fr)_minmax(0,1.2fr)]">
|
||||||
|
{/* Left column: About & details */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Identity */}
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row">
|
||||||
|
<Avatar className="h-16 w-16 sm:h-18 sm:w-18">
|
||||||
<AvatarImage src={profile.profile_picture_url || undefined} alt={fullName} />
|
<AvatarImage src={profile.profile_picture_url || undefined} alt={fullName} />
|
||||||
<AvatarFallback className="bg-gradient-to-br from-brand-100 to-brand-200 text-2xl font-bold text-brand-600">
|
<AvatarFallback className="bg-grayScale-100 text-base font-semibold text-grayScale-600">
|
||||||
{initials}
|
{initials}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
<div className="min-w-0">
|
||||||
{/* Name */}
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<h1 className="mt-4 text-2xl font-bold tracking-tight text-grayScale-600 sm:text-3xl">
|
<h2 className="text-lg font-semibold tracking-tight text-grayScale-800">{fullName}</h2>
|
||||||
{fullName}
|
<span className="rounded-full bg-grayScale-50 px-2.5 py-0.5 text-xs font-medium text-grayScale-500">
|
||||||
</h1>
|
#{profile.id}
|
||||||
|
</span>
|
||||||
{/* Role badge */}
|
</div>
|
||||||
|
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2.5 px-3 py-1",
|
"px-2.5 py-0.5 text-xs font-semibold",
|
||||||
profile.role === "ADMIN"
|
profile.role === "ADMIN"
|
||||||
? "bg-brand-500/10 text-brand-600 border border-brand-500/20"
|
? "bg-brand-500/10 text-brand-600 border border-brand-500/20"
|
||||||
: "bg-grayScale-100 text-grayScale-600 border border-grayScale-200"
|
: "bg-grayScale-50 text-grayScale-600 border border-grayScale-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Shield className="h-3 w-3 mr-1.5" />
|
<Shield className="mr-1 h-3 w-3" />
|
||||||
{profile.role}
|
{profile.role}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-grayScale-50 px-2.5 py-0.5 text-xs font-medium text-grayScale-500">
|
||||||
{/* Status pills */}
|
<Calendar className="h-3 w-3" />
|
||||||
<div className="mt-6 flex flex-wrap items-center justify-center gap-2.5">
|
Joined {formatDate(profile.created_at)}
|
||||||
{/* Active status */}
|
</span>
|
||||||
<div
|
</div>
|
||||||
|
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||||
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-xs font-semibold transition-colors",
|
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold",
|
||||||
profile.status === "ACTIVE"
|
profile.status === "ACTIVE"
|
||||||
? "border-mint-300 bg-mint-100/60 text-mint-500"
|
? "bg-mint-50 text-mint-600"
|
||||||
: "border-destructive/20 bg-destructive/10 text-destructive"
|
: "bg-destructive/10 text-destructive"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-2 w-2 rounded-full",
|
"h-1.5 w-1.5 rounded-full",
|
||||||
profile.status === "ACTIVE" ? "bg-mint-500 animate-pulse" : "bg-destructive"
|
profile.status === "ACTIVE" ? "bg-mint-500" : "bg-destructive"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{profile.status}
|
{profile.status}
|
||||||
</div>
|
</span>
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full border border-brand-100 bg-brand-50/60 px-2.5 py-0.5 text-xs font-semibold text-brand-600">
|
||||||
{/* Email verification */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-xs font-semibold transition-colors",
|
|
||||||
profile.email_verified
|
|
||||||
? "border-mint-300 bg-mint-100/60 text-mint-500"
|
|
||||||
: "border-grayScale-200 bg-grayScale-100/60 text-grayScale-400"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{profile.email_verified ? (
|
|
||||||
<CheckCircle2 className="h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<XCircle className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
Email {profile.email_verified ? "Verified" : "Unverified"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Phone verification */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-xs font-semibold transition-colors",
|
|
||||||
profile.phone_verified
|
|
||||||
? "border-mint-300 bg-mint-100/60 text-mint-500"
|
|
||||||
: "border-grayScale-200 bg-grayScale-100/60 text-grayScale-400"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{profile.phone_verified ? (
|
|
||||||
<CheckCircle2 className="h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<XCircle className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
Phone {profile.phone_verified ? "Verified" : "Unverified"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Profile completion ring */}
|
|
||||||
<div className="flex items-center gap-2 rounded-full border border-brand-200 bg-brand-100/30 px-3 py-1 text-xs font-semibold text-brand-600">
|
|
||||||
<ProgressRing percent={completionPct} />
|
<ProgressRing percent={completionPct} />
|
||||||
<span>Profile Complete</span>
|
<span>Profile complete</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* About / Contact */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
|
||||||
|
About
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-1.5 rounded-xl border border-grayScale-100 bg-grayScale-50/60 px-3 py-3">
|
||||||
|
<InfoRow icon={Phone} label="Phone" value={profile.phone_number} extra={<VerifiedIcon verified={profile.phone_verified} />} />
|
||||||
|
<InfoRow icon={Mail} label="Email" value={profile.email} extra={<VerifiedIcon verified={profile.email_verified} />} />
|
||||||
|
<InfoRow
|
||||||
|
icon={MapPin}
|
||||||
|
label="Location"
|
||||||
|
value={[profile.region, profile.country].filter(Boolean).join(", ") || "—"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Employee details */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
|
||||||
|
Employee details
|
||||||
|
</h3>
|
||||||
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-2 text-xs sm:text-sm text-grayScale-500">
|
||||||
|
<div>
|
||||||
|
<dt className="text-grayScale-400">Date of birth</dt>
|
||||||
|
<dd className="mt-0.5 font-medium text-grayScale-700">
|
||||||
|
{formatDate(profile.birth_day)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-grayScale-400">Age</dt>
|
||||||
|
<dd className="mt-0.5 font-medium text-grayScale-700">
|
||||||
|
{profile.age ? `${profile.age} years` : "—"}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-grayScale-400">Gender</dt>
|
||||||
|
<dd className="mt-0.5 font-medium text-grayScale-700">
|
||||||
|
{profile.gender || "Not specified"}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-grayScale-400">Age group</dt>
|
||||||
|
<dd className="mt-0.5 font-medium text-grayScale-700">
|
||||||
|
{profile.age_group || "—"}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-grayScale-400">Occupation</dt>
|
||||||
|
<dd className="mt-0.5 font-medium text-grayScale-700">
|
||||||
|
{profile.occupation || "—"}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-grayScale-400">Preferred language</dt>
|
||||||
|
<dd className="mt-0.5 font-medium text-grayScale-700">
|
||||||
|
{profile.preferred_language || "—"}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Middle column: Job information */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
|
||||||
|
Job information
|
||||||
|
</h3>
|
||||||
|
<div className="overflow-x-auto rounded-xl border border-grayScale-100">
|
||||||
|
<table className="w-full min-w-[600px] border-collapse text-sm">
|
||||||
|
<thead className="bg-grayScale-50 text-xs font-medium uppercase tracking-[0.12em] text-grayScale-400">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-2 text-left">Title</th>
|
||||||
|
<th className="px-4 py-2 text-left">Team</th>
|
||||||
|
<th className="px-4 py-2 text-left">Division</th>
|
||||||
|
<th className="px-4 py-2 text-left">Manager</th>
|
||||||
|
<th className="px-4 py-2 text-left">Hire date</th>
|
||||||
|
<th className="px-4 py-2 text-left">Location</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-grayScale-100 text-grayScale-700">
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-3">{profile.occupation || profile.role}</td>
|
||||||
|
<td className="px-4 py-3">{profile.role}</td>
|
||||||
|
<td className="px-4 py-3">{profile.preferred_language || "—"}</td>
|
||||||
|
<td className="px-4 py-3">—</td>
|
||||||
|
<td className="px-4 py-3">{formatDate(profile.created_at)}</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
{[profile.region, profile.country].filter(Boolean).join(", ") || "—"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Learning & goals */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<Card className="shadow-none border-grayScale-100">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold text-grayScale-700">
|
||||||
|
Learning goal
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-1">
|
||||||
|
<p className="text-sm text-grayScale-500">
|
||||||
|
{profile.learning_goal || "No learning goal specified."}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Info Cards */}
|
<Card className="shadow-none border-grayScale-100">
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
<CardHeader className="pb-2">
|
||||||
{/* Personal Information */}
|
<CardTitle className="text-sm font-semibold text-grayScale-700">
|
||||||
<Card className="group overflow-hidden border border-grayScale-100 transition-all duration-200 hover:shadow-lg">
|
Language goal
|
||||||
<div className="h-1 w-full bg-gradient-to-r from-brand-500 to-brand-600" />
|
</CardTitle>
|
||||||
<CardHeader className="pb-3">
|
</CardHeader>
|
||||||
|
<CardContent className="pt-1">
|
||||||
|
<p className="text-sm text-grayScale-500">
|
||||||
|
{profile.language_goal || "No language goal specified."}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column: Activity & account summary */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Activity */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
|
||||||
|
Activity
|
||||||
|
</h3>
|
||||||
|
<Card className="shadow-none border-grayScale-100">
|
||||||
|
<CardContent className="space-y-4 p-4">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-brand-500 to-brand-600 text-white shadow-sm">
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-brand-50 text-brand-600">
|
||||||
|
<Clock className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-grayScale-700">
|
||||||
|
Last login
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-grayScale-400">
|
||||||
|
{formatDateTime(profile.last_login)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-grayScale-50 text-grayScale-500">
|
||||||
<User className="h-4 w-4" />
|
<User className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-base font-semibold tracking-tight text-grayScale-600">
|
<div>
|
||||||
Personal Information
|
<p className="text-sm font-medium text-grayScale-700">
|
||||||
</CardTitle>
|
Account created
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-grayScale-400">
|
||||||
|
{formatDateTime(profile.created_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-0.5 px-3 pb-4">
|
|
||||||
<InfoRow icon={User} label="Full Name" value={fullName} />
|
|
||||||
<InfoRow icon={User} label="Gender" value={profile.gender || "Not specified"} />
|
|
||||||
<InfoRow icon={Calendar} label="Birthday" value={formatDate(profile.birth_day)} />
|
|
||||||
<InfoRow icon={User} label="Age Group" value={profile.age_group || "—"} />
|
|
||||||
<InfoRow icon={Briefcase} label="Occupation" value={profile.occupation || "—"} />
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Contact & Location */}
|
{/* Account summary */}
|
||||||
<Card className="group overflow-hidden border border-grayScale-100 transition-all duration-200 hover:shadow-lg">
|
<div>
|
||||||
<div className="h-1 w-full bg-gradient-to-r from-brand-400 to-brand-500" />
|
<h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
|
||||||
<CardHeader className="pb-3">
|
Account
|
||||||
<div className="flex items-center gap-3">
|
</h3>
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-brand-400 to-brand-500 text-white shadow-sm">
|
<Card className="shadow-none border-grayScale-100">
|
||||||
<Mail className="h-4 w-4" />
|
<CardContent className="space-y-3 p-4">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-grayScale-400">Role</span>
|
||||||
|
<span className="font-medium text-grayScale-700">{profile.role}</span>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-base font-semibold tracking-tight text-grayScale-600">
|
<div className="flex items-center justify-between text-sm">
|
||||||
Contact & Location
|
<span className="text-grayScale-400">Status</span>
|
||||||
</CardTitle>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-0.5 px-3 pb-4">
|
|
||||||
<InfoRow
|
|
||||||
icon={Mail}
|
|
||||||
label="Email"
|
|
||||||
value={profile.email}
|
|
||||||
extra={<VerifiedIcon verified={profile.email_verified} />}
|
|
||||||
/>
|
|
||||||
<InfoRow
|
|
||||||
icon={Phone}
|
|
||||||
label="Phone"
|
|
||||||
value={profile.phone_number}
|
|
||||||
extra={<VerifiedIcon verified={profile.phone_verified} />}
|
|
||||||
/>
|
|
||||||
<InfoRow icon={Globe} label="Country" value={profile.country || "—"} />
|
|
||||||
<InfoRow icon={MapPin} label="Region" value={profile.region || "—"} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Account Details */}
|
|
||||||
<Card className="group overflow-hidden border border-grayScale-100 transition-all duration-200 hover:shadow-lg">
|
|
||||||
<div className="h-1 w-full bg-gradient-to-r from-brand-600 to-brand-500" />
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-brand-600 to-brand-500 text-white shadow-sm">
|
|
||||||
<Shield className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-base font-semibold tracking-tight text-grayScale-600">
|
|
||||||
Account Details
|
|
||||||
</CardTitle>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-0.5 px-3 pb-4">
|
|
||||||
<InfoRow icon={Shield} label="Role" value={profile.role} />
|
|
||||||
<InfoRow
|
|
||||||
icon={Languages}
|
|
||||||
label="Language"
|
|
||||||
value={profile.preferred_language || "—"}
|
|
||||||
/>
|
|
||||||
<InfoRow
|
|
||||||
icon={Clock}
|
|
||||||
label="Last Login"
|
|
||||||
value={formatDateTime(profile.last_login)}
|
|
||||||
/>
|
|
||||||
<InfoRow
|
|
||||||
icon={Calendar}
|
|
||||||
label="Member Since"
|
|
||||||
value={formatDate(profile.created_at)}
|
|
||||||
/>
|
|
||||||
<InfoRow
|
|
||||||
icon={CheckCircle2}
|
|
||||||
label="Status"
|
|
||||||
value={profile.status}
|
|
||||||
extra={
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-2.5 w-2.5 rounded-full ring-2",
|
"inline-flex items-center gap-1.5 text-xs font-semibold",
|
||||||
profile.status === "ACTIVE"
|
profile.status === "ACTIVE"
|
||||||
? "bg-mint-500 ring-mint-100"
|
? "text-mint-600"
|
||||||
: "bg-destructive ring-destructive/20"
|
: "text-destructive"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"h-1.5 w-1.5 rounded-full",
|
||||||
|
profile.status === "ACTIVE" ? "bg-mint-500" : "bg-destructive"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
{profile.status}
|
||||||
/>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-grayScale-400">Email</span>
|
||||||
|
<span className="flex items-center gap-1 text-grayScale-700">
|
||||||
|
<span className="truncate max-w-[140px] text-right text-xs sm:text-sm">
|
||||||
|
{profile.email}
|
||||||
|
</span>
|
||||||
|
<VerifiedIcon verified={profile.email_verified} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-grayScale-400">Phone</span>
|
||||||
|
<span className="flex items-center gap-1 text-grayScale-700">
|
||||||
|
<span className="truncate max-w-[120px] text-right text-xs sm:text-sm">
|
||||||
|
{profile.phone_number || "—"}
|
||||||
|
</span>
|
||||||
|
<VerifiedIcon verified={profile.phone_verified} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ export function AnalyticsPage() {
|
||||||
const [dashboard, setDashboard] = useState<DashboardData | null>(null)
|
const [dashboard, setDashboard] = useState<DashboardData | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
|
const [activeSummaryTab, setActiveSummaryTab] = useState<"key" | "content" | "operations">("key")
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
@ -384,7 +385,51 @@ export function AnalyticsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Tabs */}
|
||||||
|
<div className="mb-4 border-b border-grayScale-200">
|
||||||
|
<div className="-mb-px flex gap-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveSummaryTab("key")}
|
||||||
|
className={cn(
|
||||||
|
"relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
|
||||||
|
activeSummaryTab === "key" ? "text-brand-600" : "text-grayScale-400 hover:text-grayScale-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Key Metrics
|
||||||
|
{activeSummaryTab === "key" && (
|
||||||
|
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveSummaryTab("content")}
|
||||||
|
className={cn(
|
||||||
|
"relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
|
||||||
|
activeSummaryTab === "content" ? "text-brand-600" : "text-grayScale-400 hover:text-grayScale-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Content & Platform
|
||||||
|
{activeSummaryTab === "content" && (
|
||||||
|
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveSummaryTab("operations")}
|
||||||
|
className={cn(
|
||||||
|
"relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
|
||||||
|
activeSummaryTab === "operations" ? "text-brand-600" : "text-grayScale-400 hover:text-grayScale-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Operations
|
||||||
|
{activeSummaryTab === "operations" && (
|
||||||
|
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{activeSummaryTab === "key" && (
|
||||||
|
<>
|
||||||
{/* ─── Key Metrics ─── */}
|
{/* ─── Key Metrics ─── */}
|
||||||
<Section title="Key Metrics" icon={TrendingUp} defaultOpen>
|
<Section title="Key Metrics" icon={TrendingUp} defaultOpen>
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
|
@ -418,9 +463,18 @@ export function AnalyticsPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSummaryTab === "content" && (
|
||||||
|
<>
|
||||||
{/* ─── Content & Platform ─── */}
|
{/* ─── Content & Platform ─── */}
|
||||||
<Section title="Content & Platform" icon={BookOpen} count={courses.total_courses + content.total_questions} defaultOpen>
|
<Section
|
||||||
|
title="Content & Platform"
|
||||||
|
icon={BookOpen}
|
||||||
|
count={courses.total_courses + content.total_questions}
|
||||||
|
defaultOpen
|
||||||
|
>
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<KpiCard
|
<KpiCard
|
||||||
icon={FolderOpen}
|
icon={FolderOpen}
|
||||||
|
|
@ -451,7 +505,11 @@ export function AnalyticsPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSummaryTab === "operations" && (
|
||||||
|
<>
|
||||||
{/* ─── Operations ─── */}
|
{/* ─── Operations ─── */}
|
||||||
<Section title="Operations" icon={Bell} defaultOpen>
|
<Section title="Operations" icon={Bell} defaultOpen>
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
|
@ -485,6 +543,8 @@ export function AnalyticsPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ─── User Analytics ─── */}
|
{/* ─── User Analytics ─── */}
|
||||||
<Section title="User Analytics" icon={Users} count={users.total_users} defaultOpen>
|
<Section title="User Analytics" icon={Users} count={users.total_users} defaultOpen>
|
||||||
|
|
|
||||||
|
|
@ -512,7 +512,7 @@ export function IssuesPage() {
|
||||||
<TableRow key={issue.id} className="group">
|
<TableRow key={issue.id} className="group">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-start gap-3 max-w-[300px]">
|
<div className="flex items-start gap-3 max-w-[300px]">
|
||||||
<div className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-grayScale-50 text-grayScale-400 group-hover:bg-brand-50 group-hover:text-brand-500 transition-colors">
|
<div className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-grayScale-50 text-grayScale-400 group-hover:bg-brand-500 group-hover:text-white transition-colors">
|
||||||
<TypeIcon className="h-4 w-4" />
|
<TypeIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
|
|
|
||||||
|
|
@ -404,7 +404,7 @@ export function UserLogPage() {
|
||||||
<TableRow key={log.id} className="group">
|
<TableRow key={log.id} className="group">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-grayScale-50 text-grayScale-400 group-hover:bg-brand-50 group-hover:text-brand-500 transition-colors">
|
<div className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-grayScale-50 text-grayScale-400 group-hover:bg-brand-500 group-hover:text-white transition-colors">
|
||||||
<ActionIcon className="h-4 w-4" />
|
<ActionIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -29,32 +29,32 @@ export function UserManagementDashboard() {
|
||||||
<Users className="h-6 w-6" />
|
<Users className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-medium text-grayScale-400">Total Users</p>
|
<p className="text-sm font-medium text-white/80">Total Users</p>
|
||||||
<p className="text-2xl font-bold text-grayScale-600">1,248</p>
|
<p className="text-2xl font-bold text-white">1,248</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="border-none bg-mint-50 shadow-sm">
|
<Card className="border-none bg-brand-50 shadow-sm">
|
||||||
<CardContent className="flex items-center gap-4 p-5">
|
<CardContent className="flex items-center gap-4 p-5">
|
||||||
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-mint-100 text-mint-600">
|
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-brand-100 text-brand-600">
|
||||||
<UserCheck className="h-6 w-6" />
|
<UserCheck className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-medium text-grayScale-400">Active Users</p>
|
<p className="text-sm font-medium text-white/80">Active Users</p>
|
||||||
<p className="text-2xl font-bold text-grayScale-600">1,180</p>
|
<p className="text-2xl font-bold text-white">1,180</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="border-none bg-gold-50 shadow-sm sm:col-span-2 lg:col-span-1">
|
<Card className="border-none bg-brand-50 shadow-sm sm:col-span-2 lg:col-span-1">
|
||||||
<CardContent className="flex items-center gap-4 p-5">
|
<CardContent className="flex items-center gap-4 p-5">
|
||||||
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-gold-100 text-gold-600">
|
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-brand-100 text-brand-600">
|
||||||
<TrendingUp className="h-6 w-6" />
|
<TrendingUp className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-medium text-grayScale-400">New This Month</p>
|
<p className="text-sm font-medium text-white/80">New This Month</p>
|
||||||
<p className="text-2xl font-bold text-grayScale-600">64</p>
|
<p className="text-2xl font-bold text-white">64</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user