From d5ceb8f19f368f872e430053b0051c9472bf7b25 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 14 Oct 2026 18:50:33 +0100 Subject: [PATCH] Scaffold the client dashboard layout Add a top bar with user info linking to account settings and sign out, an icon sidebar with tooltips and placeholder pages for each section, and a Services panel listing temporary services with a detail route. Switch icons to Phosphor light, imported per icon and excluded from Vite's dependency pre-bundling. --- web/package-lock.json | 34 +- web/package.json | 2 +- web/src/api/services/services.ts | 16 + web/src/components/toast/toast-provider.tsx | 2 +- web/src/components/ui/checkbox.tsx | 4 +- web/src/components/ui/input.tsx | 5 +- web/src/routeTree.gen.ts | 330 +++++++++++++++++- web/src/routes/__root.tsx | 9 +- web/src/routes/client/account.tsx | 9 + web/src/routes/client/affiliates.tsx | 9 + web/src/routes/client/announcements.tsx | 9 + web/src/routes/client/credit.tsx | 9 + web/src/routes/client/index.tsx | 3 +- web/src/routes/client/invoices.tsx | 9 + web/src/routes/client/knowledge-base.tsx | 9 + web/src/routes/client/network-status.tsx | 9 + web/src/routes/client/payment-methods.tsx | 9 + web/src/routes/client/quotes.tsx | 9 + web/src/routes/client/route.tsx | 163 ++++++++- web/src/routes/client/security.tsx | 9 + web/src/routes/client/services/$serviceId.tsx | 17 + web/src/routes/client/services/index.tsx | 9 + web/src/routes/client/store.tsx | 9 + web/src/routes/client/tickets.tsx | 9 + web/src/routes/client/transactions.tsx | 9 + web/src/routes/index.tsx | 6 +- web/vite.config.ts | 1 + 27 files changed, 683 insertions(+), 35 deletions(-) create mode 100644 web/src/api/services/services.ts create mode 100644 web/src/routes/client/account.tsx create mode 100644 web/src/routes/client/affiliates.tsx create mode 100644 web/src/routes/client/announcements.tsx create mode 100644 web/src/routes/client/credit.tsx create mode 100644 web/src/routes/client/invoices.tsx create mode 100644 web/src/routes/client/knowledge-base.tsx create mode 100644 web/src/routes/client/network-status.tsx create mode 100644 web/src/routes/client/payment-methods.tsx create mode 100644 web/src/routes/client/quotes.tsx create mode 100644 web/src/routes/client/security.tsx create mode 100644 web/src/routes/client/services/$serviceId.tsx create mode 100644 web/src/routes/client/services/index.tsx create mode 100644 web/src/routes/client/store.tsx create mode 100644 web/src/routes/client/tickets.tsx create mode 100644 web/src/routes/client/transactions.tsx diff --git a/web/package-lock.json b/web/package-lock.json index 9d949c6..1bf0cf5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,10 +9,10 @@ "version": "0.0.0", "dependencies": { "@fontsource-variable/inter": "^5.3.0", + "@phosphor-icons/react": "^2.1.10", "@tanstack/react-query": "^5.104.0", "@tanstack/react-router": "^1.170.40", "clsx": "^2.1.1", - "lucide-react": "^1.49.0", "openapi-fetch": "^0.17.0", "react": "^19.2.8", "react-dom": "^19.2.8", @@ -588,6 +588,19 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.12", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", @@ -1680,7 +1693,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2194,7 +2207,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -3090,21 +3103,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lucide-react": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.49.0.tgz", - "integrity": "sha512-ApfaNM4/7jBrVopqLJ84Jz8TMQ1Q1JeZ1PmKh25aeS3E1FTVcMkSh1bEQB7dfzdIrwXRP8p3z4AJjTIHHb5IIA==", - "license": "ISC", - "peerDependencies": { - "@types/react": "*", - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/web/package.json b/web/package.json index 201ae0c..bbe2d36 100644 --- a/web/package.json +++ b/web/package.json @@ -13,10 +13,10 @@ }, "dependencies": { "@fontsource-variable/inter": "^5.3.0", + "@phosphor-icons/react": "^2.1.10", "@tanstack/react-query": "^5.104.0", "@tanstack/react-router": "^1.170.40", "clsx": "^2.1.1", - "lucide-react": "^1.49.0", "openapi-fetch": "^0.17.0", "react": "^19.2.8", "react-dom": "^19.2.8", diff --git a/web/src/api/services/services.ts b/web/src/api/services/services.ts new file mode 100644 index 0000000..4266bec --- /dev/null +++ b/web/src/api/services/services.ts @@ -0,0 +1,16 @@ +export type ServiceStatus = "active" | "pending" | "suspended"; + +export type Service = { + id: string; + name: string; + detail: string; + status: ServiceStatus; +}; + +export const TEMP_SERVICES: Service[] = [ + { id: "web-01", name: "web-01", detail: "VPS 2G · 2 vCPU · 2 GB", status: "active" }, + { id: "api-01", name: "api-01", detail: "VPS 4G · 2 vCPU · 4 GB", status: "active" }, + { id: "postgres", name: "postgres", detail: "VPS 8G · 4 vCPU · 8 GB", status: "active" }, + { id: "acme-hosting", name: "acme.com", detail: "Web Hosting · Starter", status: "pending" }, + { id: "staging", name: "staging", detail: "VPS 2G · 2 vCPU · 2 GB", status: "suspended" }, +]; diff --git a/web/src/components/toast/toast-provider.tsx b/web/src/components/toast/toast-provider.tsx index a39662b..f62ca94 100644 --- a/web/src/components/toast/toast-provider.tsx +++ b/web/src/components/toast/toast-provider.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo, useRef, useState, type ReactNode } from "react"; -import { X } from "lucide-react"; +import { X } from "@phosphor-icons/react/X"; import { ToastContext, type ToastInput, type ToastStatus } from "@/components/toast/use-toast"; import { cn } from "@/lib/utils"; diff --git a/web/src/components/ui/checkbox.tsx b/web/src/components/ui/checkbox.tsx index f6573e1..b86ac01 100644 --- a/web/src/components/ui/checkbox.tsx +++ b/web/src/components/ui/checkbox.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { Check } from "lucide-react"; +import { Check } from "@phosphor-icons/react/Check"; type CheckboxProps = { label: ReactNode; @@ -19,7 +19,7 @@ export function Checkbox({ label, checked, error, onCheckedChange }: CheckboxPro onChange={(e) => onCheckedChange(e.target.checked)} className="peer size-4 cursor-pointer appearance-none border border-input bg-input/30 transition-colors outline-none checked:border-primary checked:bg-primary focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-0" /> - + {label} diff --git a/web/src/components/ui/input.tsx b/web/src/components/ui/input.tsx index d760d85..6628580 100644 --- a/web/src/components/ui/input.tsx +++ b/web/src/components/ui/input.tsx @@ -1,5 +1,6 @@ import { useId, useState, type InputHTMLAttributes } from "react"; -import { Eye, EyeOff } from "lucide-react"; +import { Eye } from "@phosphor-icons/react/Eye"; +import { EyeSlash } from "@phosphor-icons/react/EyeSlash"; import { cn } from "@/lib/utils"; @@ -35,7 +36,7 @@ export function Input({ error, className, id: idProp, type, ...props }: InputPro onClick={() => setRevealed((prev) => !prev)} className="absolute top-1/2 right-1 flex size-8 -translate-y-1/2 items-center justify-center text-muted-foreground hover:text-foreground" > - {revealed ? : } + {revealed ? : } )} diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 23f6be5..aa9f64e 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -16,6 +16,21 @@ import { Route as authLogoutRouteImport } from './routes/(auth)/logout' import { Route as GuestLoginRouteImport } from './routes/_guest/login' import { Route as GuestRegisterRouteImport } from './routes/_guest/register' import { Route as ClientIndexRouteImport } from './routes/client/index' +import { Route as ClientAccountRouteImport } from './routes/client/account' +import { Route as ClientAffiliatesRouteImport } from './routes/client/affiliates' +import { Route as ClientAnnouncementsRouteImport } from './routes/client/announcements' +import { Route as ClientCreditRouteImport } from './routes/client/credit' +import { Route as ClientInvoicesRouteImport } from './routes/client/invoices' +import { Route as ClientKnowledgeBaseRouteImport } from './routes/client/knowledge-base' +import { Route as ClientNetworkStatusRouteImport } from './routes/client/network-status' +import { Route as ClientPaymentMethodsRouteImport } from './routes/client/payment-methods' +import { Route as ClientQuotesRouteImport } from './routes/client/quotes' +import { Route as ClientSecurityRouteImport } from './routes/client/security' +import { Route as ClientStoreRouteImport } from './routes/client/store' +import { Route as ClientTicketsRouteImport } from './routes/client/tickets' +import { Route as ClientTransactionsRouteImport } from './routes/client/transactions' +import { Route as ClientServicesIndexRouteImport } from './routes/client/services/index' +import { Route as ClientServicesServiceIdRouteImport } from './routes/client/services/$serviceId' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -51,6 +66,81 @@ const ClientIndexRoute = ClientIndexRouteImport.update({ path: '/', getParentRoute: () => ClientRouteRoute, } as any) +const ClientAccountRoute = ClientAccountRouteImport.update({ + id: '/account', + path: '/account', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientAffiliatesRoute = ClientAffiliatesRouteImport.update({ + id: '/affiliates', + path: '/affiliates', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientAnnouncementsRoute = ClientAnnouncementsRouteImport.update({ + id: '/announcements', + path: '/announcements', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientCreditRoute = ClientCreditRouteImport.update({ + id: '/credit', + path: '/credit', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientInvoicesRoute = ClientInvoicesRouteImport.update({ + id: '/invoices', + path: '/invoices', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientKnowledgeBaseRoute = ClientKnowledgeBaseRouteImport.update({ + id: '/knowledge-base', + path: '/knowledge-base', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientNetworkStatusRoute = ClientNetworkStatusRouteImport.update({ + id: '/network-status', + path: '/network-status', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientPaymentMethodsRoute = ClientPaymentMethodsRouteImport.update({ + id: '/payment-methods', + path: '/payment-methods', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientQuotesRoute = ClientQuotesRouteImport.update({ + id: '/quotes', + path: '/quotes', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientSecurityRoute = ClientSecurityRouteImport.update({ + id: '/security', + path: '/security', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientStoreRoute = ClientStoreRouteImport.update({ + id: '/store', + path: '/store', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientTicketsRoute = ClientTicketsRouteImport.update({ + id: '/tickets', + path: '/tickets', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientTransactionsRoute = ClientTransactionsRouteImport.update({ + id: '/transactions', + path: '/transactions', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientServicesIndexRoute = ClientServicesIndexRouteImport.update({ + id: '/services/', + path: '/services/', + getParentRoute: () => ClientRouteRoute, +} as any) +const ClientServicesServiceIdRoute = ClientServicesServiceIdRouteImport.update({ + id: '/services/$serviceId', + path: '/services/$serviceId', + getParentRoute: () => ClientRouteRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -58,14 +148,44 @@ export interface FileRoutesByFullPath { '/logout': typeof authLogoutRoute '/login': typeof GuestLoginRoute '/register': typeof GuestRegisterRoute + '/client/account': typeof ClientAccountRoute + '/client/affiliates': typeof ClientAffiliatesRoute + '/client/announcements': typeof ClientAnnouncementsRoute + '/client/credit': typeof ClientCreditRoute + '/client/invoices': typeof ClientInvoicesRoute + '/client/knowledge-base': typeof ClientKnowledgeBaseRoute + '/client/network-status': typeof ClientNetworkStatusRoute + '/client/payment-methods': typeof ClientPaymentMethodsRoute + '/client/quotes': typeof ClientQuotesRoute + '/client/security': typeof ClientSecurityRoute + '/client/store': typeof ClientStoreRoute + '/client/tickets': typeof ClientTicketsRoute + '/client/transactions': typeof ClientTransactionsRoute '/client/': typeof ClientIndexRoute + '/client/services/$serviceId': typeof ClientServicesServiceIdRoute + '/client/services/': typeof ClientServicesIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/logout': typeof authLogoutRoute '/login': typeof GuestLoginRoute '/register': typeof GuestRegisterRoute + '/client/account': typeof ClientAccountRoute + '/client/affiliates': typeof ClientAffiliatesRoute + '/client/announcements': typeof ClientAnnouncementsRoute + '/client/credit': typeof ClientCreditRoute + '/client/invoices': typeof ClientInvoicesRoute + '/client/knowledge-base': typeof ClientKnowledgeBaseRoute + '/client/network-status': typeof ClientNetworkStatusRoute + '/client/payment-methods': typeof ClientPaymentMethodsRoute + '/client/quotes': typeof ClientQuotesRoute + '/client/security': typeof ClientSecurityRoute + '/client/store': typeof ClientStoreRoute + '/client/tickets': typeof ClientTicketsRoute + '/client/transactions': typeof ClientTransactionsRoute '/client': typeof ClientIndexRoute + '/client/services/$serviceId': typeof ClientServicesServiceIdRoute + '/client/services': typeof ClientServicesIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -75,13 +195,69 @@ export interface FileRoutesById { '/(auth)/logout': typeof authLogoutRoute '/_guest/login': typeof GuestLoginRoute '/_guest/register': typeof GuestRegisterRoute + '/client/account': typeof ClientAccountRoute + '/client/affiliates': typeof ClientAffiliatesRoute + '/client/announcements': typeof ClientAnnouncementsRoute + '/client/credit': typeof ClientCreditRoute + '/client/invoices': typeof ClientInvoicesRoute + '/client/knowledge-base': typeof ClientKnowledgeBaseRoute + '/client/network-status': typeof ClientNetworkStatusRoute + '/client/payment-methods': typeof ClientPaymentMethodsRoute + '/client/quotes': typeof ClientQuotesRoute + '/client/security': typeof ClientSecurityRoute + '/client/store': typeof ClientStoreRoute + '/client/tickets': typeof ClientTicketsRoute + '/client/transactions': typeof ClientTransactionsRoute '/client/': typeof ClientIndexRoute + '/client/services/$serviceId': typeof ClientServicesServiceIdRoute + '/client/services/': typeof ClientServicesIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/client' | '/logout' | '/login' | '/register' | '/client/' + fullPaths: + | '/' + | '/client' + | '/logout' + | '/login' + | '/register' + | '/client/account' + | '/client/affiliates' + | '/client/announcements' + | '/client/credit' + | '/client/invoices' + | '/client/knowledge-base' + | '/client/network-status' + | '/client/payment-methods' + | '/client/quotes' + | '/client/security' + | '/client/store' + | '/client/tickets' + | '/client/transactions' + | '/client/' + | '/client/services/$serviceId' + | '/client/services/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/logout' | '/login' | '/register' | '/client' + to: + | '/' + | '/logout' + | '/login' + | '/register' + | '/client/account' + | '/client/affiliates' + | '/client/announcements' + | '/client/credit' + | '/client/invoices' + | '/client/knowledge-base' + | '/client/network-status' + | '/client/payment-methods' + | '/client/quotes' + | '/client/security' + | '/client/store' + | '/client/tickets' + | '/client/transactions' + | '/client' + | '/client/services/$serviceId' + | '/client/services' id: | '__root__' | '/' @@ -90,7 +266,22 @@ export interface FileRouteTypes { | '/(auth)/logout' | '/_guest/login' | '/_guest/register' + | '/client/account' + | '/client/affiliates' + | '/client/announcements' + | '/client/credit' + | '/client/invoices' + | '/client/knowledge-base' + | '/client/network-status' + | '/client/payment-methods' + | '/client/quotes' + | '/client/security' + | '/client/store' + | '/client/tickets' + | '/client/transactions' | '/client/' + | '/client/services/$serviceId' + | '/client/services/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -151,15 +342,150 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ClientIndexRouteImport parentRoute: typeof ClientRouteRoute } + '/client/account': { + id: '/client/account' + path: '/account' + fullPath: '/client/account' + preLoaderRoute: typeof ClientAccountRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/affiliates': { + id: '/client/affiliates' + path: '/affiliates' + fullPath: '/client/affiliates' + preLoaderRoute: typeof ClientAffiliatesRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/announcements': { + id: '/client/announcements' + path: '/announcements' + fullPath: '/client/announcements' + preLoaderRoute: typeof ClientAnnouncementsRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/credit': { + id: '/client/credit' + path: '/credit' + fullPath: '/client/credit' + preLoaderRoute: typeof ClientCreditRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/invoices': { + id: '/client/invoices' + path: '/invoices' + fullPath: '/client/invoices' + preLoaderRoute: typeof ClientInvoicesRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/knowledge-base': { + id: '/client/knowledge-base' + path: '/knowledge-base' + fullPath: '/client/knowledge-base' + preLoaderRoute: typeof ClientKnowledgeBaseRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/network-status': { + id: '/client/network-status' + path: '/network-status' + fullPath: '/client/network-status' + preLoaderRoute: typeof ClientNetworkStatusRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/payment-methods': { + id: '/client/payment-methods' + path: '/payment-methods' + fullPath: '/client/payment-methods' + preLoaderRoute: typeof ClientPaymentMethodsRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/quotes': { + id: '/client/quotes' + path: '/quotes' + fullPath: '/client/quotes' + preLoaderRoute: typeof ClientQuotesRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/security': { + id: '/client/security' + path: '/security' + fullPath: '/client/security' + preLoaderRoute: typeof ClientSecurityRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/store': { + id: '/client/store' + path: '/store' + fullPath: '/client/store' + preLoaderRoute: typeof ClientStoreRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/tickets': { + id: '/client/tickets' + path: '/tickets' + fullPath: '/client/tickets' + preLoaderRoute: typeof ClientTicketsRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/transactions': { + id: '/client/transactions' + path: '/transactions' + fullPath: '/client/transactions' + preLoaderRoute: typeof ClientTransactionsRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/services/': { + id: '/client/services/' + path: '/services' + fullPath: '/client/services/' + preLoaderRoute: typeof ClientServicesIndexRouteImport + parentRoute: typeof ClientRouteRoute + } + '/client/services/$serviceId': { + id: '/client/services/$serviceId' + path: '/services/$serviceId' + fullPath: '/client/services/$serviceId' + preLoaderRoute: typeof ClientServicesServiceIdRouteImport + parentRoute: typeof ClientRouteRoute + } } } interface ClientRouteRouteChildren { + ClientAccountRoute: typeof ClientAccountRoute + ClientAffiliatesRoute: typeof ClientAffiliatesRoute + ClientAnnouncementsRoute: typeof ClientAnnouncementsRoute + ClientCreditRoute: typeof ClientCreditRoute + ClientInvoicesRoute: typeof ClientInvoicesRoute + ClientKnowledgeBaseRoute: typeof ClientKnowledgeBaseRoute + ClientNetworkStatusRoute: typeof ClientNetworkStatusRoute + ClientPaymentMethodsRoute: typeof ClientPaymentMethodsRoute + ClientQuotesRoute: typeof ClientQuotesRoute + ClientSecurityRoute: typeof ClientSecurityRoute + ClientStoreRoute: typeof ClientStoreRoute + ClientTicketsRoute: typeof ClientTicketsRoute + ClientTransactionsRoute: typeof ClientTransactionsRoute ClientIndexRoute: typeof ClientIndexRoute + ClientServicesServiceIdRoute: typeof ClientServicesServiceIdRoute + ClientServicesIndexRoute: typeof ClientServicesIndexRoute } const ClientRouteRouteChildren: ClientRouteRouteChildren = { + ClientAccountRoute: ClientAccountRoute, + ClientAffiliatesRoute: ClientAffiliatesRoute, + ClientAnnouncementsRoute: ClientAnnouncementsRoute, + ClientCreditRoute: ClientCreditRoute, + ClientInvoicesRoute: ClientInvoicesRoute, + ClientKnowledgeBaseRoute: ClientKnowledgeBaseRoute, + ClientNetworkStatusRoute: ClientNetworkStatusRoute, + ClientPaymentMethodsRoute: ClientPaymentMethodsRoute, + ClientQuotesRoute: ClientQuotesRoute, + ClientSecurityRoute: ClientSecurityRoute, + ClientStoreRoute: ClientStoreRoute, + ClientTicketsRoute: ClientTicketsRoute, + ClientTransactionsRoute: ClientTransactionsRoute, ClientIndexRoute: ClientIndexRoute, + ClientServicesServiceIdRoute: ClientServicesServiceIdRoute, + ClientServicesIndexRoute: ClientServicesIndexRoute, } const ClientRouteRouteWithChildren = ClientRouteRoute._addFileChildren( diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx index 017517e..1afa6f6 100644 --- a/web/src/routes/__root.tsx +++ b/web/src/routes/__root.tsx @@ -1,4 +1,5 @@ import type { QueryClient } from "@tanstack/react-query"; +import { IconContext } from "@phosphor-icons/react/dist/lib/context"; import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; import { ToastProvider } from "@/components/toast/toast-provider"; @@ -10,8 +11,10 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()( function RootLayout() { return ( - - - + + + + + ); } diff --git a/web/src/routes/client/account.tsx b/web/src/routes/client/account.tsx new file mode 100644 index 0000000..1a05863 --- /dev/null +++ b/web/src/routes/client/account.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/account')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Account

+} diff --git a/web/src/routes/client/affiliates.tsx b/web/src/routes/client/affiliates.tsx new file mode 100644 index 0000000..352fc29 --- /dev/null +++ b/web/src/routes/client/affiliates.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/affiliates')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Affiliates

+} diff --git a/web/src/routes/client/announcements.tsx b/web/src/routes/client/announcements.tsx new file mode 100644 index 0000000..993f292 --- /dev/null +++ b/web/src/routes/client/announcements.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/announcements')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Announcements

+} diff --git a/web/src/routes/client/credit.tsx b/web/src/routes/client/credit.tsx new file mode 100644 index 0000000..5c61b36 --- /dev/null +++ b/web/src/routes/client/credit.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/credit')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Add funds

+} diff --git a/web/src/routes/client/index.tsx b/web/src/routes/client/index.tsx index 282a78e..28698db 100644 --- a/web/src/routes/client/index.tsx +++ b/web/src/routes/client/index.tsx @@ -5,6 +5,5 @@ export const Route = createFileRoute('/client/')({ }) function RouteComponent() { - const { user } = Route.useRouteContext() - return
{user.email}
+ return

Overview

} diff --git a/web/src/routes/client/invoices.tsx b/web/src/routes/client/invoices.tsx new file mode 100644 index 0000000..0a67a34 --- /dev/null +++ b/web/src/routes/client/invoices.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/invoices')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Invoices

+} diff --git a/web/src/routes/client/knowledge-base.tsx b/web/src/routes/client/knowledge-base.tsx new file mode 100644 index 0000000..11cbbf9 --- /dev/null +++ b/web/src/routes/client/knowledge-base.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/knowledge-base')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Knowledge base

+} diff --git a/web/src/routes/client/network-status.tsx b/web/src/routes/client/network-status.tsx new file mode 100644 index 0000000..cc6cbc6 --- /dev/null +++ b/web/src/routes/client/network-status.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/network-status')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Network status

