From be9b8e9e55736fecc1c8f9104ea54944b810fabe Mon Sep 17 00:00:00 2001 From: kodemon Date: Fri, 5 Dec 2025 01:56:42 +0100 Subject: [PATCH] feat: add payment module --- .bruno/Payment/account/create.bru | 24 + .bruno/Payment/account/folder.bru | 4 + .bruno/Payment/beneficiary/Ledgers.bru | 20 + .../beneficiary}/create.bru | 6 +- .bruno/Payment/beneficiary/dashboard.bru | 20 + .bruno/Payment/beneficiary/folder.bru | 4 + .bruno/Payment/beneficiary/id.bru | 20 + .bruno/Payment/beneficiary/list.bru | 16 + .bruno/Payment/folder.bru | 4 + .bruno/Payment/ledger/create.bru | 27 + .bruno/Payment/ledger/folder.bru | 4 + .bruno/Payment/ledger/wallets.bru | 20 + .bruno/Payment/wallet/create.bru | 24 + .bruno/Payment/wallet/folder.bru | 4 + .bruno/identity/Get.bru | 19 - .bruno/identity/Roles.bru | 32 - .bruno/identity/Update.bru | 43 - .bruno/identity/folder.bru | 8 - .bruno/identity/login/code.bru | 29 - .bruno/identity/login/email.bru | 21 - .bruno/identity/login/folder.bru | 8 - .bruno/identity/login/sudo.bru | 21 - .bruno/identity/me.bru | 15 - .bruno/workspace/folder.bru | 8 - .env | 2 + api/package.json | 5 +- api/server.ts | 10 +- app/.gitignore | 24 + app/README.md | 75 + app/components.json | 22 + app/eslint.config.js | 23 + app/index.html | 13 + app/package.json | 65 + app/public/vite.svg | 1 + app/src/app/app.view.tsx | 30 + app/src/app/auth/callback.view.tsx | 26 + app/src/app/auth/components/login-form.tsx | 61 + app/src/app/auth/login.view.tsx | 11 + app/src/app/dashboard/dashboard.controller.ts | 20 + app/src/app/dashboard/dashboard.view.tsx | 12 + app/src/assets/react.svg | 1 + app/src/components/app-sidebar.tsx | 179 ++ app/src/components/chart-area-interactive.tsx | 237 +++ app/src/components/data-table.tsx | 701 ++++++++ app/src/components/nav-documents.tsx | 92 + app/src/components/nav-main.tsx | 56 + app/src/components/nav-secondary.tsx | 42 + app/src/components/nav-user.controller.ts | 48 + app/src/components/nav-user.tsx | 85 + app/src/components/section-cards.tsx | 102 ++ app/src/components/site-header.tsx | 30 + app/src/components/theme-provider.tsx | 67 + app/src/components/ui/avatar.tsx | 53 + app/src/components/ui/badge.tsx | 46 + app/src/components/ui/breadcrumb.tsx | 109 ++ app/src/components/ui/button.tsx | 60 + app/src/components/ui/card.tsx | 92 + app/src/components/ui/chart.tsx | 355 ++++ app/src/components/ui/checkbox.tsx | 30 + app/src/components/ui/drawer.tsx | 133 ++ app/src/components/ui/dropdown-menu.tsx | 257 +++ app/src/components/ui/field.tsx | 222 +++ app/src/components/ui/input.tsx | 21 + app/src/components/ui/label.tsx | 24 + app/src/components/ui/select.tsx | 185 +++ app/src/components/ui/separator.tsx | 26 + app/src/components/ui/sheet.tsx | 139 ++ app/src/components/ui/sidebar.tsx | 726 ++++++++ app/src/components/ui/skeleton.tsx | 13 + app/src/components/ui/sonner.tsx | 38 + app/src/components/ui/table.tsx | 114 ++ app/src/components/ui/tabs.tsx | 66 + app/src/components/ui/toggle-group.tsx | 83 + app/src/components/ui/toggle.tsx | 47 + app/src/components/ui/tooltip.tsx | 59 + app/src/hooks/use-mobile.ts | 19 + app/src/index.css | 120 ++ .../src/lib/controller.tsx | 172 +- app/src/lib/form.ts | 269 +++ app/src/lib/router.tsx | 47 + app/src/lib/utils.ts | 6 + app/src/main.tsx | 26 + app/src/services/adapters/http.ts | 273 +++ app/src/services/api.ts | 17 + app/src/services/zitadel.ts | 15 + app/tsconfig.app.json | 33 + app/tsconfig.json | 10 + app/tsconfig.node.json | 26 + app/vite.config.ts | 22 + apps/react/index.html | 2 +- apps/react/package.json | 10 +- .../src/components/nav-user.controller.ts | 11 +- apps/react/src/components/nav-user.tsx | 71 +- apps/react/src/components/ui/breadcrumb.tsx | 90 + apps/react/src/components/ui/field.tsx | 126 +- apps/react/src/components/ui/label.tsx | 17 +- apps/react/src/index.css | 8 +- apps/react/src/libraries/controller.tsx | 242 +++ apps/react/src/main.tsx | 3 +- apps/react/src/router.tsx | 2 +- apps/react/src/services/zitadel.ts | 9 +- apps/react/src/theme.css | 105 -- apps/react/src/themes.css | 120 ++ apps/react/src/views/app.view.tsx | 63 +- deno.json | 10 +- deno.lock | 1478 ++++++++++------- modules/account/package.json | 2 +- modules/payment/entrypoints/client.ts | 9 + modules/payment/entrypoints/server.ts | 14 + modules/payment/libraries/database.ts | 0 modules/payment/package.json | 17 + modules/payment/repositories/account.ts | 97 ++ modules/payment/repositories/beneficiary.ts | 101 ++ modules/payment/repositories/ledger.ts | 57 + modules/payment/repositories/transaction.ts | 3 + modules/payment/repositories/wallet.ts | 47 + .../payment/routes/accounts/create/handle.ts | 4 + .../payment/routes/accounts/create/spec.ts | 6 + .../routes/beneficiaries/:id/handle.ts | 12 + .../payment/routes/beneficiaries/:id/spec.ts | 8 + .../routes/beneficiaries/create/handle.ts | 4 + .../routes/beneficiaries/create/spec.ts | 5 + .../routes/beneficiaries/ledgers/handle.ts | 4 + .../routes/beneficiaries/ledgers/spec.ts | 9 + .../routes/beneficiaries/list/handle.ts | 4 + .../payment/routes/beneficiaries/list/spec.ts | 6 + modules/payment/routes/dashboard/handle.ts | 31 + modules/payment/routes/dashboard/spec.ts | 16 + .../payment/routes/ledgers/create/handle.ts | 4 + modules/payment/routes/ledgers/create/spec.ts | 6 + .../payment/routes/ledgers/wallets/handle.ts | 4 + .../payment/routes/ledgers/wallets/spec.ts | 9 + .../payment/routes/wallets/accounts/handle.ts | 4 + .../payment/routes/wallets/accounts/spec.ts | 9 + .../payment/routes/wallets/create/handle.ts | 4 + modules/payment/routes/wallets/create/spec.ts | 6 + modules/payment/schemas/account.ts | 35 + modules/payment/schemas/beneficiary.ts | 40 + modules/payment/schemas/currency.ts | 24 + modules/payment/schemas/ledger.ts | 36 + .../schemas/transaction-participant.ts | 17 + modules/payment/schemas/transaction.ts | 40 + modules/payment/schemas/wallet.ts | 35 + platform/config/dotenv.ts | 12 +- platform/config/environment.ts | 6 +- platform/config/package.json | 2 +- platform/database/client.ts | 72 +- platform/database/config.ts | 44 +- platform/database/mod.ts | 6 + platform/database/package.json | 9 +- platform/database/parser.ts | 29 - platform/logger/package.json | 2 +- platform/parse/mod.ts | 1 + platform/parse/package.json | 13 + platform/parse/time.ts | 13 + platform/parse/zod.ts | 66 + platform/relay/package.json | 2 +- platform/routes/package.json | 2 +- platform/server/package.json | 2 +- platform/spec/package.json | 2 +- 160 files changed, 8615 insertions(+), 1158 deletions(-) create mode 100644 .bruno/Payment/account/create.bru create mode 100644 .bruno/Payment/account/folder.bru create mode 100644 .bruno/Payment/beneficiary/Ledgers.bru rename .bruno/{workspace => Payment/beneficiary}/create.bru (56%) create mode 100644 .bruno/Payment/beneficiary/dashboard.bru create mode 100644 .bruno/Payment/beneficiary/folder.bru create mode 100644 .bruno/Payment/beneficiary/id.bru create mode 100644 .bruno/Payment/beneficiary/list.bru create mode 100644 .bruno/Payment/folder.bru create mode 100644 .bruno/Payment/ledger/create.bru create mode 100644 .bruno/Payment/ledger/folder.bru create mode 100644 .bruno/Payment/ledger/wallets.bru create mode 100644 .bruno/Payment/wallet/create.bru create mode 100644 .bruno/Payment/wallet/folder.bru delete mode 100644 .bruno/identity/Get.bru delete mode 100644 .bruno/identity/Roles.bru delete mode 100644 .bruno/identity/Update.bru delete mode 100644 .bruno/identity/folder.bru delete mode 100644 .bruno/identity/login/code.bru delete mode 100644 .bruno/identity/login/email.bru delete mode 100644 .bruno/identity/login/folder.bru delete mode 100644 .bruno/identity/login/sudo.bru delete mode 100644 .bruno/identity/me.bru delete mode 100644 .bruno/workspace/folder.bru create mode 100644 .env create mode 100644 app/.gitignore create mode 100644 app/README.md create mode 100644 app/components.json create mode 100644 app/eslint.config.js create mode 100644 app/index.html create mode 100644 app/package.json create mode 100644 app/public/vite.svg create mode 100644 app/src/app/app.view.tsx create mode 100644 app/src/app/auth/callback.view.tsx create mode 100644 app/src/app/auth/components/login-form.tsx create mode 100644 app/src/app/auth/login.view.tsx create mode 100644 app/src/app/dashboard/dashboard.controller.ts create mode 100644 app/src/app/dashboard/dashboard.view.tsx create mode 100644 app/src/assets/react.svg create mode 100644 app/src/components/app-sidebar.tsx create mode 100644 app/src/components/chart-area-interactive.tsx create mode 100644 app/src/components/data-table.tsx create mode 100644 app/src/components/nav-documents.tsx create mode 100644 app/src/components/nav-main.tsx create mode 100644 app/src/components/nav-secondary.tsx create mode 100644 app/src/components/nav-user.controller.ts create mode 100644 app/src/components/nav-user.tsx create mode 100644 app/src/components/section-cards.tsx create mode 100644 app/src/components/site-header.tsx create mode 100644 app/src/components/theme-provider.tsx create mode 100644 app/src/components/ui/avatar.tsx create mode 100644 app/src/components/ui/badge.tsx create mode 100644 app/src/components/ui/breadcrumb.tsx create mode 100644 app/src/components/ui/button.tsx create mode 100644 app/src/components/ui/card.tsx create mode 100644 app/src/components/ui/chart.tsx create mode 100644 app/src/components/ui/checkbox.tsx create mode 100644 app/src/components/ui/drawer.tsx create mode 100644 app/src/components/ui/dropdown-menu.tsx create mode 100644 app/src/components/ui/field.tsx create mode 100644 app/src/components/ui/input.tsx create mode 100644 app/src/components/ui/label.tsx create mode 100644 app/src/components/ui/select.tsx create mode 100644 app/src/components/ui/separator.tsx create mode 100644 app/src/components/ui/sheet.tsx create mode 100644 app/src/components/ui/sidebar.tsx create mode 100644 app/src/components/ui/skeleton.tsx create mode 100644 app/src/components/ui/sonner.tsx create mode 100644 app/src/components/ui/table.tsx create mode 100644 app/src/components/ui/tabs.tsx create mode 100644 app/src/components/ui/toggle-group.tsx create mode 100644 app/src/components/ui/toggle.tsx create mode 100644 app/src/components/ui/tooltip.tsx create mode 100644 app/src/hooks/use-mobile.ts create mode 100644 app/src/index.css rename apps/react/src/libraries/controller.ts => app/src/lib/controller.tsx (52%) create mode 100644 app/src/lib/form.ts create mode 100644 app/src/lib/router.tsx create mode 100644 app/src/lib/utils.ts create mode 100644 app/src/main.tsx create mode 100644 app/src/services/adapters/http.ts create mode 100644 app/src/services/api.ts create mode 100644 app/src/services/zitadel.ts create mode 100644 app/tsconfig.app.json create mode 100644 app/tsconfig.json create mode 100644 app/tsconfig.node.json create mode 100644 app/vite.config.ts create mode 100644 apps/react/src/components/ui/breadcrumb.tsx create mode 100644 apps/react/src/libraries/controller.tsx delete mode 100644 apps/react/src/theme.css create mode 100644 apps/react/src/themes.css create mode 100644 modules/payment/entrypoints/client.ts create mode 100644 modules/payment/entrypoints/server.ts create mode 100644 modules/payment/libraries/database.ts create mode 100644 modules/payment/package.json create mode 100644 modules/payment/repositories/account.ts create mode 100644 modules/payment/repositories/beneficiary.ts create mode 100644 modules/payment/repositories/ledger.ts create mode 100644 modules/payment/repositories/transaction.ts create mode 100644 modules/payment/repositories/wallet.ts create mode 100644 modules/payment/routes/accounts/create/handle.ts create mode 100644 modules/payment/routes/accounts/create/spec.ts create mode 100644 modules/payment/routes/beneficiaries/:id/handle.ts create mode 100644 modules/payment/routes/beneficiaries/:id/spec.ts create mode 100644 modules/payment/routes/beneficiaries/create/handle.ts create mode 100644 modules/payment/routes/beneficiaries/create/spec.ts create mode 100644 modules/payment/routes/beneficiaries/ledgers/handle.ts create mode 100644 modules/payment/routes/beneficiaries/ledgers/spec.ts create mode 100644 modules/payment/routes/beneficiaries/list/handle.ts create mode 100644 modules/payment/routes/beneficiaries/list/spec.ts create mode 100644 modules/payment/routes/dashboard/handle.ts create mode 100644 modules/payment/routes/dashboard/spec.ts create mode 100644 modules/payment/routes/ledgers/create/handle.ts create mode 100644 modules/payment/routes/ledgers/create/spec.ts create mode 100644 modules/payment/routes/ledgers/wallets/handle.ts create mode 100644 modules/payment/routes/ledgers/wallets/spec.ts create mode 100644 modules/payment/routes/wallets/accounts/handle.ts create mode 100644 modules/payment/routes/wallets/accounts/spec.ts create mode 100644 modules/payment/routes/wallets/create/handle.ts create mode 100644 modules/payment/routes/wallets/create/spec.ts create mode 100644 modules/payment/schemas/account.ts create mode 100644 modules/payment/schemas/beneficiary.ts create mode 100644 modules/payment/schemas/currency.ts create mode 100644 modules/payment/schemas/ledger.ts create mode 100644 modules/payment/schemas/transaction-participant.ts create mode 100644 modules/payment/schemas/transaction.ts create mode 100644 modules/payment/schemas/wallet.ts create mode 100644 platform/database/mod.ts delete mode 100644 platform/database/parser.ts create mode 100644 platform/parse/mod.ts create mode 100644 platform/parse/package.json create mode 100644 platform/parse/time.ts create mode 100644 platform/parse/zod.ts 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 ( +
+
{ + e.preventDefault(); + zitadel.authorize(); + }} + > + +
+ +
+ +
+ Valkyr Sandbox +
+

Welcome to Valkyr Sandbox

+ + Don't have an account? Sign up + +
+ + + + Or + + + + +
+
+ + By clicking continue, you agree to our Terms of Service and Privacy Policy. + +
+ ); +} 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 }) => ( +
{ + e.preventDefault(); + toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), { + loading: `Saving ${row.original.header}`, + success: "Done", + error: "Error", + }); + }} + > + + +
+ ), + }, + { + accessorKey: "limit", + header: () =>
Limit
, + cell: ({ row }) => ( +
{ + e.preventDefault(); + toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), { + loading: `Saving ${row.original.header}`, + success: "Done", + error: "Error", + }); + }} + > + + +
+ ), + }, + { + 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 ( +
+
+ + +

Documents

+
+ +
+
+
+ ) +} 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