Initial commit for new panel
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user