+} diff --git a/web/src/routes/client/payment-methods.tsx b/web/src/routes/client/payment-methods.tsx new file mode 100644 index 0000000..43b2b83 --- /dev/null +++ b/web/src/routes/client/payment-methods.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/payment-methods')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Payment methods

+} diff --git a/web/src/routes/client/quotes.tsx b/web/src/routes/client/quotes.tsx new file mode 100644 index 0000000..365b886 --- /dev/null +++ b/web/src/routes/client/quotes.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/quotes')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Quotes

+} diff --git a/web/src/routes/client/route.tsx b/web/src/routes/client/route.tsx index 993e110..1a12db3 100644 --- a/web/src/routes/client/route.tsx +++ b/web/src/routes/client/route.tsx @@ -1,6 +1,25 @@ -import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; +import { Link, Outlet, createFileRoute, redirect, useRouterState } from "@tanstack/react-router"; +import { ArrowsLeftRight } from "@phosphor-icons/react/ArrowsLeftRight"; +import { BookOpen } from "@phosphor-icons/react/BookOpen"; +import { CreditCard } from "@phosphor-icons/react/CreditCard"; +import { FileText } from "@phosphor-icons/react/FileText"; +import { Handshake } from "@phosphor-icons/react/Handshake"; +import { HardDrives } from "@phosphor-icons/react/HardDrives"; +import { Lifebuoy } from "@phosphor-icons/react/Lifebuoy"; +import { Megaphone } from "@phosphor-icons/react/Megaphone"; +import { Plus } from "@phosphor-icons/react/Plus"; +import { Pulse } from "@phosphor-icons/react/Pulse"; +import { Receipt } from "@phosphor-icons/react/Receipt"; +import { ShieldCheck } from "@phosphor-icons/react/ShieldCheck"; +import { ShoppingCart } from "@phosphor-icons/react/ShoppingCart"; +import { SignOut } from "@phosphor-icons/react/SignOut"; +import { SquaresFour } from "@phosphor-icons/react/SquaresFour"; +import { UserCircle } from "@phosphor-icons/react/UserCircle"; +import { Wallet } from "@phosphor-icons/react/Wallet"; import { meQuery } from "@/api/auth/me"; +import { TEMP_SERVICES, type ServiceStatus } from "@/api/services/services"; +import { cn } from "@/lib/utils"; export const Route = createFileRoute("/client")({ beforeLoad: async ({ context, location }) => { @@ -10,5 +29,145 @@ export const Route = createFileRoute("/client")({ } return { user }; }, - component: () => , + component: ClientLayout, }); + +const NAV_GROUPS = [ + [ + { label: "Overview", to: "/client", icon: SquaresFour }, + { label: "Services", to: "/client/services", icon: HardDrives }, + { label: "Invoices", to: "/client/invoices", icon: Receipt }, + { label: "Store", to: "/client/store", icon: ShoppingCart }, + { label: "Support tickets", to: "/client/tickets", icon: Lifebuoy }, + ], + [ + { label: "Quotes", to: "/client/quotes", icon: FileText }, + { label: "Transactions", to: "/client/transactions", icon: ArrowsLeftRight }, + { label: "Add funds", to: "/client/credit", icon: Wallet }, + { label: "Payment methods", to: "/client/payment-methods", icon: CreditCard }, + ], + [ + { label: "Announcements", to: "/client/announcements", icon: Megaphone }, + { label: "Knowledge base", to: "/client/knowledge-base", icon: BookOpen }, + { label: "Network status", to: "/client/network-status", icon: Pulse }, + { label: "Affiliates", to: "/client/affiliates", icon: Handshake }, + ], + [ + { label: "Security", to: "/client/security", icon: ShieldCheck }, + { label: "Account", to: "/client/account", icon: UserCircle }, + ], +] as const; + +const STATUS_CLASS: Record = { + active: "bg-success", + pending: "bg-muted-foreground", + suspended: "bg-destructive", +}; + +function ClientLayout() { + const { user } = Route.useRouteContext(); + const name = [user.given_name, user.family_name].filter(Boolean).join(" "); + const initials = (user.given_name.charAt(0) + user.family_name.charAt(0)).toUpperCase(); + const pathname = useRouterState({ select: (state) => state.location.pathname }); + const inServices = pathname === "/client/services" || pathname.startsWith("/client/services/"); + + return ( +
+
+ + Naplata + + +
+ +
+

{name}

+

{user.email}

+
+ + {initials} + + + + + +
+
+ +
+ + + {inServices && ( + + )} + +
+ +
+
+
+ ); +} diff --git a/web/src/routes/client/security.tsx b/web/src/routes/client/security.tsx new file mode 100644 index 0000000..e15f7ac --- /dev/null +++ b/web/src/routes/client/security.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/security')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Security

+} diff --git a/web/src/routes/client/services/$serviceId.tsx b/web/src/routes/client/services/$serviceId.tsx new file mode 100644 index 0000000..3763a68 --- /dev/null +++ b/web/src/routes/client/services/$serviceId.tsx @@ -0,0 +1,17 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' + +import { TEMP_SERVICES } from '@/api/services/services' + +export const Route = createFileRoute('/client/services/$serviceId')({ + loader: ({ params }) => { + const service = TEMP_SERVICES.find((s) => s.id === params.serviceId) + if (!service) throw notFound() + return service + }, + component: RouteComponent, +}) + +function RouteComponent() { + const service = Route.useLoaderData() + return

{service.name}

+} diff --git a/web/src/routes/client/services/index.tsx b/web/src/routes/client/services/index.tsx new file mode 100644 index 0000000..f14355d --- /dev/null +++ b/web/src/routes/client/services/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/services/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Services

+} diff --git a/web/src/routes/client/store.tsx b/web/src/routes/client/store.tsx new file mode 100644 index 0000000..478bb76 --- /dev/null +++ b/web/src/routes/client/store.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/store')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Store

+} diff --git a/web/src/routes/client/tickets.tsx b/web/src/routes/client/tickets.tsx new file mode 100644 index 0000000..d7c1581 --- /dev/null +++ b/web/src/routes/client/tickets.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/tickets')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Support tickets

+} diff --git a/web/src/routes/client/transactions.tsx b/web/src/routes/client/transactions.tsx new file mode 100644 index 0000000..05cc955 --- /dev/null +++ b/web/src/routes/client/transactions.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/client/transactions')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Transactions

+} diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index 8e7ce6b..a0b2016 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -1,7 +1,9 @@ import { useQuery } from "@tanstack/react-query"; import { Link, createFileRoute } from "@tanstack/react-router"; import { useState } from "react"; -import { ArrowRight, Menu, X } from "lucide-react"; +import { ArrowRight } from "@phosphor-icons/react/ArrowRight"; +import { List } from "@phosphor-icons/react/List"; +import { X } from "@phosphor-icons/react/X"; import { meQuery } from "@/api/auth/me"; @@ -58,7 +60,7 @@ function Index() { onClick={() => setMenuOpen((prev) => !prev)} className="-mr-2 flex size-10 items-center justify-center text-muted-foreground transition-colors hover:text-foreground sm:hidden" > - {menuOpen ? : } + {menuOpen ? : } diff --git a/web/vite.config.ts b/web/vite.config.ts index faff190..b16d1c9 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -8,4 +8,5 @@ export default defineConfig({ plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), tailwindcss()], resolve: { alias: { "@": path.resolve(import.meta.dirname, "src") } }, server: { proxy: { "/api": "http://localhost:8080" } }, + optimizeDeps: { exclude: ["@phosphor-icons/react"] }, });