diff --git a/.bruno/Payment/account/create.bru b/.bruno/Payment/account/create.bru
new file mode 100644
index 0000000..4dd2ca3
--- /dev/null
+++ b/.bruno/Payment/account/create.bru
@@ -0,0 +1,24 @@
+meta {
+ name: Create
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{url}}/payment/wallets
+ body: json
+ auth: inherit
+}
+
+body:json {
+ {
+ "ledgerId": "3c71d240-a375-42e1-9a78-0575bf33fabb",
+ "entityId": "some-external-entity",
+ "label": "Securities"
+ }
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/.bruno/Payment/account/folder.bru b/.bruno/Payment/account/folder.bru
new file mode 100644
index 0000000..109fad9
--- /dev/null
+++ b/.bruno/Payment/account/folder.bru
@@ -0,0 +1,4 @@
+meta {
+ name: Wallet
+ seq: 3
+}
diff --git a/.bruno/Payment/beneficiary/Ledgers.bru b/.bruno/Payment/beneficiary/Ledgers.bru
new file mode 100644
index 0000000..8eaacb6
--- /dev/null
+++ b/.bruno/Payment/beneficiary/Ledgers.bru
@@ -0,0 +1,20 @@
+meta {
+ name: Ledgers
+ type: http
+ seq: 5
+}
+
+get {
+ url: {{url}}/payment/beneficiaries/:id/ledgers
+ body: json
+ auth: inherit
+}
+
+params:path {
+ id: a0a6aa39-5d13-4717-9554-a878d7f30ea7
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/.bruno/workspace/create.bru b/.bruno/Payment/beneficiary/create.bru
similarity index 56%
rename from .bruno/workspace/create.bru
rename to .bruno/Payment/beneficiary/create.bru
index 10b7829..fe7b85d 100644
--- a/.bruno/workspace/create.bru
+++ b/.bruno/Payment/beneficiary/create.bru
@@ -5,17 +5,19 @@ meta {
}
post {
- url: {{url}}/workspace
+ url: {{url}}/payment/beneficiaries
body: json
auth: inherit
}
body:json {
{
- "name": ""
+ "tenantId": "valkyr-inc",
+ "label": "Valkyr Inc."
}
}
settings {
encodeUrl: true
+ timeout: 0
}
diff --git a/.bruno/Payment/beneficiary/dashboard.bru b/.bruno/Payment/beneficiary/dashboard.bru
new file mode 100644
index 0000000..467af1f
--- /dev/null
+++ b/.bruno/Payment/beneficiary/dashboard.bru
@@ -0,0 +1,20 @@
+meta {
+ name: Dashboard
+ type: http
+ seq: 2
+}
+
+get {
+ url: {{url}}/payment/dashboard/:id
+ body: json
+ auth: inherit
+}
+
+params:path {
+ id: 2f6dfb20-7834-484c-8472-096f72fc5f08
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/.bruno/Payment/beneficiary/folder.bru b/.bruno/Payment/beneficiary/folder.bru
new file mode 100644
index 0000000..31faaa3
--- /dev/null
+++ b/.bruno/Payment/beneficiary/folder.bru
@@ -0,0 +1,4 @@
+meta {
+ name: Beneficiary
+ seq: 1
+}
diff --git a/.bruno/Payment/beneficiary/id.bru b/.bruno/Payment/beneficiary/id.bru
new file mode 100644
index 0000000..9026f9b
--- /dev/null
+++ b/.bruno/Payment/beneficiary/id.bru
@@ -0,0 +1,20 @@
+meta {
+ name: :id
+ type: http
+ seq: 4
+}
+
+get {
+ url: {{url}}/payment/beneficiaries/:id
+ body: json
+ auth: inherit
+}
+
+params:path {
+ id: a0a6aa39-5d13-4717-9554-a878d7f30ea7
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/.bruno/Payment/beneficiary/list.bru b/.bruno/Payment/beneficiary/list.bru
new file mode 100644
index 0000000..3eac0d2
--- /dev/null
+++ b/.bruno/Payment/beneficiary/list.bru
@@ -0,0 +1,16 @@
+meta {
+ name: List
+ type: http
+ seq: 3
+}
+
+get {
+ url: {{url}}/payment/beneficiaries
+ body: json
+ auth: inherit
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/.bruno/Payment/folder.bru b/.bruno/Payment/folder.bru
new file mode 100644
index 0000000..366e4fe
--- /dev/null
+++ b/.bruno/Payment/folder.bru
@@ -0,0 +1,4 @@
+meta {
+ name: Payment
+ seq: 2
+}
diff --git a/.bruno/Payment/ledger/create.bru b/.bruno/Payment/ledger/create.bru
new file mode 100644
index 0000000..fabd92a
--- /dev/null
+++ b/.bruno/Payment/ledger/create.bru
@@ -0,0 +1,27 @@
+meta {
+ name: Create
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{url}}/payment/ledgers
+ body: json
+ auth: inherit
+}
+
+body:json {
+ {
+ "beneficiaryId": "2f6dfb20-7834-484c-8472-096f72fc5f08",
+ "label": "Sample Ledger",
+ "currencies": [
+ "NOK",
+ "SEK"
+ ]
+ }
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/.bruno/Payment/ledger/folder.bru b/.bruno/Payment/ledger/folder.bru
new file mode 100644
index 0000000..0e9edbe
--- /dev/null
+++ b/.bruno/Payment/ledger/folder.bru
@@ -0,0 +1,4 @@
+meta {
+ name: Ledger
+ seq: 2
+}
diff --git a/.bruno/Payment/ledger/wallets.bru b/.bruno/Payment/ledger/wallets.bru
new file mode 100644
index 0000000..0696533
--- /dev/null
+++ b/.bruno/Payment/ledger/wallets.bru
@@ -0,0 +1,20 @@
+meta {
+ name: Wallets
+ type: http
+ seq: 3
+}
+
+get {
+ url: {{url}}/payment/ledgers/:id/wallets
+ body: json
+ auth: inherit
+}
+
+params:path {
+ id: 3c71d240-a375-42e1-9a78-0575bf33fabb
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/.bruno/Payment/wallet/create.bru b/.bruno/Payment/wallet/create.bru
new file mode 100644
index 0000000..16af2fa
--- /dev/null
+++ b/.bruno/Payment/wallet/create.bru
@@ -0,0 +1,24 @@
+meta {
+ name: Create
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{url}}/payment/accounts
+ body: json
+ auth: inherit
+}
+
+body:json {
+ {
+ "walletId": "56f2aba8-5687-4e63-8d6a-e120b50ef891",
+ "currency": "NOK",
+ "label": "NOK Savings"
+ }
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/.bruno/Payment/wallet/folder.bru b/.bruno/Payment/wallet/folder.bru
new file mode 100644
index 0000000..7bcba0a
--- /dev/null
+++ b/.bruno/Payment/wallet/folder.bru
@@ -0,0 +1,4 @@
+meta {
+ name: Account
+ seq: 4
+}
diff --git a/.bruno/identity/Get.bru b/.bruno/identity/Get.bru
deleted file mode 100644
index 1637c3d..0000000
--- a/.bruno/identity/Get.bru
+++ /dev/null
@@ -1,19 +0,0 @@
-meta {
- name: Get
- type: http
- seq: 2
-}
-
-get {
- url: {{url}}/identity/:id
- body: none
- auth: inherit
-}
-
-params:path {
- id:
-}
-
-settings {
- encodeUrl: true
-}
diff --git a/.bruno/identity/Roles.bru b/.bruno/identity/Roles.bru
deleted file mode 100644
index d658976..0000000
--- a/.bruno/identity/Roles.bru
+++ /dev/null
@@ -1,32 +0,0 @@
-meta {
- name: Roles
- type: http
- seq: 4
-}
-
-put {
- url: {{url}}/identity/:id/roles
- body: json
- auth: inherit
-}
-
-params:path {
- id:
-}
-
-body:json {
- [
- {
- "type": "add",
- "roles": []
- },
- {
- "type": "remove",
- "roles": []
- }
- ]
-}
-
-settings {
- encodeUrl: true
-}
diff --git a/.bruno/identity/Update.bru b/.bruno/identity/Update.bru
deleted file mode 100644
index 552cde0..0000000
--- a/.bruno/identity/Update.bru
+++ /dev/null
@@ -1,43 +0,0 @@
-meta {
- name: Update
- type: http
- seq: 4
-}
-
-put {
- url: {{url}}/identity/:id
- body: json
- auth: inherit
-}
-
-params:path {
- id:
-}
-
-body:json {
- [
- {
- "type": "add",
- "key": "",
- "value": ""
- },
- {
- "type": "push",
- "key": "",
- "values": ""
- },
- {
- "type": "pop",
- "key": "",
- "values": ""
- },
- {
- "type": "remove",
- "key": ""
- }
- ]
-}
-
-settings {
- encodeUrl: true
-}
diff --git a/.bruno/identity/folder.bru b/.bruno/identity/folder.bru
deleted file mode 100644
index a89bdf5..0000000
--- a/.bruno/identity/folder.bru
+++ /dev/null
@@ -1,8 +0,0 @@
-meta {
- name: Identity
- seq: 1
-}
-
-auth {
- mode: inherit
-}
diff --git a/.bruno/identity/login/code.bru b/.bruno/identity/login/code.bru
deleted file mode 100644
index e96d53b..0000000
--- a/.bruno/identity/login/code.bru
+++ /dev/null
@@ -1,29 +0,0 @@
-meta {
- name: Code
- type: http
- seq: 3
-}
-
-post {
- url: {{url}}/identity/login/code
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "email": "john.doe@fixture.none",
- "otp": ""
- }
-}
-
-script:post-response {
- const cookies = res.getHeader('set-cookie');
- if (cookies) {
- bru.setVar("cookie", cookies.join('; '));
- }
-}
-
-settings {
- encodeUrl: true
-}
diff --git a/.bruno/identity/login/email.bru b/.bruno/identity/login/email.bru
deleted file mode 100644
index 7025c71..0000000
--- a/.bruno/identity/login/email.bru
+++ /dev/null
@@ -1,21 +0,0 @@
-meta {
- name: Email
- type: http
- seq: 2
-}
-
-post {
- url: {{url}}/identity/login/email
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "email": "john.doe@fixture.none"
- }
-}
-
-settings {
- encodeUrl: true
-}
diff --git a/.bruno/identity/login/folder.bru b/.bruno/identity/login/folder.bru
deleted file mode 100644
index 9b12963..0000000
--- a/.bruno/identity/login/folder.bru
+++ /dev/null
@@ -1,8 +0,0 @@
-meta {
- name: Login
- seq: 3
-}
-
-auth {
- mode: inherit
-}
diff --git a/.bruno/identity/login/sudo.bru b/.bruno/identity/login/sudo.bru
deleted file mode 100644
index 21f8712..0000000
--- a/.bruno/identity/login/sudo.bru
+++ /dev/null
@@ -1,21 +0,0 @@
-meta {
- name: Sudo
- type: http
- seq: 1
-}
-
-post {
- url: {{url}}/identities/login/sudo
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "email": "john.doe@fixture.none"
- }
-}
-
-settings {
- encodeUrl: true
-}
diff --git a/.bruno/identity/me.bru b/.bruno/identity/me.bru
deleted file mode 100644
index 58fdd69..0000000
--- a/.bruno/identity/me.bru
+++ /dev/null
@@ -1,15 +0,0 @@
-meta {
- name: Me
- type: http
- seq: 1
-}
-
-get {
- url: {{url}}/identity/me
- body: none
- auth: inherit
-}
-
-settings {
- encodeUrl: true
-}
diff --git a/.bruno/workspace/folder.bru b/.bruno/workspace/folder.bru
deleted file mode 100644
index 46b85fa..0000000
--- a/.bruno/workspace/folder.bru
+++ /dev/null
@@ -1,8 +0,0 @@
-meta {
- name: Workspace
- seq: 2
-}
-
-auth {
- mode: inherit
-}
diff --git a/.env b/.env
new file mode 100644
index 0000000..d7aad80
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+DB_XTDB_HOST=192.168.0.52
+DB_XTDB_PORT=6433
\ No newline at end of file
diff --git a/api/package.json b/api/package.json
index 9cd602a..177ef02 100644
--- a/api/package.json
+++ b/api/package.json
@@ -4,9 +4,8 @@
"start": "deno --allow-all --watch-hmr=routes/ server.ts"
},
"dependencies": {
- "@modules/identity": "workspace:*",
- "@module/workspace": "workspace:*",
+ "@module/payment": "workspace:*",
"@platform/config": "workspace:*",
- "zod": "4.1.12"
+ "zod": "4.1.13"
}
}
diff --git a/api/server.ts b/api/server.ts
index 060ded3..2bd5299 100644
--- a/api/server.ts
+++ b/api/server.ts
@@ -1,3 +1,4 @@
+import payment from "@module/payment/server";
import { logger } from "@platform/logger";
import { context } from "@platform/relay";
import { Api } from "@platform/server/api.ts";
@@ -19,8 +20,8 @@ const log = logger.prefix("Server");
// ### Platform
await server.bootstrap();
-await socket.bootstrap();
-await session.bootstrap();
+// await socket.bootstrap();
+// await session.bootstrap();
// ### Modules
@@ -33,6 +34,7 @@ await session.bootstrap();
*/
const api = new Api([
+ ...payment.routes,
/*...identity.routes, ...workspace.routes*/
]);
@@ -58,8 +60,8 @@ Deno.serve(
// Resolve storage context for all dependent modules.
await server.resolve(request);
- await socket.resolve();
- await session.resolve(request);
+ // await socket.resolve();
+ // await session.resolve(request);
// ### Fetch
// Execute fetch against the api instance.
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 0000000..86b2b11
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1,75 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## React Compiler
+
+The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
+
+Note: This will impact Vite dev & build performances.
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/app/components.json b/app/components.json
new file mode 100644
index 0000000..2b0833f
--- /dev/null
+++ b/app/components.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
diff --git a/app/eslint.config.js b/app/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/app/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..65e9db8
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ shadcn
+
+
+
+
+
+
diff --git a/app/package.json b/app/package.json
new file mode 100644
index 0000000..ed77ae6
--- /dev/null
+++ b/app/package.json
@@ -0,0 +1,65 @@
+{
+ "name": "shadcn",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@dnd-kit/core": "6.3.1",
+ "@dnd-kit/modifiers": "9.0.0",
+ "@dnd-kit/sortable": "10.0.0",
+ "@dnd-kit/utilities": "3.2.2",
+ "@module/account": "workspace:*",
+ "@module/payment": "workspace:*",
+ "@radix-ui/react-avatar": "1.1.11",
+ "@radix-ui/react-checkbox": "1.3.3",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-dropdown-menu": "2.1.16",
+ "@radix-ui/react-label": "2.1.8",
+ "@radix-ui/react-select": "2.2.6",
+ "@radix-ui/react-separator": "1.1.8",
+ "@radix-ui/react-slot": "1.2.4",
+ "@radix-ui/react-tabs": "1.1.13",
+ "@radix-ui/react-toggle": "1.1.10",
+ "@radix-ui/react-toggle-group": "1.1.11",
+ "@radix-ui/react-tooltip": "1.2.8",
+ "@tabler/icons-react": "3.35.0",
+ "@tailwindcss/vite": "4.1.17",
+ "@tanstack/react-router": "1.139.9",
+ "@tanstack/react-table": "8.21.3",
+ "@zitadel/react": "1.1.0",
+ "class-variance-authority": "0.7.1",
+ "clsx": "2.1.1",
+ "lucide-react": "0.555.0",
+ "next-themes": "0.4.6",
+ "react": "19.2.0",
+ "react-dom": "19.2.0",
+ "recharts": "2.15.4",
+ "sonner": "2.0.7",
+ "tailwind-merge": "3.4.0",
+ "tailwindcss": "4.1.17",
+ "vaul": "1.1.2",
+ "zod": "4.1.13"
+ },
+ "devDependencies": {
+ "@eslint/js": "9.39.1",
+ "@types/node": "24.10.1",
+ "@types/react": "19.2.7",
+ "@types/react-dom": "19.2.3",
+ "@vitejs/plugin-react": "5.1.1",
+ "babel-plugin-react-compiler": "1.0.0",
+ "eslint": "9.39.1",
+ "eslint-plugin-react-hooks": "7.0.1",
+ "eslint-plugin-react-refresh": "0.4.24",
+ "globals": "16.5.0",
+ "tw-animate-css": "1.4.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "8.46.4",
+ "vite": "7.2.4"
+ }
+}
diff --git a/app/public/vite.svg b/app/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/app/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/app/app.view.tsx b/app/src/app/app.view.tsx
new file mode 100644
index 0000000..1103c0e
--- /dev/null
+++ b/app/src/app/app.view.tsx
@@ -0,0 +1,30 @@
+import { Outlet } from "@tanstack/react-router";
+
+import { AppSidebar } from "@/components/app-sidebar";
+import { SiteHeader } from "@/components/site-header";
+import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
+
+export function AppView() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/src/app/auth/callback.view.tsx b/app/src/app/auth/callback.view.tsx
new file mode 100644
index 0000000..2f1bec9
--- /dev/null
+++ b/app/src/app/auth/callback.view.tsx
@@ -0,0 +1,26 @@
+import { useNavigate } from "@tanstack/react-router";
+import { useEffect } from "react";
+
+import { zitadel } from "../../services/zitadel.ts";
+
+export function CallbackView() {
+ const navigate = useNavigate();
+ useEffect(() => {
+ async function handleCallback() {
+ try {
+ const user = await zitadel.userManager.signinRedirectCallback();
+ if (user) {
+ navigate({ to: "/", replace: true });
+ } else {
+ navigate({ to: "/", replace: true });
+ }
+ } catch (error) {
+ console.error("Callback error", error);
+ navigate({ to: "/", replace: true });
+ }
+ }
+ handleCallback();
+ }, [navigate]);
+
+ return null;
+}
diff --git a/app/src/app/auth/components/login-form.tsx b/app/src/app/auth/components/login-form.tsx
new file mode 100644
index 0000000..200d9ce
--- /dev/null
+++ b/app/src/app/auth/components/login-form.tsx
@@ -0,0 +1,61 @@
+import { GalleryVerticalEnd } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Field, FieldDescription, FieldGroup, FieldSeparator } from "@/components/ui/field";
+import { cn } from "@/lib/utils";
+import { zitadel } from "@/services/zitadel.ts";
+
+export function LoginForm({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
diff --git a/app/src/app/auth/login.view.tsx b/app/src/app/auth/login.view.tsx
new file mode 100644
index 0000000..a9d9508
--- /dev/null
+++ b/app/src/app/auth/login.view.tsx
@@ -0,0 +1,11 @@
+import { LoginForm } from "./components/login-form.tsx";
+
+export function LoginView() {
+ return (
+
+ );
+}
diff --git a/app/src/app/dashboard/dashboard.controller.ts b/app/src/app/dashboard/dashboard.controller.ts
new file mode 100644
index 0000000..5b4de00
--- /dev/null
+++ b/app/src/app/dashboard/dashboard.controller.ts
@@ -0,0 +1,20 @@
+import { Controller } from "@/lib/controller.tsx";
+import { api } from "@/services/api.ts";
+
+export class DashboardController extends Controller<{
+ beneficiaries: any[];
+}> {
+ async onInit() {
+ return {
+ beneficiaries: await this.#getBenficiaries(),
+ };
+ }
+
+ async #getBenficiaries() {
+ const response = await api.ledger.benficiaries.list();
+ if ("error" in response) {
+ return [];
+ }
+ return response.data;
+ }
+}
diff --git a/app/src/app/dashboard/dashboard.view.tsx b/app/src/app/dashboard/dashboard.view.tsx
new file mode 100644
index 0000000..b1415c4
--- /dev/null
+++ b/app/src/app/dashboard/dashboard.view.tsx
@@ -0,0 +1,12 @@
+import { makeControllerComponent } from "@/lib/controller.tsx";
+
+import { DashboardController } from "./dashboard.controller.ts";
+
+export const DashboardView = makeControllerComponent(DashboardController, ({ beneficiaries }) => {
+ return (
+
+ Dashboard
+
{JSON.stringify(beneficiaries, null, 2)}
+
+ );
+});
diff --git a/app/src/assets/react.svg b/app/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/app/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/components/app-sidebar.tsx b/app/src/components/app-sidebar.tsx
new file mode 100644
index 0000000..3a437dd
--- /dev/null
+++ b/app/src/components/app-sidebar.tsx
@@ -0,0 +1,179 @@
+import * as React from "react"
+import {
+ IconCamera,
+ IconChartBar,
+ IconDashboard,
+ IconDatabase,
+ IconFileAi,
+ IconFileDescription,
+ IconFileWord,
+ IconFolder,
+ IconHelp,
+ IconInnerShadowTop,
+ IconListDetails,
+ IconReport,
+ IconSearch,
+ IconSettings,
+ IconUsers,
+} from "@tabler/icons-react"
+
+import { NavDocuments } from "@/components/nav-documents"
+import { NavMain } from "@/components/nav-main"
+import { NavSecondary } from "@/components/nav-secondary"
+import { NavUser } from "@/components/nav-user"
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar"
+
+const data = {
+ user: {
+ name: "shadcn",
+ email: "m@example.com",
+ avatar: "/avatars/shadcn.jpg",
+ },
+ navMain: [
+ {
+ title: "Dashboard",
+ url: "#",
+ icon: IconDashboard,
+ },
+ {
+ title: "Lifecycle",
+ url: "#",
+ icon: IconListDetails,
+ },
+ {
+ title: "Analytics",
+ url: "#",
+ icon: IconChartBar,
+ },
+ {
+ title: "Projects",
+ url: "#",
+ icon: IconFolder,
+ },
+ {
+ title: "Team",
+ url: "#",
+ icon: IconUsers,
+ },
+ ],
+ navClouds: [
+ {
+ title: "Capture",
+ icon: IconCamera,
+ isActive: true,
+ url: "#",
+ items: [
+ {
+ title: "Active Proposals",
+ url: "#",
+ },
+ {
+ title: "Archived",
+ url: "#",
+ },
+ ],
+ },
+ {
+ title: "Proposal",
+ icon: IconFileDescription,
+ url: "#",
+ items: [
+ {
+ title: "Active Proposals",
+ url: "#",
+ },
+ {
+ title: "Archived",
+ url: "#",
+ },
+ ],
+ },
+ {
+ title: "Prompts",
+ icon: IconFileAi,
+ url: "#",
+ items: [
+ {
+ title: "Active Proposals",
+ url: "#",
+ },
+ {
+ title: "Archived",
+ url: "#",
+ },
+ ],
+ },
+ ],
+ navSecondary: [
+ {
+ title: "Settings",
+ url: "#",
+ icon: IconSettings,
+ },
+ {
+ title: "Get Help",
+ url: "#",
+ icon: IconHelp,
+ },
+ {
+ title: "Search",
+ url: "#",
+ icon: IconSearch,
+ },
+ ],
+ documents: [
+ {
+ name: "Data Library",
+ url: "#",
+ icon: IconDatabase,
+ },
+ {
+ name: "Reports",
+ url: "#",
+ icon: IconReport,
+ },
+ {
+ name: "Word Assistant",
+ url: "#",
+ icon: IconFileWord,
+ },
+ ],
+}
+
+export function AppSidebar({ ...props }: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+
+ Acme Inc.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/src/components/chart-area-interactive.tsx b/app/src/components/chart-area-interactive.tsx
new file mode 100644
index 0000000..46a795e
--- /dev/null
+++ b/app/src/components/chart-area-interactive.tsx
@@ -0,0 +1,237 @@
+"use client";
+
+import * as React from "react";
+import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
+
+import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
+import { useIsMobile } from "@/hooks/use-mobile";
+
+export const description = "An interactive area chart";
+
+const chartData = [
+ { date: "2024-04-01", desktop: 222, mobile: 150 },
+ { date: "2024-04-02", desktop: 97, mobile: 180 },
+ { date: "2024-04-03", desktop: 167, mobile: 120 },
+ { date: "2024-04-04", desktop: 242, mobile: 260 },
+ { date: "2024-04-05", desktop: 373, mobile: 290 },
+ { date: "2024-04-06", desktop: 301, mobile: 340 },
+ { date: "2024-04-07", desktop: 245, mobile: 180 },
+ { date: "2024-04-08", desktop: 409, mobile: 320 },
+ { date: "2024-04-09", desktop: 59, mobile: 110 },
+ { date: "2024-04-10", desktop: 261, mobile: 190 },
+ { date: "2024-04-11", desktop: 327, mobile: 350 },
+ { date: "2024-04-12", desktop: 292, mobile: 210 },
+ { date: "2024-04-13", desktop: 342, mobile: 380 },
+ { date: "2024-04-14", desktop: 137, mobile: 220 },
+ { date: "2024-04-15", desktop: 120, mobile: 170 },
+ { date: "2024-04-16", desktop: 138, mobile: 190 },
+ { date: "2024-04-17", desktop: 446, mobile: 360 },
+ { date: "2024-04-18", desktop: 364, mobile: 410 },
+ { date: "2024-04-19", desktop: 243, mobile: 180 },
+ { date: "2024-04-20", desktop: 89, mobile: 150 },
+ { date: "2024-04-21", desktop: 137, mobile: 200 },
+ { date: "2024-04-22", desktop: 224, mobile: 170 },
+ { date: "2024-04-23", desktop: 138, mobile: 230 },
+ { date: "2024-04-24", desktop: 387, mobile: 290 },
+ { date: "2024-04-25", desktop: 215, mobile: 250 },
+ { date: "2024-04-26", desktop: 75, mobile: 130 },
+ { date: "2024-04-27", desktop: 383, mobile: 420 },
+ { date: "2024-04-28", desktop: 122, mobile: 180 },
+ { date: "2024-04-29", desktop: 315, mobile: 240 },
+ { date: "2024-04-30", desktop: 454, mobile: 380 },
+ { date: "2024-05-01", desktop: 165, mobile: 220 },
+ { date: "2024-05-02", desktop: 293, mobile: 310 },
+ { date: "2024-05-03", desktop: 247, mobile: 190 },
+ { date: "2024-05-04", desktop: 385, mobile: 420 },
+ { date: "2024-05-05", desktop: 481, mobile: 390 },
+ { date: "2024-05-06", desktop: 498, mobile: 520 },
+ { date: "2024-05-07", desktop: 388, mobile: 300 },
+ { date: "2024-05-08", desktop: 149, mobile: 210 },
+ { date: "2024-05-09", desktop: 227, mobile: 180 },
+ { date: "2024-05-10", desktop: 293, mobile: 330 },
+ { date: "2024-05-11", desktop: 335, mobile: 270 },
+ { date: "2024-05-12", desktop: 197, mobile: 240 },
+ { date: "2024-05-13", desktop: 197, mobile: 160 },
+ { date: "2024-05-14", desktop: 448, mobile: 490 },
+ { date: "2024-05-15", desktop: 473, mobile: 380 },
+ { date: "2024-05-16", desktop: 338, mobile: 400 },
+ { date: "2024-05-17", desktop: 499, mobile: 420 },
+ { date: "2024-05-18", desktop: 315, mobile: 350 },
+ { date: "2024-05-19", desktop: 235, mobile: 180 },
+ { date: "2024-05-20", desktop: 177, mobile: 230 },
+ { date: "2024-05-21", desktop: 82, mobile: 140 },
+ { date: "2024-05-22", desktop: 81, mobile: 120 },
+ { date: "2024-05-23", desktop: 252, mobile: 290 },
+ { date: "2024-05-24", desktop: 294, mobile: 220 },
+ { date: "2024-05-25", desktop: 201, mobile: 250 },
+ { date: "2024-05-26", desktop: 213, mobile: 170 },
+ { date: "2024-05-27", desktop: 420, mobile: 460 },
+ { date: "2024-05-28", desktop: 233, mobile: 190 },
+ { date: "2024-05-29", desktop: 78, mobile: 130 },
+ { date: "2024-05-30", desktop: 340, mobile: 280 },
+ { date: "2024-05-31", desktop: 178, mobile: 230 },
+ { date: "2024-06-01", desktop: 178, mobile: 200 },
+ { date: "2024-06-02", desktop: 470, mobile: 410 },
+ { date: "2024-06-03", desktop: 103, mobile: 160 },
+ { date: "2024-06-04", desktop: 439, mobile: 380 },
+ { date: "2024-06-05", desktop: 88, mobile: 140 },
+ { date: "2024-06-06", desktop: 294, mobile: 250 },
+ { date: "2024-06-07", desktop: 323, mobile: 370 },
+ { date: "2024-06-08", desktop: 385, mobile: 320 },
+ { date: "2024-06-09", desktop: 438, mobile: 480 },
+ { date: "2024-06-10", desktop: 155, mobile: 200 },
+ { date: "2024-06-11", desktop: 92, mobile: 150 },
+ { date: "2024-06-12", desktop: 492, mobile: 420 },
+ { date: "2024-06-13", desktop: 81, mobile: 130 },
+ { date: "2024-06-14", desktop: 426, mobile: 380 },
+ { date: "2024-06-15", desktop: 307, mobile: 350 },
+ { date: "2024-06-16", desktop: 371, mobile: 310 },
+ { date: "2024-06-17", desktop: 475, mobile: 520 },
+ { date: "2024-06-18", desktop: 107, mobile: 170 },
+ { date: "2024-06-19", desktop: 341, mobile: 290 },
+ { date: "2024-06-20", desktop: 408, mobile: 450 },
+ { date: "2024-06-21", desktop: 169, mobile: 210 },
+ { date: "2024-06-22", desktop: 317, mobile: 270 },
+ { date: "2024-06-23", desktop: 480, mobile: 530 },
+ { date: "2024-06-24", desktop: 132, mobile: 180 },
+ { date: "2024-06-25", desktop: 141, mobile: 190 },
+ { date: "2024-06-26", desktop: 434, mobile: 380 },
+ { date: "2024-06-27", desktop: 448, mobile: 490 },
+ { date: "2024-06-28", desktop: 149, mobile: 200 },
+ { date: "2024-06-29", desktop: 103, mobile: 160 },
+ { date: "2024-06-30", desktop: 446, mobile: 400 },
+];
+
+const chartConfig = {
+ visitors: {
+ label: "Visitors",
+ },
+ desktop: {
+ label: "Desktop",
+ color: "var(--primary)",
+ },
+ mobile: {
+ label: "Mobile",
+ color: "var(--primary)",
+ },
+} satisfies ChartConfig;
+
+export function ChartAreaInteractive() {
+ const isMobile = useIsMobile();
+ const [timeRange, setTimeRange] = React.useState("90d");
+
+ React.useEffect(() => {
+ if (isMobile) {
+ setTimeRange("7d");
+ }
+ }, [isMobile]);
+
+ const filteredData = chartData.filter((item) => {
+ const date = new Date(item.date);
+ const referenceDate = new Date("2024-06-30");
+ let daysToSubtract = 90;
+ if (timeRange === "30d") {
+ daysToSubtract = 30;
+ } else if (timeRange === "7d") {
+ daysToSubtract = 7;
+ }
+ const startDate = new Date(referenceDate);
+ startDate.setDate(startDate.getDate() - daysToSubtract);
+ return date >= startDate;
+ });
+
+ return (
+
+
+ Total Visitors
+
+ Total for the last 3 months
+ Last 3 months
+
+
+
+ Last 3 months
+ Last 30 days
+ Last 7 days
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ const date = new Date(value);
+ return date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ });
+ }}
+ />
+ {
+ return new Date(value).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ });
+ }}
+ indicator="dot"
+ />
+ }
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/app/src/components/data-table.tsx b/app/src/components/data-table.tsx
new file mode 100644
index 0000000..9e5751d
--- /dev/null
+++ b/app/src/components/data-table.tsx
@@ -0,0 +1,701 @@
+import {
+ closestCenter,
+ DndContext,
+ type DragEndEvent,
+ KeyboardSensor,
+ MouseSensor,
+ TouchSensor,
+ type UniqueIdentifier,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core";
+import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
+import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+import {
+ IconChevronDown,
+ IconChevronLeft,
+ IconChevronRight,
+ IconChevronsLeft,
+ IconChevronsRight,
+ IconCircleCheckFilled,
+ IconDotsVertical,
+ IconGripVertical,
+ IconLayoutColumns,
+ IconLoader,
+ IconPlus,
+ IconTrendingUp,
+} from "@tabler/icons-react";
+import {
+ type ColumnDef,
+ type ColumnFiltersState,
+ flexRender,
+ getCoreRowModel,
+ getFacetedRowModel,
+ getFacetedUniqueValues,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ type Row,
+ type SortingState,
+ useReactTable,
+ type VisibilityState,
+} from "@tanstack/react-table";
+import * as React from "react";
+import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
+import { toast } from "sonner";
+import { z } from "zod";
+
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from "@/components/ui/drawer";
+import {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Separator } from "@/components/ui/separator";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { useIsMobile } from "@/hooks/use-mobile";
+
+export const schema = z.object({
+ id: z.number(),
+ header: z.string(),
+ type: z.string(),
+ status: z.string(),
+ target: z.string(),
+ limit: z.string(),
+ reviewer: z.string(),
+});
+
+// Create a separate component for the drag handle
+function DragHandle({ id }: { id: number }) {
+ const { attributes, listeners } = useSortable({
+ id,
+ });
+
+ return (
+
+ );
+}
+
+const columns: ColumnDef>[] = [
+ {
+ id: "drag",
+ header: () => null,
+ cell: ({ row }) => ,
+ },
+ {
+ id: "select",
+ header: ({ table }) => (
+
+ table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ />
+
+ ),
+ cell: ({ row }) => (
+
+ row.toggleSelected(!!value)}
+ aria-label="Select row"
+ />
+
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: "header",
+ header: "Header",
+ cell: ({ row }) => {
+ return ;
+ },
+ enableHiding: false,
+ },
+ {
+ accessorKey: "type",
+ header: "Section Type",
+ cell: ({ row }) => (
+
+
+ {row.original.type}
+
+
+ ),
+ },
+ {
+ accessorKey: "status",
+ header: "Status",
+ cell: ({ row }) => (
+
+ {row.original.status === "Done" ? (
+
+ ) : (
+
+ )}
+ {row.original.status}
+
+ ),
+ },
+ {
+ accessorKey: "target",
+ header: () => Target
,
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: "limit",
+ header: () => Limit
,
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: "reviewer",
+ header: "Reviewer",
+ cell: ({ row }) => {
+ const isAssigned = row.original.reviewer !== "Assign reviewer";
+
+ if (isAssigned) {
+ return row.original.reviewer;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+ },
+ },
+ {
+ id: "actions",
+ cell: () => (
+
+
+
+
+
+ Edit
+ Make a copy
+ Favorite
+
+ Delete
+
+
+ ),
+ },
+];
+
+function DraggableRow({ row }: { row: Row> }) {
+ const { transform, transition, setNodeRef, isDragging } = useSortable({
+ id: row.original.id,
+ });
+
+ return (
+
+ {row.getVisibleCells().map((cell) => (
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ ))}
+
+ );
+}
+
+export function DataTable({ data: initialData }: { data: z.infer[] }) {
+ const [data, setData] = React.useState(() => initialData);
+ const [rowSelection, setRowSelection] = React.useState({});
+ const [columnVisibility, setColumnVisibility] = React.useState({});
+ const [columnFilters, setColumnFilters] = React.useState([]);
+ const [sorting, setSorting] = React.useState([]);
+ const [pagination, setPagination] = React.useState({
+ pageIndex: 0,
+ pageSize: 10,
+ });
+ const sortableId = React.useId();
+ const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}));
+
+ const dataIds = React.useMemo(() => data?.map(({ id }) => id) || [], [data]);
+
+ const table = useReactTable({
+ data,
+ columns,
+ state: {
+ sorting,
+ columnVisibility,
+ rowSelection,
+ columnFilters,
+ pagination,
+ },
+ getRowId: (row) => row.id.toString(),
+ enableRowSelection: true,
+ onRowSelectionChange: setRowSelection,
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ onColumnVisibilityChange: setColumnVisibility,
+ onPaginationChange: setPagination,
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFacetedRowModel: getFacetedRowModel(),
+ getFacetedUniqueValues: getFacetedUniqueValues(),
+ });
+
+ function handleDragEnd(event: DragEndEvent) {
+ const { active, over } = event;
+ if (active && over && active.id !== over.id) {
+ setData((data) => {
+ const oldIndex = dataIds.indexOf(active.id);
+ const newIndex = dataIds.indexOf(over.id);
+ return arrayMove(data, oldIndex, newIndex);
+ });
+ }
+ }
+
+ return (
+
+
+
+
+
+ Outline
+
+ Past Performance 3
+
+
+ Key Personnel 2
+
+ Focus Documents
+
+
+
+
+
+
+
+ {table
+ .getAllColumns()
+ .filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
+ .map((column) => {
+ return (
+ column.toggleVisibility(!!value)}
+ >
+ {column.id}
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(header.column.columnDef.header, header.getContext())}
+
+ );
+ })}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+
+ {table.getRowModel().rows.map((row) => (
+
+ ))}
+
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s)
+ selected.
+
+
+
+
+
+
+
+ Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const chartData = [
+ { month: "January", desktop: 186, mobile: 80 },
+ { month: "February", desktop: 305, mobile: 200 },
+ { month: "March", desktop: 237, mobile: 120 },
+ { month: "April", desktop: 73, mobile: 190 },
+ { month: "May", desktop: 209, mobile: 130 },
+ { month: "June", desktop: 214, mobile: 140 },
+];
+
+const chartConfig = {
+ desktop: {
+ label: "Desktop",
+ color: "var(--primary)",
+ },
+ mobile: {
+ label: "Mobile",
+ color: "var(--primary)",
+ },
+} satisfies ChartConfig;
+
+function TableCellViewer({ item }: { item: z.infer }) {
+ const isMobile = useIsMobile();
+
+ return (
+
+
+
+
+
+
+ {item.header}
+ Showing total visitors for the last 6 months
+
+
+ {!isMobile && (
+ <>
+
+
+
+ value.slice(0, 3)}
+ hide
+ />
+ } />
+
+
+
+
+
+
+
+ Trending up by 5.2% this month
+
+
+ Showing total visitors for the last 6 months. This is just some random text to test the layout. It
+ spans multiple lines and should wrap around.
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/src/components/nav-documents.tsx b/app/src/components/nav-documents.tsx
new file mode 100644
index 0000000..b551e71
--- /dev/null
+++ b/app/src/components/nav-documents.tsx
@@ -0,0 +1,92 @@
+"use client"
+
+import {
+ IconDots,
+ IconFolder,
+ IconShare3,
+ IconTrash,
+ type Icon,
+} from "@tabler/icons-react"
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import {
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar,
+} from "@/components/ui/sidebar"
+
+export function NavDocuments({
+ items,
+}: {
+ items: {
+ name: string
+ url: string
+ icon: Icon
+ }[]
+}) {
+ const { isMobile } = useSidebar()
+
+ return (
+
+ Documents
+
+ {items.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+ More
+
+
+
+
+
+ Open
+
+
+
+ Share
+
+
+
+
+ Delete
+
+
+
+
+ ))}
+
+
+
+ More
+
+
+
+
+ )
+}
diff --git a/app/src/components/nav-main.tsx b/app/src/components/nav-main.tsx
new file mode 100644
index 0000000..82afe7f
--- /dev/null
+++ b/app/src/components/nav-main.tsx
@@ -0,0 +1,56 @@
+import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react"
+
+import { Button } from "@/components/ui/button"
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar"
+
+export function NavMain({
+ items,
+}: {
+ items: {
+ title: string
+ url: string
+ icon?: Icon
+ }[]
+}) {
+ return (
+
+
+
+
+
+
+ Quick Create
+
+
+
+
+
+ {items.map((item) => (
+
+
+ {item.icon && }
+ {item.title}
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/app/src/components/nav-secondary.tsx b/app/src/components/nav-secondary.tsx
new file mode 100644
index 0000000..3f3636f
--- /dev/null
+++ b/app/src/components/nav-secondary.tsx
@@ -0,0 +1,42 @@
+"use client"
+
+import * as React from "react"
+import { type Icon } from "@tabler/icons-react"
+
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar"
+
+export function NavSecondary({
+ items,
+ ...props
+}: {
+ items: {
+ title: string
+ url: string
+ icon: Icon
+ }[]
+} & React.ComponentPropsWithoutRef) {
+ return (
+
+
+
+ {items.map((item) => (
+
+
+
+
+ {item.title}
+
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/app/src/components/nav-user.controller.ts b/app/src/components/nav-user.controller.ts
new file mode 100644
index 0000000..6f8def2
--- /dev/null
+++ b/app/src/components/nav-user.controller.ts
@@ -0,0 +1,48 @@
+import { Controller } from "@/lib/controller.tsx";
+import { type User as ZitadelUser, zitadel } from "@/services/zitadel.ts";
+
+export class NavUserController extends Controller<{
+ user: User;
+}> {
+ async onInit() {
+ return {
+ user: await this.#getAuthenticatedUser(),
+ };
+ }
+
+ async #getAuthenticatedUser(): Promise {
+ const user = await zitadel.userManager.getUser();
+ if (user === null) {
+ throw new Error("Failed to resolve user session");
+ }
+ return getUserProfile(user);
+ }
+
+ signout() {
+ zitadel.signout();
+ }
+}
+
+function getUserProfile({ profile }: ZitadelUser): User {
+ const user: User = { name: "Unknown", email: "unknown@acme.none", avatar: "" };
+ if (profile.name) {
+ user.name = profile.name;
+ } else if (profile.given_name && profile.family_name) {
+ user.name = `${profile.given_name} ${profile.family_name}`;
+ } else if (profile.given_name) {
+ user.name = profile.given_name;
+ }
+ if (profile.email) {
+ user.email = profile.email;
+ }
+ if (profile.picture !== undefined) {
+ user.avatar = profile.picture;
+ }
+ return user;
+}
+
+type User = {
+ name: string;
+ email: string;
+ avatar: string;
+};
diff --git a/app/src/components/nav-user.tsx b/app/src/components/nav-user.tsx
new file mode 100644
index 0000000..bb2899b
--- /dev/null
+++ b/app/src/components/nav-user.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import { IconCreditCard, IconDotsVertical, IconLogout, IconNotification, IconUserCircle } from "@tabler/icons-react";
+
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/components/ui/sidebar";
+import { makeControllerComponent } from "@/lib/controller.tsx";
+
+import { NavUserController } from "./nav-user.controller.ts";
+
+export const NavUser = makeControllerComponent(NavUserController, ({ user, signout }) => {
+ const { isMobile } = useSidebar();
+ return (
+
+
+
+
+
+
+
+ CN
+
+
+ {user.name}
+ {user.email}
+
+
+
+
+
+
+
+
+
+ CN
+
+
+ {user.name}
+ {user.email}
+
+
+
+
+
+
+
+ Account
+
+
+
+ Billing
+
+
+
+ Notifications
+
+
+
+ signout()}>
+
+ Log out
+
+
+
+
+
+ );
+});
diff --git a/app/src/components/section-cards.tsx b/app/src/components/section-cards.tsx
new file mode 100644
index 0000000..f714d25
--- /dev/null
+++ b/app/src/components/section-cards.tsx
@@ -0,0 +1,102 @@
+import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
+
+import { Badge } from "@/components/ui/badge"
+import {
+ Card,
+ CardAction,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+
+export function SectionCards() {
+ return (
+
+
+
+ Total Revenue
+
+ $1,250.00
+
+
+
+
+ +12.5%
+
+
+
+
+
+ Trending up this month
+
+
+ Visitors for the last 6 months
+
+
+
+
+
+ New Customers
+
+ 1,234
+
+
+
+
+ -20%
+
+
+
+
+
+ Down 20% this period
+
+
+ Acquisition needs attention
+
+
+
+
+
+ Active Accounts
+
+ 45,678
+
+
+
+
+ +12.5%
+
+
+
+
+
+ Strong user retention
+
+ Engagement exceed targets
+
+
+
+
+ Growth Rate
+
+ 4.5%
+
+
+
+
+ +4.5%
+
+
+
+
+
+ Steady performance increase
+
+ Meets growth projections
+
+
+
+ )
+}
diff --git a/app/src/components/site-header.tsx b/app/src/components/site-header.tsx
new file mode 100644
index 0000000..59c4f02
--- /dev/null
+++ b/app/src/components/site-header.tsx
@@ -0,0 +1,30 @@
+import { Button } from "@/components/ui/button"
+import { Separator } from "@/components/ui/separator"
+import { SidebarTrigger } from "@/components/ui/sidebar"
+
+export function SiteHeader() {
+ return (
+
+ )
+}
diff --git a/app/src/components/theme-provider.tsx b/app/src/components/theme-provider.tsx
new file mode 100644
index 0000000..4d676bd
--- /dev/null
+++ b/app/src/components/theme-provider.tsx
@@ -0,0 +1,67 @@
+import { createContext, useContext, useEffect, useState } from "react";
+
+type Theme = "dark" | "light" | "system";
+
+type ThemeProviderProps = {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+type ThemeProviderState = {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+};
+
+const initialState: ThemeProviderState = {
+ theme: "system",
+ setTheme: () => null,
+};
+
+const ThemeProviderContext = createContext(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "system",
+ storageKey = "vite-ui-theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme);
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ root.classList.remove("light", "dark");
+
+ if (theme === "system") {
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
+
+ return context;
+};
diff --git a/app/src/components/ui/avatar.tsx b/app/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..71e428b
--- /dev/null
+++ b/app/src/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/app/src/components/ui/badge.tsx b/app/src/components/ui/badge.tsx
new file mode 100644
index 0000000..fd3a406
--- /dev/null
+++ b/app/src/components/ui/badge.tsx
@@ -0,0 +1,46 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span"
+
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/app/src/components/ui/breadcrumb.tsx b/app/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..eb88f32
--- /dev/null
+++ b/app/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/app/src/components/ui/button.tsx b/app/src/components/ui/button.tsx
new file mode 100644
index 0000000..21409a0
--- /dev/null
+++ b/app/src/components/ui/button.tsx
@@ -0,0 +1,60 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/app/src/components/ui/card.tsx b/app/src/components/ui/card.tsx
new file mode 100644
index 0000000..681ad98
--- /dev/null
+++ b/app/src/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/app/src/components/ui/chart.tsx b/app/src/components/ui/chart.tsx
new file mode 100644
index 0000000..48d2724
--- /dev/null
+++ b/app/src/components/ui/chart.tsx
@@ -0,0 +1,355 @@
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+function ChartContainer({
+ id,
+ className,
+ children,
+ config,
+ ...props
+}: React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+}) {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+