Initial commit for new panel

This commit is contained in:
AmirReza Jamali
2025-09-09 11:20:26 +03:30
commit 1d4ccb3575
343 changed files with 20031 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
import { DotIcon } from "@/assets/icons";
import { formatMessageTime } from "@/lib/format-message-time";
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { getChatsData } from "../fetch";
export async function ChatsCard() {
const data = await getChatsData();
return (
<div className="col-span-12 rounded-[10px] bg-white py-6 shadow-1 dark:bg-gray-dark dark:shadow-card xl:col-span-4">
<h2 className="mb-5.5 px-7.5 text-body-2xlg font-bold text-dark dark:text-white">
Chats
</h2>
<ul>
{data.map((chat, key) => (
<li key={key}>
<Link
href="/"
className="flex items-center gap-4.5 px-7.5 py-3 outline-none hover:bg-gray-2 focus-visible:bg-gray-2 dark:hover:bg-dark-2 dark:focus-visible:bg-dark-2"
>
<div className="relative shrink-0">
<Image
src={chat.profile}
width={56}
height={56}
className="size-14 rounded-full object-cover"
alt={"Avatar for " + chat.name}
/>
<span
className={cn(
"absolute bottom-0 right-0 size-3.5 rounded-full ring-2 ring-white dark:ring-dark-2",
chat.isActive ? "bg-green" : "bg-orange-light",
)}
/>
</div>
<div className="relative flex-grow">
<h3 className="font-medium text-dark dark:text-white">
{chat.name}
</h3>
<div className="flex flex-wrap items-center gap-2">
<span
className={cn(
"truncate text-sm font-medium dark:text-dark-5 xl:max-w-[8rem]",
chat.unreadCount && "text-dark-4 dark:text-dark-6",
)}
>
{chat.lastMessage.content}
</span>
<DotIcon />
<time
className="text-xs"
dateTime={chat.lastMessage.timestamp}
>
{formatMessageTime(chat.lastMessage.timestamp)}
</time>
</div>
{!!chat.unreadCount && (
<div className="pointer-events-none absolute right-0 top-1/2 aspect-square max-w-fit -translate-y-1/2 select-none rounded-full bg-primary px-2 py-0.5 text-sm font-medium text-white">
{chat.unreadCount}
</div>
)}
</div>
</Link>
</li>
))}
</ul>
</div>
);
}
@@ -0,0 +1,53 @@
import { ArrowDownIcon, ArrowUpIcon } from "@/assets/icons";
import { cn } from "@/lib/utils";
import type { JSX, SVGProps } from "react";
type PropsType = {
label: string;
data: {
value: number | string;
growthRate: number;
};
Icon: (props: SVGProps<SVGSVGElement>) => JSX.Element;
};
export function OverviewCard({ label, data, Icon }: PropsType) {
const isDecreasing = data.growthRate < 0;
return (
<div className="rounded-[10px] bg-white p-6 shadow-1 dark:bg-gray-dark">
<Icon />
<div className="mt-6 flex items-end justify-between">
<dl>
<dt className="mb-1.5 text-heading-6 font-bold text-dark dark:text-white">
{data.value}
</dt>
<dd className="text-sm font-medium text-dark-6">{label}</dd>
</dl>
<dl
className={cn(
"text-sm font-medium",
isDecreasing ? "text-red" : "text-green",
)}
>
<dt className="flex items-center gap-1.5">
{data.growthRate}%
{isDecreasing ? (
<ArrowDownIcon aria-hidden />
) : (
<ArrowUpIcon aria-hidden />
)}
</dt>
<dd className="sr-only">
{label} {isDecreasing ? "Decreased" : "Increased"} by{" "}
{data.growthRate}%
</dd>
</dl>
</div>
</div>
);
}
@@ -0,0 +1,73 @@
import type { SVGProps } from "react";
type SVGPropsType = SVGProps<SVGSVGElement>;
export function Views(props: SVGPropsType) {
return (
<svg width={58} height={58} viewBox="0 0 58 58" fill="none" {...props}>
<circle cx={29} cy={29} r={29} fill="#3FD97F" />
<path
d="M26.562 29a2.437 2.437 0 114.875 0 2.437 2.437 0 01-4.875 0z"
fill="#fff"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18.166 29c0 1.776.46 2.374 1.382 3.57 1.838 2.389 4.922 5.097 9.452 5.097s7.614-2.708 9.452-5.096c.92-1.197 1.381-1.795 1.381-3.57 0-1.777-.46-2.375-1.381-3.571-1.838-2.389-4.922-5.096-9.452-5.096s-7.614 2.707-9.452 5.096c-.921 1.196-1.381 1.794-1.381 3.57zM29 24.938a4.063 4.063 0 100 8.125 4.063 4.063 0 000-8.125z"
fill="#fff"
/>
</svg>
);
}
export function Profit(props: SVGPropsType) {
return (
<svg width={58} height={58} viewBox="0 0 58 58" fill="none" {...props}>
<circle cx={29} cy={29} r={29} fill="#FF9C55" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M29 39.833c5.983 0 10.833-4.85 10.833-10.833 0-5.983-4.85-10.834-10.833-10.834-5.983 0-10.834 4.85-10.834 10.834 0 5.983 4.85 10.833 10.834 10.833zm.812-17.333a.812.812 0 10-1.625 0v.343c-1.766.316-3.25 1.643-3.25 3.448 0 2.077 1.964 3.521 4.063 3.521 1.491 0 2.437.982 2.437 1.896 0 .915-.946 1.896-2.437 1.896-1.491 0-2.438-.981-2.438-1.896a.812.812 0 10-1.625 0c0 1.805 1.484 3.132 3.25 3.449v.343a.812.812 0 101.625 0v-.343c1.767-.317 3.25-1.644 3.25-3.449 0-2.077-1.963-3.52-4.062-3.52-1.491 0-2.438-.982-2.438-1.896 0-.915.947-1.896 2.438-1.896s2.437.98 2.437 1.895a.813.813 0 001.625 0c0-1.805-1.483-3.132-3.25-3.448V22.5z"
fill="#fff"
/>
</svg>
);
}
export function Product(props: SVGPropsType) {
return (
<svg width={58} height={58} viewBox="0 0 58 58" fill="none" {...props}>
<circle cx={29} cy={29} r={29} fill="#8155FF" />
<path
d="M35.043 20.8l-2.167-1.136c-1.902-.998-2.853-1.498-3.876-1.498-1.023 0-1.974.5-3.876 1.498L22.958 20.8c-1.922 1.008-3.051 1.6-3.752 2.394L29 28.09l9.794-4.896c-.7-.793-1.83-1.386-3.751-2.394zM39.56 24.628l-9.747 4.874v10.227c.777-.194 1.662-.658 3.063-1.393l2.167-1.137c2.33-1.223 3.496-1.835 4.143-2.934.647-1.099.647-2.467.647-5.202v-.127c0-2.05 0-3.332-.272-4.308zM28.188 39.73V29.501l-9.749-4.874c-.272.976-.272 2.258-.272 4.308v.127c0 2.735 0 4.103.647 5.202.647 1.1 1.813 1.71 4.144 2.934l2.166 1.137c1.4.735 2.286 1.2 3.064 1.393z"
fill="#fff"
/>
</svg>
);
}
export function Users(props: SVGPropsType) {
return (
<svg width={58} height={58} viewBox="0 0 58 58" fill="none" {...props}>
<circle cx={29} cy={29} r={29} fill="#18BFFF" />
<ellipse
cx={25.7511}
cy={22.4998}
rx={4.33333}
ry={4.33333}
fill="#fff"
/>
<ellipse
cx={25.7511}
cy={34.4178}
rx={7.58333}
ry={4.33333}
fill="#fff"
/>
<path
d="M38.75 34.417c0 1.795-2.206 3.25-4.898 3.25.793-.867 1.339-1.955 1.339-3.248 0-1.295-.547-2.384-1.342-3.252 2.693 0 4.9 1.455 4.9 3.25zM35.5 22.501a3.25 3.25 0 01-4.364 3.054 6.163 6.163 0 00.805-3.055c0-1.11-.293-2.152-.804-3.053A3.25 3.25 0 0135.5 22.5z"
fill="#fff"
/>
</svg>
);
}
@@ -0,0 +1,48 @@
import { compactFormat } from "@/lib/format-number";
import { getOverviewData } from "../../fetch";
import { OverviewCard } from "./card";
import * as icons from "./icons";
export async function OverviewCardsGroup() {
const { views, profit, products, users } = await getOverviewData();
return (
<div className="grid gap-4 sm:grid-cols-2 sm:gap-6 xl:grid-cols-4 2xl:gap-7.5">
<OverviewCard
label="Total Views"
data={{
...views,
value: compactFormat(views.value),
}}
Icon={icons.Views}
/>
<OverviewCard
label="Total Profit"
data={{
...profit,
value: "$" + compactFormat(profit.value),
}}
Icon={icons.Profit}
/>
<OverviewCard
label="Total Products"
data={{
...products,
value: compactFormat(products.value),
}}
Icon={icons.Product}
/>
<OverviewCard
label="Total Users"
data={{
...users,
value: compactFormat(users.value),
}}
Icon={icons.Users}
/>
</div>
);
}
@@ -0,0 +1,26 @@
import { Skeleton } from "@/components/ui/skeleton";
export function OverviewCardsSkeleton() {
return (
<div className="grid gap-4 sm:grid-cols-2 sm:gap-6 xl:grid-cols-4 2xl:gap-7.5">
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="rounded-[10px] bg-white p-6 shadow-1 dark:bg-gray-dark"
>
<Skeleton className="size-12 rounded-full" />
<div className="mt-6 flex items-end justify-between">
<div>
<Skeleton className="mb-1.5 h-7 w-18" />
<Skeleton className="h-5 w-20" />
</div>
<Skeleton className="h-5 w-15" />
</div>
</div>
))}
</div>
);
}
@@ -0,0 +1,17 @@
"use client";
import dynamic from "next/dynamic";
const Map = dynamic(() => import("./map"), { ssr: false });
export function RegionLabels() {
return (
<div className="col-span-12 rounded-[10px] bg-white p-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card xl:col-span-7">
<h2 className="mb-7 text-body-2xlg font-bold text-dark dark:text-white">
Region labels
</h2>
<Map />
</div>
);
}
@@ -0,0 +1,47 @@
"use client";
import jsVectorMap from "jsvectormap";
import { useEffect } from "react";
import "@/js/us-aea-en";
export default function Map() {
useEffect(() => {
new jsVectorMap({
selector: "#mapOne",
map: "us_aea_en",
zoomButtons: true,
regionStyle: {
initial: {
fill: "#C8D0D8",
},
hover: {
fillOpacity: 1,
fill: "#3056D3",
},
},
regionLabelStyle: {
initial: {
fontWeight: "semibold",
fill: "#fff",
},
hover: {
cursor: "pointer",
},
},
labels: {
regions: {
render(code: string) {
return code.split("-")[1];
},
},
},
});
}, []);
return (
<div className="h-[422px]">
<div id="mapOne" className="mapOne map-btn" />
</div>
);
}
+91
View File
@@ -0,0 +1,91 @@
export async function getOverviewData() {
// Fake delay
await new Promise((resolve) => setTimeout(resolve, 2000));
return {
views: {
value: 3456,
growthRate: 0.43,
},
profit: {
value: 4220,
growthRate: 4.35,
},
products: {
value: 3456,
growthRate: 2.59,
},
users: {
value: 3456,
growthRate: -0.95,
},
};
}
export async function getChatsData() {
// Fake delay
await new Promise((resolve) => setTimeout(resolve, 1000));
return [
{
name: "Jacob Jones",
profile: "/images/user/user-01.png",
isActive: true,
lastMessage: {
content: "See you tomorrow at the meeting!",
type: "text",
timestamp: "2024-12-19T14:30:00Z",
isRead: false,
},
unreadCount: 3,
},
{
name: "Wilium Smith",
profile: "/images/user/user-03.png",
isActive: true,
lastMessage: {
content: "Thanks for the update",
type: "text",
timestamp: "2024-12-19T10:15:00Z",
isRead: true,
},
unreadCount: 0,
},
{
name: "Johurul Haque",
profile: "/images/user/user-04.png",
isActive: false,
lastMessage: {
content: "What's up?",
type: "text",
timestamp: "2024-12-19T10:15:00Z",
isRead: true,
},
unreadCount: 0,
},
{
name: "M. Chowdhury",
profile: "/images/user/user-05.png",
isActive: false,
lastMessage: {
content: "Where are you now?",
type: "text",
timestamp: "2024-12-19T10:15:00Z",
isRead: true,
},
unreadCount: 2,
},
{
name: "Akagami",
profile: "/images/user/user-07.png",
isActive: false,
lastMessage: {
content: "Hey, how are you?",
type: "text",
timestamp: "2024-12-19T10:15:00Z",
isRead: true,
},
unreadCount: 0,
},
];
}
+62
View File
@@ -0,0 +1,62 @@
import { PaymentsOverview } from "@/components/Charts/payments-overview";
import { UsedDevices } from "@/components/Charts/used-devices";
import { WeeksProfit } from "@/components/Charts/weeks-profit";
import { TopChannels } from "@/components/Tables/top-channels";
import { TopChannelsSkeleton } from "@/components/Tables/top-channels/skeleton";
import { createTimeFrameExtractor } from "@/utils/timeframe-extractor";
import { Suspense } from "react";
import { ChatsCard } from "./_components/chats-card";
import { OverviewCardsGroup } from "./_components/overview-cards";
import { OverviewCardsSkeleton } from "./_components/overview-cards/skeleton";
import { RegionLabels } from "./_components/region-labels";
type PropsType = {
searchParams: Promise<{
selected_time_frame?: string;
}>;
};
export default async function Home({ searchParams }: PropsType) {
const { selected_time_frame } = await searchParams;
const extractTimeFrame = createTimeFrameExtractor(selected_time_frame);
return (
<>
<Suspense fallback={<OverviewCardsSkeleton />}>
<OverviewCardsGroup />
</Suspense>
<div className="mt-4 grid grid-cols-12 gap-4 md:mt-6 md:gap-6 2xl:mt-9 2xl:gap-7.5">
<PaymentsOverview
className="col-span-12 xl:col-span-7"
key={extractTimeFrame("payments_overview")}
timeFrame={extractTimeFrame("payments_overview")?.split(":")[1]}
/>
<WeeksProfit
key={extractTimeFrame("weeks_profit")}
timeFrame={extractTimeFrame("weeks_profit")?.split(":")[1]}
className="col-span-12 xl:col-span-5"
/>
<UsedDevices
className="col-span-12 xl:col-span-5"
key={extractTimeFrame("used_devices")}
timeFrame={extractTimeFrame("used_devices")?.split(":")[1]}
/>
<RegionLabels />
<div className="col-span-12 grid xl:col-span-8">
<Suspense fallback={<TopChannelsSkeleton />}>
<TopChannels />
</Suspense>
</div>
<Suspense fallback={null}>
<ChatsCard />
</Suspense>
</div>
</>
);
}