Add theme system with color presets, fix login page styling, persist auth session
Theme system with 5 color presets (Slate, Emerald, Violet, Amber, Rose) and light/dark/system mode. User menu in sidebar with theme picker and sign out. Login page uses standalone dark branded styling with autofill override. Auth persists in sessionStorage across refreshes.
This commit is contained in:
@@ -6,6 +6,14 @@
|
|||||||
<title>Forte Admin</title>
|
<title>Forte Admin</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script>
|
||||||
|
// Apply mode before React renders to prevent flash
|
||||||
|
(function() {
|
||||||
|
var mode = localStorage.getItem('forte-mode') || 'system';
|
||||||
|
var isDark = mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
if (isDark) document.documentElement.classList.add('dark');
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,94 +1,29 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@variant dark (&:is(.dark, .dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-background: hsl(0 0% 100%);
|
/* Defaults — overridden at runtime by theme store */
|
||||||
--color-foreground: hsl(240 10% 3.9%);
|
--color-background: hsl(210 20% 97%);
|
||||||
|
--color-foreground: hsl(222 47% 11%);
|
||||||
--color-card: hsl(0 0% 100%);
|
--color-card: hsl(0 0% 100%);
|
||||||
--color-card-foreground: hsl(240 10% 3.9%);
|
--color-card-foreground: hsl(222 47% 11%);
|
||||||
--color-popover: hsl(0 0% 100%);
|
--color-popover: hsl(0 0% 100%);
|
||||||
--color-popover-foreground: hsl(240 10% 3.9%);
|
--color-popover-foreground: hsl(222 47% 11%);
|
||||||
--color-primary: hsl(240 5.9% 10%);
|
--color-primary: hsl(215 25% 27%);
|
||||||
--color-primary-foreground: hsl(0 0% 98%);
|
--color-primary-foreground: hsl(210 40% 98%);
|
||||||
--color-secondary: hsl(240 4.8% 95.9%);
|
--color-secondary: hsl(210 40% 94%);
|
||||||
--color-secondary-foreground: hsl(240 5.9% 10%);
|
--color-secondary-foreground: hsl(222 47% 11%);
|
||||||
--color-muted: hsl(240 4.8% 95.9%);
|
--color-muted: hsl(210 40% 94%);
|
||||||
--color-muted-foreground: hsl(240 3.8% 46.1%);
|
--color-muted-foreground: hsl(215 16% 47%);
|
||||||
--color-accent: hsl(240 4.8% 95.9%);
|
--color-accent: hsl(210 40% 94%);
|
||||||
--color-accent-foreground: hsl(240 5.9% 10%);
|
--color-accent-foreground: hsl(222 47% 11%);
|
||||||
--color-destructive: hsl(0 84.2% 60.2%);
|
--color-destructive: hsl(0 72% 51%);
|
||||||
--color-destructive-foreground: hsl(0 0% 98%);
|
--color-destructive-foreground: hsl(210 40% 98%);
|
||||||
--color-border: hsl(240 5.9% 90%);
|
--color-border: hsl(214 32% 89%);
|
||||||
--color-input: hsl(240 5.9% 90%);
|
--color-input: hsl(214 32% 89%);
|
||||||
--color-ring: hsl(240 5.9% 10%);
|
--color-ring: hsl(215 25% 27%);
|
||||||
--color-sidebar: hsl(0 0% 98%);
|
--radius: 0.5rem;
|
||||||
--color-sidebar-foreground: hsl(240 5.3% 26.1%);
|
|
||||||
--color-sidebar-primary: hsl(240 5.9% 10%);
|
|
||||||
--color-sidebar-primary-foreground: hsl(0 0% 98%);
|
|
||||||
--color-sidebar-accent: hsl(240 4.8% 95.9%);
|
|
||||||
--color-sidebar-accent-foreground: hsl(240 5.9% 10%);
|
|
||||||
--color-sidebar-border: hsl(220 13% 91%);
|
|
||||||
--color-sidebar-ring: hsl(217.2 91.2% 59.8%);
|
|
||||||
--radius: 0.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--color-background: hsl(240 10% 3.9%);
|
|
||||||
--color-foreground: hsl(0 0% 98%);
|
|
||||||
--color-card: hsl(240 10% 3.9%);
|
|
||||||
--color-card-foreground: hsl(0 0% 98%);
|
|
||||||
--color-popover: hsl(240 10% 3.9%);
|
|
||||||
--color-popover-foreground: hsl(0 0% 98%);
|
|
||||||
--color-primary: hsl(0 0% 98%);
|
|
||||||
--color-primary-foreground: hsl(240 5.9% 10%);
|
|
||||||
--color-secondary: hsl(240 3.7% 15.9%);
|
|
||||||
--color-secondary-foreground: hsl(0 0% 98%);
|
|
||||||
--color-muted: hsl(240 3.7% 15.9%);
|
|
||||||
--color-muted-foreground: hsl(240 5% 64.9%);
|
|
||||||
--color-accent: hsl(240 3.7% 15.9%);
|
|
||||||
--color-accent-foreground: hsl(0 0% 98%);
|
|
||||||
--color-destructive: hsl(0 62.8% 30.6%);
|
|
||||||
--color-destructive-foreground: hsl(0 0% 98%);
|
|
||||||
--color-border: hsl(240 3.7% 15.9%);
|
|
||||||
--color-input: hsl(240 3.7% 15.9%);
|
|
||||||
--color-ring: hsl(240 4.9% 83.9%);
|
|
||||||
--color-sidebar: hsl(240 5.9% 10%);
|
|
||||||
--color-sidebar-foreground: hsl(240 4.8% 95.9%);
|
|
||||||
--color-sidebar-primary: hsl(224.3 76.3% 48%);
|
|
||||||
--color-sidebar-primary-foreground: hsl(0 0% 100%);
|
|
||||||
--color-sidebar-accent: hsl(240 3.7% 15.9%);
|
|
||||||
--color-sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
|
||||||
--color-sidebar-border: hsl(240 3.7% 15.9%);
|
|
||||||
--color-sidebar-ring: hsl(217.2 91.2% 59.8%);
|
|
||||||
--sidebar: hsl(240 5.9% 10%);
|
|
||||||
--sidebar-foreground: hsl(240 4.8% 95.9%);
|
|
||||||
--sidebar-primary: hsl(224.3 76.3% 48%);
|
|
||||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
|
||||||
--sidebar-accent: hsl(240 3.7% 15.9%);
|
|
||||||
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
|
||||||
--sidebar-border: hsl(240 3.7% 15.9%);
|
|
||||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family:
|
|
||||||
"Inter",
|
|
||||||
ui-sans-serif,
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--sidebar: hsl(0 0% 98%);
|
|
||||||
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
|
||||||
--sidebar-primary: hsl(240 5.9% 10%);
|
|
||||||
--sidebar-primary-foreground: hsl(0 0% 98%);
|
|
||||||
--sidebar-accent: hsl(240 4.8% 95.9%);
|
|
||||||
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
|
||||||
--sidebar-border: hsl(220 13% 91%);
|
|
||||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -101,3 +36,38 @@ body {
|
|||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sidebar: hsl(210 25% 95%);
|
||||||
|
--sidebar-foreground: hsl(215 16% 47%);
|
||||||
|
--sidebar-primary: hsl(215 25% 27%);
|
||||||
|
--sidebar-primary-foreground: hsl(210 40% 98%);
|
||||||
|
--sidebar-accent: hsl(210 40% 92%);
|
||||||
|
--sidebar-accent-foreground: hsl(222 47% 11%);
|
||||||
|
--sidebar-border: hsl(214 32% 89%);
|
||||||
|
--sidebar-ring: hsl(215 25% 27%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
"Inter",
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input {
|
||||||
|
background-color: #1a2740 !important;
|
||||||
|
border-color: #2a3a52 !important;
|
||||||
|
color: #d0d8e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input:-webkit-autofill,
|
||||||
|
.login-input:-webkit-autofill:hover,
|
||||||
|
.login-input:-webkit-autofill:focus {
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px #1a2740 inset !important;
|
||||||
|
-webkit-text-fill-color: #d0d8e0 !important;
|
||||||
|
border-color: #2a3a52 !important;
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const buttonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90 dark:bg-transparent dark:border dark:border-primary dark:text-primary dark:hover:bg-primary/10",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||||
outline:
|
outline:
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base text-foreground shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
|
|||||||
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
useAuthStore.getState().logout()
|
useAuthStore.getState().logout()
|
||||||
window.location.href = '/login'
|
// Don't use window.location — that causes a full reload and flash
|
||||||
|
// The router's beforeLoad guard will redirect to /login on next navigation
|
||||||
throw new ApiError('Unauthorized', 401)
|
throw new ApiError('Unauthorized', 401)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
379
packages/admin/src/lib/themes.ts
Normal file
379
packages/admin/src/lib/themes.ts
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
export interface ThemeColors {
|
||||||
|
background: string
|
||||||
|
foreground: string
|
||||||
|
card: string
|
||||||
|
cardForeground: string
|
||||||
|
popover: string
|
||||||
|
popoverForeground: string
|
||||||
|
primary: string
|
||||||
|
primaryForeground: string
|
||||||
|
secondary: string
|
||||||
|
secondaryForeground: string
|
||||||
|
muted: string
|
||||||
|
mutedForeground: string
|
||||||
|
accent: string
|
||||||
|
accentForeground: string
|
||||||
|
destructive: string
|
||||||
|
destructiveForeground: string
|
||||||
|
border: string
|
||||||
|
input: string
|
||||||
|
ring: string
|
||||||
|
sidebar: string
|
||||||
|
sidebarForeground: string
|
||||||
|
sidebarPrimary: string
|
||||||
|
sidebarPrimaryForeground: string
|
||||||
|
sidebarAccent: string
|
||||||
|
sidebarAccentForeground: string
|
||||||
|
sidebarBorder: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemePreset {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
light: ThemeColors
|
||||||
|
dark: ThemeColors
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themes: ThemePreset[] = [
|
||||||
|
{
|
||||||
|
name: 'slate',
|
||||||
|
label: 'Slate',
|
||||||
|
light: {
|
||||||
|
background: '210 20% 97%',
|
||||||
|
foreground: '222 47% 11%',
|
||||||
|
card: '0 0% 100%',
|
||||||
|
cardForeground: '222 47% 11%',
|
||||||
|
popover: '0 0% 100%',
|
||||||
|
popoverForeground: '222 47% 11%',
|
||||||
|
primary: '215 25% 27%',
|
||||||
|
primaryForeground: '210 40% 98%',
|
||||||
|
secondary: '210 40% 94%',
|
||||||
|
secondaryForeground: '222 47% 11%',
|
||||||
|
muted: '210 40% 94%',
|
||||||
|
mutedForeground: '215 16% 47%',
|
||||||
|
accent: '210 40% 94%',
|
||||||
|
accentForeground: '222 47% 11%',
|
||||||
|
destructive: '0 72% 51%',
|
||||||
|
destructiveForeground: '210 40% 98%',
|
||||||
|
border: '214 32% 89%',
|
||||||
|
input: '214 32% 89%',
|
||||||
|
ring: '215 25% 27%',
|
||||||
|
sidebar: '210 25% 95%',
|
||||||
|
sidebarForeground: '215 16% 47%',
|
||||||
|
sidebarPrimary: '215 25% 27%',
|
||||||
|
sidebarPrimaryForeground: '210 40% 98%',
|
||||||
|
sidebarAccent: '210 40% 92%',
|
||||||
|
sidebarAccentForeground: '222 47% 11%',
|
||||||
|
sidebarBorder: '214 32% 89%',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: '222 30% 8%',
|
||||||
|
foreground: '210 20% 86%',
|
||||||
|
card: '222 30% 10%',
|
||||||
|
cardForeground: '210 20% 86%',
|
||||||
|
popover: '222 30% 10%',
|
||||||
|
popoverForeground: '210 20% 86%',
|
||||||
|
primary: '215 20% 72%',
|
||||||
|
primaryForeground: '222 30% 8%',
|
||||||
|
secondary: '217 20% 15%',
|
||||||
|
secondaryForeground: '210 20% 86%',
|
||||||
|
muted: '217 20% 15%',
|
||||||
|
mutedForeground: '215 15% 55%',
|
||||||
|
accent: '217 20% 15%',
|
||||||
|
accentForeground: '210 20% 86%',
|
||||||
|
destructive: '0 55% 48%',
|
||||||
|
destructiveForeground: '210 20% 92%',
|
||||||
|
border: '217 20% 17%',
|
||||||
|
input: '217 20% 17%',
|
||||||
|
ring: '215 20% 55%',
|
||||||
|
sidebar: '222 30% 7%',
|
||||||
|
sidebarForeground: '215 15% 65%',
|
||||||
|
sidebarPrimary: '215 20% 72%',
|
||||||
|
sidebarPrimaryForeground: '0 0% 100%',
|
||||||
|
sidebarAccent: '217 20% 13%',
|
||||||
|
sidebarAccentForeground: '210 20% 86%',
|
||||||
|
sidebarBorder: '217 20% 15%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'emerald',
|
||||||
|
label: 'Emerald',
|
||||||
|
light: {
|
||||||
|
background: '150 20% 97%',
|
||||||
|
foreground: '150 40% 10%',
|
||||||
|
card: '0 0% 100%',
|
||||||
|
cardForeground: '150 40% 10%',
|
||||||
|
popover: '0 0% 100%',
|
||||||
|
popoverForeground: '150 40% 10%',
|
||||||
|
primary: '160 84% 29%',
|
||||||
|
primaryForeground: '150 20% 98%',
|
||||||
|
secondary: '150 20% 93%',
|
||||||
|
secondaryForeground: '150 40% 10%',
|
||||||
|
muted: '150 20% 93%',
|
||||||
|
mutedForeground: '150 10% 45%',
|
||||||
|
accent: '150 20% 93%',
|
||||||
|
accentForeground: '150 40% 10%',
|
||||||
|
destructive: '0 72% 51%',
|
||||||
|
destructiveForeground: '150 20% 98%',
|
||||||
|
border: '150 15% 88%',
|
||||||
|
input: '150 15% 88%',
|
||||||
|
ring: '160 84% 29%',
|
||||||
|
sidebar: '150 20% 95%',
|
||||||
|
sidebarForeground: '150 10% 45%',
|
||||||
|
sidebarPrimary: '160 84% 29%',
|
||||||
|
sidebarPrimaryForeground: '150 20% 98%',
|
||||||
|
sidebarAccent: '150 20% 91%',
|
||||||
|
sidebarAccentForeground: '150 40% 10%',
|
||||||
|
sidebarBorder: '150 15% 88%',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: '155 30% 7%',
|
||||||
|
foreground: '150 15% 85%',
|
||||||
|
card: '155 30% 9%',
|
||||||
|
cardForeground: '150 15% 85%',
|
||||||
|
popover: '155 30% 9%',
|
||||||
|
popoverForeground: '150 15% 85%',
|
||||||
|
primary: '160 72% 42%',
|
||||||
|
primaryForeground: '155 30% 7%',
|
||||||
|
secondary: '155 20% 14%',
|
||||||
|
secondaryForeground: '150 15% 85%',
|
||||||
|
muted: '155 20% 14%',
|
||||||
|
mutedForeground: '150 10% 52%',
|
||||||
|
accent: '155 20% 14%',
|
||||||
|
accentForeground: '150 15% 85%',
|
||||||
|
destructive: '0 55% 48%',
|
||||||
|
destructiveForeground: '150 15% 92%',
|
||||||
|
border: '155 20% 16%',
|
||||||
|
input: '155 20% 16%',
|
||||||
|
ring: '160 72% 42%',
|
||||||
|
sidebar: '155 30% 6%',
|
||||||
|
sidebarForeground: '150 10% 62%',
|
||||||
|
sidebarPrimary: '160 72% 42%',
|
||||||
|
sidebarPrimaryForeground: '0 0% 100%',
|
||||||
|
sidebarAccent: '155 20% 12%',
|
||||||
|
sidebarAccentForeground: '150 15% 85%',
|
||||||
|
sidebarBorder: '155 20% 14%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'violet',
|
||||||
|
label: 'Violet',
|
||||||
|
light: {
|
||||||
|
background: '260 15% 97%',
|
||||||
|
foreground: '260 40% 11%',
|
||||||
|
card: '0 0% 100%',
|
||||||
|
cardForeground: '260 40% 11%',
|
||||||
|
popover: '0 0% 100%',
|
||||||
|
popoverForeground: '260 40% 11%',
|
||||||
|
primary: '262 83% 58%',
|
||||||
|
primaryForeground: '260 15% 98%',
|
||||||
|
secondary: '260 20% 94%',
|
||||||
|
secondaryForeground: '260 40% 11%',
|
||||||
|
muted: '260 20% 94%',
|
||||||
|
mutedForeground: '260 10% 47%',
|
||||||
|
accent: '260 20% 94%',
|
||||||
|
accentForeground: '260 40% 11%',
|
||||||
|
destructive: '0 72% 51%',
|
||||||
|
destructiveForeground: '260 15% 98%',
|
||||||
|
border: '260 15% 89%',
|
||||||
|
input: '260 15% 89%',
|
||||||
|
ring: '262 83% 58%',
|
||||||
|
sidebar: '260 18% 95%',
|
||||||
|
sidebarForeground: '260 10% 47%',
|
||||||
|
sidebarPrimary: '262 83% 58%',
|
||||||
|
sidebarPrimaryForeground: '260 15% 98%',
|
||||||
|
sidebarAccent: '260 20% 92%',
|
||||||
|
sidebarAccentForeground: '260 40% 11%',
|
||||||
|
sidebarBorder: '260 15% 89%',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: '260 30% 8%',
|
||||||
|
foreground: '260 15% 86%',
|
||||||
|
card: '260 30% 10%',
|
||||||
|
cardForeground: '260 15% 86%',
|
||||||
|
popover: '260 30% 10%',
|
||||||
|
popoverForeground: '260 15% 86%',
|
||||||
|
primary: '262 83% 65%',
|
||||||
|
primaryForeground: '260 30% 8%',
|
||||||
|
secondary: '260 20% 15%',
|
||||||
|
secondaryForeground: '260 15% 86%',
|
||||||
|
muted: '260 20% 15%',
|
||||||
|
mutedForeground: '260 10% 54%',
|
||||||
|
accent: '260 20% 15%',
|
||||||
|
accentForeground: '260 15% 86%',
|
||||||
|
destructive: '0 55% 48%',
|
||||||
|
destructiveForeground: '260 15% 92%',
|
||||||
|
border: '260 20% 17%',
|
||||||
|
input: '260 20% 17%',
|
||||||
|
ring: '262 83% 65%',
|
||||||
|
sidebar: '260 30% 7%',
|
||||||
|
sidebarForeground: '260 10% 64%',
|
||||||
|
sidebarPrimary: '262 83% 65%',
|
||||||
|
sidebarPrimaryForeground: '0 0% 100%',
|
||||||
|
sidebarAccent: '260 20% 13%',
|
||||||
|
sidebarAccentForeground: '260 15% 86%',
|
||||||
|
sidebarBorder: '260 20% 15%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amber',
|
||||||
|
label: 'Amber',
|
||||||
|
light: {
|
||||||
|
background: '36 20% 97%',
|
||||||
|
foreground: '28 40% 10%',
|
||||||
|
card: '0 0% 100%',
|
||||||
|
cardForeground: '28 40% 10%',
|
||||||
|
popover: '0 0% 100%',
|
||||||
|
popoverForeground: '28 40% 10%',
|
||||||
|
primary: '25 95% 40%',
|
||||||
|
primaryForeground: '36 20% 98%',
|
||||||
|
secondary: '36 25% 93%',
|
||||||
|
secondaryForeground: '28 40% 10%',
|
||||||
|
muted: '36 25% 93%',
|
||||||
|
mutedForeground: '28 10% 47%',
|
||||||
|
accent: '36 25% 93%',
|
||||||
|
accentForeground: '28 40% 10%',
|
||||||
|
destructive: '0 72% 51%',
|
||||||
|
destructiveForeground: '36 20% 98%',
|
||||||
|
border: '36 20% 88%',
|
||||||
|
input: '36 20% 88%',
|
||||||
|
ring: '25 95% 40%',
|
||||||
|
sidebar: '36 22% 95%',
|
||||||
|
sidebarForeground: '28 10% 47%',
|
||||||
|
sidebarPrimary: '25 95% 40%',
|
||||||
|
sidebarPrimaryForeground: '36 20% 98%',
|
||||||
|
sidebarAccent: '36 25% 91%',
|
||||||
|
sidebarAccentForeground: '28 40% 10%',
|
||||||
|
sidebarBorder: '36 20% 88%',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: '28 25% 7%',
|
||||||
|
foreground: '36 15% 85%',
|
||||||
|
card: '28 25% 9%',
|
||||||
|
cardForeground: '36 15% 85%',
|
||||||
|
popover: '28 25% 9%',
|
||||||
|
popoverForeground: '36 15% 85%',
|
||||||
|
primary: '30 90% 52%',
|
||||||
|
primaryForeground: '28 25% 7%',
|
||||||
|
secondary: '28 18% 14%',
|
||||||
|
secondaryForeground: '36 15% 85%',
|
||||||
|
muted: '28 18% 14%',
|
||||||
|
mutedForeground: '28 10% 52%',
|
||||||
|
accent: '28 18% 14%',
|
||||||
|
accentForeground: '36 15% 85%',
|
||||||
|
destructive: '0 55% 48%',
|
||||||
|
destructiveForeground: '36 15% 92%',
|
||||||
|
border: '28 18% 16%',
|
||||||
|
input: '28 18% 16%',
|
||||||
|
ring: '30 90% 52%',
|
||||||
|
sidebar: '28 25% 6%',
|
||||||
|
sidebarForeground: '28 10% 62%',
|
||||||
|
sidebarPrimary: '30 90% 52%',
|
||||||
|
sidebarPrimaryForeground: '0 0% 100%',
|
||||||
|
sidebarAccent: '28 18% 12%',
|
||||||
|
sidebarAccentForeground: '36 15% 85%',
|
||||||
|
sidebarBorder: '28 18% 14%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rose',
|
||||||
|
label: 'Rose',
|
||||||
|
light: {
|
||||||
|
background: '350 15% 97%',
|
||||||
|
foreground: '350 40% 10%',
|
||||||
|
card: '0 0% 100%',
|
||||||
|
cardForeground: '350 40% 10%',
|
||||||
|
popover: '0 0% 100%',
|
||||||
|
popoverForeground: '350 40% 10%',
|
||||||
|
primary: '347 77% 50%',
|
||||||
|
primaryForeground: '350 15% 98%',
|
||||||
|
secondary: '350 20% 94%',
|
||||||
|
secondaryForeground: '350 40% 10%',
|
||||||
|
muted: '350 20% 94%',
|
||||||
|
mutedForeground: '350 10% 47%',
|
||||||
|
accent: '350 20% 94%',
|
||||||
|
accentForeground: '350 40% 10%',
|
||||||
|
destructive: '0 72% 51%',
|
||||||
|
destructiveForeground: '350 15% 98%',
|
||||||
|
border: '350 15% 89%',
|
||||||
|
input: '350 15% 89%',
|
||||||
|
ring: '347 77% 50%',
|
||||||
|
sidebar: '350 18% 95%',
|
||||||
|
sidebarForeground: '350 10% 47%',
|
||||||
|
sidebarPrimary: '347 77% 50%',
|
||||||
|
sidebarPrimaryForeground: '350 15% 98%',
|
||||||
|
sidebarAccent: '350 20% 92%',
|
||||||
|
sidebarAccentForeground: '350 40% 10%',
|
||||||
|
sidebarBorder: '350 15% 89%',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: '350 25% 7%',
|
||||||
|
foreground: '350 12% 86%',
|
||||||
|
card: '350 25% 9%',
|
||||||
|
cardForeground: '350 12% 86%',
|
||||||
|
popover: '350 25% 9%',
|
||||||
|
popoverForeground: '350 12% 86%',
|
||||||
|
primary: '347 77% 58%',
|
||||||
|
primaryForeground: '350 25% 7%',
|
||||||
|
secondary: '350 18% 14%',
|
||||||
|
secondaryForeground: '350 12% 86%',
|
||||||
|
muted: '350 18% 14%',
|
||||||
|
mutedForeground: '350 10% 54%',
|
||||||
|
accent: '350 18% 14%',
|
||||||
|
accentForeground: '350 12% 86%',
|
||||||
|
destructive: '0 55% 48%',
|
||||||
|
destructiveForeground: '350 12% 92%',
|
||||||
|
border: '350 18% 16%',
|
||||||
|
input: '350 18% 16%',
|
||||||
|
ring: '347 77% 58%',
|
||||||
|
sidebar: '350 25% 6%',
|
||||||
|
sidebarForeground: '350 10% 62%',
|
||||||
|
sidebarPrimary: '347 77% 58%',
|
||||||
|
sidebarPrimaryForeground: '0 0% 100%',
|
||||||
|
sidebarAccent: '350 18% 12%',
|
||||||
|
sidebarAccentForeground: '350 12% 86%',
|
||||||
|
sidebarBorder: '350 18% 14%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const CSS_VAR_MAP: Record<keyof ThemeColors, string> = {
|
||||||
|
background: '--color-background',
|
||||||
|
foreground: '--color-foreground',
|
||||||
|
card: '--color-card',
|
||||||
|
cardForeground: '--color-card-foreground',
|
||||||
|
popover: '--color-popover',
|
||||||
|
popoverForeground: '--color-popover-foreground',
|
||||||
|
primary: '--color-primary',
|
||||||
|
primaryForeground: '--color-primary-foreground',
|
||||||
|
secondary: '--color-secondary',
|
||||||
|
secondaryForeground: '--color-secondary-foreground',
|
||||||
|
muted: '--color-muted',
|
||||||
|
mutedForeground: '--color-muted-foreground',
|
||||||
|
accent: '--color-accent',
|
||||||
|
accentForeground: '--color-accent-foreground',
|
||||||
|
destructive: '--color-destructive',
|
||||||
|
destructiveForeground: '--color-destructive-foreground',
|
||||||
|
border: '--color-border',
|
||||||
|
input: '--color-input',
|
||||||
|
ring: '--color-ring',
|
||||||
|
sidebar: '--sidebar',
|
||||||
|
sidebarForeground: '--sidebar-foreground',
|
||||||
|
sidebarPrimary: '--sidebar-primary',
|
||||||
|
sidebarPrimaryForeground: '--sidebar-primary-foreground',
|
||||||
|
sidebarAccent: '--sidebar-accent',
|
||||||
|
sidebarAccentForeground: '--sidebar-accent-foreground',
|
||||||
|
sidebarBorder: '--sidebar-border',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyThemeColors(colors: ThemeColors) {
|
||||||
|
const root = document.documentElement
|
||||||
|
for (const [key, cssVar] of Object.entries(CSS_VAR_MAP)) {
|
||||||
|
const value = colors[key as keyof ThemeColors]
|
||||||
|
root.style.setProperty(cssVar, `hsl(${value})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThemeByName(name: string): ThemePreset {
|
||||||
|
return themes.find((t) => t.name === name) ?? themes[0]
|
||||||
|
}
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
import { createFileRoute, Outlet, Link, redirect, useRouter } from '@tanstack/react-router'
|
||||||
import { useAuthStore } from '@/stores/auth.store'
|
import { useAuthStore } from '@/stores/auth.store'
|
||||||
|
import { useThemeStore } from '@/stores/theme.store'
|
||||||
|
import { themes } from '@/lib/themes'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { Users, Sun, Moon, Monitor, LogOut, User, Palette } from 'lucide-react'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authenticated')({
|
export const Route = createFileRoute('/_authenticated')({
|
||||||
beforeLoad: () => {
|
beforeLoad: () => {
|
||||||
@@ -11,15 +26,102 @@ export const Route = createFileRoute('/_authenticated')({
|
|||||||
component: AuthenticatedLayout,
|
component: AuthenticatedLayout,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function ModeIcon() {
|
||||||
|
const mode = useThemeStore((s) => s.mode)
|
||||||
|
if (mode === 'dark') return <Moon className="h-4 w-4" />
|
||||||
|
if (mode === 'light') return <Sun className="h-4 w-4" />
|
||||||
|
return <Monitor className="h-4 w-4" />
|
||||||
|
}
|
||||||
|
|
||||||
function AuthenticatedLayout() {
|
function AuthenticatedLayout() {
|
||||||
|
const router = useRouter()
|
||||||
|
const user = useAuthStore((s) => s.user)
|
||||||
|
const logout = useAuthStore((s) => s.logout)
|
||||||
|
const { mode, colorTheme, setMode, setColorTheme } = useThemeStore()
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
logout()
|
||||||
|
router.navigate({ to: '/login', replace: true })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<nav className="w-56 border-r border-border bg-sidebar min-h-screen p-4 space-y-2">
|
<nav className="w-56 border-r border-border bg-sidebar min-h-screen flex flex-col">
|
||||||
<h2 className="text-lg font-semibold text-sidebar-foreground mb-4">Forte</h2>
|
<div className="p-4">
|
||||||
<a href="/accounts" className="block px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent">
|
<h2 className="text-lg font-semibold text-sidebar-foreground">Forte</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 px-3 space-y-1">
|
||||||
|
<Link
|
||||||
|
to="/accounts"
|
||||||
|
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
||||||
|
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
||||||
|
>
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
Accounts
|
Accounts
|
||||||
</a>
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border-t border-sidebar-border">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="w-full justify-start gap-2 text-sm text-sidebar-foreground">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
<span className="truncate">{user?.firstName} {user?.lastName}</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="w-48">
|
||||||
|
<DropdownMenuLabel className="font-normal">
|
||||||
|
<p className="text-sm font-medium">{user?.firstName} {user?.lastName}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{user?.email}</p>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<ModeIcon />
|
||||||
|
<span className="ml-2">Mode</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuItem onClick={() => setMode('light')}>
|
||||||
|
<Sun className="mr-2 h-4 w-4" />
|
||||||
|
Light {mode === 'light' && '•'}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setMode('dark')}>
|
||||||
|
<Moon className="mr-2 h-4 w-4" />
|
||||||
|
Dark {mode === 'dark' && '•'}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setMode('system')}>
|
||||||
|
<Monitor className="mr-2 h-4 w-4" />
|
||||||
|
System {mode === 'system' && '•'}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<Palette className="h-4 w-4" />
|
||||||
|
<span className="ml-2">Color</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
{themes.map((t) => (
|
||||||
|
<DropdownMenuItem key={t.name} onClick={() => setColorTheme(t.name)}>
|
||||||
|
<span
|
||||||
|
className="mr-2 h-4 w-4 rounded-full border inline-block"
|
||||||
|
style={{ backgroundColor: `hsl(${t.light.primary})` }}
|
||||||
|
/>
|
||||||
|
{t.label} {colorTheme === t.name && '•'}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={handleLogout}>
|
||||||
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
|
Sign out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main className="flex-1 p-6">
|
<main className="flex-1 p-6">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
import { createFileRoute, useRouter, redirect } from '@tanstack/react-router'
|
||||||
import { useAuthStore } from '@/stores/auth.store'
|
import { useAuthStore } from '@/stores/auth.store'
|
||||||
import { login } from '@/api/auth'
|
import { login } from '@/api/auth'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/login')({
|
export const Route = createFileRoute('/login')({
|
||||||
|
beforeLoad: () => {
|
||||||
|
const { token } = useAuthStore.getState()
|
||||||
|
if (token) {
|
||||||
|
throw redirect({ to: '/accounts' })
|
||||||
|
}
|
||||||
|
},
|
||||||
component: LoginPage,
|
component: LoginPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const router = useRouter()
|
||||||
const setAuth = useAuthStore((s) => s.setAuth)
|
const setAuth = useAuthStore((s) => s.setAuth)
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
@@ -27,7 +29,8 @@ function LoginPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await login(email, password)
|
const res = await login(email, password)
|
||||||
setAuth(res.token, res.user)
|
setAuth(res.token, res.user)
|
||||||
navigate({ to: '/accounts' })
|
await router.invalidate()
|
||||||
|
await router.navigate({ to: '/accounts', replace: true })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Login failed')
|
setError(err instanceof Error ? err.message : 'Login failed')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -36,44 +39,55 @@ function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
<div
|
||||||
<Card className="w-full max-w-sm">
|
className="flex min-h-screen items-center justify-center"
|
||||||
<CardHeader className="text-center">
|
style={{ background: 'linear-gradient(135deg, #0f1724 0%, #142038 100%)' }}
|
||||||
<CardTitle className="text-2xl">Forte</CardTitle>
|
>
|
||||||
<p className="text-sm text-muted-foreground">Sign in to your account</p>
|
<div
|
||||||
</CardHeader>
|
className="w-full max-w-sm rounded-xl border p-8 shadow-2xl"
|
||||||
<CardContent>
|
style={{ backgroundColor: '#131c2e', borderColor: '#1e2d45' }}
|
||||||
|
>
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>Forte</h1>
|
||||||
|
<p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Music Store Management</p>
|
||||||
|
</div>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">Email</Label>
|
<label className="text-sm font-medium" style={{ color: '#b0bec5' }}>Email</label>
|
||||||
<Input
|
<input
|
||||||
id="email"
|
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
|
className="h-9 w-full rounded-md border px-3 py-1 text-sm outline-none login-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password">Password</Label>
|
<label className="text-sm font-medium" style={{ color: '#b0bec5' }}>Password</label>
|
||||||
<Input
|
<input
|
||||||
id="password"
|
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
|
className="h-9 w-full rounded-md border px-3 py-1 text-sm outline-none login-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-sm text-destructive">{error}</p>
|
<p className="text-sm" style={{ color: '#e57373' }}>{error}</p>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" className="w-full" disabled={loading}>
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="h-9 w-full rounded-md border text-sm font-medium transition-colors disabled:opacity-50"
|
||||||
|
style={{ backgroundColor: 'transparent', color: '#d0d8e0', borderColor: '#3a4a62' }}
|
||||||
|
onMouseEnter={(e) => { (e.target as HTMLElement).style.backgroundColor = '#1e2d45' }}
|
||||||
|
onMouseLeave={(e) => { (e.target as HTMLElement).style.backgroundColor = 'transparent' }}
|
||||||
|
>
|
||||||
{loading ? 'Signing in...' : 'Sign in'}
|
{loading ? 'Signing in...' : 'Sign in'}
|
||||||
</Button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,17 +21,39 @@ function decodeJwtPayload(token: string): { id: string; companyId: string; role:
|
|||||||
return JSON.parse(atob(payload))
|
return JSON.parse(atob(payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>((set) => ({
|
function loadSession(): { token: string; user: User; companyId: string } | null {
|
||||||
token: null,
|
try {
|
||||||
user: null,
|
const raw = sessionStorage.getItem('forte-auth')
|
||||||
companyId: null,
|
if (!raw) return null
|
||||||
|
return JSON.parse(raw)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSession(token: string, user: User, companyId: string) {
|
||||||
|
sessionStorage.setItem('forte-auth', JSON.stringify({ token, user, companyId }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSession() {
|
||||||
|
sessionStorage.removeItem('forte-auth')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>((set) => {
|
||||||
|
const initial = typeof window !== 'undefined' ? loadSession() : null
|
||||||
|
return {
|
||||||
|
token: initial?.token ?? null,
|
||||||
|
user: initial?.user ?? null,
|
||||||
|
companyId: initial?.companyId ?? null,
|
||||||
|
|
||||||
setAuth: (token, user) => {
|
setAuth: (token, user) => {
|
||||||
const payload = decodeJwtPayload(token)
|
const payload = decodeJwtPayload(token)
|
||||||
|
saveSession(token, user, payload.companyId)
|
||||||
set({ token, user, companyId: payload.companyId })
|
set({ token, user, companyId: payload.companyId })
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
|
clearSession()
|
||||||
set({ token: null, user: null, companyId: null })
|
set({ token: null, user: null, companyId: null })
|
||||||
},
|
},
|
||||||
}))
|
}})
|
||||||
|
|||||||
50
packages/admin/src/stores/theme.store.ts
Normal file
50
packages/admin/src/stores/theme.store.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { applyThemeColors, getThemeByName } from '@/lib/themes'
|
||||||
|
|
||||||
|
type Mode = 'light' | 'dark' | 'system'
|
||||||
|
|
||||||
|
interface ThemeState {
|
||||||
|
mode: Mode
|
||||||
|
colorTheme: string
|
||||||
|
setMode: (mode: Mode) => void
|
||||||
|
setColorTheme: (name: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDarkMode(mode: Mode): boolean {
|
||||||
|
return mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(mode: Mode, colorTheme: string) {
|
||||||
|
const dark = isDarkMode(mode)
|
||||||
|
document.documentElement.classList.toggle('dark', dark)
|
||||||
|
const theme = getThemeByName(colorTheme)
|
||||||
|
applyThemeColors(dark ? theme.dark : theme.light)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useThemeStore = create<ThemeState>((set) => {
|
||||||
|
const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('forte-mode') as Mode : null) ?? 'system'
|
||||||
|
const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('forte-color-theme') : null) ?? 'slate'
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
apply(initialMode, initialColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: initialMode,
|
||||||
|
colorTheme: initialColor,
|
||||||
|
|
||||||
|
setMode: (mode) => {
|
||||||
|
localStorage.setItem('forte-mode', mode)
|
||||||
|
const colorTheme = useThemeStore.getState().colorTheme
|
||||||
|
apply(mode, colorTheme)
|
||||||
|
set({ mode })
|
||||||
|
},
|
||||||
|
|
||||||
|
setColorTheme: (name) => {
|
||||||
|
localStorage.setItem('forte-color-theme', name)
|
||||||
|
const mode = useThemeStore.getState().mode
|
||||||
|
apply(mode, name)
|
||||||
|
set({ colorTheme: name })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user