update dashboard ux

This commit is contained in:
root
2026-06-24 03:04:01 -04:00
parent 2739f14e45
commit 572d115003
11 changed files with 449 additions and 36 deletions
+108 -4
View File
@@ -2,13 +2,25 @@
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { Calendar, Car, Users, DollarSign, AlertTriangle, Clock, XCircle, Plus, BarChart3, FileText, TrendingUp } from 'lucide-react'
import { Calendar, Car, Users, DollarSign, AlertTriangle, Clock, XCircle, Plus, BarChart3, FileText, TrendingUp, MapPin } from 'lucide-react'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'
import StatCard from '@/components/ui/StatCard'
import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
import { formatCurrency } from '@rentaldrivego/types'
/**
* Location breakdown item — requires backend field on /analytics/dashboard.
* Flagged as a contract change (not faked).
*/
interface LocationBreakdown {
name: string
bookingCount: number
revenue: number
/** 0100 utilization percentage */
utilizationPct: number
}
interface DashboardData {
kpis: {
totalBookings: number
@@ -19,6 +31,12 @@ interface DashboardData {
vehiclesChange: number
revenueChange: number
customersChange: number
/** Requires backend field — flagged as contract change */
utilizationRate?: number
/** Requires backend field — flagged as contract change */
revenuePerVehicle?: number
/** Requires backend field — flagged as contract change */
fulfillmentRate?: number
}
recentReservations: {
id: string
@@ -31,6 +49,8 @@ interface DashboardData {
totalAmount: number
}[]
sourceBreakdown: { source: string; count: number; revenue: number }[]
/** Requires backend field — flagged as contract change */
locationBreakdown?: LocationBreakdown[]
subscription: {
status: string
planName: string
@@ -132,6 +152,48 @@ function SubscriptionBanner({
)
}
function BranchPerformanceList({
locations,
formatCurrencyFn,
}: {
locations: LocationBreakdown[]
formatCurrencyFn: (amount: number, currency: string) => string
}) {
const maxCount = Math.max(...locations.map((l) => l.bookingCount), 1)
return (
<div className="glass-card p-6">
<div className="mb-4 flex items-center gap-2">
<MapPin className="h-4 w-4 text-orange-500" />
<h2 className="text-base font-semibold text-blue-950 dark:text-stone-100">Location Performance</h2>
</div>
<div className="space-y-4">
{locations.map((loc) => (
<div key={loc.name}>
<div className="mb-1 flex items-center justify-between">
<span className="text-sm font-medium text-blue-950 dark:text-stone-100">{loc.name}</span>
<span className="text-xs font-mono text-stone-500 dark:text-stone-400">
{loc.bookingCount} bookings · {formatCurrencyFn(loc.revenue, 'MAD')}
</span>
</div>
<div className="flex items-center gap-2">
<div className="progress-bar flex-1">
<div
className="progress-fill blue"
style={{ width: `${(loc.bookingCount / maxCount) * 100}%` }}
/>
</div>
<span className="w-8 text-right text-xs font-mono text-stone-500 dark:text-stone-400">
{loc.utilizationPct}%
</span>
</div>
</div>
))}
</div>
</div>
)
}
export default function DashboardPage() {
const { dict, language } = useDashboardI18n()
const d = dict.dashboardPage
@@ -251,6 +313,7 @@ export default function DashboardPage() {
icon={Calendar}
iconColor="text-blue-600"
iconBg="bg-blue-50"
pulse
/>
<StatCard
title={d.kpiActiveVehicles}
@@ -270,6 +333,7 @@ export default function DashboardPage() {
icon={DollarSign}
iconColor="text-orange-600"
iconBg="bg-orange-50"
valueGradient
/>
) : null}
<StatCard
@@ -283,9 +347,41 @@ export default function DashboardPage() {
/>
</div>
{/* New metrics row — contract change flag */}
{kpis.utilizationRate !== undefined && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<StatCard
title="Utilization Rate"
value={`${kpis.utilizationRate}%`}
icon={TrendingUp}
iconColor="text-blue-600"
iconBg="bg-blue-50"
progressPercent={kpis.utilizationRate}
target={100}
/>
<StatCard
title="Revenue / Vehicle"
value={formatCurrency(kpis.revenuePerVehicle ?? 0, 'MAD')}
icon={DollarSign}
iconColor="text-orange-600"
iconBg="bg-orange-50"
/>
<StatCard
title="Fulfillment Rate"
value={`${kpis.fulfillmentRate ?? 0}%`}
icon={BarChart3}
iconColor="text-green-600"
iconBg="bg-green-50"
progressPercent={kpis.fulfillmentRate ?? 0}
target={100}
/>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Booking Sources Chart */}
<div className="card p-6 lg:col-span-2">
{/* Left column — 2/3: Booking Sources + Location Performance */}
<div className="lg:col-span-2 space-y-6">
<div className="card p-6">
<h2 className="text-base font-semibold text-slate-900 mb-4">{d.bookingSources}</h2>
{data?.sourceBreakdown && data.sourceBreakdown.length > 0 ? (
<ResponsiveContainer width="100%" height={240}>
@@ -308,7 +404,15 @@ export default function DashboardPage() {
)}
</div>
{/* Quick stats */}
{data?.locationBreakdown && data.locationBreakdown.length > 0 ? (
<BranchPerformanceList
locations={data.locationBreakdown}
formatCurrencyFn={(a, c) => formatCurrency(a, c as 'MAD')}
/>
) : null}
</div>
{/* Right column — 1/3: Quick Stats */}
<div className="card p-6">
<h2 className="text-base font-semibold text-slate-900 mb-4">{d.quickStats}</h2>
<div className="space-y-4">
+76
View File
@@ -17,6 +17,20 @@ html.dark {
--sidebar-bg: #0a1128;
--sidebar-text: #94a3b8;
--sidebar-active: #ffffff;
--bg-elevated: rgb(255 255 255 / 0.82);
--border-accent: rgb(231 229 228 / 0.8);
--glass-bg: rgb(255 255 255 / 0.72);
--shadow-card: 0 30px 80px rgba(28, 25, 23, 0.08);
--shadow-glow: 0 0 20px rgba(249, 115, 22, 0.15);
}
html.dark {
--bg-elevated: rgb(11 25 55 / 0.78);
--border-accent: rgb(30 60 140 / 0.40);
--glass-bg: rgb(11 25 55 / 0.65);
--shadow-card: 0 30px 80px rgba(0, 0, 0, 0.36);
--shadow-glow: 0 0 20px rgba(249, 115, 22, 0.25);
}
@layer base {
@@ -41,6 +55,68 @@ html.dark body {
}
@layer components {
.glass-card {
border-color: var(--border-accent);
background-color: var(--glass-bg);
box-shadow: var(--shadow-card);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
@apply rounded-[1.75rem] border transition-colors;
}
html.dark .glass-card {
border-color: rgb(30 60 140 / 0.40);
background-color: rgb(11 25 55 / 0.65);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.36);
}
.progress-bar {
@apply h-2 w-full overflow-hidden rounded-full;
background-color: rgb(231 229 228 / 0.5);
}
html.dark .progress-bar {
background-color: rgb(30 60 140 / 0.3);
}
.progress-fill {
@apply h-full rounded-full transition-all duration-500;
}
.progress-fill.blue {
@apply bg-gradient-to-r from-blue-500 to-blue-400;
}
.progress-fill.orange {
@apply bg-gradient-to-r from-orange-500 to-orange-400;
}
html.dark .progress-fill.blue {
@apply bg-gradient-to-r from-blue-400 to-blue-300;
}
html.dark .progress-fill.orange {
@apply bg-gradient-to-r from-orange-400 to-orange-300;
}
/* Pulse glow animation for live indicators */
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 4px rgba(249, 115, 22, 0.3); }
50% { box-shadow: 0 0 12px rgba(249, 115, 22, 0.6); }
}
.pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.pulse-glow,
.progress-fill {
animation: none !important;
transition: none !important;
}
}
.btn-primary {
@apply inline-flex items-center gap-2 rounded-full bg-orange-600 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-orange-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-orange-500 dark:hover:bg-orange-400;
}
+8 -1
View File
@@ -1,9 +1,16 @@
import type { Metadata } from 'next'
import { cookies } from 'next/headers'
import { JetBrains_Mono } from 'next/font/google'
import { DashboardI18nProvider } from '@/components/I18nProvider'
import { SHARED_LANGUAGE_COOKIE } from '@/lib/preferences'
import './globals.css'
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
display: 'swap',
})
export const metadata: Metadata = {
title: 'RentalDriveGo Dashboard',
description: 'Manage your rental car business',
@@ -30,7 +37,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
}}
/>
</head>
<body className="font-sans">
<body className={`font-sans ${jetbrainsMono.variable}`}>
<DashboardI18nProvider initialLanguage={initialLanguage}>{children}</DashboardI18nProvider>
</body>
</html>
@@ -303,7 +303,9 @@ export default function Sidebar() {
const className = [
'flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-medium transition-colors',
paddingClass,
active ? 'bg-orange-500 text-white' : 'text-slate-400 hover:bg-blue-900/40 hover:text-white',
active
? 'bg-orange-500 text-white shadow-[0_0_12px_rgba(249,115,22,0.3)] dark:shadow-[0_0_16px_rgba(249,115,22,0.35)]'
: 'text-slate-400 hover:bg-blue-900/40 hover:text-white',
].join(' ')
if (item.itemType === 'EXTERNAL_LINK' && item.routeOrUrl) {
@@ -364,22 +366,31 @@ export default function Sidebar() {
<aside
className={[
'fixed inset-y-0 z-40 flex w-64 flex-col border-r border-blue-900/50 bg-[#0a1128]/94 backdrop-blur-xl transition-transform duration-300',
'fixed inset-y-0 z-40 flex w-64 flex-col border-r transition-colors duration-300',
theme === 'dark'
? 'border-blue-900/50 bg-blue-950/80 backdrop-blur-xl'
: 'border-stone-200/80 bg-white/92 backdrop-blur-xl shadow-[4px_0_20px_rgba(0,0,0,0.04)]',
isRtl ? 'right-0' : 'left-0',
'lg:translate-x-0',
open ? 'translate-x-0' : (isRtl ? 'translate-x-full' : '-translate-x-full'),
].join(' ')}
>
<a href={marketplaceUrl} target="_top" className="flex items-center gap-3 border-b border-blue-900/50 px-6 py-5">
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg bg-orange-400">
<a
href={marketplaceUrl}
target="_top"
className="flex items-center gap-3 border-b px-6 py-5 transition-colors border-blue-900/50 dark:border-blue-900/50"
>
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-orange-400 to-orange-500 shadow-sm">
{brand?.logoUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={brand.logoUrl} alt={brand.displayName} className="h-8 w-8 object-cover" />
) : (
<Car className="h-4 w-4 text-blue-950" />
<Car className="h-4 w-4 text-white" />
)}
</div>
<span className={`truncate text-sm font-bold tracking-wide ${theme === 'light' ? 'text-blue-600' : 'text-white'}`}>
<span className={`truncate text-sm font-bold tracking-wide ${
theme === 'dark' ? 'text-white' : 'text-blue-950'
}`}>
{brand?.displayName ?? 'RentalDriveGo'}
</span>
</a>
@@ -394,7 +405,9 @@ export default function Sidebar() {
href={item.href}
className={[
'flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-medium transition-colors',
active ? 'bg-orange-500 text-white' : 'text-slate-400 hover:bg-blue-900/40 hover:text-white',
active
? 'bg-orange-500 text-white shadow-[0_0_12px_rgba(249,115,22,0.3)] dark:shadow-[0_0_16px_rgba(249,115,22,0.35)]'
: 'text-slate-400 hover:bg-blue-900/40 hover:text-white',
].join(' ')}
>
<Icon className="h-4 w-4 flex-shrink-0" />
@@ -189,7 +189,19 @@ export default function TopBar() {
return (
<header className="relative z-[70] flex h-16 items-center justify-between border-b border-stone-200/80 bg-white/78 px-6 backdrop-blur-xl transition-colors dark:border-blue-900 dark:bg-blue-950/76">
<h1 className="text-lg font-semibold text-blue-950 dark:text-stone-50">{title}</h1>
<div className="flex items-center gap-4 min-w-0">
<h1 className="text-lg font-semibold text-blue-950 dark:text-stone-50 shrink-0">{title}</h1>
<div className="hidden sm:block relative max-w-xs w-full">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-stone-400 dark:text-stone-500 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
<input
type="search"
placeholder="Search…"
className="w-full rounded-2xl border border-stone-200 bg-stone-50/60 pl-9 pr-4 py-2 text-sm text-stone-900 placeholder:text-stone-400 transition-colors focus:border-orange-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-orange-500/20 dark:border-blue-800 dark:bg-blue-950/30 dark:text-slate-100 dark:placeholder:text-slate-500 dark:focus:border-orange-400 dark:focus:bg-blue-950/50"
/>
</div>
</div>
<div className="flex items-center gap-3">
<div className="relative">
+37 -3
View File
@@ -9,6 +9,14 @@ interface StatCardProps {
icon: LucideIcon
iconColor?: string
iconBg?: string
/** Optional target value to show a progress bar beneath the value */
target?: number
/** Current progress (0100) when target is set */
progressPercent?: number
/** Show a subtle pulse/glow animation on the card */
pulse?: boolean
/** Apply a gradient to the value text */
valueGradient?: boolean
}
export default function StatCard({
@@ -19,16 +27,42 @@ export default function StatCard({
icon: Icon,
iconColor = 'text-orange-700 dark:text-orange-300',
iconBg = 'bg-orange-50 dark:bg-orange-950/30',
target,
progressPercent,
pulse,
valueGradient,
}: StatCardProps) {
const isPositive = change !== undefined && change >= 0
const isNegative = change !== undefined && change < 0
return (
<div className="card p-6">
<div className={`card p-6 ${pulse ? 'pulse-glow' : ''}`}>
<div className="flex items-start justify-between">
<div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-stone-500 dark:text-stone-400">{title}</p>
<p className="mt-1 text-2xl font-bold text-blue-950 dark:text-stone-50">{value}</p>
<p
className={[
'mt-1 text-2xl font-bold',
valueGradient
? 'bg-gradient-to-r from-blue-600 to-orange-500 bg-clip-text text-transparent dark:from-blue-400 dark:to-orange-300'
: 'text-blue-950 dark:text-stone-50',
].join(' ')}
>
{value}
</p>
{target !== undefined && progressPercent !== undefined && (
<div className="mt-2 flex items-center gap-2">
<div className="progress-bar flex-1">
<div
className="progress-fill orange"
style={{ width: `${Math.min(progressPercent, 100)}%` }}
/>
</div>
<span className="text-xs font-mono text-stone-500 dark:text-stone-400">
{Math.round(progressPercent)}%
</span>
</div>
)}
{change !== undefined && (
<div className="mt-2 flex items-center gap-1">
<span
+1
View File
@@ -17,6 +17,7 @@ const config: Config = {
},
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui'],
mono: ['JetBrains Mono', 'ui-monospace', 'SFMono-Regular', 'monospace'],
},
},
},
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
That diagram shows the core shift: right now your homepage is basically one flat layer (cards in a grid), and the redesign organizes it into four layers that sit at different depths and move at different speeds. Here's the full plan, grounded in what's actually in your codebase (HomeContent.tsx, StatsStrip, HowItWorks, TestimonialsSection, globals.css).
What's flat about it today
Every section is border + bg-white/80 + backdrop-blur repeated down the page — hero, surface card, pillars, features, testimonials all use the same recipe. The only motion in the entire codebase is the number counter in StatsStrip. No parallax, no scroll-triggered reveals, no cursor reactivity, no layering — it reads like a brochure, not a product.
Layer 1 — Background ambient
Replace the static site-glow radial gradients with a position:fixed canvas/SVG layer behind everything: a slow-drifting gradient mesh (CSS @keyframes on background-position, 2030s loop) plus a few large soft-edged blobs in your orange/blue brand colors that shift opacity per section as you scroll (driven by scroll progress, not JS-per-frame). This is pure atmosphere — zero interaction, lowest z-index, pointer-events:none.
Layer 2 — Motion / parallax
A position:absolute layer holding decorative shapes (car silhouette outlines, route-line squiggles, dashed "road" paths) that translate at a fraction of scroll speed (translateY(scrollY * 0.3)), so they appear to drift slower than the content in front of them. This is what makes a page feel "deep" instead of stacked. Good candidates: behind the hero, behind the stats strip, behind testimonials.
Layer 3 — Content (rebuilt, not just restyled)
This is your existing copy/cards, but reworked so each section enters with a scroll-triggered animation (fade + slight rise via IntersectionObserver, same pattern your Counter already uses — just extend it) rather than appearing fully rendered. Specific upgrades:
Hero: stagger the kicker → title → body → CTAs → metric cards (100ms apart) instead of all at once.
Pillars/features grid: cards tilt slightly on mouse-move (cheap CSS transform: perspective() tied to mouse position) instead of sitting static.
Stats strip: already has a counter — add a connecting animated line/progress bar that draws in alongside the counters.
Steps/How-it-works: convert from a static list into a scroll-linked progress rail — a vertical line that fills as the user scrolls past each step.
Layer 4 — Foreground interactive
Small elements that float above content and react to the user: a cursor-follow glow on hero CTAs, a sticky "live" badge that pulses (you already have dict.liveLabel — give it a pulse animation), micro-toasts like "3 companies just joined" drifting in periodically near the surface card. These sit at the highest z-index and are the layer most associated with "alive."
Implementation plan
Pick the motion engine. Since this is React/Next.js, the cleanest path is Framer Motion (npm install framer-motion) for entrance/scroll animations, plus plain CSS @keyframes for ambient/looping effects (cheaper, no JS per frame). Avoid heavy scroll libraries (GSAP ScrollTrigger) unless you want more cinematic control — Framer Motion's whileInView + useScroll/useTransform covers everything above.
Build a <BackgroundLayers /> component mounted once in (public)/layout.tsx, rendering the ambient + parallax layers as fixed/absolute siblings behind {children}. Keeps it out of every page's content logic.
Extract a reusable <Reveal> wrapper (IntersectionObserver + fade/rise) and wrap each existing section in HomeContent.tsx with it — minimal rewrite of your current JSX, just wrapping.
Upgrade StatsStrip and HowItWorks with the progress-rail/connecting-line treatment since they already have the data shape for it.
Add the foreground layer last — it's the smallest amount of code but the highest "feels dynamic" payoff, so don't front-load it; do it once the depth/motion skeleton works.
Respect prefers-reduced-motion throughout — wrap all loops/parallax in the media query so it degrades to the current static design for users who need that.
Suggested order of work: (1) background ambient layer, (2) Reveal wrapper on existing sections, (3) parallax shapes, (4) stats/steps progress treatment, (5) foreground micro-interactions, (6) cursor-tilt on cards.
+34
View File
@@ -0,0 +1,34 @@
Proposed plan
Phase 1 — Design tokens & primitives
Add .glass-card as the dark-mode companion to the existing .card (light mode) — same component, theme-aware via html.dark selector, so call sites don't need to branch.
Add --bg-elevated, --border-accent, --glass-bg, glow shadows for dark; pick matching light-mode equivalents (soft shadow/border tints) so cards feel like one consistent design language in either mode, not "light mode = old style, dark mode = new style."
Add .progress-bar / .progress-fill classes with light and dark variants (the mockup's blue/orange gradient fills work in both; just need lighter track colors for light mode).
Add JetBrains Mono via next/font for metric values — works the same in both modes.
Guard pulse/glow animations behind prefers-reduced-motion (applies regardless of theme).
Phase 2 — StatCard v2
Extend StatCard.tsx with optional target, progressPercent, pulse, valueGradient props, each with explicit light/dark Tailwind variants (dark: prefixes), backward-compatible with current usage.
Update StatCard.test.ts, including a check that dark-mode classes are present.
Phase 3 — Dashboard home restructure
Reorganize (dashboard)/page.tsx into a 2/3 + 1/3 grid: bookings chart + new "Branch/Location Performance" list (left), quick stats (right) — laid out identically in both modes, only colors/surfaces change.
New metrics (utilization rate, revenue/vehicle, fulfillment rate, branch breakdown) require backend fields on /analytics/dashboard (utilizationRate, fulfillmentRate, locationBreakdown[]) — flagged as a contract change, not faked.
Drop the fixed-airport SVG map; replace with a data-driven branch list with progress bars, styled for both themes.
Phase 4 — Sidebar & TopBar polish
Sidebar: glass background in dark mode, equivalent frosted-white treatment in light mode; gradient logo badge and active-item glow tuned per theme so contrast stays good in both.
TopBar: restyled search input with light/dark variants; keep only status indicators backed by real data.
Phase 5 — i18n & RTL pass
All new strings via the dictionary for fr/en/ar.
Verify glass-cards, progress bars, and branch list mirror correctly in dir="rtl", in both light and dark.
Phase 6 — QA
Update/extend tests.
Manual visual check of every changed component in 4 combinations: light+LTR, light+RTL, dark+LTR, dark+RTL.
+105
View File
@@ -10638,6 +10638,111 @@
"devDependencies": {
"typescript": "^5.4.0"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.2.8",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.8.tgz",
"integrity": "sha512-R/MDVX4Cj+bzvTNkDWNyA09yCPBZB0J1m8P3VAmWAzx/J7DctA1wxya1LU3CcPv2szER3AImHEtsgScauH8SsQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.2.8",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.8.tgz",
"integrity": "sha512-XH2u8C/meLJmEZbN0G5MRZp8tvxBz5KQ+xv4vX34zL8gL3//5qkc5M/aLsvNAfg5/KbcjIVe3h6aiO0wJcNWKA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.2.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.8.tgz",
"integrity": "sha512-zyA8bCtlAXOzZD6ri6hIi5Z7hW5+07ea81KK0xdezmJHrbrAgvwIRd6VbkddJTapmZVSmWzbpAdYoi1HZNF7KA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.2.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.8.tgz",
"integrity": "sha512-SD/R9QU6+wUB3lH6InhOLu1eP3wle4sSjsNdMwk24yFwaWUB5TAtscvFbOqh1AYBsJvHRez3tYHETfbBFd4dRg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.2.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.8.tgz",
"integrity": "sha512-F1Al3cRKhYR7/V1RaXnHceqilNLFDe47w7Li/Asph1SklaHHm/b2FahLar2nWct9l9K143NlqbAUFQMjx30QYA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.2.8",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.8.tgz",
"integrity": "sha512-LxhCLToY0id1CkmYFi9sTM2RfoBWszr6mmMrBtmTFqKF0fXX3ZIbM8m1sKoNFIrZFn7gFroT7cnZea5neufSHg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.2.8",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.8.tgz",
"integrity": "sha512-xfGxs1zIkTOLzH8i1WWCeTqQhNqyrv3aVT55PMy7Ul3dfxlA3In7hNEJ3Uo2qpalEBOlkG6G6FLRMWbUrxL+qQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}