chore(monitor/frontend): demolish template shell + landing for clean dashboard canvas
Cleared: - core/app/shell.tsx + shell.module.scss (sidebar+header layout) - core/app/toast.module.scss (replaced by inline ethos toaster styling) - core/lib/shell.ui.store.ts (sidebar collapsed state, no longer needed) - pages/landing/ (template marketing copy) - pages/dashboard/ template placeholder (replaced with minimal scaffold) - components/index.tsx (empty stub) Kept: - All auth infrastructure (apiClient, auth.store, ProtectedRoute) - Auth pages (login/register/settings/admin) — they keep the template's visual style for now; not on the operator's hot path - All my Phase 5 work (ethos tokens, snapshot/ws/stores) Routes now: / -> dashboard scaffold (no Shell), /login + /register standalone, /settings + /admin auth-gated standalone (no Shell wrapper).
This commit is contained in:
parent
7a97aac091
commit
50b0e96735
|
|
@ -1,7 +1,5 @@
|
|||
// ===========================
|
||||
// ©AngelaMos | 2025
|
||||
// ©AngelaMos | 2026
|
||||
// App.tsx
|
||||
// ===========================
|
||||
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
|
|
@ -10,26 +8,26 @@ import { Toaster } from 'sonner'
|
|||
|
||||
import { queryClient } from '@/core/api'
|
||||
import { router } from '@/core/app/routers'
|
||||
import '@/core/app/toast.module.scss'
|
||||
|
||||
export default function App(): React.ReactElement {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="app">
|
||||
<RouterProvider router={router} />
|
||||
<Toaster
|
||||
position="top-right"
|
||||
duration={2000}
|
||||
theme="dark"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: 'hsl(0, 0%, 12.2%)',
|
||||
border: '1px solid hsl(0, 0%, 18%)',
|
||||
color: 'hsl(0, 0%, 98%)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster
|
||||
position="top-right"
|
||||
duration={2500}
|
||||
theme="dark"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: 'var(--bg-panel)',
|
||||
border: '1px solid var(--fg-4)',
|
||||
color: 'var(--fg-1)',
|
||||
borderRadius: 0,
|
||||
fontFamily: 'var(--font-sans)',
|
||||
fontSize: 'var(--type-body)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
* ©AngelaMos | 2025
|
||||
* index.tsx
|
||||
*/
|
||||
|
|
@ -1,18 +1,15 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2025
|
||||
// ©AngelaMos | 2026
|
||||
// routers.tsx
|
||||
// ===================
|
||||
|
||||
import { createBrowserRouter, type RouteObject } from 'react-router-dom'
|
||||
import { UserRole } from '@/api/types'
|
||||
import { ROUTES } from '@/config'
|
||||
import { ProtectedRoute } from './protected-route'
|
||||
import { Shell } from './shell'
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
{
|
||||
path: ROUTES.HOME,
|
||||
lazy: () => import('@/pages/landing'),
|
||||
lazy: () => import('@/pages/dashboard'),
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGIN,
|
||||
|
|
@ -26,17 +23,8 @@ const routes: RouteObject[] = [
|
|||
element: <ProtectedRoute />,
|
||||
children: [
|
||||
{
|
||||
element: <Shell />,
|
||||
children: [
|
||||
{
|
||||
path: ROUTES.DASHBOARD,
|
||||
lazy: () => import('@/pages/dashboard'),
|
||||
},
|
||||
{
|
||||
path: ROUTES.SETTINGS,
|
||||
lazy: () => import('@/pages/settings'),
|
||||
},
|
||||
],
|
||||
path: ROUTES.SETTINGS,
|
||||
lazy: () => import('@/pages/settings'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -44,23 +32,14 @@ const routes: RouteObject[] = [
|
|||
element: <ProtectedRoute allowedRoles={[UserRole.ADMIN]} />,
|
||||
children: [
|
||||
{
|
||||
element: <Shell />,
|
||||
children: [
|
||||
{
|
||||
path: ROUTES.ADMIN.USERS,
|
||||
lazy: () => import('@/pages/admin'),
|
||||
},
|
||||
],
|
||||
path: ROUTES.ADMIN.USERS,
|
||||
lazy: () => import('@/pages/admin'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ROUTES.UNAUTHORIZED,
|
||||
lazy: () => import('@/pages/landing'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
lazy: () => import('@/pages/landing'),
|
||||
lazy: () => import('@/pages/dashboard'),
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,331 +0,0 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2025
|
||||
// shell.module.scss
|
||||
// ===================
|
||||
|
||||
@use '@/styles' as *;
|
||||
|
||||
$sidebar-width: 240px;
|
||||
$sidebar-collapsed-width: 64px;
|
||||
$header-height: 56px;
|
||||
|
||||
.shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: $sidebar-width;
|
||||
background: $bg-surface-100;
|
||||
border-right: 1px solid $border-default;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: $z-fixed;
|
||||
@include transition-fast;
|
||||
|
||||
&.collapsed {
|
||||
width: $sidebar-collapsed-width;
|
||||
}
|
||||
|
||||
@include breakpoint-down('sm') {
|
||||
transform: translateX(-100%);
|
||||
|
||||
&.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
width: $sidebar-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarHeader {
|
||||
height: $header-height;
|
||||
padding: 0 $space-3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid $border-default;
|
||||
|
||||
.sidebar.collapsed & {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-default;
|
||||
@include transition-fast;
|
||||
|
||||
.sidebar.collapsed & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
flex: 1;
|
||||
padding: $space-3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-1;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-3;
|
||||
padding: $space-2 $space-3;
|
||||
border-radius: $radius-md;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-light;
|
||||
@include transition-fast;
|
||||
|
||||
@include hover {
|
||||
background: $bg-surface-200;
|
||||
color: $text-default;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $bg-selection;
|
||||
color: $text-default;
|
||||
}
|
||||
|
||||
.sidebar.collapsed & {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.navIcon {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navLabel {
|
||||
@include transition-fast;
|
||||
|
||||
.sidebar.collapsed & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.adminItem {
|
||||
margin-top: auto;
|
||||
border-top: 1px solid $border-default;
|
||||
padding-top: $space-3;
|
||||
}
|
||||
|
||||
.collapseBtn {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: $radius-md;
|
||||
color: $text-light;
|
||||
@include flex-center;
|
||||
@include transition-fast;
|
||||
|
||||
svg {
|
||||
width: 23.5px;
|
||||
height: 23.5px;
|
||||
}
|
||||
|
||||
@include hover {
|
||||
background: $bg-surface-200;
|
||||
color: $text-default;
|
||||
}
|
||||
|
||||
@include breakpoint-down('sm') {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarFooter {
|
||||
padding: $space-3;
|
||||
border-top: 1px solid $border-default;
|
||||
}
|
||||
|
||||
.logoutBtn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-3;
|
||||
padding: $space-3;
|
||||
border-radius: $radius-md;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-default;
|
||||
@include transition-fast;
|
||||
|
||||
@include hover {
|
||||
background: $bg-surface-200;
|
||||
}
|
||||
|
||||
.sidebar.collapsed & {
|
||||
justify-content: center;
|
||||
|
||||
.logoutText {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logoutIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logoutText {
|
||||
font-weight: $font-weight-medium;
|
||||
@include transition-fast;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(0, 0, 0, 50%);
|
||||
z-index: calc($z-fixed - 1);
|
||||
display: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
@include breakpoint-down('sm') {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: $sidebar-width;
|
||||
min-width: 0;
|
||||
@include transition-fast;
|
||||
|
||||
&.collapsed {
|
||||
margin-left: $sidebar-collapsed-width;
|
||||
}
|
||||
|
||||
@include breakpoint-down('sm') {
|
||||
margin-left: 0;
|
||||
|
||||
&.collapsed {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: $header-height;
|
||||
background: $bg-surface-100;
|
||||
border-bottom: 1px solid $border-default;
|
||||
z-index: $z-sticky;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 $space-4;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-3;
|
||||
}
|
||||
|
||||
.menuBtn {
|
||||
display: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: $radius-md;
|
||||
color: $text-light;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include transition-fast;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
@include hover {
|
||||
background: $bg-surface-200;
|
||||
color: $text-default;
|
||||
}
|
||||
|
||||
@media (width <= 479px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.pageTitle {
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $text-default;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-3;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: $radius-full;
|
||||
background: $bg-surface-300;
|
||||
color: $text-light;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
@include flex-center;
|
||||
cursor: pointer;
|
||||
@include transition-fast;
|
||||
|
||||
@include hover {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading {
|
||||
@include flex-center;
|
||||
height: 100%;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.error {
|
||||
@include flex-column-center;
|
||||
height: 100%;
|
||||
gap: $space-4;
|
||||
padding: $space-6;
|
||||
color: $error-default;
|
||||
|
||||
h2 {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: $font-mono;
|
||||
font-size: $font-size-sm;
|
||||
padding: $space-4;
|
||||
background: $bg-surface-200;
|
||||
border-radius: $radius-lg;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
/**
|
||||
* ©AngelaMos | 2025
|
||||
* shell.tsx
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { GiCardAceClubs, GiCardJoker, GiExitDoor } from 'react-icons/gi'
|
||||
import { LuChevronLeft, LuChevronRight, LuMenu, LuShield } from 'react-icons/lu'
|
||||
import { Link, NavLink, Outlet, useLocation } from 'react-router-dom'
|
||||
import { useLogout } from '@/api/hooks'
|
||||
import { ROUTES } from '@/config'
|
||||
import { useIsAdmin, useUIStore, useUser } from '@/core/lib'
|
||||
import styles from './shell.module.scss'
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ path: ROUTES.DASHBOARD, label: 'Dashboard', icon: GiCardJoker },
|
||||
{ path: ROUTES.SETTINGS, label: 'Settings', icon: GiCardAceClubs },
|
||||
]
|
||||
|
||||
const ADMIN_NAV_ITEM = {
|
||||
path: ROUTES.ADMIN.USERS,
|
||||
label: 'Admin',
|
||||
icon: LuShield,
|
||||
}
|
||||
|
||||
function ShellErrorFallback({ error }: { error: Error }): React.ReactElement {
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<h2>Something went wrong</h2>
|
||||
<pre>{error.message}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ShellLoading(): React.ReactElement {
|
||||
return <div className={styles.loading}>Loading...</div>
|
||||
}
|
||||
|
||||
function getPageTitle(pathname: string, isAdmin: boolean): string {
|
||||
if (isAdmin && pathname === ADMIN_NAV_ITEM.path) {
|
||||
return ADMIN_NAV_ITEM.label
|
||||
}
|
||||
const item = NAV_ITEMS.find((i) => i.path === pathname)
|
||||
return item?.label ?? 'Dashboard'
|
||||
}
|
||||
|
||||
export function Shell(): React.ReactElement {
|
||||
const location = useLocation()
|
||||
const { sidebarOpen, sidebarCollapsed, toggleSidebar, toggleSidebarCollapsed } =
|
||||
useUIStore()
|
||||
const { mutate: logout } = useLogout()
|
||||
const isAdmin = useIsAdmin()
|
||||
const user = useUser()
|
||||
|
||||
const pageTitle = getPageTitle(location.pathname, isAdmin)
|
||||
const avatarLetter =
|
||||
user?.full_name?.[0]?.toUpperCase() ?? user?.email?.[0]?.toUpperCase() ?? 'U'
|
||||
|
||||
return (
|
||||
<div className={styles.shell}>
|
||||
<aside
|
||||
className={`${styles.sidebar} ${sidebarOpen ? styles.open : ''} ${sidebarCollapsed ? styles.collapsed : ''}`}
|
||||
>
|
||||
<div className={styles.sidebarHeader}>
|
||||
<span className={styles.logo}>NavBar Template</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.collapseBtn}
|
||||
onClick={toggleSidebarCollapsed}
|
||||
aria-label={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{sidebarCollapsed ? <LuChevronRight /> : <LuChevronLeft />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className={styles.nav}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`${styles.navItem} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
onClick={() => sidebarOpen && toggleSidebar()}
|
||||
>
|
||||
<item.icon className={styles.navIcon} />
|
||||
<span className={styles.navLabel}>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
{isAdmin && (
|
||||
<NavLink
|
||||
to={ADMIN_NAV_ITEM.path}
|
||||
className={({ isActive }) =>
|
||||
`${styles.navItem} ${styles.adminItem} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
onClick={() => sidebarOpen && toggleSidebar()}
|
||||
>
|
||||
<ADMIN_NAV_ITEM.icon className={styles.navIcon} />
|
||||
<span className={styles.navLabel}>{ADMIN_NAV_ITEM.label}</span>
|
||||
</NavLink>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className={styles.sidebarFooter}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.logoutBtn}
|
||||
onClick={() => logout()}
|
||||
>
|
||||
<GiExitDoor className={styles.logoutIcon} />
|
||||
<span className={styles.logoutText}>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{sidebarOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.overlay}
|
||||
onClick={toggleSidebar}
|
||||
onKeyDown={(e) => e.key === 'Escape' && toggleSidebar()}
|
||||
aria-label="Close sidebar"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`${styles.main} ${sidebarCollapsed ? styles.collapsed : ''}`}
|
||||
>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuBtn}
|
||||
onClick={toggleSidebar}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<LuMenu />
|
||||
</button>
|
||||
<h1 className={styles.pageTitle}>{pageTitle}</h1>
|
||||
</div>
|
||||
|
||||
<div className={styles.headerRight}>
|
||||
<Link to={ROUTES.SETTINGS} className={styles.avatar}>
|
||||
{avatarLetter}
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className={styles.content}>
|
||||
<ErrorBoundary FallbackComponent={ShellErrorFallback}>
|
||||
<Suspense fallback={<ShellLoading />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2025
|
||||
// toast.module.scss
|
||||
// ===================
|
||||
|
||||
@use '@/styles' as *;
|
||||
|
||||
:global {
|
||||
[data-sonner-toaster] {
|
||||
--normal-bg: #{$bg-surface-100};
|
||||
--normal-border: #{$border-default};
|
||||
--normal-text: #{$text-default};
|
||||
|
||||
--success-bg: #{$bg-surface-100};
|
||||
--success-border: #{$border-default};
|
||||
--success-text: #{$text-default};
|
||||
|
||||
--error-bg: #{$bg-surface-100};
|
||||
--error-border: #{$error-default};
|
||||
--error-text: #{$text-default};
|
||||
|
||||
--warning-bg: #{$bg-surface-100};
|
||||
--warning-border: #{$border-default};
|
||||
--warning-text: #{$text-default};
|
||||
|
||||
--info-bg: #{$bg-surface-100};
|
||||
--info-border: #{$border-default};
|
||||
--info-text: #{$text-default};
|
||||
|
||||
font-family: $font-sans;
|
||||
}
|
||||
|
||||
[data-sonner-toast] {
|
||||
border-radius: $radius-md;
|
||||
padding: $space-3 $space-4;
|
||||
font-size: $font-size-sm;
|
||||
border: 1px solid $border-default;
|
||||
background: $bg-surface-100;
|
||||
color: $text-default;
|
||||
|
||||
[data-title] {
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
[data-description] {
|
||||
color: $text-light;
|
||||
font-size: $font-size-xs;
|
||||
}
|
||||
|
||||
[data-close-button] {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: $text-muted;
|
||||
@include transition-fast;
|
||||
|
||||
@include hover {
|
||||
color: $text-default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type='error'] {
|
||||
border-color: $error-default;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2025
|
||||
// ©AngelaMos | 2026
|
||||
// index.ts
|
||||
// ===================
|
||||
|
||||
export * from './auth.form.store'
|
||||
export * from './auth.store'
|
||||
export * from './shell.ui.store'
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
/**
|
||||
* ©AngelaMos | 2025
|
||||
* ui.store.ts
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { devtools, persist } from 'zustand/middleware'
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
interface UIState {
|
||||
theme: Theme
|
||||
sidebarOpen: boolean
|
||||
sidebarCollapsed: boolean
|
||||
setTheme: (theme: Theme) => void
|
||||
toggleSidebar: () => void
|
||||
setSidebarOpen: (open: boolean) => void
|
||||
toggleSidebarCollapsed: () => void
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
theme: 'dark',
|
||||
sidebarOpen: false,
|
||||
sidebarCollapsed: false,
|
||||
|
||||
setTheme: (theme) => set({ theme }, false, 'ui/setTheme'),
|
||||
|
||||
toggleSidebar: () =>
|
||||
set(
|
||||
(state) => ({ sidebarOpen: !state.sidebarOpen }),
|
||||
false,
|
||||
'ui/toggleSidebar'
|
||||
),
|
||||
|
||||
setSidebarOpen: (open) =>
|
||||
set({ sidebarOpen: open }, false, 'ui/setSidebarOpen'),
|
||||
|
||||
toggleSidebarCollapsed: () =>
|
||||
set(
|
||||
(state) => ({ sidebarCollapsed: !state.sidebarCollapsed }),
|
||||
false,
|
||||
'ui/toggleSidebarCollapsed'
|
||||
),
|
||||
}),
|
||||
{
|
||||
name: 'ui-storage',
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
sidebarCollapsed: state.sidebarCollapsed,
|
||||
}),
|
||||
}
|
||||
),
|
||||
{ name: 'UIStore' }
|
||||
)
|
||||
)
|
||||
|
||||
export const useTheme = (): Theme => useUIStore((s) => s.theme)
|
||||
export const useSidebarOpen = (): boolean => useUIStore((s) => s.sidebarOpen)
|
||||
export const useSidebarCollapsed = (): boolean =>
|
||||
useUIStore((s) => s.sidebarCollapsed)
|
||||
|
|
@ -1,152 +1,19 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2025
|
||||
// ©AngelaMos | 2026
|
||||
// dashboard.module.scss
|
||||
// ===================
|
||||
|
||||
@use '@/styles' as *;
|
||||
|
||||
.page {
|
||||
min-height: calc(100vh - 56px);
|
||||
padding: $space-6;
|
||||
background-color: $bg-default;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: $space-6;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-default;
|
||||
margin-bottom: $space-2;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-lighter;
|
||||
}
|
||||
|
||||
.userCard {
|
||||
.scaffold {
|
||||
height: 100dvh;
|
||||
width: 100%;
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-4;
|
||||
padding: $space-5;
|
||||
background: $bg-surface-100;
|
||||
border: 1px solid $border-default;
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: $space-8;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: $radius-full;
|
||||
background: $bg-surface-300;
|
||||
color: $text-default;
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-semibold;
|
||||
@include flex-center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
@include flex-column;
|
||||
gap: $space-1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.userName {
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $text-default;
|
||||
@include truncate;
|
||||
}
|
||||
|
||||
.userEmail {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-light;
|
||||
@include truncate;
|
||||
}
|
||||
|
||||
.userRole {
|
||||
display: inline-flex;
|
||||
align-self: flex-start;
|
||||
padding: $space-0-5 $space-2;
|
||||
background: $bg-surface-200;
|
||||
border-radius: $radius-full;
|
||||
font-size: $font-size-xs;
|
||||
color: $text-lighter;
|
||||
.label {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--type-label);
|
||||
letter-spacing: var(--letter-spacing-label);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: $tracking-wide;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: $space-8;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $text-light;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: $tracking-wide;
|
||||
margin-bottom: $space-4;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: $space-4;
|
||||
|
||||
@include breakpoint-up('md') {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: $space-4;
|
||||
background: $bg-surface-100;
|
||||
border: 1px solid $border-default;
|
||||
border-radius: $radius-lg;
|
||||
}
|
||||
|
||||
.hookName {
|
||||
display: inline-block;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $text-default;
|
||||
background: $bg-surface-200;
|
||||
padding: $space-1 $space-2;
|
||||
border-radius: $radius-sm;
|
||||
margin-bottom: $space-2;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-light;
|
||||
margin-bottom: $space-2;
|
||||
line-height: $line-height-relaxed;
|
||||
}
|
||||
|
||||
.file {
|
||||
font-size: $font-size-xs;
|
||||
color: $text-muted;
|
||||
font-family: $font-mono;
|
||||
}
|
||||
|
||||
.list {
|
||||
@include flex-column;
|
||||
gap: $space-2;
|
||||
padding-left: $space-5;
|
||||
list-style: disc;
|
||||
|
||||
li {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-light;
|
||||
}
|
||||
color: var(--fg-3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,88 +1,12 @@
|
|||
/**
|
||||
* ©AngelaMos | 2025
|
||||
* index.tsx
|
||||
*/
|
||||
// ©AngelaMos | 2026
|
||||
// index.tsx
|
||||
|
||||
import { useUser } from '@/core/lib'
|
||||
import styles from './dashboard.module.scss'
|
||||
|
||||
const AVAILABLE_STORES = [
|
||||
{
|
||||
name: 'useUser()',
|
||||
file: 'core/lib/auth.store.ts',
|
||||
description: 'Get current authenticated user',
|
||||
},
|
||||
{
|
||||
name: 'useIsAuthenticated()',
|
||||
file: 'core/lib/auth.store.ts',
|
||||
description: 'Check if user is logged in',
|
||||
},
|
||||
{
|
||||
name: 'useIsAdmin()',
|
||||
file: 'core/lib/auth.store.ts',
|
||||
description: 'Check if user has admin role',
|
||||
},
|
||||
]
|
||||
|
||||
const SUGGESTED_FEATURES = [
|
||||
'User stats and metrics',
|
||||
'Recent activity feed',
|
||||
'Quick actions',
|
||||
'Charts and analytics',
|
||||
'Notifications overview',
|
||||
'Task/project summary',
|
||||
]
|
||||
|
||||
export function Component(): React.ReactElement {
|
||||
const user = useUser()
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.title}>
|
||||
Welcome{user?.full_name ? `, ${user.full_name}` : ''}
|
||||
</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Template page — build your dashboard here
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.userCard}>
|
||||
<div className={styles.avatar}>
|
||||
{user?.full_name?.[0]?.toUpperCase() ??
|
||||
user?.email?.[0]?.toUpperCase() ??
|
||||
'U'}
|
||||
</div>
|
||||
<div className={styles.userInfo}>
|
||||
<span className={styles.userName}>{user?.full_name ?? 'User'}</span>
|
||||
<span className={styles.userEmail}>{user?.email}</span>
|
||||
<span className={styles.userRole}>{user?.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Available Stores</h2>
|
||||
<div className={styles.grid}>
|
||||
{AVAILABLE_STORES.map((store) => (
|
||||
<div key={store.name} className={styles.card}>
|
||||
<code className={styles.hookName}>{store.name}</code>
|
||||
<p className={styles.description}>{store.description}</p>
|
||||
<span className={styles.file}>{store.file}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Suggested Features</h2>
|
||||
<ul className={styles.list}>
|
||||
{SUGGESTED_FEATURES.map((feature) => (
|
||||
<li key={feature}>{feature}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div className={styles.scaffold}>
|
||||
<span className={styles.label}>MONITOR THE SITUATION · SCAFFOLD</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
/**
|
||||
* ©AngelaMos | 2026
|
||||
* index.tsx
|
||||
*/
|
||||
|
||||
import { FiGithub } from 'react-icons/fi'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ROUTES } from '@/config'
|
||||
import styles from './landing.module.scss'
|
||||
|
||||
export function Component(): React.ReactElement {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<header className={styles.header}>
|
||||
<h1 className={styles.title}>Full Stack Template</h1>
|
||||
<p className={styles.subtitle}>by Carter Perez</p>
|
||||
<a
|
||||
href="https://github.com/CarterPerez-dev/fullstack-template"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.github}
|
||||
aria-label="View source on GitHub"
|
||||
>
|
||||
<FiGithub />
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<div className={styles.content}>
|
||||
<p className={styles.description}>
|
||||
Boilerplate for medium-large scale applications. Built with modern
|
||||
patterns, strict typing, and security best practices.
|
||||
</p>
|
||||
|
||||
<div className={styles.sections}>
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Frontend</h2>
|
||||
<ul className={styles.features}>
|
||||
<li>React 19 + TypeScript with strict mode</li>
|
||||
<li>TanStack Query for server state caching</li>
|
||||
<li>Zustand stores with persistence</li>
|
||||
<li>Axios interceptors with auto token refresh</li>
|
||||
<li>Zod runtime validation on API responses</li>
|
||||
<li>SCSS modules with design tokens</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Backend</h2>
|
||||
<ul className={styles.features}>
|
||||
<li>DDD + DI Architecture</li>
|
||||
<li>FastAPI with async/await throughout</li>
|
||||
<li>SQLAlchemy 2.0+ async with connection pooling</li>
|
||||
<li>JWT auth with token rotation and replay detection</li>
|
||||
<li>Argon2id hashing with timing safe verification</li>
|
||||
<li>Pydantic v2 strict validation</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Infrastructure</h2>
|
||||
<ul className={styles.features}>
|
||||
<li>Docker Compose with dev/prod configs</li>
|
||||
<li>Nginx reverse proxy with rate limiting</li>
|
||||
<li>PostgreSQL 18 + Redis 7</li>
|
||||
<li>Health checks and graceful shutdown</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>DevOps</h2>
|
||||
<ul className={styles.features}>
|
||||
<li>GitHub Actions CI (Ruff, Pylint, Mypy, Biome)</li>
|
||||
<li>Strict type checking on both ends</li>
|
||||
<li>Alembic async migrations</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Link to={ROUTES.REGISTER} className={styles.button}>
|
||||
Try Auth Flow
|
||||
</Link>
|
||||
<a
|
||||
href="/api/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.buttonOutline}
|
||||
>
|
||||
API Docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Component.displayName = 'Landing'
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2025
|
||||
// landing.module.scss
|
||||
// ===================
|
||||
|
||||
@use '@/styles' as *;
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
@include flex-column-center;
|
||||
background-color: $bg-default;
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
$bg-landing 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 20px 20px;
|
||||
padding: $space-8;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: $space-5;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: $font-size-4xl;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-default;
|
||||
letter-spacing: $tracking-tight;
|
||||
margin-bottom: $space-2;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: $font-size-lg;
|
||||
color: $text-light;
|
||||
margin-bottom: $space-3;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: inline-flex;
|
||||
color: $text-muted;
|
||||
font-size: $font-size-2xl;
|
||||
@include transition-fast;
|
||||
|
||||
@include hover {
|
||||
&:hover {
|
||||
color: $text-default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 800px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: $font-size-base;
|
||||
color: $text-light;
|
||||
line-height: $line-height-relaxed;
|
||||
margin-bottom: $space-8;
|
||||
}
|
||||
|
||||
.sections {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $space-6;
|
||||
margin-bottom: $space-10;
|
||||
text-align: left;
|
||||
|
||||
@include breakpoint-down(md) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: $space-5;
|
||||
background-color: $bg-surface-75;
|
||||
border: 1px solid $border-muted;
|
||||
border-radius: $radius-lg;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-default;
|
||||
margin-bottom: $space-3;
|
||||
letter-spacing: $tracking-wide;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-2;
|
||||
|
||||
li {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-muted;
|
||||
|
||||
&::before {
|
||||
content: '→';
|
||||
margin-right: $space-2;
|
||||
color: $text-lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: $space-3;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $space-3 $space-5;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $bg-default;
|
||||
background-color: $text-default;
|
||||
border-radius: $radius-md;
|
||||
text-decoration: none;
|
||||
@include transition-fast;
|
||||
|
||||
@include hover {
|
||||
&:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonOutline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $space-3 $space-5;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $text-default;
|
||||
background-color: transparent;
|
||||
border: 1px solid $border-default;
|
||||
border-radius: $radius-md;
|
||||
text-decoration: none;
|
||||
@include transition-fast;
|
||||
|
||||
@include hover {
|
||||
&:hover {
|
||||
border-color: $border-strong;
|
||||
background-color: $bg-surface-75;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue