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"] },
});