Bulk commit: Stand ende 22.01.
This commit is contained in:
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
.env.local
|
||||
11
.env.production
Normal file
11
.env.production
Normal file
@@ -0,0 +1,11 @@
|
||||
VITE_BACKEND_URI="PRODUCTION_BACKEND_URI"
|
||||
VITE_RPC_URI="PRODUCTION_RPC_URI"
|
||||
# VITE_BACKEND_URI="https://vt-api.kocoder.xyz"
|
||||
# VITE_RPC_URI="https://vt-rpc.kocoder.xyz"
|
||||
|
||||
OIDC_USE_MOCK=false
|
||||
OIDC_ISSUER_URI=https://keycloak.kocoder.xyz/realms/che
|
||||
OIDC_CLIENT_ID=eventory
|
||||
|
||||
VITE_ENVIRONMENT="PRODUCTION_ENVIRONMENT"
|
||||
|
||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM node:24.11.1-trixie-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN npm i --legacy-peer-deps
|
||||
RUN npm run build
|
||||
|
||||
FROM node:24.11.1-trixie AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/.output .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "node", "./server/index.mjs" ]
|
||||
@@ -10,6 +10,7 @@
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
@@ -17,5 +18,7 @@
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
"registries": {
|
||||
"@diceui": "https://diceui.com/r/{name}.json"
|
||||
}
|
||||
}
|
||||
|
||||
226
k3s.yaml
Normal file
226
k3s.yaml
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: vt
|
||||
labels:
|
||||
name: vt
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: vt-fe
|
||||
namespace: vt
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: vt-fe
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: vt-fe
|
||||
spec:
|
||||
containers:
|
||||
- name: vt-fe
|
||||
image: git.kocoder.xyz/kocoded/vt-fe:2025120501
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "500m"
|
||||
env:
|
||||
- name: BACKEND_URI
|
||||
value: "https://vt-api.kocoder.xyz"
|
||||
- name: RPC_URI
|
||||
value: "https://vt-rpc.kocoder.xyz"
|
||||
- name: OIDC_USE_MOCK
|
||||
value: "false"
|
||||
- name: OIDC_ISSUER_URI
|
||||
value: https://keycloak.kocoder.xyz/realms/che
|
||||
- name: OIDC_CLIENT_ID
|
||||
value: eventory
|
||||
- name: VITE_ENVIRONMENT
|
||||
value: "development"
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: vt-be
|
||||
namespace: vt
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: vt-be
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: vt-be
|
||||
spec:
|
||||
containers:
|
||||
- name: vt-be
|
||||
image: git.kocoder.xyz/kocoded/vt-be:2025120302
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "500m"
|
||||
env:
|
||||
- name: CLIENT_ID
|
||||
value: "golang-vt-backend"
|
||||
- name: CLIENT_SECRET
|
||||
value: "awumIoacqNmwKTxRilQSM9cDmA7xA0j0"
|
||||
- name: BACKEND_URI
|
||||
value: "https://vt-api.kocoder.xyz"
|
||||
- name: FRONTEND_URI
|
||||
value: "https://vt.kocoder.xyz"
|
||||
- name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|
||||
value: "https://otel.kocoder.xyz/v1/traces"
|
||||
- name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
|
||||
value: "https://otel.kocoder.xyz/v1/metrics"
|
||||
- name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
|
||||
value: "https://otel.kocoder.xyz/v1/logs"
|
||||
- name: DB_DSN
|
||||
value: "host=10.1.0.2 user=vt password=20a1c7809cd065bc5afe7c36fde26abf625316c8a83cc841b435c9acf3619b1f dbname=vt port=5432 sslmode=prefer TimeZone=Europe/Vienna"
|
||||
- name: VALKEY_HOST
|
||||
value: "10.1.0.2"
|
||||
- name: VALKEY_PORT
|
||||
value: "6379"
|
||||
- name: VALKEY_USER
|
||||
value: "default"
|
||||
- name: VALKEY_PASS
|
||||
value: "Konsti2007!"
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: "api"
|
||||
- containerPort: 3002
|
||||
name: "rpc"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: vt-fe
|
||||
namespace: vt
|
||||
spec:
|
||||
selector:
|
||||
app: vt-fe
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: vt-be
|
||||
namespace: vt
|
||||
spec:
|
||||
selector:
|
||||
app: vt-be
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
- port: 3002
|
||||
targetPort: 3002
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: vt-kocoder-xyz-prod
|
||||
namespace: vt
|
||||
spec:
|
||||
secretName: vt-tls
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
commonName: "vt.kocoder.xyz"
|
||||
dnsNames:
|
||||
- "vt.kocoder.xyz"
|
||||
- "vt-api.kocoder.xyz"
|
||||
- "vt-rpc.kocoder.xyz"
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: vt-fe
|
||||
namespace: vt
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
- websecure
|
||||
routes:
|
||||
- kind: Rule
|
||||
match: Host(`vt.kocoder.xyz`)
|
||||
observability:
|
||||
accessLogs: true
|
||||
metrics: true
|
||||
tracing: true
|
||||
priority: 10
|
||||
services:
|
||||
- kind: Service
|
||||
name: vt-fe
|
||||
namespace: vt
|
||||
passHostHeader: true
|
||||
port: 3000
|
||||
responseForwarding:
|
||||
flushInterval: 1ms
|
||||
scheme: http
|
||||
sticky:
|
||||
cookie:
|
||||
httpOnly: true
|
||||
name: cookie
|
||||
secure: true
|
||||
strategy: wrr
|
||||
weight: 10
|
||||
- kind: Rule
|
||||
match: Host(`vt-api.kocoder.xyz`)
|
||||
observability:
|
||||
accessLogs: true
|
||||
metrics: true
|
||||
tracing: true
|
||||
priority: 10
|
||||
services:
|
||||
- kind: Service
|
||||
name: vt-be
|
||||
namespace: vt
|
||||
passHostHeader: true
|
||||
port: 3000
|
||||
responseForwarding:
|
||||
flushInterval: 1ms
|
||||
scheme: http
|
||||
sticky:
|
||||
cookie:
|
||||
httpOnly: true
|
||||
name: cookie
|
||||
secure: true
|
||||
strategy: wrr
|
||||
weight: 10
|
||||
- kind: Rule
|
||||
match: Host(`vt-rpc.kocoder.xyz`)
|
||||
observability:
|
||||
accessLogs: true
|
||||
metrics: true
|
||||
tracing: true
|
||||
priority: 10
|
||||
services:
|
||||
- kind: Service
|
||||
name: vt-be
|
||||
namespace: vt
|
||||
passHostHeader: true
|
||||
port: 3002
|
||||
responseForwarding:
|
||||
flushInterval: 1ms
|
||||
scheme: http
|
||||
sticky:
|
||||
cookie:
|
||||
httpOnly: true
|
||||
name: cookie
|
||||
secure: true
|
||||
strategy: wrr
|
||||
weight: 10
|
||||
tls:
|
||||
secretName: vt-tls
|
||||
11786
package-lock.json
generated
11786
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
161
package.json
161
package.json
@@ -4,21 +4,27 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --port 3001 --host",
|
||||
"start": "node .output/server/index.mjs",
|
||||
"start": "node dist/server/server.js",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"serve": "NODE_ENV=production node ./server.js",
|
||||
"dev:server": "node ./server.js",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint",
|
||||
"format": "prettier",
|
||||
"check": "prettier --write . && eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bitnoi.se/react-scheduler": "^0.3.1",
|
||||
"@connectrpc/connect": "^2.1.0",
|
||||
"@connectrpc/connect-query": "^2.2.0",
|
||||
"@connectrpc/connect-web": "^2.1.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@dragdroptouch/drag-drop-touch": "^2.0.3",
|
||||
"@faker-js/faker": "^9.6.0",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -34,81 +40,100 @@
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@t3-oss/env-core": "^0.12.0",
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
"@t3-oss/env-core": "^0.13.8",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/react-devtools": "^0.2.2",
|
||||
"@tanstack/react-form": "^1.0.0",
|
||||
"@tanstack/react-query": "^5.66.5",
|
||||
"@tanstack/react-query-devtools": "^5.84.2",
|
||||
"@tanstack/react-router": "^1.130.2",
|
||||
"@tanstack/react-router-devtools": "^1.131.5",
|
||||
"@tanstack/react-router-ssr-query": "^1.131.7",
|
||||
"@tanstack/react-start": "^1.131.7",
|
||||
"@tanstack/react-store": "^0.7.0",
|
||||
"@tanstack/nitro-v2-vite-plugin": "^1.139.0",
|
||||
"@tanstack/react-devtools": "^0.8",
|
||||
"@tanstack/react-form": "^1.23.8",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-router": "^1.134.13",
|
||||
"@tanstack/react-router-devtools": "^1.134.13",
|
||||
"@tanstack/react-router-ssr-query": "^1.134.13",
|
||||
"@tanstack/react-start": "^1.134.14",
|
||||
"@tanstack/react-store": "^0.8.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@tanstack/router-plugin": "^1.121.2",
|
||||
"@tanstack/store": "^0.7.0",
|
||||
"@tanstack/zod-adapter": "^1.133.36",
|
||||
"@tiptap/extension-blockquote": "^3.4.2",
|
||||
"@tiptap/extension-bold": "^3.4.2",
|
||||
"@tiptap/extension-bullet-list": "^3.4.2",
|
||||
"@tiptap/extension-code": "^3.4.2",
|
||||
"@tiptap/extension-code-block": "^3.4.2",
|
||||
"@tiptap/extension-details": "^3.4.2",
|
||||
"@tiptap/extension-emoji": "^3.4.2",
|
||||
"@tiptap/extension-hard-break": "^3.4.2",
|
||||
"@tiptap/extension-heading": "^3.4.2",
|
||||
"@tiptap/extension-highlight": "^3.4.2",
|
||||
"@tiptap/extension-horizontal-rule": "^3.4.2",
|
||||
"@tiptap/extension-italic": "^3.4.2",
|
||||
"@tiptap/extension-link": "^3.4.2",
|
||||
"@tiptap/extension-list-item": "^3.4.2",
|
||||
"@tiptap/extension-mathematics": "^3.4.2",
|
||||
"@tiptap/extension-mention": "^3.4.2",
|
||||
"@tiptap/extension-ordered-list": "^3.4.2",
|
||||
"@tiptap/extension-paragraph": "^3.4.2",
|
||||
"@tiptap/extension-strike": "^3.4.2",
|
||||
"@tiptap/extension-subscript": "^3.4.2",
|
||||
"@tiptap/extension-superscript": "^3.4.2",
|
||||
"@tiptap/extension-table": "^3.4.2",
|
||||
"@tiptap/extension-task-item": "^3.4.2",
|
||||
"@tiptap/extension-task-list": "^3.4.2",
|
||||
"@tiptap/extension-text-style": "^3.4.2",
|
||||
"@tiptap/extension-underline": "^3.4.2",
|
||||
"@tiptap/extensions": "^3.4.2",
|
||||
"@tiptap/pm": "^3.4.2",
|
||||
"@tiptap/react": "^3.4.2",
|
||||
"@tiptap/starter-kit": "^3.4.2",
|
||||
"@tanstack/router-plugin": "^1.134.14",
|
||||
"@tanstack/store": "^0.8.0",
|
||||
"@tanstack/zod-adapter": "^1.134.13",
|
||||
"@tiptap/extension-blockquote": "^3.10.3",
|
||||
"@tiptap/extension-bold": "^3.10.3",
|
||||
"@tiptap/extension-bullet-list": "^3.10.3",
|
||||
"@tiptap/extension-code": "^3.10.3",
|
||||
"@tiptap/extension-code-block": "^3.10.3",
|
||||
"@tiptap/extension-details": "^3.10.3",
|
||||
"@tiptap/extension-emoji": "^3.10.3",
|
||||
"@tiptap/extension-hard-break": "^3.10.3",
|
||||
"@tiptap/extension-heading": "^3.10.3",
|
||||
"@tiptap/extension-highlight": "^3.10.3",
|
||||
"@tiptap/extension-horizontal-rule": "^3.10.3",
|
||||
"@tiptap/extension-italic": "^3.10.3",
|
||||
"@tiptap/extension-link": "^3.10.3",
|
||||
"@tiptap/extension-list-item": "^3.10.3",
|
||||
"@tiptap/extension-mathematics": "^3.10.3",
|
||||
"@tiptap/extension-mention": "^3.10.3",
|
||||
"@tiptap/extension-ordered-list": "^3.10.3",
|
||||
"@tiptap/extension-paragraph": "^3.10.3",
|
||||
"@tiptap/extension-strike": "^3.10.3",
|
||||
"@tiptap/extension-subscript": "^3.10.3",
|
||||
"@tiptap/extension-superscript": "^3.10.3",
|
||||
"@tiptap/extension-table": "^3.10.3",
|
||||
"@tiptap/extension-task-item": "^3.10.3",
|
||||
"@tiptap/extension-task-list": "^3.10.3",
|
||||
"@tiptap/extension-text-style": "^3.10.3",
|
||||
"@tiptap/extension-underline": "^3.10.3",
|
||||
"@tiptap/extensions": "^3.10.3",
|
||||
"@tiptap/pm": "^3.10.3",
|
||||
"@tiptap/react": "^3.10.3",
|
||||
"@tiptap/starter-kit": "^3.10.3",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^24.10.1",
|
||||
"@unpic/pixels": "^1.3.0",
|
||||
"@unpic/placeholder": "^0.1.2",
|
||||
"@vercel/og": "^0.8.5",
|
||||
"blurhash": "^2.0.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"compression": "^1.8.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.476.0",
|
||||
"react": "^19.0.0",
|
||||
"express": "^5.2.1",
|
||||
"get-port": "^7.1.0",
|
||||
"lucide-react": "^0.553.0",
|
||||
"maplibre-gl": "^5.15.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"nitro": "^3.0.1-alpha.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"oidc-spa": "^9.1.3",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-resizable-panels": "^4.4.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.6",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"zod": "^3.24.2"
|
||||
"webcrypto-liner": "^0.1.38",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/devtools-event-client": "^0.2.1",
|
||||
"@tanstack/eslint-config": "^0.3.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"jsdom": "^26.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.0.5",
|
||||
"web-vitals": "^4.2.4"
|
||||
"@bufbuild/protobuf": "^2.10.1",
|
||||
"@tanstack/devtools-event-client": "^0.3.4",
|
||||
"@tanstack/eslint-config": "^0.3.2",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"baseline-browser-mapping": "^2.9.14",
|
||||
"jsdom": "^27.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7",
|
||||
"vitest": "^4.0.8",
|
||||
"web-vitals": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { registerGlobalMiddleware } from '@tanstack/react-start'
|
||||
|
||||
registerGlobalMiddleware({
|
||||
middleware: [],
|
||||
})
|
||||
27
src/components/404.tsx
Normal file
27
src/components/404.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
function NotFound() {
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-auto flex-col justify-center px-6 py-24 sm:py-64 lg:px-8">
|
||||
<p className="text-base/8 font-semibold text-indigo-600 dark:text-indigo-400">
|
||||
404
|
||||
</p>
|
||||
<h1 className="mt-4 text-pretty text-5xl font-semibold tracking-tight text-gray-900 sm:text-6xl dark:text-white">
|
||||
Page not found
|
||||
</h1>
|
||||
<p className="mt-6 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8 dark:text-gray-400">
|
||||
Sorry, we couldn’t find the page you’re looking for.
|
||||
</p>
|
||||
<div className="mt-10">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="text-sm/7 font-semibold text-indigo-600 dark:text-indigo-400"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to home
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFound
|
||||
@@ -24,11 +24,6 @@ import {
|
||||
import { NavProjects } from '@/features/Projects/components/list'
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: 'shadcn',
|
||||
email: 'm@example.com',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/124599?v=4',
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
@@ -89,7 +84,7 @@ const data = {
|
||||
},
|
||||
{
|
||||
title: 'Kalender',
|
||||
url: '/kalendar',
|
||||
url: '/calendar',
|
||||
icon: Settings2,
|
||||
},
|
||||
{
|
||||
@@ -155,7 +150,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
<NavUser />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Link, useRouterState } from '@tanstack/react-router'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
@@ -7,10 +7,23 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from './ui/breadcrumb'
|
||||
import { useCurrentMandant } from '@/features/Mandant/queries'
|
||||
import { Fragment } from 'react/jsx-runtime'
|
||||
import { useQuery } from '@connectrpc/connect-query'
|
||||
import { getCurrentTenant } from '@/gen/mandant/v1/mandant-MandantService_connectquery'
|
||||
|
||||
function Breadcrumbs() {
|
||||
const { data: currentMandant } = useCurrentMandant()
|
||||
const { data: currentMandant } = useQuery(getCurrentTenant, {}, {})
|
||||
|
||||
const matches = useRouterState({ select: (s) => s.matches })
|
||||
|
||||
const breadcrumbs = matches
|
||||
.filter((match) => match.context.breadcrumb)
|
||||
.map(({ pathname, context }) => {
|
||||
return {
|
||||
title: context.breadcrumb,
|
||||
path: pathname,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
@@ -23,9 +36,18 @@ function Breadcrumbs() {
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Dashboard</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
{breadcrumbs.map(({ title, path }, index) => (
|
||||
<Fragment key={path}>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>
|
||||
<Link to={path}>{title}</Link>
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
{index < breadcrumbs.length - 1 && ( // Conditional separator
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
import * as React from 'react'
|
||||
import { ChevronDownIcon } from 'lucide-react'
|
||||
import type { DateRange } from 'react-day-picker'
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Calendar } from '@/components/ui/calendar'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
} from '@/components/ui/popover'
|
||||
|
||||
export default function Calendar23() {
|
||||
const [range, setRange] = React.useState<DateRange | undefined>(undefined)
|
||||
@@ -30,7 +30,7 @@ export default function Calendar23() {
|
||||
>
|
||||
{range?.from && range?.to
|
||||
? `${range.from.toLocaleDateString()} - ${range.to.toLocaleDateString()}`
|
||||
: "Select date"}
|
||||
: 'Select date'}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
64
src/components/calendar-24.tsx
Normal file
64
src/components/calendar-24.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { ChevronDownIcon } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Calendar } from '@/components/ui/calendar'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
|
||||
export default function Calendar24({ defaultDate }: { defaultDate: Date }) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(defaultDate)
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-3">
|
||||
<Label htmlFor="date-picker" className="px-1">
|
||||
Date
|
||||
</Label>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date-picker"
|
||||
className="w-32 justify-between font-normal"
|
||||
>
|
||||
{date ? date.toLocaleDateString() : 'Select date'}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDate(date)
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Label htmlFor="time-picker" className="px-1">
|
||||
Time
|
||||
</Label>
|
||||
<Input
|
||||
type="time"
|
||||
id="time-picker"
|
||||
step="1"
|
||||
defaultValue="10:30:00"
|
||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
src/components/calendar.tsx
Normal file
52
src/components/calendar.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { addDays, format, startOfWeek } from 'date-fns'
|
||||
|
||||
const daysInWeek = 7
|
||||
const weeksInView = 5
|
||||
const startOfCalendar = startOfWeek(new Date(), { weekStartsOn: 1 })
|
||||
|
||||
function CalendarEntry() {
|
||||
const date = new Date()
|
||||
const dayOfTheMonth = date.getDate() - 1
|
||||
return (
|
||||
<div
|
||||
className={`absolute top-10 bg-emerald-500 w-14`}
|
||||
style={{
|
||||
left: (((dayOfTheMonth % daysInWeek) * 1) / daysInWeek) * 100 + `%`,
|
||||
}}
|
||||
>
|
||||
a
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Calendar() {
|
||||
return (
|
||||
<div
|
||||
className={`bg-muted grid grid-cols-${daysInWeek} gap-1 border-4 border-muted h-full w-full select-none relative`}
|
||||
>
|
||||
{new Array(weeksInView * daysInWeek).fill(0).map((v, iw) => {
|
||||
const date = addDays(startOfCalendar, iw)
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
date.getMonth() === startOfCalendar.getMonth()
|
||||
? 'bg-background'
|
||||
: 'bg-background/40'
|
||||
}
|
||||
>
|
||||
{format(date, 'dd')}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="absolute top-0 left-0 w-full h-full">
|
||||
<CalendarEntry />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Blank() {
|
||||
return <div className="grid-cols-7"></div>
|
||||
}
|
||||
|
||||
export default Calendar
|
||||
@@ -68,10 +68,11 @@ export function DataTable<TData, TValue>({
|
||||
|
||||
const flatData = useMemo(() => {
|
||||
const res = data?.pages.flatMap((page) => page.data) ?? []
|
||||
// console.log(res)
|
||||
return res
|
||||
}, [data])
|
||||
|
||||
const totalDBRowCount = data?.pages[0]?.meta?.totalProjectsCount ?? 0
|
||||
const totalDBRowCount = data?.pages[0]?.meta?.totalCount ?? 0
|
||||
const totalFetched = flatData.length
|
||||
|
||||
// called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
|
||||
@@ -80,17 +81,20 @@ export function DataTable<TData, TValue>({
|
||||
if (containerRefElement) {
|
||||
const { scrollHeight, scrollTop, clientHeight } = containerRefElement
|
||||
// once the user has scrolled within 500px of the bottom of the table, fetch more data if we can
|
||||
console.log(
|
||||
scrollHeight,
|
||||
scrollTop,
|
||||
clientHeight,
|
||||
isFetching,
|
||||
totalFetched,
|
||||
totalDBRowCount,
|
||||
)
|
||||
// console.log(
|
||||
// scrollHeight,
|
||||
// scrollTop,
|
||||
// clientHeight,
|
||||
// isFetching,
|
||||
// totalFetched,
|
||||
// totalDBRowCount,
|
||||
// scrollHeight - scrollTop - clientHeight < 300,
|
||||
// !isFetching,
|
||||
// totalFetched < totalDBRowCount,
|
||||
// )
|
||||
|
||||
if (
|
||||
scrollHeight - scrollTop - clientHeight < 100 &&
|
||||
scrollHeight - scrollTop - clientHeight < 300 &&
|
||||
!isFetching &&
|
||||
totalFetched < totalDBRowCount
|
||||
) {
|
||||
@@ -124,14 +128,14 @@ export function DataTable<TData, TValue>({
|
||||
return (
|
||||
<div className="h-full max-h-full" id="">
|
||||
<div className="flex items-center py-4">
|
||||
<Input
|
||||
{/* <Input
|
||||
placeholder="Filter names..."
|
||||
value={table.getColumn('name')?.getFilterValue() as string}
|
||||
onChange={(event) =>
|
||||
table.getColumn('name')?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
/> */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createKeycloakUtils, isKeycloak } from 'oidc-spa/keycloak'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Field, FieldDescription, FieldGroup } from '@/components/ui/field'
|
||||
import { env } from '@/env'
|
||||
import { useOidc } from '@/lib/oidc'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import NotFound from './404'
|
||||
|
||||
function Logo() {
|
||||
return (
|
||||
@@ -16,6 +19,7 @@ function Logo() {
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className='h-full w-full'
|
||||
>
|
||||
<line x1="100" y1="80" x2="420" y2="80" />
|
||||
<polyline points="100,80 160,40 220,80 280,40 340,80 400,40 420,80" />
|
||||
@@ -43,6 +47,18 @@ export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
const { isUserLoggedIn, isOidcReady } = useOidc()
|
||||
|
||||
if (!isOidcReady || isUserLoggedIn) {
|
||||
return <NotFound />
|
||||
}
|
||||
|
||||
return <Login className={className} {...props} />
|
||||
}
|
||||
|
||||
function Login({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
const { login } = useOidc({ assert: 'user not logged in' })
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
@@ -57,29 +73,26 @@ export function LoginForm({
|
||||
</p>
|
||||
</div>
|
||||
<Field className="">
|
||||
<Button variant="outline" type="button" asChild>
|
||||
<a href={`${env.VITE_BACKEND_URI}/api/auth`}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="">Login with Che</span>
|
||||
</a>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={() => login({ redirectUrl: '/dashboard' })}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="">Login with Che</span>
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<div className="bg-muted relative hidden md:block">
|
||||
<img
|
||||
src="/placeholder.svg"
|
||||
alt="Image"
|
||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
<div className="absolute relative top-1/2 left-1/2 h-32 w-32 max-h-32 max-w-32 -translate-x-1/2 -translate-y-1/2 opacity-10">
|
||||
<Logo />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -90,3 +103,4 @@ export function LoginForm({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
1106
src/components/ui/kanban.tsx
Normal file
1106
src/components/ui/kanban.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1305
src/components/ui/map.tsx
Normal file
1305
src/components/ui/map.tsx
Normal file
File diff suppressed because it is too large
Load Diff
54
src/components/ui/resizable.tsx
Normal file
54
src/components/ui/resizable.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as React from "react"
|
||||
import { GripVerticalIcon } from "lucide-react"
|
||||
import * as ResizablePrimitive from "react-resizable-panels"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ResizablePanelGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.Group>) {
|
||||
return (
|
||||
<ResizablePrimitive.Group
|
||||
data-slot="resizable-panel-group"
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ResizablePanel({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
||||
}
|
||||
|
||||
function ResizableHandle({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
|
||||
withHandle?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ResizablePrimitive.Separator
|
||||
data-slot="resizable-handle"
|
||||
className={cn(
|
||||
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||
<GripVerticalIcon className="size-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.Separator>
|
||||
)
|
||||
}
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||
10
src/env.ts
10
src/env.ts
@@ -1,4 +1,5 @@
|
||||
import { createEnv } from '@t3-oss/env-core'
|
||||
import { vite } from '@t3-oss/env-core/presets-zod'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const env = createEnv({
|
||||
@@ -14,14 +15,17 @@ export const env = createEnv({
|
||||
|
||||
client: {
|
||||
VITE_APP_TITLE: z.string().min(1).optional(),
|
||||
VITE_BACKEND_URI: z.string(),
|
||||
VITE_RPC_URI: z.string().optional(),
|
||||
VITE_BACKEND_URI: z.string().optional(),
|
||||
VITE_ENVIRONMENT: z.string().optional(),
|
||||
},
|
||||
|
||||
/**
|
||||
* What object holds the environment variables at runtime. This is usually
|
||||
* `process.env` or `import.meta.env`.
|
||||
*/
|
||||
runtimeEnv: import.meta.env,
|
||||
runtimeEnv: process.env || import.meta.env,
|
||||
// extends: [vite()],
|
||||
|
||||
/**
|
||||
* By default, this library will feed the environment variables directly to
|
||||
@@ -38,3 +42,5 @@ export const env = createEnv({
|
||||
*/
|
||||
emptyStringAsUndefined: true,
|
||||
})
|
||||
|
||||
console.log(env)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { BadgeCheck, Bell, ChevronsUpDown, LogOut } from 'lucide-react'
|
||||
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useProfile } from '../queries'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -20,23 +17,14 @@ import {
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { env } from '@/env'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useOidc } from '@/lib/oidc'
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
}) {
|
||||
export function NavUser() {
|
||||
const { logout, decodedIdToken } = useOidc()
|
||||
const { isMobile } = useSidebar()
|
||||
const { data } = useProfile()
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
if (!logout) return
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
@@ -48,12 +36,17 @@ export function NavUser({
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={data.picture} alt={data.name} />
|
||||
<AvatarImage
|
||||
src={decodedIdToken.picture}
|
||||
alt={decodedIdToken.name}
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">KH</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{data.name}</span>
|
||||
<span className="truncate text-xs">{data.Email}</span>
|
||||
<span className="truncate font-medium">
|
||||
{decodedIdToken.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">{decodedIdToken.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
@@ -67,12 +60,19 @@ export function NavUser({
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={data.picture} alt={data.name} />
|
||||
<AvatarImage
|
||||
src={decodedIdToken.picture}
|
||||
alt={decodedIdToken.name}
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{data.name}</span>
|
||||
<span className="truncate text-xs">{data.Email}</span>
|
||||
<span className="truncate font-medium">
|
||||
{decodedIdToken.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{decodedIdToken.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
@@ -99,12 +99,17 @@ export function NavUser({
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<a href={`${env.VITE_BACKEND_URI}/api/auth/logout`}>
|
||||
<Button
|
||||
onClick={() => logout({ redirectTo: 'specific url', url: '/' })}
|
||||
variant={'ghost'}
|
||||
className="w-full"
|
||||
asChild
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</a>
|
||||
</Button>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { env } from '@/env'
|
||||
import { fetchWithAuth } from '@/lib/oidc'
|
||||
import { getBackendURI } from '@/routes'
|
||||
|
||||
const sessionKeys = {
|
||||
all: ['sessions'] as const,
|
||||
@@ -28,8 +29,8 @@ export function useCurrentSession() {
|
||||
return useQuery<Session>({
|
||||
queryKey: sessionKeys.current(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(
|
||||
env.VITE_BACKEND_URI + '/api/auth/currentSession',
|
||||
const data = await fetchWithAuth(
|
||||
(await getBackendURI()) + '/api/auth/currentSession',
|
||||
{
|
||||
credentials: 'include',
|
||||
},
|
||||
@@ -41,17 +42,18 @@ export function useCurrentSession() {
|
||||
|
||||
export function useProfile() {
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return useQuery<User>({
|
||||
queryKey: sessionKeys.current(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(env.VITE_BACKEND_URI + '/v1/users/current', {
|
||||
credentials: 'include',
|
||||
})
|
||||
const data = await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/users/current',
|
||||
{
|
||||
credentials: 'include',
|
||||
},
|
||||
)
|
||||
if (data.status == 401) {
|
||||
queryClient.invalidateQueries()
|
||||
navigate({ to: '/' })
|
||||
}
|
||||
return await data.json()
|
||||
},
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
useQueryClient,
|
||||
useSuspenseQuery,
|
||||
} from '@tanstack/react-query'
|
||||
import { env } from '@/env'
|
||||
import { fetchWithAuth } from '@/lib/oidc'
|
||||
import { getBackendURI } from '@/routes'
|
||||
|
||||
const ansprechpartnerKeys = {
|
||||
all: ['ansprechpartner'] as const,
|
||||
@@ -36,8 +37,8 @@ export function useAllAnsprechpartners() {
|
||||
return useQuery<Ansprechpartner>({
|
||||
queryKey: ansprechpartnerKeys.lists(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(
|
||||
env.VITE_BACKEND_URI + '/v1/ansprechpartner/all',
|
||||
const data = await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/ansprechpartner/all',
|
||||
{
|
||||
credentials: 'include',
|
||||
},
|
||||
@@ -51,8 +52,8 @@ export function useAnsprechpartner(id: number) {
|
||||
return useSuspenseQuery<Ansprechpartner>({
|
||||
queryKey: ansprechpartnerKeys.detail(id),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(
|
||||
env.VITE_BACKEND_URI + '/v1/ansprechpartner/' + id,
|
||||
const data = await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/ansprechpartner/' + id,
|
||||
{ credentials: 'include' },
|
||||
)
|
||||
return await data.json()
|
||||
@@ -65,8 +66,8 @@ export function useAnsprechpartnerEditMutation() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (ansprechpartner: Ansprechpartner) => {
|
||||
await fetch(
|
||||
env.VITE_BACKEND_URI + '/v1/ansprechpartner/' + ansprechpartner.ID,
|
||||
await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/ansprechpartner/' + ansprechpartner.ID,
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
|
||||
@@ -1,11 +1,135 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { createContext, Fragment, useContext, type ReactElement, type ReactNode } from 'react'
|
||||
import KanbanDropzone from './KanbanDropzone'
|
||||
import { DndContext } from '@dnd-kit/core'
|
||||
import {
|
||||
Sortable,
|
||||
SortableContent,
|
||||
SortableItem,
|
||||
SortableOverlay,
|
||||
} from "@/components/ui/sortable";
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
|
||||
function Kanban({ children }: { children: ReactNode }) {
|
||||
type ColumnRendererProps = {
|
||||
name: string
|
||||
itemCount: number
|
||||
}
|
||||
|
||||
type KanbanContextType = {
|
||||
columns: any[]
|
||||
setColumns: (cols: any[]) => void
|
||||
columnRenderer: ReactElement<ColumnRendererProps, any>
|
||||
}
|
||||
|
||||
const kanbanContext = createContext<KanbanContextType>({ columns: [], setColumns: () => {}, columnRenderer: <></> });
|
||||
|
||||
export const useKanban = () => {
|
||||
const ctx = useContext(kanbanContext)
|
||||
|
||||
const reorderColumns = (name: string, toIndex: number) => {
|
||||
const fromIndex = ctx.columns.findIndex((col: any) => col.name == name);
|
||||
console.log(fromIndex, toIndex, ctx.columns)
|
||||
let fromCompIndex;
|
||||
if (toIndex < fromIndex) {
|
||||
fromCompIndex = fromIndex - 1;
|
||||
} else {
|
||||
fromCompIndex = 0;
|
||||
}
|
||||
|
||||
if (fromIndex === -1 || toIndex < 0 || toIndex >= ctx.columns.length) { console.log("invalid"); return; }
|
||||
|
||||
|
||||
const updatedColumns = [...ctx.columns];
|
||||
|
||||
const movedColumn = ctx.columns[fromIndex];
|
||||
updatedColumns.splice(toIndex, 0, movedColumn);
|
||||
console.log("valid", movedColumn, fromIndex, fromCompIndex, toIndex, updatedColumns);
|
||||
updatedColumns.splice(fromCompIndex, 1);
|
||||
console.log(movedColumn, fromIndex, fromCompIndex, toIndex, updatedColumns);
|
||||
|
||||
ctx.setColumns(updatedColumns);
|
||||
}
|
||||
|
||||
return { reorderColumns, ...ctx }
|
||||
}
|
||||
|
||||
export type KanbanColumn = {
|
||||
id: number,
|
||||
name: string
|
||||
}
|
||||
|
||||
interface TrickCardProps
|
||||
extends Omit<React.ComponentPropsWithoutRef<typeof SortableItem>, "value"> {
|
||||
trick: KanbanColumn;
|
||||
}
|
||||
|
||||
function TrickCard({ trick, ...props }: TrickCardProps) {
|
||||
return (
|
||||
<div className="box-border flex gap-2 h-full min-w-full max-w-screen overflow-x-auto snap-x snap-mandatory md:snap-none">
|
||||
{children}
|
||||
</div>
|
||||
<SortableItem value={trick.id} asChild {...props}>
|
||||
<div className="block flex-col gap-1 w-sm shrink-0 rounded-md border bg-zinc-100 p-4 text-foreground shadow-sm dark:bg-zinc-900">
|
||||
<div className="font-medium text-sm leading-tight sm:text-base">
|
||||
{trick.name}
|
||||
</div>
|
||||
<span className="line-clamp-2 hidden text-muted-foreground text-sm sm:inline-block">
|
||||
{trick.id}
|
||||
</span>
|
||||
</div>
|
||||
</SortableItem>
|
||||
);
|
||||
}
|
||||
|
||||
function KanbanOLD({ columns, setColumns, columnRenderer }: { columns: KanbanColumn[], setColumns: (v: KanbanColumn[]) => void, columnRenderer: ReactElement<ColumnRendererProps,any> }) {
|
||||
return (
|
||||
<Sortable value={columns} onValueChange={setColumns} getItemValue={(c) => c.id} orientation='horizontal'>
|
||||
<SortableContent className="flex flex-row gap-2.5 h-full bg-emerald-500 min-w-full overflow-scroll">
|
||||
{columns.map((c) => <TrickCard key={c.id} trick={c} asHandle />)}
|
||||
</SortableContent>
|
||||
<SortableOverlay>
|
||||
{(activeItem) => {
|
||||
const c = columns.find((c) => c.id === activeItem.value);
|
||||
|
||||
if (!c) return null;
|
||||
|
||||
return <TrickCard trick={c} />;
|
||||
}}
|
||||
</SortableOverlay>
|
||||
</Sortable>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function KanbanInner() {
|
||||
const { columns, columnRenderer } = useKanban();
|
||||
|
||||
return (
|
||||
<div className="box-border flex gap-2 h-full min-w-full max-w-screen overflow-x-auto snap-x snap-mandatory md:snap-none">
|
||||
<KanbanDropzone idx={1} />
|
||||
{columns.map((column, index) => (
|
||||
<Fragment key={column.id}>
|
||||
<div className="snap-start md:snap-none">
|
||||
{<columnRenderer.type name={column.name} value={String(column.id)} />}
|
||||
</div>
|
||||
<KanbanDropzone idx={index + 2} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Kanban
|
||||
|
||||
@@ -1,59 +1,91 @@
|
||||
import { listTodos } from '@/gen/todo/v1/todo-TodoService_connectquery'
|
||||
import { Field, Operation } from '@/gen/todo/v1/todo_pb'
|
||||
import { useSuspenseQuery } from '@connectrpc/connect-query'
|
||||
import { useDroppable } from '@dnd-kit/core'
|
||||
import { GripVertical, Tickets } from 'lucide-react'
|
||||
import { useRef } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { Suspense, useRef } from 'react'
|
||||
|
||||
function KanbanColumn({
|
||||
children,
|
||||
name,
|
||||
itemCount,
|
||||
value,
|
||||
}: {
|
||||
children: ReactNode
|
||||
name: string
|
||||
itemCount: number
|
||||
name: string,
|
||||
value: string,
|
||||
}) {
|
||||
const column = useRef<HTMLDivElement>(null)
|
||||
// const column = useRef<HTMLDivElement>(null)
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: name
|
||||
})
|
||||
|
||||
const handleDraggableStart = () => {
|
||||
column.current?.setAttribute('draggable', 'true')
|
||||
}
|
||||
// const handleDragStart = (e: any) => {
|
||||
// e.dataTransfer?.setData('text/custom', name)
|
||||
// }
|
||||
|
||||
const handleDraggableStop = () => {
|
||||
column.current?.setAttribute('draggable', 'false')
|
||||
}
|
||||
|
||||
const handleDragStart = (e: DragEvent) => {
|
||||
e.dataTransfer?.setData('text/custom', 'ASDF')
|
||||
}
|
||||
|
||||
const handleDragStop = () => {
|
||||
column.current?.setAttribute('draggable', 'false')
|
||||
}
|
||||
// const handleDragStop = () => {
|
||||
// column.current?.setAttribute('draggable', 'false')
|
||||
// }
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-w-96 bg-accent rounded-md p-4 snap-center"
|
||||
ref={column}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragStop}
|
||||
className="min-w-96 h-full bg-accent rounded-md p-4 snap-center"
|
||||
ref={setNodeRef}
|
||||
>
|
||||
<div className="flex justify-between mb-2">
|
||||
<div className="flex place-items-center">
|
||||
<GripVertical
|
||||
size={20}
|
||||
className="mr-2 cursor-grab"
|
||||
onMouseDown={handleDraggableStart}
|
||||
onMouseUp={handleDraggableStop}
|
||||
/>
|
||||
<p>{name}</p>
|
||||
</div>
|
||||
<div className="flex place-items-center gap-2">
|
||||
{itemCount}
|
||||
<Tickets size={20} />
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<KanbanColumnInner name={name} value={value} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function KanbanColumnInner({ columnRef, name, value }: { columnRef: React.RefObject<HTMLDivElement | null>, name: string, value: string }) {
|
||||
const { data } = useSuspenseQuery(listTodos, {
|
||||
perPage: 10,
|
||||
page: 0,
|
||||
orberBy: 'id',
|
||||
asc: false,
|
||||
filters: [
|
||||
{
|
||||
field: Field.FieldStatus,
|
||||
operation: Operation.Equals,
|
||||
value: value,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
const handleDraggableStart = () => {
|
||||
columnRef.current?.setAttribute('draggable', 'true')
|
||||
}
|
||||
|
||||
const handleDraggableStop = () => {
|
||||
columnRef.current?.setAttribute('draggable', 'false')
|
||||
}
|
||||
|
||||
return (<>
|
||||
<div className="flex justify-between mb-2">
|
||||
<div className="flex place-items-center">
|
||||
<GripVertical
|
||||
size={20}
|
||||
className="mr-2 cursor-grab"
|
||||
onMouseDown={handleDraggableStart}
|
||||
onMouseUp={handleDraggableStop}
|
||||
/>
|
||||
<p>{name}</p>
|
||||
</div>
|
||||
<div className="flex place-items-center gap-2">
|
||||
{data?.meta?.totalCount || 0}
|
||||
<Tickets size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[500px] overflow-y-auto">
|
||||
{data?.data.map((todo) => (
|
||||
<div key={todo.id} className="bg-muted rounded-md p-2 mb-2">
|
||||
<p className="font-semibold">{todo.title}</p>
|
||||
<p className="text-sm text-muted-foreground">{todo.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>);
|
||||
}
|
||||
|
||||
export default KanbanColumn
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
import { useRef } from 'react'
|
||||
import { useKanban } from './Kanban';
|
||||
|
||||
function KanbanDropzone() {
|
||||
function KanbanDropzone({idx}: {idx: number}) {
|
||||
const ctx = useKanban();
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleDragEnter = () => {
|
||||
console.log('DRAGOVER')
|
||||
const handleDragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
if (!ref.current) return
|
||||
ref.current.style.backgroundColor = '#41b2b2'
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
console.log('DRAGOVER')
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
if (!ref.current) return
|
||||
ref.current.style.backgroundColor = 'transparent'
|
||||
}
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!ref.current) return
|
||||
ref.current.style.backgroundColor = 'transparent'
|
||||
console.log('Dropped', e.dataTransfer.getData('text/custom'))
|
||||
ctx.reorderColumns(e.dataTransfer.getData('text/custom'), idx);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-w-1 h-full block"
|
||||
ref={ref}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { useEffect } from 'react'
|
||||
import { ChevronDown, Plus } from 'lucide-react'
|
||||
|
||||
import {
|
||||
useAllMandanten,
|
||||
useCurrentMandant,
|
||||
useCurrentMandantMutation,
|
||||
} from '../queries'
|
||||
import type { Mandant } from '../queries'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -21,12 +15,16 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { useMutation, useQuery } from '@connectrpc/connect-query'
|
||||
import { getAllTenants, getCurrentTenant, setCurrentTenant } from '@/gen/mandant/v1/mandant-MandantService_connectquery'
|
||||
import { GetTenantResponse } from '@/gen/mandant/v1/mandant_pb'
|
||||
|
||||
export function TeamSwitcher() {
|
||||
const { data: currentMandant } = useCurrentMandant()
|
||||
const { data: mandanten } = useAllMandanten()
|
||||
const { data: currentMandant } = useQuery(getCurrentTenant, {}, {})
|
||||
const { data: allMandanten } = useQuery(getAllTenants, {}, {})
|
||||
const mandanten = allMandanten?.data || []
|
||||
|
||||
const editCurrentTeamMutation = useCurrentMandantMutation()
|
||||
const editCurrentTeamMutation = useMutation(setCurrentTenant, {})
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
@@ -37,12 +35,13 @@ export function TeamSwitcher() {
|
||||
if (mandanten && currentMandant) {
|
||||
const mandant = mandanten[numKey]
|
||||
|
||||
if (mandant.ID === currentMandant.ID) {
|
||||
if (mandant.id === currentMandant.id) {
|
||||
return
|
||||
}
|
||||
console.log(mandant, currentMandant)
|
||||
|
||||
|
||||
editCurrentTeamMutation.mutate(mandanten[numKey])
|
||||
editCurrentTeamMutation.mutate({ tenantId: mandanten[numKey].id })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,17 +110,17 @@ function MandantDMI({
|
||||
currentMandant,
|
||||
index,
|
||||
}: {
|
||||
mandant: Mandant
|
||||
currentMandant: Mandant
|
||||
mandant: GetTenantResponse
|
||||
currentMandant: GetTenantResponse
|
||||
index: number
|
||||
}) {
|
||||
const editCurrentMandantMutaiton = useCurrentMandantMutation()
|
||||
const editCurrentMandantMutaiton = useMutation(setCurrentTenant, {})
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={mandant.ID}
|
||||
onClick={() => editCurrentMandantMutaiton.mutate(mandant)}
|
||||
disabled={mandant.ID === currentMandant.ID}
|
||||
key={mandant.id}
|
||||
onClick={() => editCurrentMandantMutaiton.mutate({ tenantId: mandant.id })}
|
||||
disabled={mandant.id === currentMandant.id}
|
||||
className="gap-2 p-2"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { env } from '@/env'
|
||||
import { fetchWithAuth } from '@/lib/oidc'
|
||||
import { getBackendURI } from '@/routes'
|
||||
|
||||
const mandantKeys = {
|
||||
all: ['mandant'] as const,
|
||||
@@ -19,9 +20,12 @@ export function useCurrentMandant() {
|
||||
return useQuery<Mandant>({
|
||||
queryKey: mandantKeys.current(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(env.VITE_BACKEND_URI + '/v1/mandant/current', {
|
||||
credentials: 'include',
|
||||
})
|
||||
const data = await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/mandant/current',
|
||||
{
|
||||
credentials: 'include',
|
||||
},
|
||||
)
|
||||
return await data.json()
|
||||
},
|
||||
})
|
||||
@@ -31,9 +35,12 @@ export function useAllMandanten() {
|
||||
return useQuery<Array<Mandant>>({
|
||||
queryKey: mandantKeys.lists(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(env.VITE_BACKEND_URI + '/v1/mandant/all', {
|
||||
credentials: 'include',
|
||||
})
|
||||
const data = await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/mandant/all',
|
||||
{
|
||||
credentials: 'include',
|
||||
},
|
||||
)
|
||||
return await data.json()
|
||||
},
|
||||
})
|
||||
@@ -44,19 +51,25 @@ export function useCurrentMandantMutation() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (mandant: Mandant) => {
|
||||
const res = await fetch(env.VITE_BACKEND_URI + '/v1/mandant/current', {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
const res = await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/mandant/current',
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(mandant),
|
||||
credentials: 'include',
|
||||
},
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(mandant),
|
||||
credentials: 'include',
|
||||
})
|
||||
)
|
||||
const newCurrentMandant = await res.json()
|
||||
queryClient.setQueryData(
|
||||
mandantKeys.current(),
|
||||
(_: Mandant) => newCurrentMandant,
|
||||
)
|
||||
},
|
||||
onSettled(data, error, variables, onMutateResult, context) {
|
||||
queryClient.invalidateQueries({ queryKey: [] });
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Folder, MoreHorizontal, Share, Trash2 } from 'lucide-react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useAllProjects } from '../queries'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -17,13 +16,12 @@ import {
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { useInfiniteQuery } from '@connectrpc/connect-query'
|
||||
import { listProjects } from '@/gen/project/v1/project-ProjectService_connectquery'
|
||||
|
||||
export function NavProjects() {
|
||||
const { isMobile } = useSidebar()
|
||||
const { data: projects } = useAllProjects({
|
||||
fetchSize: 5,
|
||||
sorting: [],
|
||||
})
|
||||
const { data: projects } = useInfiniteQuery(listProjects, { perPage: 5, orberBy: 'id', asc: true, page: 0 }, { getNextPageParam: () => -1, pageParamKey: 'perPage' })
|
||||
|
||||
if (!projects) {
|
||||
return <p>Loading...</p>
|
||||
@@ -36,8 +34,7 @@ export function NavProjects() {
|
||||
{projects.pages[0].data.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link to="/projects/view/$id" params={{ id: item.ID.toString() }}>
|
||||
{item.icon}
|
||||
<Link to="/projects/view/$id" params={{ id: item.id.toString() }}>
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
@@ -8,14 +8,12 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useAllProjects } from '../queries'
|
||||
import type {
|
||||
ColumnDef,
|
||||
RowSelectionState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table'
|
||||
import type { PaginatedProject } from '../queries'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -28,10 +26,26 @@ import {
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DataTableColumnHeader } from '@/components/data-table-column-header'
|
||||
import { DataTable } from '@/components/data-table'
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useSuspenseInfiniteQuery,
|
||||
} from '@connectrpc/connect-query'
|
||||
import { listProjects } from '@/gen/project/v1/project-ProjectService_connectquery'
|
||||
import { GetProjectResponse } from '@/gen/project/v1/project_pb'
|
||||
|
||||
const iconSize = 16
|
||||
|
||||
export const columnDefs: Array<ColumnDef<PaginatedProject>> = [
|
||||
const getBooleanColor = (val: boolean | undefined) => {
|
||||
if (val == null) {
|
||||
return 'black'
|
||||
} else if (val) {
|
||||
return 'var(--color-emerald-500)'
|
||||
} else {
|
||||
return 'var(--color-red-500)'
|
||||
}
|
||||
}
|
||||
|
||||
export const columnDefs: Array<ColumnDef<GetProjectResponse>> = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
@@ -56,6 +70,9 @@ export const columnDefs: Array<ColumnDef<PaginatedProject>> = [
|
||||
enableHiding: false,
|
||||
size: 26,
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column, header }) => {
|
||||
@@ -67,6 +84,14 @@ export const columnDefs: Array<ColumnDef<PaginatedProject>> = [
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row, getValue }) => {
|
||||
const project = row.original
|
||||
return (
|
||||
<Link to="/projects/view/$id" params={{ id: project.id.toString() }}>
|
||||
{getValue() as string}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
@@ -79,14 +104,30 @@ export const columnDefs: Array<ColumnDef<PaginatedProject>> = [
|
||||
{
|
||||
accessorKey: 'progress',
|
||||
header: 'Project progress',
|
||||
cell: () => {
|
||||
cell: ({ row }) => {
|
||||
const project = row.original
|
||||
return (
|
||||
<div className="flex flex-row gap-2 items-center h-4">
|
||||
<SpeakerIcon size={iconSize} />
|
||||
<CircleUserIcon size={iconSize} />
|
||||
<CircleCheckIcon size={iconSize} />
|
||||
<ReceiptEuroIcon size={iconSize} />
|
||||
<ClipboardCheckIcon size={iconSize} />
|
||||
<SpeakerIcon
|
||||
size={iconSize}
|
||||
color={getBooleanColor(project.isMaterialized)}
|
||||
/>
|
||||
<CircleUserIcon
|
||||
size={iconSize}
|
||||
color={getBooleanColor(project.isPersonalized)}
|
||||
/>
|
||||
<CircleCheckIcon
|
||||
size={iconSize}
|
||||
color={getBooleanColor(project.isConfirmed)}
|
||||
/>
|
||||
<ReceiptEuroIcon
|
||||
size={iconSize}
|
||||
color={getBooleanColor(project.isPaid)}
|
||||
/>
|
||||
<ClipboardCheckIcon
|
||||
size={iconSize}
|
||||
color={getBooleanColor(project.isDone)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -134,7 +175,7 @@ export const columnDefs: Array<ColumnDef<PaginatedProject>> = [
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(project.ID.toString())
|
||||
navigator.clipboard.writeText(project.id.toString())
|
||||
}
|
||||
>
|
||||
Copy project ID
|
||||
@@ -143,7 +184,7 @@ export const columnDefs: Array<ColumnDef<PaginatedProject>> = [
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
to="/projects/view/$id"
|
||||
params={{ id: project.ID.toString() }}
|
||||
params={{ id: project.id.toString() }}
|
||||
>
|
||||
View Project
|
||||
</Link>
|
||||
@@ -159,7 +200,9 @@ export const columnDefs: Array<ColumnDef<PaginatedProject>> = [
|
||||
const fetchSize = 25
|
||||
|
||||
function ProjectsTable() {
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{ id: 'id', desc: false },
|
||||
])
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
description: false,
|
||||
client: false,
|
||||
@@ -171,10 +214,25 @@ function ProjectsTable() {
|
||||
|
||||
const [selected, setSelected] = useState<RowSelectionState>({})
|
||||
|
||||
const { data, fetchNextPage, isFetching, isLoading } = useAllProjects({
|
||||
fetchSize,
|
||||
sorting,
|
||||
})
|
||||
const { data, fetchNextPage, isFetching, isLoading } =
|
||||
useSuspenseInfiniteQuery(
|
||||
listProjects,
|
||||
{
|
||||
$typeName: 'project.v1.ListProjectsRequest',
|
||||
page: 0,
|
||||
perPage: fetchSize,
|
||||
orberBy: 'id',
|
||||
asc: true,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (_, p) => {
|
||||
console.log(p.length)
|
||||
return p.length * fetchSize
|
||||
// return ()
|
||||
},
|
||||
pageParamKey: 'page',
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
|
||||
112
src/features/Projects/components/timelinetable.tsx
Normal file
112
src/features/Projects/components/timelinetable.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useState } from 'react'
|
||||
import { useParams } from '@tanstack/react-router'
|
||||
import { useAllTimelineEntries } from '../queries'
|
||||
import type { Paginated, TimelineEntry } from '../queries'
|
||||
import type {
|
||||
ColumnDef,
|
||||
RowSelectionState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { DataTableColumnHeader } from '@/components/data-table-column-header'
|
||||
import { DataTable } from '@/components/data-table'
|
||||
import Calendar24 from '@/components/calendar-24'
|
||||
|
||||
export const columnDefs: Array<ColumnDef<Paginated<TimelineEntry>>> = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
className="aspect-square"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 26,
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column, header }) => {
|
||||
return (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title="Name"
|
||||
style={{ width: header.getSize() }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'from',
|
||||
header: 'From',
|
||||
cell: ({ getValue }) => {
|
||||
return <Calendar24 defaultDate={getValue()} />
|
||||
},
|
||||
size: 400,
|
||||
},
|
||||
{
|
||||
accessorKey: 'thru',
|
||||
header: 'Thru',
|
||||
cell: ({ getValue }) => {
|
||||
return <Calendar24 defaultDate={getValue()} />
|
||||
},
|
||||
size: 400,
|
||||
},
|
||||
]
|
||||
|
||||
const fetchSize = 25
|
||||
|
||||
function TimelineTable() {
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
description: false,
|
||||
client: false,
|
||||
location: false,
|
||||
type: false,
|
||||
enddate: false,
|
||||
startdate: false,
|
||||
})
|
||||
|
||||
const [selected, setSelected] = useState<RowSelectionState>({})
|
||||
|
||||
const { id } = useParams({ from: '/_sidebar/projects/view/$id/timeline' })
|
||||
|
||||
const { data, fetchNextPage, isFetching, isLoading } = useAllTimelineEntries({
|
||||
projectId: Number(id),
|
||||
fetchSize,
|
||||
sorting,
|
||||
})
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columnDefs}
|
||||
data={data}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetching={isFetching}
|
||||
isLoading={isLoading}
|
||||
setSorting={setSorting}
|
||||
sorting={sorting}
|
||||
rowSelection={selected}
|
||||
setRowSelection={setSelected}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineTable
|
||||
152
src/features/Projects/components/todotable.tsx
Normal file
152
src/features/Projects/components/todotable.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { useState } from 'react'
|
||||
import { useParams } from '@tanstack/react-router'
|
||||
import type {
|
||||
ColumnDef,
|
||||
RowSelectionState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { DataTableColumnHeader } from '@/components/data-table-column-header'
|
||||
import { DataTable } from '@/components/data-table'
|
||||
import { useSuspenseInfiniteQuery } from '@connectrpc/connect-query'
|
||||
import { listTodos } from '@/gen/todo/v1/todo-TodoService_connectquery'
|
||||
import { ListProjectsResponse } from '@/gen/project/v1/project_pb'
|
||||
import { ListTodosResponse, Status } from '@/gen/todo/v1/todo_pb'
|
||||
|
||||
const getColorFromTodoState = (state: Status) => {
|
||||
switch (state) {
|
||||
case Status.Todo:
|
||||
return 'data-[state=checked]:bg-yellow-500 data-[state=checked]:border-yellow-500 bg-yellow-500 border-yellow-500'
|
||||
case Status.NeedsMoreInfo:
|
||||
return 'data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500 bg-orange-500 border-orange-500'
|
||||
case Status.Doing:
|
||||
return 'data-[state=checked]:bg-sky-500 data-[state=checked]:border-sky-500 bg-sky-500 border-sky-500'
|
||||
case Status.Done:
|
||||
return 'data-[state=checked]:bg-emerald-500 data-[state=checked]:border-emerald-500 bg-emerald-500 border-emerald-500'
|
||||
}
|
||||
}
|
||||
|
||||
export const columnDefs: Array<ColumnDef<ListTodosResponse>> = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
className="aspect-square"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
className={getColorFromTodoState(row.original.status)}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 26,
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: ({ column, header }) => {
|
||||
return (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title="Status"
|
||||
style={{ width: header.getSize() }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
Status[row.original.status] === undefined ? <p>Unbekannt</p> :<p>{Status[row.original.status]}</p>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'title',
|
||||
header: ({ column, header }) => {
|
||||
return (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title="Titel"
|
||||
style={{ width: header.getSize() }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: ({ column, header }) => {
|
||||
return (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title="Beschreibung"
|
||||
style={{ width: header.getSize() }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 200,
|
||||
},
|
||||
]
|
||||
|
||||
const fetchSize = 25
|
||||
|
||||
function TodoTable() {
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||
|
||||
const [selected, setSelected] = useState<RowSelectionState>({})
|
||||
|
||||
const { id } = useParams({ from: '/_sidebar/projects/view/$id/todos' })
|
||||
|
||||
// const { data, fetchNextPage, isFetching, isLoading } = useAllProjectTodos({
|
||||
// projectId: Number(id),
|
||||
// fetchSize,
|
||||
// sorting,
|
||||
// })
|
||||
|
||||
const { data, fetchNextPage, isFetching, isLoading } =
|
||||
useSuspenseInfiniteQuery(
|
||||
listTodos,
|
||||
{
|
||||
page: 0,
|
||||
perPage: fetchSize,
|
||||
orberBy: 'id',
|
||||
asc: false,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (_, p) => {
|
||||
console.log(p.length)
|
||||
return p.length * fetchSize
|
||||
// return ()
|
||||
},
|
||||
pageParamKey: 'page',
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columnDefs}
|
||||
data={data}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetching={isFetching}
|
||||
isLoading={isLoading}
|
||||
setSorting={setSorting}
|
||||
sorting={sorting}
|
||||
rowSelection={selected}
|
||||
setRowSelection={setSelected}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TodoTable
|
||||
@@ -1,11 +1,14 @@
|
||||
import {
|
||||
infiniteQueryOptions,
|
||||
keepPreviousData,
|
||||
queryOptions,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { env } from '@/env'
|
||||
import { fetchWithAuth, getOidc } from '@/lib/oidc'
|
||||
import { getBackendURI } from '@/routes'
|
||||
|
||||
const projectKeys = {
|
||||
all: ['projects'] as const,
|
||||
@@ -15,10 +18,26 @@ const projectKeys = {
|
||||
get: (id: number) => [...projectKeys.all, 'get', id] as const,
|
||||
}
|
||||
|
||||
export type PaginatedProject = {
|
||||
data: Array<Project>
|
||||
const timelineKeys = {
|
||||
all: ['timeline'] as const,
|
||||
lists: () => [...timelineKeys.all, 'list'] as const,
|
||||
getAll: (fetchSize: number, sorting: any) =>
|
||||
[...timelineKeys.lists(), 'all', fetchSize, sorting] as const,
|
||||
get: (id: number) => [...timelineKeys.all, 'get', id] as const,
|
||||
}
|
||||
|
||||
const todoKeys = {
|
||||
all: ['todos'] as const,
|
||||
lists: () => [...todoKeys.all, 'list'] as const,
|
||||
getAll: (fetchSize: number, sorting: any) =>
|
||||
[...todoKeys.lists(), 'all', fetchSize, sorting] as const,
|
||||
get: (id: number) => [...todoKeys.all, 'get', id] as const,
|
||||
}
|
||||
|
||||
export type Paginated<TData> = {
|
||||
data: Array<TData>
|
||||
meta: {
|
||||
totalProjectsCount: number
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,22 +49,62 @@ export type Project = {
|
||||
MandantID: number
|
||||
}
|
||||
|
||||
export type TimelineEntry = {
|
||||
ID: number
|
||||
name: string
|
||||
from: Date
|
||||
manualFrom: boolean
|
||||
thru: Date
|
||||
manualThru: boolean
|
||||
}
|
||||
|
||||
export function getProjectQueryObject(id: number) {
|
||||
return {
|
||||
return queryOptions<Project>({
|
||||
queryKey: projectKeys.get(id),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(env.VITE_BACKEND_URI + '/v1/projects/' + id, {
|
||||
credentials: 'include',
|
||||
})
|
||||
const data = await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/projects/' + id,
|
||||
{
|
||||
credentials: 'include',
|
||||
},
|
||||
)
|
||||
return await data.json()
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useProject(id: number) {
|
||||
return useQuery<Project>(getProjectQueryObject(id))
|
||||
}
|
||||
|
||||
export function getAllProjectsQueryObject({ fetchSize, sorting }: any) {
|
||||
return infiniteQueryOptions<Paginated<Project>>({
|
||||
queryKey: projectKeys.getAll(fetchSize, sorting),
|
||||
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
const start = (pageParam as number) * fetchSize
|
||||
|
||||
const { id, desc } = sorting[0]
|
||||
|
||||
const searchParams = new URLSearchParams()
|
||||
|
||||
searchParams.append('id', id)
|
||||
searchParams.append('desc', desc)
|
||||
searchParams.append('per-page', fetchSize)
|
||||
searchParams.append('offset', start.toString())
|
||||
|
||||
const url = (await getBackendURI()) + '/v1/projects/all?' + searchParams
|
||||
const data = await fetchWithAuth(url)
|
||||
return await data.json()
|
||||
},
|
||||
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (_lastGroup, groups) => groups.length,
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
export function useAllProjects({
|
||||
fetchSize,
|
||||
sorting,
|
||||
@@ -53,23 +112,79 @@ export function useAllProjects({
|
||||
fetchSize: number
|
||||
sorting: any
|
||||
}) {
|
||||
return useInfiniteQuery<PaginatedProject>({
|
||||
queryKey: projectKeys.getAll(fetchSize, sorting),
|
||||
return useInfiniteQuery<Paginated<Project>>(
|
||||
getAllProjectsQueryObject({ fetchSize, sorting }),
|
||||
)
|
||||
}
|
||||
|
||||
export function useAllTimelineEntries({
|
||||
projectId,
|
||||
fetchSize,
|
||||
sorting,
|
||||
}: {
|
||||
projectId: number
|
||||
fetchSize: number
|
||||
sorting: any
|
||||
}) {
|
||||
return useInfiniteQuery<Paginated<TimelineEntry>>({
|
||||
queryKey: timelineKeys.getAll(fetchSize, sorting),
|
||||
|
||||
queryFn: ({ pageParam = 0 }) => {
|
||||
return {
|
||||
data: Array(25).fill({
|
||||
ID: projectId,
|
||||
name: 'Aufbau',
|
||||
from: new Date('2005-11-13'),
|
||||
thru: new Date('2005-11-14'),
|
||||
manualFrom: false,
|
||||
manualThru: false,
|
||||
}),
|
||||
meta: {
|
||||
totalCount: 75,
|
||||
},
|
||||
}
|
||||
// const data = await setTimeout<Array<Paginated<Project>>>(() => {}, 2500)
|
||||
// return data
|
||||
},
|
||||
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (_lastGroup, groups) => groups.length,
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
export function useAllProjectTodos({
|
||||
projectId,
|
||||
fetchSize,
|
||||
sorting,
|
||||
}: {
|
||||
projectId: number
|
||||
fetchSize: number
|
||||
sorting: any
|
||||
}) {
|
||||
return useInfiniteQuery<Paginated<TimelineEntry>>({
|
||||
queryKey: todoKeys.getAll(fetchSize, sorting),
|
||||
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
const start = (pageParam as number) * fetchSize
|
||||
const data = await fetch(
|
||||
env.VITE_BACKEND_URI +
|
||||
'/v1/projects/all?' +
|
||||
new URLSearchParams(sorting[0]),
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'X-OFFSET': start.toString(),
|
||||
'X-PER-PAGE': fetchSize.toString(),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const { id, desc } = sorting[0]
|
||||
|
||||
const searchParams = new URLSearchParams()
|
||||
|
||||
searchParams.append('id', id)
|
||||
searchParams.append('desc', desc)
|
||||
searchParams.append('per-page', fetchSize.toString())
|
||||
searchParams.append('offset', start.toString())
|
||||
|
||||
const url =
|
||||
(await getBackendURI()) +
|
||||
'/v1/todos/project/' +
|
||||
projectId +
|
||||
'?' +
|
||||
searchParams
|
||||
const data = await fetchWithAuth(url)
|
||||
return await data.json()
|
||||
},
|
||||
|
||||
@@ -83,14 +198,25 @@ export function useAllProjects({
|
||||
export function useProjectEdit(id: number) {
|
||||
return useMutation({
|
||||
mutationFn: async (project: Project) => {
|
||||
await fetch(env.VITE_BACKEND_URI + '/v1/projects/' + id + '/edit', {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
const oidc = await getOidc()
|
||||
if (!oidc.isUserLoggedIn) {
|
||||
throw Error('Logical error in our application flow')
|
||||
}
|
||||
|
||||
const accessToken = await oidc.getAccessToken()
|
||||
|
||||
await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/projects/' + id + '/edit',
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(project),
|
||||
credentials: 'include',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(project),
|
||||
credentials: 'include',
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -100,14 +226,17 @@ export function useProjectCreate() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (project: Project) => {
|
||||
const res = await fetch(env.VITE_BACKEND_URI + '/v1/projects/new', {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
const res = await fetchWithAuth(
|
||||
(await getBackendURI()) + '/v1/projects/new',
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(project),
|
||||
credentials: 'include',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(project),
|
||||
credentials: 'include',
|
||||
})
|
||||
)
|
||||
const newCurrentMandant = await res.json()
|
||||
queryClient.invalidateQueries({ queryKey: projectKeys.lists() })
|
||||
queryClient.setQueryData(
|
||||
|
||||
20
src/fetch-polyfill.js
Normal file
20
src/fetch-polyfill.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// fetch-polyfill.js
|
||||
import fetch, {
|
||||
Blob,
|
||||
blobFrom,
|
||||
blobFromSync,
|
||||
File,
|
||||
fileFrom,
|
||||
fileFromSync,
|
||||
FormData,
|
||||
Headers,
|
||||
Request,
|
||||
Response,
|
||||
} from 'node-fetch'
|
||||
|
||||
if (!globalThis.fetch) {
|
||||
globalThis.fetch = fetch
|
||||
globalThis.Headers = Headers
|
||||
globalThis.Request = Request
|
||||
globalThis.Response = Response
|
||||
}
|
||||
20
src/gen/mandant/v1/mandant-MandantService_connectquery.ts
Normal file
20
src/gen/mandant/v1/mandant-MandantService_connectquery.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// @generated by protoc-gen-connect-query v2.2.0 with parameter "target=ts"
|
||||
// @generated from file mandant/v1/mandant.proto (package mandant.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import { MandantService } from "./mandant_pb";
|
||||
|
||||
/**
|
||||
* @generated from rpc mandant.v1.MandantService.GetCurrentTenant
|
||||
*/
|
||||
export const getCurrentTenant = MandantService.method.getCurrentTenant;
|
||||
|
||||
/**
|
||||
* @generated from rpc mandant.v1.MandantService.GetAllTenants
|
||||
*/
|
||||
export const getAllTenants = MandantService.method.getAllTenants;
|
||||
|
||||
/**
|
||||
* @generated from rpc mandant.v1.MandantService.SetCurrentTenant
|
||||
*/
|
||||
export const setCurrentTenant = MandantService.method.setCurrentTenant;
|
||||
217
src/gen/mandant/v1/mandant_pb.ts
Normal file
217
src/gen/mandant/v1/mandant_pb.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated from file mandant/v1/mandant.proto (package mandant.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file mandant/v1/mandant.proto.
|
||||
*/
|
||||
export const file_mandant_v1_mandant: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChhtYW5kYW50L3YxL21hbmRhbnQucHJvdG8SCm1hbmRhbnQudjEiGQoXR2V0Q3VycmVudFRlbmFudFJlcXVlc3QiHgoQR2V0VGVuYW50UmVxdWVzdBIKCgJpZBgBIAEoAyJYChFHZXRUZW5hbnRSZXNwb25zZRIKCgJpZBgBIAEoAxIMCgRuYW1lGAIgASgJEgwKBHBsYW4YAyABKAkSDAoEbG9nbxgEIAEoCRINCgVjb2xvchgFIAEoCSJSChFMaXN0VGVuYW50UmVxdWVzdBIMCgRwYWdlGAEgASgFEhAKCHBlcl9wYWdlGAIgASgFEhAKCG9yYmVyX2J5GAMgASgJEgsKA2FzYxgEIAEoCCIeCghNZXRhZGF0YRISCgp0b3RhbENvdW50GAEgASgFImcKFExpc3RQcm9qZWN0c1Jlc3BvbnNlEisKBGRhdGEYASADKAsyHS5tYW5kYW50LnYxLkdldFRlbmFudFJlc3BvbnNlEiIKBG1ldGEYAiABKAsyFC5tYW5kYW50LnYxLk1ldGFkYXRhIiwKF1NldEN1cnJlbnRUZW5hbnRSZXF1ZXN0EhEKCXRlbmFudF9pZBgBIAEoAyIrChhTZXRDdXJyZW50VGVuYW50UmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCDKZAgoOTWFuZGFudFNlcnZpY2USVgoQR2V0Q3VycmVudFRlbmFudBIjLm1hbmRhbnQudjEuR2V0Q3VycmVudFRlbmFudFJlcXVlc3QaHS5tYW5kYW50LnYxLkdldFRlbmFudFJlc3BvbnNlElAKDUdldEFsbFRlbmFudHMSHS5tYW5kYW50LnYxLkxpc3RUZW5hbnRSZXF1ZXN0GiAubWFuZGFudC52MS5MaXN0UHJvamVjdHNSZXNwb25zZRJdChBTZXRDdXJyZW50VGVuYW50EiMubWFuZGFudC52MS5TZXRDdXJyZW50VGVuYW50UmVxdWVzdBokLm1hbmRhbnQudjEuU2V0Q3VycmVudFRlbmFudFJlc3BvbnNlQpwBCg5jb20ubWFuZGFudC52MUIMTWFuZGFudFByb3RvUAFaM2dpdC5rb2NvZGVyLnh5ei9rb2NvZGVkL3Z0L2dlbi9tYW5kYW50L3YxO21hbmRhbnR2MaICA01YWKoCCk1hbmRhbnQuVjHKAgpNYW5kYW50XFYx4gIWTWFuZGFudFxWMVxHUEJNZXRhZGF0YeoCC01hbmRhbnQ6OlYxYgZwcm90bzM");
|
||||
|
||||
/**
|
||||
* @generated from message mandant.v1.GetCurrentTenantRequest
|
||||
*/
|
||||
export type GetCurrentTenantRequest = Message<"mandant.v1.GetCurrentTenantRequest"> & {
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message mandant.v1.GetCurrentTenantRequest.
|
||||
* Use `create(GetCurrentTenantRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetCurrentTenantRequestSchema: GenMessage<GetCurrentTenantRequest> = /*@__PURE__*/
|
||||
messageDesc(file_mandant_v1_mandant, 0);
|
||||
|
||||
/**
|
||||
* @generated from message mandant.v1.GetTenantRequest
|
||||
*/
|
||||
export type GetTenantRequest = Message<"mandant.v1.GetTenantRequest"> & {
|
||||
/**
|
||||
* @generated from field: int64 id = 1;
|
||||
*/
|
||||
id: bigint;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message mandant.v1.GetTenantRequest.
|
||||
* Use `create(GetTenantRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetTenantRequestSchema: GenMessage<GetTenantRequest> = /*@__PURE__*/
|
||||
messageDesc(file_mandant_v1_mandant, 1);
|
||||
|
||||
/**
|
||||
* @generated from message mandant.v1.GetTenantResponse
|
||||
*/
|
||||
export type GetTenantResponse = Message<"mandant.v1.GetTenantResponse"> & {
|
||||
/**
|
||||
* @generated from field: int64 id = 1;
|
||||
*/
|
||||
id: bigint;
|
||||
|
||||
/**
|
||||
* @generated from field: string name = 2;
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string plan = 3;
|
||||
*/
|
||||
plan: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string logo = 4;
|
||||
*/
|
||||
logo: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string color = 5;
|
||||
*/
|
||||
color: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message mandant.v1.GetTenantResponse.
|
||||
* Use `create(GetTenantResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetTenantResponseSchema: GenMessage<GetTenantResponse> = /*@__PURE__*/
|
||||
messageDesc(file_mandant_v1_mandant, 2);
|
||||
|
||||
/**
|
||||
* @generated from message mandant.v1.ListTenantRequest
|
||||
*/
|
||||
export type ListTenantRequest = Message<"mandant.v1.ListTenantRequest"> & {
|
||||
/**
|
||||
* @generated from field: int32 page = 1;
|
||||
*/
|
||||
page: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 per_page = 2;
|
||||
*/
|
||||
perPage: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string orber_by = 3;
|
||||
*/
|
||||
orberBy: string;
|
||||
|
||||
/**
|
||||
* @generated from field: bool asc = 4;
|
||||
*/
|
||||
asc: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message mandant.v1.ListTenantRequest.
|
||||
* Use `create(ListTenantRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const ListTenantRequestSchema: GenMessage<ListTenantRequest> = /*@__PURE__*/
|
||||
messageDesc(file_mandant_v1_mandant, 3);
|
||||
|
||||
/**
|
||||
* @generated from message mandant.v1.Metadata
|
||||
*/
|
||||
export type Metadata = Message<"mandant.v1.Metadata"> & {
|
||||
/**
|
||||
* @generated from field: int32 totalCount = 1;
|
||||
*/
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message mandant.v1.Metadata.
|
||||
* Use `create(MetadataSchema)` to create a new message.
|
||||
*/
|
||||
export const MetadataSchema: GenMessage<Metadata> = /*@__PURE__*/
|
||||
messageDesc(file_mandant_v1_mandant, 4);
|
||||
|
||||
/**
|
||||
* @generated from message mandant.v1.ListProjectsResponse
|
||||
*/
|
||||
export type ListProjectsResponse = Message<"mandant.v1.ListProjectsResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated mandant.v1.GetTenantResponse data = 1;
|
||||
*/
|
||||
data: GetTenantResponse[];
|
||||
|
||||
/**
|
||||
* @generated from field: mandant.v1.Metadata meta = 2;
|
||||
*/
|
||||
meta?: Metadata;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message mandant.v1.ListProjectsResponse.
|
||||
* Use `create(ListProjectsResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const ListProjectsResponseSchema: GenMessage<ListProjectsResponse> = /*@__PURE__*/
|
||||
messageDesc(file_mandant_v1_mandant, 5);
|
||||
|
||||
/**
|
||||
* @generated from message mandant.v1.SetCurrentTenantRequest
|
||||
*/
|
||||
export type SetCurrentTenantRequest = Message<"mandant.v1.SetCurrentTenantRequest"> & {
|
||||
/**
|
||||
* @generated from field: int64 tenant_id = 1;
|
||||
*/
|
||||
tenantId: bigint;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message mandant.v1.SetCurrentTenantRequest.
|
||||
* Use `create(SetCurrentTenantRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const SetCurrentTenantRequestSchema: GenMessage<SetCurrentTenantRequest> = /*@__PURE__*/
|
||||
messageDesc(file_mandant_v1_mandant, 6);
|
||||
|
||||
/**
|
||||
* @generated from message mandant.v1.SetCurrentTenantResponse
|
||||
*/
|
||||
export type SetCurrentTenantResponse = Message<"mandant.v1.SetCurrentTenantResponse"> & {
|
||||
/**
|
||||
* @generated from field: bool success = 1;
|
||||
*/
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message mandant.v1.SetCurrentTenantResponse.
|
||||
* Use `create(SetCurrentTenantResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const SetCurrentTenantResponseSchema: GenMessage<SetCurrentTenantResponse> = /*@__PURE__*/
|
||||
messageDesc(file_mandant_v1_mandant, 7);
|
||||
|
||||
/**
|
||||
* @generated from service mandant.v1.MandantService
|
||||
*/
|
||||
export const MandantService: GenService<{
|
||||
/**
|
||||
* @generated from rpc mandant.v1.MandantService.GetCurrentTenant
|
||||
*/
|
||||
getCurrentTenant: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetCurrentTenantRequestSchema;
|
||||
output: typeof GetTenantResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc mandant.v1.MandantService.GetAllTenants
|
||||
*/
|
||||
getAllTenants: {
|
||||
methodKind: "unary";
|
||||
input: typeof ListTenantRequestSchema;
|
||||
output: typeof ListProjectsResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc mandant.v1.MandantService.SetCurrentTenant
|
||||
*/
|
||||
setCurrentTenant: {
|
||||
methodKind: "unary";
|
||||
input: typeof SetCurrentTenantRequestSchema;
|
||||
output: typeof SetCurrentTenantResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_mandant_v1_mandant, 0);
|
||||
|
||||
80
src/gen/messagebus/v1/messagebus_pb.ts
Normal file
80
src/gen/messagebus/v1/messagebus_pb.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated from file messagebus/v1/messagebus.proto (package messagebus.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file messagebus/v1/messagebus.proto.
|
||||
*/
|
||||
export const file_messagebus_v1_messagebus: GenFile = /*@__PURE__*/
|
||||
fileDesc("Ch5tZXNzYWdlYnVzL3YxL21lc3NhZ2VidXMucHJvdG8SDW1lc3NhZ2VidXMudjEiJAoQTWVzc2FnZUJ1c0VudGl0eRIQCghxdWVyeUtleRgBIAEoCSIvCi1TdWJzY3JpYmVUb0Nvbm5lY3RJbnZhbGlkYXRpb25SZXF1ZXN0c1JlcXVlc3QqOwoUTWVzc2FnZUJ1c0VudGl0eVR5cGUSCQoFT1RIRVIQABIYChRJTlZBTElEQVRJT05fUkVRVUVTVBABMp8BChFNZXNzYWdlQnVzU2VydmljZRKJAQomU3Vic2NyaWJlVG9Db25uZWN0SW52YWxpZGF0aW9uUmVxdWVzdHMSPC5tZXNzYWdlYnVzLnYxLlN1YnNjcmliZVRvQ29ubmVjdEludmFsaWRhdGlvblJlcXVlc3RzUmVxdWVzdBofLm1lc3NhZ2VidXMudjEuTWVzc2FnZUJ1c0VudGl0eTABQrQBChFjb20ubWVzc2FnZWJ1cy52MUIPTWVzc2FnZWJ1c1Byb3RvUAFaOWdpdC5rb2NvZGVyLnh5ei9rb2NvZGVkL3Z0L2dlbi9tZXNzYWdlYnVzL3YxO21lc3NhZ2VidXN2MaICA01YWKoCDU1lc3NhZ2VidXMuVjHKAg1NZXNzYWdlYnVzXFYx4gIZTWVzc2FnZWJ1c1xWMVxHUEJNZXRhZGF0YeoCDk1lc3NhZ2VidXM6OlYxYgZwcm90bzM");
|
||||
|
||||
/**
|
||||
* @generated from message messagebus.v1.MessageBusEntity
|
||||
*/
|
||||
export type MessageBusEntity = Message<"messagebus.v1.MessageBusEntity"> & {
|
||||
/**
|
||||
* @generated from field: string queryKey = 1;
|
||||
*/
|
||||
queryKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message messagebus.v1.MessageBusEntity.
|
||||
* Use `create(MessageBusEntitySchema)` to create a new message.
|
||||
*/
|
||||
export const MessageBusEntitySchema: GenMessage<MessageBusEntity> = /*@__PURE__*/
|
||||
messageDesc(file_messagebus_v1_messagebus, 0);
|
||||
|
||||
/**
|
||||
* @generated from message messagebus.v1.SubscribeToConnectInvalidationRequestsRequest
|
||||
*/
|
||||
export type SubscribeToConnectInvalidationRequestsRequest = Message<"messagebus.v1.SubscribeToConnectInvalidationRequestsRequest"> & {
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message messagebus.v1.SubscribeToConnectInvalidationRequestsRequest.
|
||||
* Use `create(SubscribeToConnectInvalidationRequestsRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const SubscribeToConnectInvalidationRequestsRequestSchema: GenMessage<SubscribeToConnectInvalidationRequestsRequest> = /*@__PURE__*/
|
||||
messageDesc(file_messagebus_v1_messagebus, 1);
|
||||
|
||||
/**
|
||||
* @generated from enum messagebus.v1.MessageBusEntityType
|
||||
*/
|
||||
export enum MessageBusEntityType {
|
||||
/**
|
||||
* @generated from enum value: OTHER = 0;
|
||||
*/
|
||||
OTHER = 0,
|
||||
|
||||
/**
|
||||
* @generated from enum value: INVALIDATION_REQUEST = 1;
|
||||
*/
|
||||
INVALIDATION_REQUEST = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the enum messagebus.v1.MessageBusEntityType.
|
||||
*/
|
||||
export const MessageBusEntityTypeSchema: GenEnum<MessageBusEntityType> = /*@__PURE__*/
|
||||
enumDesc(file_messagebus_v1_messagebus, 0);
|
||||
|
||||
/**
|
||||
* @generated from service messagebus.v1.MessageBusService
|
||||
*/
|
||||
export const MessageBusService: GenService<{
|
||||
/**
|
||||
* @generated from rpc messagebus.v1.MessageBusService.SubscribeToConnectInvalidationRequests
|
||||
*/
|
||||
subscribeToConnectInvalidationRequests: {
|
||||
methodKind: "server_streaming";
|
||||
input: typeof SubscribeToConnectInvalidationRequestsRequestSchema;
|
||||
output: typeof MessageBusEntitySchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_messagebus_v1_messagebus, 0);
|
||||
|
||||
15
src/gen/project/v1/project-ProjectService_connectquery.ts
Normal file
15
src/gen/project/v1/project-ProjectService_connectquery.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// @generated by protoc-gen-connect-query v2.2.0 with parameter "target=ts"
|
||||
// @generated from file project/v1/project.proto (package project.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import { ProjectService } from "./project_pb";
|
||||
|
||||
/**
|
||||
* @generated from rpc project.v1.ProjectService.GetProject
|
||||
*/
|
||||
export const getProject = ProjectService.method.getProject;
|
||||
|
||||
/**
|
||||
* @generated from rpc project.v1.ProjectService.ListProjects
|
||||
*/
|
||||
export const listProjects = ProjectService.method.listProjects;
|
||||
177
src/gen/project/v1/project_pb.ts
Normal file
177
src/gen/project/v1/project_pb.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated from file project/v1/project.proto (package project.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file project/v1/project.proto.
|
||||
*/
|
||||
export const file_project_v1_project: GenFile = /*@__PURE__*/
|
||||
fileDesc("Chhwcm9qZWN0L3YxL3Byb2plY3QucHJvdG8SCnByb2plY3QudjEiHwoRR2V0UHJvamVjdFJlcXVlc3QSCgoCaWQYASABKAUilwIKEkdldFByb2plY3RSZXNwb25zZRIKCgJpZBgBIAEoAxIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEhwKD2lzX21hdGVyaWFsaXplZBgFIAEoCEgAiAEBEhwKD2lzX3BlcnNvbmFsaXplZBgGIAEoCEgBiAEBEhkKDGlzX2NvbmZpcm1lZBgHIAEoCEgCiAEBEhQKB2lzX3BhaWQYCCABKAhIA4gBARIUCgdpc19kb25lGAkgASgISASIAQFCEgoQX2lzX21hdGVyaWFsaXplZEISChBfaXNfcGVyc29uYWxpemVkQg8KDV9pc19jb25maXJtZWRCCgoIX2lzX3BhaWRCCgoIX2lzX2RvbmUiVAoTTGlzdFByb2plY3RzUmVxdWVzdBIMCgRwYWdlGAEgASgFEhAKCHBlcl9wYWdlGAIgASgFEhAKCG9yYmVyX2J5GAMgASgJEgsKA2FzYxgEIAEoCCIeCghNZXRhZGF0YRISCgp0b3RhbENvdW50GAEgASgFImgKFExpc3RQcm9qZWN0c1Jlc3BvbnNlEiwKBGRhdGEYASADKAsyHi5wcm9qZWN0LnYxLkdldFByb2plY3RSZXNwb25zZRIiCgRtZXRhGAIgASgLMhQucHJvamVjdC52MS5NZXRhZGF0YTKwAQoOUHJvamVjdFNlcnZpY2USSwoKR2V0UHJvamVjdBIdLnByb2plY3QudjEuR2V0UHJvamVjdFJlcXVlc3QaHi5wcm9qZWN0LnYxLkdldFByb2plY3RSZXNwb25zZRJRCgxMaXN0UHJvamVjdHMSHy5wcm9qZWN0LnYxLkxpc3RQcm9qZWN0c1JlcXVlc3QaIC5wcm9qZWN0LnYxLkxpc3RQcm9qZWN0c1Jlc3BvbnNlQpwBCg5jb20ucHJvamVjdC52MUIMUHJvamVjdFByb3RvUAFaM2dpdC5rb2NvZGVyLnh5ei9rb2NvZGVkL3Z0L2dlbi9wcm9qZWN0L3YxO3Byb2plY3R2MaICA1BYWKoCClByb2plY3QuVjHKAgpQcm9qZWN0XFYx4gIWUHJvamVjdFxWMVxHUEJNZXRhZGF0YeoCC1Byb2plY3Q6OlYxYgZwcm90bzM");
|
||||
|
||||
/**
|
||||
* @generated from message project.v1.GetProjectRequest
|
||||
*/
|
||||
export type GetProjectRequest = Message<"project.v1.GetProjectRequest"> & {
|
||||
/**
|
||||
* @generated from field: int32 id = 1;
|
||||
*/
|
||||
id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message project.v1.GetProjectRequest.
|
||||
* Use `create(GetProjectRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetProjectRequestSchema: GenMessage<GetProjectRequest> = /*@__PURE__*/
|
||||
messageDesc(file_project_v1_project, 0);
|
||||
|
||||
/**
|
||||
* @generated from message project.v1.GetProjectResponse
|
||||
*/
|
||||
export type GetProjectResponse = Message<"project.v1.GetProjectResponse"> & {
|
||||
/**
|
||||
* @generated from field: int64 id = 1;
|
||||
*/
|
||||
id: bigint;
|
||||
|
||||
/**
|
||||
* @generated from field: string name = 2;
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string description = 3;
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional bool is_materialized = 5;
|
||||
*/
|
||||
isMaterialized?: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: optional bool is_personalized = 6;
|
||||
*/
|
||||
isPersonalized?: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: optional bool is_confirmed = 7;
|
||||
*/
|
||||
isConfirmed?: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: optional bool is_paid = 8;
|
||||
*/
|
||||
isPaid?: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: optional bool is_done = 9;
|
||||
*/
|
||||
isDone?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message project.v1.GetProjectResponse.
|
||||
* Use `create(GetProjectResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetProjectResponseSchema: GenMessage<GetProjectResponse> = /*@__PURE__*/
|
||||
messageDesc(file_project_v1_project, 1);
|
||||
|
||||
/**
|
||||
* @generated from message project.v1.ListProjectsRequest
|
||||
*/
|
||||
export type ListProjectsRequest = Message<"project.v1.ListProjectsRequest"> & {
|
||||
/**
|
||||
* @generated from field: int32 page = 1;
|
||||
*/
|
||||
page: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 per_page = 2;
|
||||
*/
|
||||
perPage: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string orber_by = 3;
|
||||
*/
|
||||
orberBy: string;
|
||||
|
||||
/**
|
||||
* @generated from field: bool asc = 4;
|
||||
*/
|
||||
asc: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message project.v1.ListProjectsRequest.
|
||||
* Use `create(ListProjectsRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const ListProjectsRequestSchema: GenMessage<ListProjectsRequest> = /*@__PURE__*/
|
||||
messageDesc(file_project_v1_project, 2);
|
||||
|
||||
/**
|
||||
* @generated from message project.v1.Metadata
|
||||
*/
|
||||
export type Metadata = Message<"project.v1.Metadata"> & {
|
||||
/**
|
||||
* @generated from field: int32 totalCount = 1;
|
||||
*/
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message project.v1.Metadata.
|
||||
* Use `create(MetadataSchema)` to create a new message.
|
||||
*/
|
||||
export const MetadataSchema: GenMessage<Metadata> = /*@__PURE__*/
|
||||
messageDesc(file_project_v1_project, 3);
|
||||
|
||||
/**
|
||||
* @generated from message project.v1.ListProjectsResponse
|
||||
*/
|
||||
export type ListProjectsResponse = Message<"project.v1.ListProjectsResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated project.v1.GetProjectResponse data = 1;
|
||||
*/
|
||||
data: GetProjectResponse[];
|
||||
|
||||
/**
|
||||
* @generated from field: project.v1.Metadata meta = 2;
|
||||
*/
|
||||
meta?: Metadata;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message project.v1.ListProjectsResponse.
|
||||
* Use `create(ListProjectsResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const ListProjectsResponseSchema: GenMessage<ListProjectsResponse> = /*@__PURE__*/
|
||||
messageDesc(file_project_v1_project, 4);
|
||||
|
||||
/**
|
||||
* @generated from service project.v1.ProjectService
|
||||
*/
|
||||
export const ProjectService: GenService<{
|
||||
/**
|
||||
* @generated from rpc project.v1.ProjectService.GetProject
|
||||
*/
|
||||
getProject: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetProjectRequestSchema;
|
||||
output: typeof GetProjectResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc project.v1.ProjectService.ListProjects
|
||||
*/
|
||||
listProjects: {
|
||||
methodKind: "unary";
|
||||
input: typeof ListProjectsRequestSchema;
|
||||
output: typeof ListProjectsResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_project_v1_project, 0);
|
||||
|
||||
15
src/gen/todo/v1/todo-ProjectService_connectquery.ts
Normal file
15
src/gen/todo/v1/todo-ProjectService_connectquery.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// @generated by protoc-gen-connect-query v2.2.0 with parameter "target=ts"
|
||||
// @generated from file todo/v1/todo.proto (package todo.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import { ProjectService } from "./todo_pb";
|
||||
|
||||
/**
|
||||
* @generated from rpc todo.v1.ProjectService.GetProject
|
||||
*/
|
||||
export const getProject = ProjectService.method.getProject;
|
||||
|
||||
/**
|
||||
* @generated from rpc todo.v1.ProjectService.ListProjects
|
||||
*/
|
||||
export const listProjects = ProjectService.method.listProjects;
|
||||
15
src/gen/todo/v1/todo-TodoService_connectquery.ts
Normal file
15
src/gen/todo/v1/todo-TodoService_connectquery.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// @generated by protoc-gen-connect-query v2.2.0 with parameter "target=ts"
|
||||
// @generated from file todo/v1/todo.proto (package todo.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import { TodoService } from "./todo_pb";
|
||||
|
||||
/**
|
||||
* @generated from rpc todo.v1.TodoService.GetTodo
|
||||
*/
|
||||
export const getTodo = TodoService.method.getTodo;
|
||||
|
||||
/**
|
||||
* @generated from rpc todo.v1.TodoService.ListTodos
|
||||
*/
|
||||
export const listTodos = TodoService.method.listTodos;
|
||||
287
src/gen/todo/v1/todo_pb.ts
Normal file
287
src/gen/todo/v1/todo_pb.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated from file todo/v1/todo.proto (package todo.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file todo/v1/todo.proto.
|
||||
*/
|
||||
export const file_todo_v1_todo: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChJ0b2RvL3YxL3RvZG8ucHJvdG8SB3RvZG8udjEiHQoPR2V0VG9kb3NSZXF1ZXN0EgoKAmlkGAEgASgFImMKEEdldFRvZG9zUmVzcG9uc2USCgoCaWQYASABKAMSDQoFdGl0bGUYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSHwoGc3RhdHVzGAQgASgOMg8udG9kby52MS5TdGF0dXMicwoQTGlzdFRvZG9zUmVxdWVzdBIMCgRwYWdlGAEgASgFEhAKCHBlcl9wYWdlGAIgASgFEhAKCG9yYmVyX2J5GAMgASgJEgsKA2FzYxgEIAEoCBIgCgdmaWx0ZXJzGAUgAygLMg8udG9kby52MS5GaWx0ZXIiXQoGRmlsdGVyEh0KBWZpZWxkGAEgASgOMg4udG9kby52MS5GaWVsZBINCgV2YWx1ZRgCIAEoCRIlCglvcGVyYXRpb24YAyABKA4yEi50b2RvLnYxLk9wZXJhdGlvbiIeCghNZXRhZGF0YRISCgp0b3RhbENvdW50GAEgASgFIl0KEUxpc3RUb2Rvc1Jlc3BvbnNlEicKBGRhdGEYASADKAsyGS50b2RvLnYxLkdldFRvZG9zUmVzcG9uc2USHwoEbWV0YRgCIAEoCzIRLnRvZG8udjEuTWV0YWRhdGEqOgoGU3RhdHVzEggKBFRvZG8QABIRCg1OZWVkc01vcmVJbmZvEAESCQoFRG9pbmcQAhIICgREb25lEAMqSwoFRmllbGQSCwoHRmllbGRJZBAAEg4KCkZpZWxkVGl0bGUQARIUChBGaWVsZERlc2NyaXB0aW9uEAISDwoLRmllbGRTdGF0dXMQAypPCglPcGVyYXRpb24SCgoGRXF1YWxzEAASDQoJTm90RXF1YWxzEAESDwoLR3JlYXRlclRoYW4QAhIMCghMZXNzVGhhbhADEggKBExpa2UQBDKRAQoLVG9kb1NlcnZpY2USPgoHR2V0VG9kbxIYLnRvZG8udjEuR2V0VG9kb3NSZXF1ZXN0GhkudG9kby52MS5HZXRUb2Rvc1Jlc3BvbnNlEkIKCUxpc3RUb2RvcxIZLnRvZG8udjEuTGlzdFRvZG9zUmVxdWVzdBoaLnRvZG8udjEuTGlzdFRvZG9zUmVzcG9uc2VChAEKC2NvbS50b2RvLnYxQglUb2RvUHJvdG9QAVotZ2l0LmtvY29kZXIueHl6L2tvY29kZWQvdnQvZ2VuL3RvZG8vdjE7dG9kb3YxogIDVFhYqgIHVG9kby5WMcoCB1RvZG9cVjHiAhNUb2RvXFYxXEdQQk1ldGFkYXRh6gIIVG9kbzo6VjFiBnByb3RvMw");
|
||||
|
||||
/**
|
||||
* @generated from message todo.v1.GetTodosRequest
|
||||
*/
|
||||
export type GetTodosRequest = Message<"todo.v1.GetTodosRequest"> & {
|
||||
/**
|
||||
* @generated from field: int32 id = 1;
|
||||
*/
|
||||
id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message todo.v1.GetTodosRequest.
|
||||
* Use `create(GetTodosRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetTodosRequestSchema: GenMessage<GetTodosRequest> = /*@__PURE__*/
|
||||
messageDesc(file_todo_v1_todo, 0);
|
||||
|
||||
/**
|
||||
* @generated from message todo.v1.GetTodosResponse
|
||||
*/
|
||||
export type GetTodosResponse = Message<"todo.v1.GetTodosResponse"> & {
|
||||
/**
|
||||
* @generated from field: int64 id = 1;
|
||||
*/
|
||||
id: bigint;
|
||||
|
||||
/**
|
||||
* @generated from field: string title = 2;
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string description = 3;
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* @generated from field: todo.v1.Status status = 4;
|
||||
*/
|
||||
status: Status;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message todo.v1.GetTodosResponse.
|
||||
* Use `create(GetTodosResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetTodosResponseSchema: GenMessage<GetTodosResponse> = /*@__PURE__*/
|
||||
messageDesc(file_todo_v1_todo, 1);
|
||||
|
||||
/**
|
||||
* @generated from message todo.v1.ListTodosRequest
|
||||
*/
|
||||
export type ListTodosRequest = Message<"todo.v1.ListTodosRequest"> & {
|
||||
/**
|
||||
* @generated from field: int32 page = 1;
|
||||
*/
|
||||
page: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 per_page = 2;
|
||||
*/
|
||||
perPage: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string orber_by = 3;
|
||||
*/
|
||||
orberBy: string;
|
||||
|
||||
/**
|
||||
* @generated from field: bool asc = 4;
|
||||
*/
|
||||
asc: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated todo.v1.Filter filters = 5;
|
||||
*/
|
||||
filters: Filter[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message todo.v1.ListTodosRequest.
|
||||
* Use `create(ListTodosRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const ListTodosRequestSchema: GenMessage<ListTodosRequest> = /*@__PURE__*/
|
||||
messageDesc(file_todo_v1_todo, 2);
|
||||
|
||||
/**
|
||||
* @generated from message todo.v1.Filter
|
||||
*/
|
||||
export type Filter = Message<"todo.v1.Filter"> & {
|
||||
/**
|
||||
* @generated from field: todo.v1.Field field = 1;
|
||||
*/
|
||||
field: Field;
|
||||
|
||||
/**
|
||||
* @generated from field: string value = 2;
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* @generated from field: todo.v1.Operation operation = 3;
|
||||
*/
|
||||
operation: Operation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message todo.v1.Filter.
|
||||
* Use `create(FilterSchema)` to create a new message.
|
||||
*/
|
||||
export const FilterSchema: GenMessage<Filter> = /*@__PURE__*/
|
||||
messageDesc(file_todo_v1_todo, 3);
|
||||
|
||||
/**
|
||||
* @generated from message todo.v1.Metadata
|
||||
*/
|
||||
export type Metadata = Message<"todo.v1.Metadata"> & {
|
||||
/**
|
||||
* @generated from field: int32 totalCount = 1;
|
||||
*/
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message todo.v1.Metadata.
|
||||
* Use `create(MetadataSchema)` to create a new message.
|
||||
*/
|
||||
export const MetadataSchema: GenMessage<Metadata> = /*@__PURE__*/
|
||||
messageDesc(file_todo_v1_todo, 4);
|
||||
|
||||
/**
|
||||
* @generated from message todo.v1.ListTodosResponse
|
||||
*/
|
||||
export type ListTodosResponse = Message<"todo.v1.ListTodosResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated todo.v1.GetTodosResponse data = 1;
|
||||
*/
|
||||
data: GetTodosResponse[];
|
||||
|
||||
/**
|
||||
* @generated from field: todo.v1.Metadata meta = 2;
|
||||
*/
|
||||
meta?: Metadata;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message todo.v1.ListTodosResponse.
|
||||
* Use `create(ListTodosResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const ListTodosResponseSchema: GenMessage<ListTodosResponse> = /*@__PURE__*/
|
||||
messageDesc(file_todo_v1_todo, 5);
|
||||
|
||||
/**
|
||||
* @generated from enum todo.v1.Status
|
||||
*/
|
||||
export enum Status {
|
||||
/**
|
||||
* @generated from enum value: Todo = 0;
|
||||
*/
|
||||
Todo = 0,
|
||||
|
||||
/**
|
||||
* @generated from enum value: NeedsMoreInfo = 1;
|
||||
*/
|
||||
NeedsMoreInfo = 1,
|
||||
|
||||
/**
|
||||
* @generated from enum value: Doing = 2;
|
||||
*/
|
||||
Doing = 2,
|
||||
|
||||
/**
|
||||
* @generated from enum value: Done = 3;
|
||||
*/
|
||||
Done = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the enum todo.v1.Status.
|
||||
*/
|
||||
export const StatusSchema: GenEnum<Status> = /*@__PURE__*/
|
||||
enumDesc(file_todo_v1_todo, 0);
|
||||
|
||||
/**
|
||||
* @generated from enum todo.v1.Field
|
||||
*/
|
||||
export enum Field {
|
||||
/**
|
||||
* @generated from enum value: FieldId = 0;
|
||||
*/
|
||||
FieldId = 0,
|
||||
|
||||
/**
|
||||
* @generated from enum value: FieldTitle = 1;
|
||||
*/
|
||||
FieldTitle = 1,
|
||||
|
||||
/**
|
||||
* @generated from enum value: FieldDescription = 2;
|
||||
*/
|
||||
FieldDescription = 2,
|
||||
|
||||
/**
|
||||
* @generated from enum value: FieldStatus = 3;
|
||||
*/
|
||||
FieldStatus = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the enum todo.v1.Field.
|
||||
*/
|
||||
export const FieldSchema: GenEnum<Field> = /*@__PURE__*/
|
||||
enumDesc(file_todo_v1_todo, 1);
|
||||
|
||||
/**
|
||||
* @generated from enum todo.v1.Operation
|
||||
*/
|
||||
export enum Operation {
|
||||
/**
|
||||
* @generated from enum value: Equals = 0;
|
||||
*/
|
||||
Equals = 0,
|
||||
|
||||
/**
|
||||
* @generated from enum value: NotEquals = 1;
|
||||
*/
|
||||
NotEquals = 1,
|
||||
|
||||
/**
|
||||
* @generated from enum value: GreaterThan = 2;
|
||||
*/
|
||||
GreaterThan = 2,
|
||||
|
||||
/**
|
||||
* @generated from enum value: LessThan = 3;
|
||||
*/
|
||||
LessThan = 3,
|
||||
|
||||
/**
|
||||
* @generated from enum value: Like = 4;
|
||||
*/
|
||||
Like = 4,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the enum todo.v1.Operation.
|
||||
*/
|
||||
export const OperationSchema: GenEnum<Operation> = /*@__PURE__*/
|
||||
enumDesc(file_todo_v1_todo, 2);
|
||||
|
||||
/**
|
||||
* @generated from service todo.v1.TodoService
|
||||
*/
|
||||
export const TodoService: GenService<{
|
||||
/**
|
||||
* @generated from rpc todo.v1.TodoService.GetTodo
|
||||
*/
|
||||
getTodo: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetTodosRequestSchema;
|
||||
output: typeof GetTodosResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc todo.v1.TodoService.ListTodos
|
||||
*/
|
||||
listTodos: {
|
||||
methodKind: "unary";
|
||||
input: typeof ListTodosRequestSchema;
|
||||
output: typeof ListTodosResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_todo_v1_todo, 0);
|
||||
|
||||
@@ -55,7 +55,7 @@ function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
||||
* Accepts callback refs and RefObject(s)
|
||||
*/
|
||||
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: we don't want to re-run this callback when the refs change
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values
|
||||
return React.useCallback(composeRefs(...refs), refs);
|
||||
}
|
||||
|
||||
|
||||
248
src/lib/ogimage.ts
Normal file
248
src/lib/ogimage.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { env } from '@/env'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { encode } from 'blurhash'
|
||||
import { getPixels } from '@unpic/pixels'
|
||||
import { blurhashToCssGradientString } from '@unpic/placeholder'
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
|
||||
const SITE = {
|
||||
TITLE: 'Eventory',
|
||||
URL_SHORT: 'https://vt.kocoder.xyz',
|
||||
}
|
||||
/**
|
||||
* Loads a font from Google Fonts
|
||||
* @param font - The font name
|
||||
* @param text - The text to load the font for
|
||||
* @param weight - The font weight to load
|
||||
* @returns The font data
|
||||
*/
|
||||
async function loadFont(font: string, text: string, weight: number) {
|
||||
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}&text=${encodeURIComponent(text)}`
|
||||
const css = await (await fetch(url)).text()
|
||||
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)
|
||||
if (resource) {
|
||||
const response = await fetch(resource[1])
|
||||
if (response.status == 200) {
|
||||
return await response.arrayBuffer()
|
||||
}
|
||||
}
|
||||
throw new Error('failed to load font data')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a SEO image for a blog post or project
|
||||
* @param title - The title of the blog post or project
|
||||
* @param text - The text to display in the image
|
||||
* @param description - The description of the blog post or project
|
||||
* @param tags - The tags of the blog post or project
|
||||
* @param image - The image to display in the image
|
||||
* @returns The SEO image
|
||||
*/
|
||||
export interface SeoImageGeneratorProps {
|
||||
title: string
|
||||
text: string
|
||||
image?: {
|
||||
src: string
|
||||
width: number
|
||||
height: number
|
||||
format: 'png' | 'jpg' | 'jpeg' | 'webp' | 'tiff' | 'gif' | 'svg' | 'avif'
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateSeoImage({
|
||||
title,
|
||||
image,
|
||||
text,
|
||||
}: SeoImageGeneratorProps) {
|
||||
console.log(env.VITE_ENVIRONMENT, env.VITE_ENVIRONMENT == 'development')
|
||||
const assetsPrefix =
|
||||
env.VITE_ENVIRONMENT == 'development' ? './public' : './dist'
|
||||
const siteImage = fs.readFileSync(path.resolve(assetsPrefix, 'logo512.png'))
|
||||
|
||||
const siteImageElement = {
|
||||
type: 'img',
|
||||
props: {
|
||||
tw: 'w-6 h-6 rounded-full mr-2',
|
||||
src: siteImage.buffer,
|
||||
},
|
||||
}
|
||||
|
||||
const siteTitleElement = {
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: ' opacity-70',
|
||||
style: {
|
||||
fontFamily: 'Geist',
|
||||
},
|
||||
children: SITE.TITLE,
|
||||
},
|
||||
}
|
||||
|
||||
const separatorElement = {
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: 'px-2 opacity-70',
|
||||
children: '•',
|
||||
},
|
||||
}
|
||||
|
||||
const siteSubtitleElement = {
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: 'opacity-70',
|
||||
style: {
|
||||
fontFamily: 'Geist',
|
||||
},
|
||||
children: title,
|
||||
},
|
||||
}
|
||||
|
||||
const siteImageAndTitleContainer = {
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: 'flex items-center justify-start w-full mb-2',
|
||||
children: [
|
||||
siteImageElement,
|
||||
siteTitleElement,
|
||||
separatorElement,
|
||||
siteSubtitleElement,
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
let imageElement = null
|
||||
let shouldBlur = false
|
||||
if (image && shouldBlur) {
|
||||
const img = fs.readFileSync(
|
||||
env.VITE_ENVIRONMENT === 'development'
|
||||
? path.resolve(image.src.replace(/\?.*/, '').replace('/@fs', ''))
|
||||
: path.resolve(image.src.replace('/', 'dist/')),
|
||||
)
|
||||
const imgData = await getPixels(new Uint8Array(img))
|
||||
const data = Uint8ClampedArray.from(imgData.data)
|
||||
const blurhash = encode(data, imgData.width, imgData.height, 4, 4)
|
||||
const gradient = blurhashToCssGradientString(blurhash, 5, 5)
|
||||
|
||||
imageElement = {
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: 'w-full h-full absolute inset-0 rotate-90',
|
||||
style: {
|
||||
opacity: 0.07,
|
||||
background: gradient,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const gradientElement = {
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
background:
|
||||
'radial-gradient(circle at bottom right, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0))',
|
||||
},
|
||||
tw: 'w-full h-full absolute flex inset-0 opacity-40',
|
||||
},
|
||||
}
|
||||
|
||||
const mainTextElement = {
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: 'text-5xl leading-none text-left',
|
||||
style: {
|
||||
fontFamily: 'Geist',
|
||||
fontWeight: '400',
|
||||
},
|
||||
children: text,
|
||||
},
|
||||
}
|
||||
|
||||
const containerElement = {
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: 'flex flex-col items-center justify-center max-w-3xl w-full',
|
||||
children: [
|
||||
siteImageAndTitleContainer,
|
||||
{
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: 'shrink flex w-full',
|
||||
children: [mainTextElement],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const footerElement = {
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: 'absolute right-[15px] bottom-[15px] flex items-center justify-center opacity-70 text-xs',
|
||||
children: [SITE.URL_SHORT],
|
||||
},
|
||||
}
|
||||
|
||||
const html = {
|
||||
type: 'div',
|
||||
props: {
|
||||
tw: 'relative w-full h-full flex flex-col items-center justify-center relative relative text-white',
|
||||
style: {
|
||||
background: '#0a0a0a',
|
||||
fontFamily: 'Geist',
|
||||
fontSmoothing: 'antialiased',
|
||||
},
|
||||
children: [
|
||||
imageElement,
|
||||
gradientElement,
|
||||
containerElement,
|
||||
footerElement,
|
||||
],
|
||||
},
|
||||
} as any
|
||||
|
||||
const weights = [100, 200, 300, 400, 900] as const
|
||||
|
||||
const fontConfigs = [
|
||||
...weights.map((weight) => ({ name: 'Geist', weight })),
|
||||
...weights.map((weight) => ({ name: 'Geist Mono', weight })),
|
||||
]
|
||||
|
||||
const fonts = await Promise.all(
|
||||
fontConfigs.map(async ({ name, weight }) => ({
|
||||
name,
|
||||
data: await loadFont(name, text, weight),
|
||||
style: 'normal' as const,
|
||||
weight,
|
||||
})),
|
||||
)
|
||||
|
||||
return new ImageResponse(html, {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts,
|
||||
debug: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a SEO image for a blog post or project
|
||||
* @param entry - The entry to generate the SEO image for
|
||||
* @param type - The type of the entry
|
||||
* @returns The SEO image
|
||||
*/
|
||||
export interface SeoImageGeneratorForContentProps {
|
||||
entry: object
|
||||
type: string
|
||||
}
|
||||
|
||||
export async function generateSeoImageForContent() {
|
||||
return generateSeoImage({
|
||||
title: 'Eventory',
|
||||
text: 'A simple deellele',
|
||||
// image: entry.data.cover,
|
||||
|
||||
// ...rest,
|
||||
})
|
||||
}
|
||||
157
src/lib/oidc.ts
Normal file
157
src/lib/oidc.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
// import 'webcrypto-liner/build/webcrypto-liner.shim'
|
||||
import { oidcSpa } from 'oidc-spa/react-tanstack-start'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const {
|
||||
bootstrapOidc,
|
||||
useOidc,
|
||||
getOidc,
|
||||
// NOTE: Each time you enforceLogin on a route the oidc-spa vite plugin
|
||||
// will automatically switch this route to `ssr: false`.
|
||||
// This ensures that everything that can be SSR'd is and the rest is delayed to the client.
|
||||
enforceLogin,
|
||||
oidcFnMiddleware,
|
||||
oidcRequestMiddleware,
|
||||
} = oidcSpa
|
||||
.withExpectedDecodedIdTokenShape({
|
||||
decodedIdTokenSchema: z.object({
|
||||
name: z.string(),
|
||||
picture: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
preferred_username: z.string().optional(),
|
||||
realm_access: z.object({ roles: z.array(z.string()) }).optional(),
|
||||
}),
|
||||
decodedIdToken_mock: {
|
||||
name: 'John Doe',
|
||||
preferred_username: 'john.doe',
|
||||
realm_access: {
|
||||
roles: ['realm-admin'],
|
||||
},
|
||||
},
|
||||
})
|
||||
.withAccessTokenValidation({
|
||||
type: 'RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens',
|
||||
expectedAudience: (/* { paramsOfBootstrap, process } */) => 'account',
|
||||
accessTokenClaimsSchema: z.object({
|
||||
sub: z.string(),
|
||||
realm_access: z.object({ roles: z.array(z.string()) }).optional(),
|
||||
}),
|
||||
accessTokenClaims_mock: {
|
||||
sub: 'u123',
|
||||
realm_access: {
|
||||
roles: ['realm-admin'],
|
||||
},
|
||||
},
|
||||
})
|
||||
// See: https://docs.oidc-spa.dev/features/auto-login#tanstack-start
|
||||
// .withAutoLogin()
|
||||
.createUtils()
|
||||
|
||||
// Can be call anywhere, even in the body of a React component.
|
||||
// All subsequent calls will be safely ignored.
|
||||
bootstrapOidc(({ process }) =>
|
||||
process.env.OIDC_USE_MOCK === 'true'
|
||||
? {
|
||||
implementation: 'mock',
|
||||
isUserInitiallyLoggedIn: true,
|
||||
}
|
||||
: {
|
||||
implementation: 'real',
|
||||
issuerUri: process.env.OIDC_ISSUER_URI,
|
||||
clientId: process.env.OIDC_CLIENT_ID,
|
||||
debugLogs: false,
|
||||
},
|
||||
)
|
||||
|
||||
export const fetchWithAuth: typeof fetch = async (input, init) => {
|
||||
const oidc = await getOidc()
|
||||
|
||||
if (oidc.isUserLoggedIn) {
|
||||
const accessToken = await oidc.getAccessToken()
|
||||
const headers = new Headers(init?.headers)
|
||||
headers.set('Authorization', `Bearer ${accessToken}`)
|
||||
;(init ??= {}).headers = headers
|
||||
}
|
||||
|
||||
return fetch(input, init)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// import { createOidcBackend } from 'oidc-spa/backend'
|
||||
|
||||
// const zDecodedAccessToken = z.object({
|
||||
// sub: z.string(),
|
||||
// aud: z.union([z.string(), z.array(z.string())]),
|
||||
// realm_access: z.object({
|
||||
// roles: z.array(z.string()),
|
||||
// }),
|
||||
// // Some other info you might want to read from the accessToken, example:
|
||||
// // preferred_username: z.string()
|
||||
// })
|
||||
|
||||
// export type DecodedAccessToken = z.infer<typeof zDecodedAccessToken>
|
||||
|
||||
// export async function createDecodeAccessToken(params: {
|
||||
// issuerUri: string
|
||||
// audience: string
|
||||
// }) {
|
||||
// const { issuerUri, audience } = params
|
||||
|
||||
// const { verifyAndDecodeAccessToken } = await createOidcBackend({
|
||||
// issuerUri,
|
||||
// decodedAccessTokenSchema: zDecodedAccessToken,
|
||||
// })
|
||||
|
||||
// async function decodeAccessToken(params: {
|
||||
// authorizationHeaderValue: string | undefined
|
||||
// requiredRole?: string
|
||||
// }): Promise<DecodedAccessToken> {
|
||||
// const { authorizationHeaderValue, requiredRole } = params
|
||||
|
||||
// if (authorizationHeaderValue === undefined) {
|
||||
// throw Error('401 Unauthorized')
|
||||
// }
|
||||
|
||||
// const result = await verifyAndDecodeAccessToken({
|
||||
// accessToken: authorizationHeaderValue.replace(/^Bearer /, ''),
|
||||
// })
|
||||
|
||||
// if (!result) throw new Error(`The result is invalid`)
|
||||
|
||||
// if (!result.isValid) {
|
||||
// switch (result.errorCase) {
|
||||
// case 'does not respect schema':
|
||||
// throw new Error(
|
||||
// `The access token does not respect the schema ${result.errorMessage}`,
|
||||
// )
|
||||
// case 'invalid signature':
|
||||
// case 'expired':
|
||||
// throw new Error('401')
|
||||
// }
|
||||
// }
|
||||
|
||||
// const { decodedAccessToken } = result
|
||||
|
||||
// if (
|
||||
// requiredRole !== undefined &&
|
||||
// !decodedAccessToken.realm_access.roles.includes(requiredRole)
|
||||
// ) {
|
||||
// throw new Error('401')
|
||||
// }
|
||||
|
||||
// {
|
||||
// const { aud } = decodedAccessToken
|
||||
|
||||
// const aud_array = typeof aud === 'string' ? [aud] : aud
|
||||
|
||||
// if (!aud_array.includes(audience)) {
|
||||
// throw new Error('401')
|
||||
// }
|
||||
// }
|
||||
|
||||
// return decodedAccessToken
|
||||
// }
|
||||
|
||||
// return { decodeAccessToken }
|
||||
// }
|
||||
@@ -3,43 +3,70 @@ import { twMerge } from 'tailwind-merge'
|
||||
import { useEffect } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import type { ClassValue } from 'clsx'
|
||||
import { env } from '@/env'
|
||||
import { getBackendURI } from '@/routes'
|
||||
import { createCallbackClient, createClient } from '@connectrpc/connect'
|
||||
import { MessageBusService } from '@/gen/messagebus/v1/messagebus_pb'
|
||||
import { useTransport } from '@connectrpc/connect-query'
|
||||
|
||||
export function cn(...inputs: Array<ClassValue>) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export const useReactQuerySubscription = () => {
|
||||
const transport = useTransport();
|
||||
const mbsclient = createCallbackClient(MessageBusService, transport)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let interval: any
|
||||
const websocket = new WebSocket(env.VITE_BACKEND_URI + '/ws')
|
||||
websocket.onopen = () => {
|
||||
console.log('[Message Bus] opened.')
|
||||
interval = setInterval(() => {
|
||||
websocket.send('PING')
|
||||
}, 10000)
|
||||
}
|
||||
let closer = mbsclient.subscribeToConnectInvalidationRequests({}, (res) => {
|
||||
console.log(res);
|
||||
}, (err) => {
|
||||
console.log("Closed Subscription. Error is: ", err)
|
||||
});
|
||||
|
||||
websocket.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
const queryKey = [...data.entity, data.id].filter(Boolean)
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
console.log('[Message Bus] invalidating: ' + event.data)
|
||||
}
|
||||
return closer
|
||||
}, [])
|
||||
|
||||
websocket.onclose = (e) => {
|
||||
console.warn('[Message Bus] closed!', e)
|
||||
interval.close()
|
||||
}
|
||||
|
||||
websocket.onerror = () => {
|
||||
console.error('[Message Bus] errored!')
|
||||
}
|
||||
|
||||
return () => {
|
||||
websocket.close()
|
||||
}
|
||||
}, [queryClient])
|
||||
// let callback: (code?: number, reason?: string) => void | undefined
|
||||
|
||||
// useEffect(() => {
|
||||
// getBackendURI().then((uri) => {
|
||||
// let interval: any
|
||||
// const websocket = new WebSocket(uri + '/ws')
|
||||
|
||||
// websocket.onopen = () => {
|
||||
// console.log('[Message Bus] opened.')
|
||||
// interval = setInterval(() => {
|
||||
// websocket.send('PING')
|
||||
// }, 10000)
|
||||
// }
|
||||
|
||||
// websocket.onmessage = (event) => {
|
||||
// const data = JSON.parse(event.data)
|
||||
// const queryKey = [...data.entity, data.id].filter(Boolean)
|
||||
// console.log('[Message Bus] invalidating: ' + event.data)
|
||||
// queryClient.invalidateQueries({ queryKey })
|
||||
// }
|
||||
|
||||
// websocket.onclose = (e) => {
|
||||
// console.warn('[Message Bus] closed!', e)
|
||||
// clearInterval(interval)
|
||||
// }
|
||||
|
||||
// websocket.onerror = () => {
|
||||
// console.error('[Message Bus] errored!')
|
||||
// }
|
||||
|
||||
// callback = websocket.close
|
||||
// })
|
||||
|
||||
// return () => {
|
||||
// if (callback != null) {
|
||||
// return callback()
|
||||
// }
|
||||
// }
|
||||
// }, [queryClient])
|
||||
}
|
||||
|
||||
@@ -9,19 +9,22 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as ImageDotpngRouteImport } from './routes/image[.]png'
|
||||
import { Route as SidebarRouteImport } from './routes/_sidebar'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DemoTanstackQueryRouteImport } from './routes/demo.tanstack-query'
|
||||
import { Route as SidebarScannerRouteImport } from './routes/_sidebar/scanner'
|
||||
import { Route as SidebarProjectsRouteImport } from './routes/_sidebar/projects'
|
||||
import { Route as SidebarNotificationsRouteImport } from './routes/_sidebar/notifications'
|
||||
import { Route as SidebarKanbanRouteImport } from './routes/_sidebar/kanban'
|
||||
import { Route as SidebarKalendarRouteImport } from './routes/_sidebar/kalendar'
|
||||
import { Route as SidebarDocumentsRouteImport } from './routes/_sidebar/documents'
|
||||
import { Route as SidebarDashboardRouteImport } from './routes/_sidebar/dashboard'
|
||||
import { Route as SidebarChangelogRouteImport } from './routes/_sidebar/changelog'
|
||||
import { Route as SidebarCalendarRouteImport } from './routes/_sidebar/calendar'
|
||||
import { Route as SidebarAboutRouteImport } from './routes/_sidebar/about'
|
||||
import { Route as SidebarStorageIndexRouteImport } from './routes/_sidebar/storage/index'
|
||||
import { Route as SidebarProjectsIndexRouteImport } from './routes/_sidebar/projects/index'
|
||||
import { Route as SidebarNotificationsIndexRouteImport } from './routes/_sidebar/notifications/index'
|
||||
import { Route as SidebarCrmIndexRouteImport } from './routes/_sidebar/crm/index'
|
||||
import { Route as SidebarStorageRacksRouteImport } from './routes/_sidebar/storage/racks'
|
||||
import { Route as SidebarStorageMaterialRouteImport } from './routes/_sidebar/storage/material'
|
||||
@@ -42,6 +45,11 @@ import { Route as SidebarProjectsViewIdFinanceRouteImport } from './routes/_side
|
||||
import { Route as SidebarProjectsViewIdEquipmentRouteImport } from './routes/_sidebar/projects/view/$id/equipment'
|
||||
import { Route as SidebarProjectsViewIdAuditRouteImport } from './routes/_sidebar/projects/view/$id/audit'
|
||||
|
||||
const ImageDotpngRoute = ImageDotpngRouteImport.update({
|
||||
id: '/image.png',
|
||||
path: '/image.png',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const SidebarRoute = SidebarRouteImport.update({
|
||||
id: '/_sidebar',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
@@ -61,6 +69,11 @@ const SidebarScannerRoute = SidebarScannerRouteImport.update({
|
||||
path: '/scanner',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
} as any)
|
||||
const SidebarProjectsRoute = SidebarProjectsRouteImport.update({
|
||||
id: '/projects',
|
||||
path: '/projects',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
} as any)
|
||||
const SidebarNotificationsRoute = SidebarNotificationsRouteImport.update({
|
||||
id: '/notifications',
|
||||
path: '/notifications',
|
||||
@@ -71,11 +84,6 @@ const SidebarKanbanRoute = SidebarKanbanRouteImport.update({
|
||||
path: '/kanban',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
} as any)
|
||||
const SidebarKalendarRoute = SidebarKalendarRouteImport.update({
|
||||
id: '/kalendar',
|
||||
path: '/kalendar',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
} as any)
|
||||
const SidebarDocumentsRoute = SidebarDocumentsRouteImport.update({
|
||||
id: '/documents',
|
||||
path: '/documents',
|
||||
@@ -91,6 +99,11 @@ const SidebarChangelogRoute = SidebarChangelogRouteImport.update({
|
||||
path: '/changelog',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
} as any)
|
||||
const SidebarCalendarRoute = SidebarCalendarRouteImport.update({
|
||||
id: '/calendar',
|
||||
path: '/calendar',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
} as any)
|
||||
const SidebarAboutRoute = SidebarAboutRouteImport.update({
|
||||
id: '/about',
|
||||
path: '/about',
|
||||
@@ -102,10 +115,16 @@ const SidebarStorageIndexRoute = SidebarStorageIndexRouteImport.update({
|
||||
getParentRoute: () => SidebarRoute,
|
||||
} as any)
|
||||
const SidebarProjectsIndexRoute = SidebarProjectsIndexRouteImport.update({
|
||||
id: '/projects/',
|
||||
path: '/projects/',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => SidebarProjectsRoute,
|
||||
} as any)
|
||||
const SidebarNotificationsIndexRoute =
|
||||
SidebarNotificationsIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => SidebarNotificationsRoute,
|
||||
} as any)
|
||||
const SidebarCrmIndexRoute = SidebarCrmIndexRouteImport.update({
|
||||
id: '/crm/',
|
||||
path: '/crm/',
|
||||
@@ -122,19 +141,19 @@ const SidebarStorageMaterialRoute = SidebarStorageMaterialRouteImport.update({
|
||||
getParentRoute: () => SidebarRoute,
|
||||
} as any)
|
||||
const SidebarProjectsCurrentRoute = SidebarProjectsCurrentRouteImport.update({
|
||||
id: '/projects/current',
|
||||
path: '/projects/current',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
id: '/current',
|
||||
path: '/current',
|
||||
getParentRoute: () => SidebarProjectsRoute,
|
||||
} as any)
|
||||
const SidebarProjectsCreateRoute = SidebarProjectsCreateRouteImport.update({
|
||||
id: '/projects/create',
|
||||
path: '/projects/create',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
id: '/create',
|
||||
path: '/create',
|
||||
getParentRoute: () => SidebarProjectsRoute,
|
||||
} as any)
|
||||
const SidebarProjectsArchiveRoute = SidebarProjectsArchiveRouteImport.update({
|
||||
id: '/projects/archive',
|
||||
path: '/projects/archive',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
id: '/archive',
|
||||
path: '/archive',
|
||||
getParentRoute: () => SidebarProjectsRoute,
|
||||
} as any)
|
||||
const SidebarCrmLeieferantenRoute = SidebarCrmLeieferantenRouteImport.update({
|
||||
id: '/crm/leieferanten',
|
||||
@@ -163,9 +182,9 @@ const SidebarCrmAnsprechpartnerRoute =
|
||||
getParentRoute: () => SidebarRoute,
|
||||
} as any)
|
||||
const SidebarProjectsViewIdRoute = SidebarProjectsViewIdRouteImport.update({
|
||||
id: '/projects/view/$id',
|
||||
path: '/projects/view/$id',
|
||||
getParentRoute: () => SidebarRoute,
|
||||
id: '/view/$id',
|
||||
path: '/view/$id',
|
||||
getParentRoute: () => SidebarProjectsRoute,
|
||||
} as any)
|
||||
const SidebarProjectsViewIdIndexRoute =
|
||||
SidebarProjectsViewIdIndexRouteImport.update({
|
||||
@@ -212,13 +231,15 @@ const SidebarProjectsViewIdAuditRoute =
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/image.png': typeof ImageDotpngRoute
|
||||
'/about': typeof SidebarAboutRoute
|
||||
'/calendar': typeof SidebarCalendarRoute
|
||||
'/changelog': typeof SidebarChangelogRoute
|
||||
'/dashboard': typeof SidebarDashboardRoute
|
||||
'/documents': typeof SidebarDocumentsRoute
|
||||
'/kalendar': typeof SidebarKalendarRoute
|
||||
'/kanban': typeof SidebarKanbanRoute
|
||||
'/notifications': typeof SidebarNotificationsRoute
|
||||
'/notifications': typeof SidebarNotificationsRouteWithChildren
|
||||
'/projects': typeof SidebarProjectsRouteWithChildren
|
||||
'/scanner': typeof SidebarScannerRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/crm/ansprechpartner': typeof SidebarCrmAnsprechpartnerRoute
|
||||
@@ -232,7 +253,8 @@ export interface FileRoutesByFullPath {
|
||||
'/storage/material': typeof SidebarStorageMaterialRoute
|
||||
'/storage/racks': typeof SidebarStorageRacksRoute
|
||||
'/crm': typeof SidebarCrmIndexRoute
|
||||
'/projects': typeof SidebarProjectsIndexRoute
|
||||
'/notifications/': typeof SidebarNotificationsIndexRoute
|
||||
'/projects/': typeof SidebarProjectsIndexRoute
|
||||
'/storage': typeof SidebarStorageIndexRoute
|
||||
'/projects/view/$id': typeof SidebarProjectsViewIdRouteWithChildren
|
||||
'/projects/view/$id/audit': typeof SidebarProjectsViewIdAuditRoute
|
||||
@@ -245,13 +267,13 @@ export interface FileRoutesByFullPath {
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/image.png': typeof ImageDotpngRoute
|
||||
'/about': typeof SidebarAboutRoute
|
||||
'/calendar': typeof SidebarCalendarRoute
|
||||
'/changelog': typeof SidebarChangelogRoute
|
||||
'/dashboard': typeof SidebarDashboardRoute
|
||||
'/documents': typeof SidebarDocumentsRoute
|
||||
'/kalendar': typeof SidebarKalendarRoute
|
||||
'/kanban': typeof SidebarKanbanRoute
|
||||
'/notifications': typeof SidebarNotificationsRoute
|
||||
'/scanner': typeof SidebarScannerRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/crm/ansprechpartner': typeof SidebarCrmAnsprechpartnerRoute
|
||||
@@ -265,6 +287,7 @@ export interface FileRoutesByTo {
|
||||
'/storage/material': typeof SidebarStorageMaterialRoute
|
||||
'/storage/racks': typeof SidebarStorageRacksRoute
|
||||
'/crm': typeof SidebarCrmIndexRoute
|
||||
'/notifications': typeof SidebarNotificationsIndexRoute
|
||||
'/projects': typeof SidebarProjectsIndexRoute
|
||||
'/storage': typeof SidebarStorageIndexRoute
|
||||
'/projects/view/$id/audit': typeof SidebarProjectsViewIdAuditRoute
|
||||
@@ -279,13 +302,15 @@ export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/_sidebar': typeof SidebarRouteWithChildren
|
||||
'/image.png': typeof ImageDotpngRoute
|
||||
'/_sidebar/about': typeof SidebarAboutRoute
|
||||
'/_sidebar/calendar': typeof SidebarCalendarRoute
|
||||
'/_sidebar/changelog': typeof SidebarChangelogRoute
|
||||
'/_sidebar/dashboard': typeof SidebarDashboardRoute
|
||||
'/_sidebar/documents': typeof SidebarDocumentsRoute
|
||||
'/_sidebar/kalendar': typeof SidebarKalendarRoute
|
||||
'/_sidebar/kanban': typeof SidebarKanbanRoute
|
||||
'/_sidebar/notifications': typeof SidebarNotificationsRoute
|
||||
'/_sidebar/notifications': typeof SidebarNotificationsRouteWithChildren
|
||||
'/_sidebar/projects': typeof SidebarProjectsRouteWithChildren
|
||||
'/_sidebar/scanner': typeof SidebarScannerRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/_sidebar/crm/ansprechpartner': typeof SidebarCrmAnsprechpartnerRoute
|
||||
@@ -299,6 +324,7 @@ export interface FileRoutesById {
|
||||
'/_sidebar/storage/material': typeof SidebarStorageMaterialRoute
|
||||
'/_sidebar/storage/racks': typeof SidebarStorageRacksRoute
|
||||
'/_sidebar/crm/': typeof SidebarCrmIndexRoute
|
||||
'/_sidebar/notifications/': typeof SidebarNotificationsIndexRoute
|
||||
'/_sidebar/projects/': typeof SidebarProjectsIndexRoute
|
||||
'/_sidebar/storage/': typeof SidebarStorageIndexRoute
|
||||
'/_sidebar/projects/view/$id': typeof SidebarProjectsViewIdRouteWithChildren
|
||||
@@ -314,13 +340,15 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/image.png'
|
||||
| '/about'
|
||||
| '/calendar'
|
||||
| '/changelog'
|
||||
| '/dashboard'
|
||||
| '/documents'
|
||||
| '/kalendar'
|
||||
| '/kanban'
|
||||
| '/notifications'
|
||||
| '/projects'
|
||||
| '/scanner'
|
||||
| '/demo/tanstack-query'
|
||||
| '/crm/ansprechpartner'
|
||||
@@ -334,7 +362,8 @@ export interface FileRouteTypes {
|
||||
| '/storage/material'
|
||||
| '/storage/racks'
|
||||
| '/crm'
|
||||
| '/projects'
|
||||
| '/notifications/'
|
||||
| '/projects/'
|
||||
| '/storage'
|
||||
| '/projects/view/$id'
|
||||
| '/projects/view/$id/audit'
|
||||
@@ -347,13 +376,13 @@ export interface FileRouteTypes {
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/image.png'
|
||||
| '/about'
|
||||
| '/calendar'
|
||||
| '/changelog'
|
||||
| '/dashboard'
|
||||
| '/documents'
|
||||
| '/kalendar'
|
||||
| '/kanban'
|
||||
| '/notifications'
|
||||
| '/scanner'
|
||||
| '/demo/tanstack-query'
|
||||
| '/crm/ansprechpartner'
|
||||
@@ -367,6 +396,7 @@ export interface FileRouteTypes {
|
||||
| '/storage/material'
|
||||
| '/storage/racks'
|
||||
| '/crm'
|
||||
| '/notifications'
|
||||
| '/projects'
|
||||
| '/storage'
|
||||
| '/projects/view/$id/audit'
|
||||
@@ -380,13 +410,15 @@ export interface FileRouteTypes {
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/_sidebar'
|
||||
| '/image.png'
|
||||
| '/_sidebar/about'
|
||||
| '/_sidebar/calendar'
|
||||
| '/_sidebar/changelog'
|
||||
| '/_sidebar/dashboard'
|
||||
| '/_sidebar/documents'
|
||||
| '/_sidebar/kalendar'
|
||||
| '/_sidebar/kanban'
|
||||
| '/_sidebar/notifications'
|
||||
| '/_sidebar/projects'
|
||||
| '/_sidebar/scanner'
|
||||
| '/demo/tanstack-query'
|
||||
| '/_sidebar/crm/ansprechpartner'
|
||||
@@ -400,6 +432,7 @@ export interface FileRouteTypes {
|
||||
| '/_sidebar/storage/material'
|
||||
| '/_sidebar/storage/racks'
|
||||
| '/_sidebar/crm/'
|
||||
| '/_sidebar/notifications/'
|
||||
| '/_sidebar/projects/'
|
||||
| '/_sidebar/storage/'
|
||||
| '/_sidebar/projects/view/$id'
|
||||
@@ -415,11 +448,19 @@ export interface FileRouteTypes {
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
SidebarRoute: typeof SidebarRouteWithChildren
|
||||
ImageDotpngRoute: typeof ImageDotpngRoute
|
||||
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/image.png': {
|
||||
id: '/image.png'
|
||||
path: '/image.png'
|
||||
fullPath: '/image.png'
|
||||
preLoaderRoute: typeof ImageDotpngRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_sidebar': {
|
||||
id: '/_sidebar'
|
||||
path: ''
|
||||
@@ -448,6 +489,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SidebarScannerRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
}
|
||||
'/_sidebar/projects': {
|
||||
id: '/_sidebar/projects'
|
||||
path: '/projects'
|
||||
fullPath: '/projects'
|
||||
preLoaderRoute: typeof SidebarProjectsRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
}
|
||||
'/_sidebar/notifications': {
|
||||
id: '/_sidebar/notifications'
|
||||
path: '/notifications'
|
||||
@@ -462,13 +510,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SidebarKanbanRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
}
|
||||
'/_sidebar/kalendar': {
|
||||
id: '/_sidebar/kalendar'
|
||||
path: '/kalendar'
|
||||
fullPath: '/kalendar'
|
||||
preLoaderRoute: typeof SidebarKalendarRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
}
|
||||
'/_sidebar/documents': {
|
||||
id: '/_sidebar/documents'
|
||||
path: '/documents'
|
||||
@@ -490,6 +531,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SidebarChangelogRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
}
|
||||
'/_sidebar/calendar': {
|
||||
id: '/_sidebar/calendar'
|
||||
path: '/calendar'
|
||||
fullPath: '/calendar'
|
||||
preLoaderRoute: typeof SidebarCalendarRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
}
|
||||
'/_sidebar/about': {
|
||||
id: '/_sidebar/about'
|
||||
path: '/about'
|
||||
@@ -506,10 +554,17 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
'/_sidebar/projects/': {
|
||||
id: '/_sidebar/projects/'
|
||||
path: '/projects'
|
||||
fullPath: '/projects'
|
||||
path: '/'
|
||||
fullPath: '/projects/'
|
||||
preLoaderRoute: typeof SidebarProjectsIndexRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
parentRoute: typeof SidebarProjectsRoute
|
||||
}
|
||||
'/_sidebar/notifications/': {
|
||||
id: '/_sidebar/notifications/'
|
||||
path: '/'
|
||||
fullPath: '/notifications/'
|
||||
preLoaderRoute: typeof SidebarNotificationsIndexRouteImport
|
||||
parentRoute: typeof SidebarNotificationsRoute
|
||||
}
|
||||
'/_sidebar/crm/': {
|
||||
id: '/_sidebar/crm/'
|
||||
@@ -534,24 +589,24 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
'/_sidebar/projects/current': {
|
||||
id: '/_sidebar/projects/current'
|
||||
path: '/projects/current'
|
||||
path: '/current'
|
||||
fullPath: '/projects/current'
|
||||
preLoaderRoute: typeof SidebarProjectsCurrentRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
parentRoute: typeof SidebarProjectsRoute
|
||||
}
|
||||
'/_sidebar/projects/create': {
|
||||
id: '/_sidebar/projects/create'
|
||||
path: '/projects/create'
|
||||
path: '/create'
|
||||
fullPath: '/projects/create'
|
||||
preLoaderRoute: typeof SidebarProjectsCreateRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
parentRoute: typeof SidebarProjectsRoute
|
||||
}
|
||||
'/_sidebar/projects/archive': {
|
||||
id: '/_sidebar/projects/archive'
|
||||
path: '/projects/archive'
|
||||
path: '/archive'
|
||||
fullPath: '/projects/archive'
|
||||
preLoaderRoute: typeof SidebarProjectsArchiveRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
parentRoute: typeof SidebarProjectsRoute
|
||||
}
|
||||
'/_sidebar/crm/leieferanten': {
|
||||
id: '/_sidebar/crm/leieferanten'
|
||||
@@ -590,10 +645,10 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
'/_sidebar/projects/view/$id': {
|
||||
id: '/_sidebar/projects/view/$id'
|
||||
path: '/projects/view/$id'
|
||||
path: '/view/$id'
|
||||
fullPath: '/projects/view/$id'
|
||||
preLoaderRoute: typeof SidebarProjectsViewIdRouteImport
|
||||
parentRoute: typeof SidebarRoute
|
||||
parentRoute: typeof SidebarProjectsRoute
|
||||
}
|
||||
'/_sidebar/projects/view/$id/': {
|
||||
id: '/_sidebar/projects/view/$id/'
|
||||
@@ -647,6 +702,17 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
}
|
||||
|
||||
interface SidebarNotificationsRouteChildren {
|
||||
SidebarNotificationsIndexRoute: typeof SidebarNotificationsIndexRoute
|
||||
}
|
||||
|
||||
const SidebarNotificationsRouteChildren: SidebarNotificationsRouteChildren = {
|
||||
SidebarNotificationsIndexRoute: SidebarNotificationsIndexRoute,
|
||||
}
|
||||
|
||||
const SidebarNotificationsRouteWithChildren =
|
||||
SidebarNotificationsRoute._addFileChildren(SidebarNotificationsRouteChildren)
|
||||
|
||||
interface SidebarProjectsViewIdRouteChildren {
|
||||
SidebarProjectsViewIdAuditRoute: typeof SidebarProjectsViewIdAuditRoute
|
||||
SidebarProjectsViewIdEquipmentRoute: typeof SidebarProjectsViewIdEquipmentRoute
|
||||
@@ -672,54 +738,66 @@ const SidebarProjectsViewIdRouteWithChildren =
|
||||
SidebarProjectsViewIdRouteChildren,
|
||||
)
|
||||
|
||||
interface SidebarProjectsRouteChildren {
|
||||
SidebarProjectsArchiveRoute: typeof SidebarProjectsArchiveRoute
|
||||
SidebarProjectsCreateRoute: typeof SidebarProjectsCreateRoute
|
||||
SidebarProjectsCurrentRoute: typeof SidebarProjectsCurrentRoute
|
||||
SidebarProjectsIndexRoute: typeof SidebarProjectsIndexRoute
|
||||
SidebarProjectsViewIdRoute: typeof SidebarProjectsViewIdRouteWithChildren
|
||||
}
|
||||
|
||||
const SidebarProjectsRouteChildren: SidebarProjectsRouteChildren = {
|
||||
SidebarProjectsArchiveRoute: SidebarProjectsArchiveRoute,
|
||||
SidebarProjectsCreateRoute: SidebarProjectsCreateRoute,
|
||||
SidebarProjectsCurrentRoute: SidebarProjectsCurrentRoute,
|
||||
SidebarProjectsIndexRoute: SidebarProjectsIndexRoute,
|
||||
SidebarProjectsViewIdRoute: SidebarProjectsViewIdRouteWithChildren,
|
||||
}
|
||||
|
||||
const SidebarProjectsRouteWithChildren = SidebarProjectsRoute._addFileChildren(
|
||||
SidebarProjectsRouteChildren,
|
||||
)
|
||||
|
||||
interface SidebarRouteChildren {
|
||||
SidebarAboutRoute: typeof SidebarAboutRoute
|
||||
SidebarCalendarRoute: typeof SidebarCalendarRoute
|
||||
SidebarChangelogRoute: typeof SidebarChangelogRoute
|
||||
SidebarDashboardRoute: typeof SidebarDashboardRoute
|
||||
SidebarDocumentsRoute: typeof SidebarDocumentsRoute
|
||||
SidebarKalendarRoute: typeof SidebarKalendarRoute
|
||||
SidebarKanbanRoute: typeof SidebarKanbanRoute
|
||||
SidebarNotificationsRoute: typeof SidebarNotificationsRoute
|
||||
SidebarNotificationsRoute: typeof SidebarNotificationsRouteWithChildren
|
||||
SidebarProjectsRoute: typeof SidebarProjectsRouteWithChildren
|
||||
SidebarScannerRoute: typeof SidebarScannerRoute
|
||||
SidebarCrmAnsprechpartnerRoute: typeof SidebarCrmAnsprechpartnerRoute
|
||||
SidebarCrmDienstleisterRoute: typeof SidebarCrmDienstleisterRoute
|
||||
SidebarCrmFirmenRoute: typeof SidebarCrmFirmenRoute
|
||||
SidebarCrmKostenstelleRoute: typeof SidebarCrmKostenstelleRoute
|
||||
SidebarCrmLeieferantenRoute: typeof SidebarCrmLeieferantenRoute
|
||||
SidebarProjectsArchiveRoute: typeof SidebarProjectsArchiveRoute
|
||||
SidebarProjectsCreateRoute: typeof SidebarProjectsCreateRoute
|
||||
SidebarProjectsCurrentRoute: typeof SidebarProjectsCurrentRoute
|
||||
SidebarStorageMaterialRoute: typeof SidebarStorageMaterialRoute
|
||||
SidebarStorageRacksRoute: typeof SidebarStorageRacksRoute
|
||||
SidebarCrmIndexRoute: typeof SidebarCrmIndexRoute
|
||||
SidebarProjectsIndexRoute: typeof SidebarProjectsIndexRoute
|
||||
SidebarStorageIndexRoute: typeof SidebarStorageIndexRoute
|
||||
SidebarProjectsViewIdRoute: typeof SidebarProjectsViewIdRouteWithChildren
|
||||
}
|
||||
|
||||
const SidebarRouteChildren: SidebarRouteChildren = {
|
||||
SidebarAboutRoute: SidebarAboutRoute,
|
||||
SidebarCalendarRoute: SidebarCalendarRoute,
|
||||
SidebarChangelogRoute: SidebarChangelogRoute,
|
||||
SidebarDashboardRoute: SidebarDashboardRoute,
|
||||
SidebarDocumentsRoute: SidebarDocumentsRoute,
|
||||
SidebarKalendarRoute: SidebarKalendarRoute,
|
||||
SidebarKanbanRoute: SidebarKanbanRoute,
|
||||
SidebarNotificationsRoute: SidebarNotificationsRoute,
|
||||
SidebarNotificationsRoute: SidebarNotificationsRouteWithChildren,
|
||||
SidebarProjectsRoute: SidebarProjectsRouteWithChildren,
|
||||
SidebarScannerRoute: SidebarScannerRoute,
|
||||
SidebarCrmAnsprechpartnerRoute: SidebarCrmAnsprechpartnerRoute,
|
||||
SidebarCrmDienstleisterRoute: SidebarCrmDienstleisterRoute,
|
||||
SidebarCrmFirmenRoute: SidebarCrmFirmenRoute,
|
||||
SidebarCrmKostenstelleRoute: SidebarCrmKostenstelleRoute,
|
||||
SidebarCrmLeieferantenRoute: SidebarCrmLeieferantenRoute,
|
||||
SidebarProjectsArchiveRoute: SidebarProjectsArchiveRoute,
|
||||
SidebarProjectsCreateRoute: SidebarProjectsCreateRoute,
|
||||
SidebarProjectsCurrentRoute: SidebarProjectsCurrentRoute,
|
||||
SidebarStorageMaterialRoute: SidebarStorageMaterialRoute,
|
||||
SidebarStorageRacksRoute: SidebarStorageRacksRoute,
|
||||
SidebarCrmIndexRoute: SidebarCrmIndexRoute,
|
||||
SidebarProjectsIndexRoute: SidebarProjectsIndexRoute,
|
||||
SidebarStorageIndexRoute: SidebarStorageIndexRoute,
|
||||
SidebarProjectsViewIdRoute: SidebarProjectsViewIdRouteWithChildren,
|
||||
}
|
||||
|
||||
const SidebarRouteWithChildren =
|
||||
@@ -728,8 +806,19 @@ const SidebarRouteWithChildren =
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
SidebarRoute: SidebarRouteWithChildren,
|
||||
ImageDotpngRoute: ImageDotpngRoute,
|
||||
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { startInstance } from './start.tsx'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
config: Awaited<ReturnType<typeof startInstance.getOptions>>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,65 @@
|
||||
import { createRouter as createTanstackRouter } from '@tanstack/react-router'
|
||||
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
|
||||
import * as TanstackQuery from './integrations/tanstack-query/root-provider'
|
||||
import { createConnectTransport } from '@connectrpc/connect-web'
|
||||
import { TransportProvider } from '@connectrpc/connect-query'
|
||||
|
||||
// Import the generated route tree
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import { Interceptor } from '@connectrpc/connect'
|
||||
import { getOidc } from './lib/oidc'
|
||||
import { getRuntimeRPCURI } from './routes'
|
||||
import NotFound from './components/404'
|
||||
|
||||
const logingIC: Interceptor = (next) => (req) => {
|
||||
console.log(
|
||||
`[ConnectRPC] ${new Date().toLocaleDateString()} - ${req.service}`,
|
||||
)
|
||||
return next(req)
|
||||
}
|
||||
|
||||
const auth: Interceptor = (next) => async (req) => {
|
||||
const oidc = await getOidc()
|
||||
if (oidc && oidc.getAccessToken)
|
||||
req.header.set('Authentication', 'Bearer ' + (await oidc.getAccessToken()))
|
||||
|
||||
return await next(req)
|
||||
}
|
||||
|
||||
const transport = async () =>{
|
||||
const baseUrl = await getRuntimeRPCURI();
|
||||
console.log("BASE_URL", baseUrl)
|
||||
return createConnectTransport({
|
||||
baseUrl: baseUrl!,
|
||||
interceptors: [logingIC, auth],
|
||||
})
|
||||
}
|
||||
|
||||
// Create a new router instance
|
||||
export const createRouter = () => {
|
||||
export const getRouter = async () => {
|
||||
const rqContext = TanstackQuery.getContext()
|
||||
|
||||
const finalTransport = await transport()
|
||||
|
||||
const router = createTanstackRouter({
|
||||
routeTree,
|
||||
context: { ...rqContext },
|
||||
context: { ...rqContext, transport: finalTransport },
|
||||
defaultPreload: 'intent',
|
||||
Wrap: (props: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TanstackQuery.Provider {...rqContext}>
|
||||
{props.children}
|
||||
</TanstackQuery.Provider>
|
||||
<TransportProvider transport={finalTransport}>
|
||||
<TanstackQuery.Provider {...rqContext}>
|
||||
{props.children}
|
||||
</TanstackQuery.Provider>
|
||||
</TransportProvider>
|
||||
)
|
||||
},
|
||||
defaultErrorComponent: ({ error }) => {
|
||||
return <div>ERROR {error.message}</div>
|
||||
},
|
||||
defaultNotFoundComponent: () => {
|
||||
return <NotFound />
|
||||
},
|
||||
})
|
||||
|
||||
setupRouterSsrQueryIntegration({ router, queryClient: rqContext.queryClient })
|
||||
@@ -30,6 +70,6 @@ export const createRouter = () => {
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof createRouter>
|
||||
router: ReturnType<typeof getRouter>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
createRootRouteWithContext,
|
||||
} from '@tanstack/react-router'
|
||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
||||
import { TanstackDevtools } from '@tanstack/react-devtools'
|
||||
import { TanStackDevtools } from '@tanstack/react-devtools'
|
||||
import { useEffect } from 'react'
|
||||
import { enableDragDropTouch } from '@dragdroptouch/drag-drop-touch'
|
||||
|
||||
@@ -16,9 +16,11 @@ import appCss from '../styles.css?url'
|
||||
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import { useReactQuerySubscription } from '@/lib/utils'
|
||||
|
||||
import { Transport } from '@connectrpc/connect'
|
||||
// import * as wc from 'webcrypto-liner';
|
||||
interface MyRouterContext {
|
||||
queryClient: QueryClient
|
||||
transport: Transport
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
@@ -34,6 +36,39 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
{
|
||||
title: 'Eventory',
|
||||
},
|
||||
{
|
||||
name: 'og:title',
|
||||
content: 'Eventory',
|
||||
},
|
||||
{
|
||||
name: 'og:description',
|
||||
content: 'A simple self-hostable rentalware.',
|
||||
},
|
||||
{
|
||||
name: 'og:image',
|
||||
content: '/image.png',
|
||||
},
|
||||
{
|
||||
name: 'og:url',
|
||||
content: 'https://vt.kocoder.xyz',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'twitter:title',
|
||||
content: 'Eventory',
|
||||
},
|
||||
{
|
||||
name: 'twitter:description',
|
||||
content: 'A simple self-hostable rentalware.',
|
||||
},
|
||||
{
|
||||
name: 'twitter:image',
|
||||
content: '/image.png',
|
||||
},
|
||||
{
|
||||
name: 'twitter:url',
|
||||
content: 'https://vt.kocoder.xyz',
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
@@ -41,6 +76,28 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
href: appCss,
|
||||
},
|
||||
],
|
||||
scripts: [
|
||||
// {
|
||||
// src: 'https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.7.0/polyfill.min.js',
|
||||
// },
|
||||
// {
|
||||
// src: 'https://cdnjs.cloudflare.com/ajax/libs/asmCrypto/2.3.2/asmcrypto.all.es8.min.js',
|
||||
// },
|
||||
// {
|
||||
// src: 'https://cdn.rawgit.com/indutny/elliptic/master/dist/elliptic.min.js',
|
||||
// },
|
||||
// ...(import.meta.env.MODE != 'development'
|
||||
// ? []
|
||||
// : [
|
||||
// {
|
||||
// src: '/node_modules/webcrypto-liner/dist/webcrypto-liner.shim.js',
|
||||
// },
|
||||
// ]),
|
||||
// {
|
||||
// type: "module",
|
||||
// src: "https://cdn.jsdelivr.net/npm/webcrypto-liner@1.4.3/+esm"
|
||||
// }
|
||||
],
|
||||
}),
|
||||
|
||||
shellComponent: RootDocument,
|
||||
@@ -59,7 +116,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<TanstackDevtools
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: 'middle-right',
|
||||
}}
|
||||
|
||||
@@ -7,16 +7,27 @@ import {
|
||||
SidebarTrigger,
|
||||
} from '@/components/ui/sidebar'
|
||||
import Breadcrumbs from '@/components/applicationBreadcrumbs'
|
||||
import { enforceLogin } from '@/lib/oidc'
|
||||
import { Suspense } from 'react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import NotFound from '@/components/404'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: enforceLogin,
|
||||
errorComponent: ({ error }) => {
|
||||
return <div>ERROR {error.message}</div>
|
||||
},
|
||||
notFoundComponent: () => {
|
||||
return <NotFound />
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset className="sidebar-width block max-h-screen">
|
||||
<SidebarInset className="sidebar-width max-h-screen flex">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
@@ -27,8 +38,10 @@ function RouteComponent() {
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-4 pt-0 full-h w-full overflow-scroll">
|
||||
<Outlet />
|
||||
<div className="p-4 pt-0 flex grow w-full overflow-scroll">
|
||||
<Suspense fallback={<Skeleton className="w-full h-full"></Skeleton>}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
|
||||
15
src/routes/_sidebar/calendar.tsx
Normal file
15
src/routes/_sidebar/calendar.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import Calendar from '@/components/calendar'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/calendar')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Calendar',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <Calendar />
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/dashboard')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Dashboard',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Map, MapControls, MapMarker, MarkerContent, MarkerPopup, MarkerTooltip } from '@/components/ui/map'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/documents')({
|
||||
@@ -5,5 +6,35 @@ export const Route = createFileRoute('/_sidebar/documents')({
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_sidebar/documents"!</div>
|
||||
return <div className='w-full h-full rounded-md overflow-hidden '>
|
||||
<Map
|
||||
center={{lat: 48.140134, lon:16.318053}}
|
||||
zoom={17}
|
||||
>
|
||||
<MapControls position='bottom-right' showZoom
|
||||
showCompass
|
||||
showLocate
|
||||
showFullscreen
|
||||
/>
|
||||
<MapMarker
|
||||
key={"E5/27"}
|
||||
longitude={16.318053}
|
||||
latitude={48.140134}
|
||||
draggable
|
||||
>
|
||||
<MarkerContent>
|
||||
<div className="size-4 rounded-full bg-emerald-500 border-2 border-white shadow-lg" />
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>Ellmingergasse 5/27</MarkerTooltip>
|
||||
<MarkerPopup>
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-foreground">Ellmingergasse 5/27</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{48.140134.toFixed(4)}, {16.318053.toFixed(4)}
|
||||
</p>
|
||||
</div>
|
||||
</MarkerPopup>
|
||||
</MapMarker>
|
||||
</Map>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,287 +1,159 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Kanban, KanbanBoard, KanbanColumn, KanbanColumnHandle, KanbanItem, KanbanOverlay } from '@/components/ui/kanban'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { GripVertical } from 'lucide-react'
|
||||
|
||||
import KanbanColumn from '@/features/Kanban/components/KanbanColumn'
|
||||
import Kanban from '@/features/Kanban/components/Kanban'
|
||||
import KanbanCard from '@/features/Kanban/components/KanbanCard'
|
||||
import KanbanDropzone from '@/features/Kanban/components/KanbanDropzone'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/kanban')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Kanban',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const kanbanboard = {
|
||||
columns: [
|
||||
{
|
||||
name: 'Todo',
|
||||
itemCount: 5,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id: 0,
|
||||
name: 'Backlog'
|
||||
},
|
||||
{
|
||||
name: 'Doing',
|
||||
itemCount: 6,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id: 1,
|
||||
name: 'Todo'
|
||||
},
|
||||
{
|
||||
name: 'Done',
|
||||
itemCount: 20,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id: 2,
|
||||
name: 'Doing'
|
||||
},
|
||||
{
|
||||
name: 'Todo',
|
||||
itemCount: 5,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id: 3,
|
||||
name: 'Done'
|
||||
},
|
||||
{
|
||||
name: 'Doing',
|
||||
itemCount: 6,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id: 4,
|
||||
name: 'Backlog'
|
||||
},
|
||||
{
|
||||
name: 'Done',
|
||||
itemCount: 20,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id: 5,
|
||||
name: 'Todo'
|
||||
},
|
||||
{
|
||||
name: 'Todo',
|
||||
itemCount: 5,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id: 6,
|
||||
name: 'Doing'
|
||||
},
|
||||
{
|
||||
name: 'Doing',
|
||||
itemCount: 6,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Done',
|
||||
itemCount: 20,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Todo',
|
||||
itemCount: 5,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Doing',
|
||||
itemCount: 6,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Done',
|
||||
itemCount: 20,
|
||||
children: [
|
||||
{
|
||||
name: 'Hello World!',
|
||||
path: 'https://google.com',
|
||||
description: 'Das ist eine Testkarte',
|
||||
labels: [
|
||||
{
|
||||
name: 'flow::7 Done',
|
||||
className: 'bg-emerald-500',
|
||||
},
|
||||
{
|
||||
name: 'Wichtig',
|
||||
className: 'bg-red-500',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id: 7,
|
||||
name: 'Done'
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
priority: "low" | "medium" | "high";
|
||||
assignee?: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
const COLUMN_TITLES: Record<string, string> = {
|
||||
backlog: "Backlog",
|
||||
inProgress: "In Progress",
|
||||
done: "Done",
|
||||
};
|
||||
|
||||
function RouteComponent() {
|
||||
// const [columns, setColumns] = useState(kanbanboard.columns)
|
||||
|
||||
const [columns, setColumns] = useState<string[]>([
|
||||
"backlog",
|
||||
"inProgress",
|
||||
"done",
|
||||
"sprintbacl",
|
||||
"toStart",
|
||||
"notDone",
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<Kanban>
|
||||
{kanbanboard.columns.map((val) => {
|
||||
return (
|
||||
<>
|
||||
<KanbanColumn name={val.name} itemCount={val.itemCount}>
|
||||
{val.children.map((card) => {
|
||||
return <KanbanCard card={card} />
|
||||
})}
|
||||
</KanbanColumn>
|
||||
<KanbanDropzone />
|
||||
</>
|
||||
)
|
||||
})}
|
||||
<Kanban value={columns} onValueChange={setColumns} getItemValue={(c) => c.id}>
|
||||
<KanbanBoard className="flex min-w-full overflow-scroll">
|
||||
{Object.entries(columns).map(([columnValue]) => (
|
||||
<KanbanColumn key={columnValue} value={columnValue} className='min-w-md'>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm">
|
||||
{COLUMN_TITLES[columnValue]}
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="pointer-events-none rounded-sm"
|
||||
>
|
||||
0
|
||||
</Badge>
|
||||
</div>
|
||||
<KanbanColumnHandle asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</KanbanColumnHandle>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-0.5">
|
||||
{/* List Tasks using Tanstack Query
|
||||
|
||||
{tasks.map((task) => (
|
||||
<KanbanItem key={task.id} value={task.id} asHandle asChild>
|
||||
<div className="rounded-md border bg-card p-3 shadow-xs">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="line-clamp-1 font-medium text-sm">
|
||||
{task.title}
|
||||
</span>
|
||||
<Badge
|
||||
variant={
|
||||
task.priority === "high"
|
||||
? "destructive"
|
||||
: task.priority === "medium"
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
className="pointer-events-none h-5 rounded-sm px-1.5 text-[11px] capitalize"
|
||||
>
|
||||
{task.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-muted-foreground text-xs">
|
||||
{task.assignee && (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="size-2 rounded-full bg-primary/20" />
|
||||
<span className="line-clamp-1">
|
||||
{task.assignee}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{task.dueDate && (
|
||||
<time className="text-[10px] tabular-nums">
|
||||
{task.dueDate}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</KanbanItem>
|
||||
))}
|
||||
|
||||
*/}
|
||||
</div>
|
||||
</KanbanColumn>
|
||||
))}
|
||||
</KanbanBoard>
|
||||
<KanbanOverlay>
|
||||
<div className="size-full rounded-md bg-primary/10" />
|
||||
</KanbanOverlay>
|
||||
</Kanban>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,32 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable"
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/notifications')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_sidebar/notifications"!</div>
|
||||
return (
|
||||
<ResizablePanelGroup
|
||||
className="w-full max-w-full rounded-lg border"
|
||||
>
|
||||
<ResizablePanel defaultSize={"90px"}>
|
||||
<div className="flex h-[200px] items-center justify-center p-6">
|
||||
<span className="font-semibold">One</span>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={"200px"}>
|
||||
<ResizablePanel defaultSize={25}>
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/kalendar')({
|
||||
export const Route = createFileRoute('/_sidebar/notifications/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_sidebar/kalendar"!</div>
|
||||
return <div>Hello "/_sidebar/notifications"!</div>
|
||||
}
|
||||
14
src/routes/_sidebar/projects.tsx
Normal file
14
src/routes/_sidebar/projects.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Projekte',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <Outlet />
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ProjectService } from '@/gen/project/v1/project_pb'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createQueryOptions } from '@connectrpc/connect-query'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects/current')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const {} = useQuery(createQueryOptions())
|
||||
return <div>Hello "/_sidebar/projects/current"!</div>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,40 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import ProjectsTable from '@/features/Projects/components/table'
|
||||
import { createInfiniteQueryOptions } from '@connectrpc/connect-query'
|
||||
import { listProjects } from '@/gen/project/v1/project-ProjectService_connectquery'
|
||||
|
||||
const fetchSize = 25
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects/')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Projekte',
|
||||
}
|
||||
},
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureInfiniteQueryData(
|
||||
createInfiniteQueryOptions(
|
||||
listProjects,
|
||||
{
|
||||
$typeName: 'project.v1.ListProjectsRequest',
|
||||
page: 0,
|
||||
perPage: fetchSize,
|
||||
orberBy: 'id',
|
||||
asc: true,
|
||||
},
|
||||
{
|
||||
transport: context.transport,
|
||||
getNextPageParam: (_, p) => {
|
||||
console.log(p.length)
|
||||
return p.length * fetchSize
|
||||
// return ()
|
||||
},
|
||||
pageParamKey: 'page',
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
|
||||
@@ -3,33 +3,57 @@ import {
|
||||
Outlet,
|
||||
createFileRoute,
|
||||
useLocation,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { getProjectQueryObject, useProject } from '@/features/Projects/queries'
|
||||
import { getProject } from '@/gen/project/v1/project-ProjectService_connectquery'
|
||||
import { createQueryOptions, useSuspenseQuery } from '@connectrpc/connect-query'
|
||||
import { Suspense } from 'react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects/view/$id')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: ({ params: { id } }) => {
|
||||
return {
|
||||
breadcrumb: '' + id,
|
||||
}
|
||||
},
|
||||
loader: async ({ params: { id }, context }) => {
|
||||
// await context.queryClient.ensureQueryData(getProjectQueryObject(Number(id)))
|
||||
await context.queryClient.ensureQueryData(
|
||||
createQueryOptions(
|
||||
getProject,
|
||||
{
|
||||
id: Number(id),
|
||||
},
|
||||
{
|
||||
transport: context.transport,
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function ProjectName({ id }: { id: string }) {
|
||||
const { data } = useSuspenseQuery(getProject, {
|
||||
id: Number(id),
|
||||
})
|
||||
|
||||
return <div className="my-2 text-xl">{data?.name + ' #' + data?.id}</div>
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useParams()
|
||||
const router = useLocation()
|
||||
const { data, isLoading } = useProject(Number(id))
|
||||
|
||||
const res = router.pathname.split('/')
|
||||
const val = res[res.length - 1]
|
||||
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="my-2 text-xl">{data?.name + ' #' + data?.ID}</div>
|
||||
<Tabs defaultValue={val} className="w-full">
|
||||
<TabsList className="w-full overflow-auto">
|
||||
<div className="max-h-full grow overflow-auto flex flex-col">
|
||||
<Suspense fallback={<Skeleton className="w-full h-[31px] my-2" />}>
|
||||
<ProjectName id={id} />
|
||||
</Suspense>
|
||||
<Tabs value={val} className="w-full flex-none">
|
||||
<TabsList className="w-full flex-wrap *:h-9 h-[inherit]">
|
||||
<TabsTrigger value={id} asChild>
|
||||
<Link to="/projects/view/$id" params={{ id }}>
|
||||
General
|
||||
@@ -67,7 +91,11 @@ function RouteComponent() {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<Outlet />
|
||||
</>
|
||||
<div className="w-full grow overflow-scroll">
|
||||
<Suspense fallback={<Skeleton className="w-full h-full mt-2" />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,240 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { format } from 'date-fns'
|
||||
import {
|
||||
BotIcon,
|
||||
CircleUserIcon,
|
||||
ClipboardCheckIcon,
|
||||
ProjectorIcon,
|
||||
ReceiptEuroIcon,
|
||||
SpeakerIcon,
|
||||
} from 'lucide-react'
|
||||
import { ReactNode } from 'react'
|
||||
import { string } from 'zod'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects/view/$id/audit')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Audit',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_sidebar/projects/view/$id/audit"!</div>
|
||||
enum TimelineAction {
|
||||
General,
|
||||
Equipment,
|
||||
Personal,
|
||||
Finance,
|
||||
Todo,
|
||||
}
|
||||
|
||||
enum TimelineActionType {
|
||||
Good,
|
||||
Bad,
|
||||
Neutral,
|
||||
}
|
||||
|
||||
type TimelineEntry = {
|
||||
title: string
|
||||
description: string
|
||||
date: Date
|
||||
action: TimelineAction
|
||||
actionType: TimelineActionType
|
||||
}
|
||||
|
||||
const timelineEntries: Array<TimelineEntry> = [
|
||||
{
|
||||
title: 'Payed #5',
|
||||
description: 'Client payed bill number 5.',
|
||||
action: TimelineAction.Finance,
|
||||
date: new Date(2025, 12, 5, 10, 54, 23),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'Confirmed #1',
|
||||
description: 'Client confirmed project 1.',
|
||||
action: TimelineAction.General,
|
||||
date: new Date(2025, 12, 4, 11),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'New Task',
|
||||
description: 'Konstantin Hintermayer added task 1.',
|
||||
action: TimelineAction.Todo,
|
||||
date: new Date(),
|
||||
actionType: TimelineActionType.Neutral,
|
||||
},
|
||||
{
|
||||
title: 'Payed #5',
|
||||
description: 'Client payed bill number 5.',
|
||||
action: TimelineAction.Finance,
|
||||
date: new Date(2025, 12, 5, 10, 54, 23),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'Confirmed #1',
|
||||
description: 'Client confirmed project 1.',
|
||||
action: TimelineAction.General,
|
||||
date: new Date(2025, 12, 4, 11),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'New Task',
|
||||
description: 'Konstantin Hintermayer added task 1.',
|
||||
action: TimelineAction.Todo,
|
||||
date: new Date(),
|
||||
actionType: TimelineActionType.Neutral,
|
||||
},
|
||||
{
|
||||
title: 'Payed #5',
|
||||
description: 'Client payed bill number 5.',
|
||||
action: TimelineAction.Finance,
|
||||
date: new Date(2025, 12, 5, 10, 54, 23),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'Confirmed #1',
|
||||
description: 'Client confirmed project 1.',
|
||||
action: TimelineAction.General,
|
||||
date: new Date(2025, 12, 4, 11),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'New Task',
|
||||
description: 'Konstantin Hintermayer added task 1.',
|
||||
action: TimelineAction.Todo,
|
||||
date: new Date(),
|
||||
actionType: TimelineActionType.Neutral,
|
||||
},
|
||||
{
|
||||
title: 'Payed #5',
|
||||
description: 'Client payed bill number 5.',
|
||||
action: TimelineAction.Finance,
|
||||
date: new Date(2025, 12, 5, 10, 54, 23),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'Confirmed #1',
|
||||
description: 'Client confirmed project 1.',
|
||||
action: TimelineAction.General,
|
||||
date: new Date(2025, 12, 4, 11),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'New Task',
|
||||
description: 'Konstantin Hintermayer added task 1.',
|
||||
action: TimelineAction.Todo,
|
||||
date: new Date(),
|
||||
actionType: TimelineActionType.Neutral,
|
||||
},
|
||||
{
|
||||
title: 'Payed #5',
|
||||
description: 'Client payed bill number 5.',
|
||||
action: TimelineAction.Finance,
|
||||
date: new Date(2025, 12, 5, 10, 54, 23),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'Confirmed #1',
|
||||
description: 'Client confirmed project 1.',
|
||||
action: TimelineAction.General,
|
||||
date: new Date(2025, 12, 4, 11),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'New Task',
|
||||
description: 'Konstantin Hintermayer added task 1.',
|
||||
action: TimelineAction.Todo,
|
||||
date: new Date(),
|
||||
actionType: TimelineActionType.Neutral,
|
||||
},
|
||||
{
|
||||
title: 'Payed #5',
|
||||
description: 'Client payed bill number 5.',
|
||||
action: TimelineAction.Finance,
|
||||
date: new Date(2025, 12, 5, 10, 54, 23),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'Confirmed #1',
|
||||
description: 'Client confirmed project 1.',
|
||||
action: TimelineAction.General,
|
||||
date: new Date(2025, 12, 4, 11),
|
||||
actionType: TimelineActionType.Good,
|
||||
},
|
||||
{
|
||||
title: 'New Task',
|
||||
description: 'Konstantin Hintermayer added task 1.',
|
||||
action: TimelineAction.Todo,
|
||||
date: new Date(),
|
||||
actionType: TimelineActionType.Neutral,
|
||||
},
|
||||
]
|
||||
|
||||
function GetIconFromAction(ta: TimelineAction) {
|
||||
switch (ta) {
|
||||
case TimelineAction.Equipment:
|
||||
return <SpeakerIcon className="h-6" />
|
||||
case TimelineAction.Finance:
|
||||
return <ReceiptEuroIcon className="h-6" />
|
||||
case TimelineAction.General:
|
||||
return <BotIcon className="h-6" />
|
||||
case TimelineAction.Personal:
|
||||
return <CircleUserIcon className="h-6" />
|
||||
case TimelineAction.Todo:
|
||||
return <ClipboardCheckIcon className="h-6" />
|
||||
default:
|
||||
return <p>Icon not found</p>
|
||||
}
|
||||
}
|
||||
|
||||
function GetColorFromActionType(at: TimelineActionType) {
|
||||
switch (at) {
|
||||
case TimelineActionType.Bad:
|
||||
return 'bg-red-500'
|
||||
case TimelineActionType.Good:
|
||||
return 'bg-green-500'
|
||||
case TimelineActionType.Neutral:
|
||||
return 'bg-muted'
|
||||
}
|
||||
}
|
||||
|
||||
function Timeline({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="relative">
|
||||
{children}
|
||||
<div className="h-full absolute top-0 left-timeline-line w-[2px] bg-muted"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineEntryComponent({ te }: { te: TimelineEntry }) {
|
||||
return (
|
||||
<div className="flex m-2 gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
`h-8 aspect-square flex items-center justify-center rounded-full border-2 border-background z-10`,
|
||||
GetColorFromActionType(te.actionType),
|
||||
)}
|
||||
>
|
||||
{GetIconFromAction(te.action)}
|
||||
</div>
|
||||
<div>
|
||||
{format(te.date, 'hh:mm dd.MM.yyyy')} {te.title}
|
||||
<br />
|
||||
{te.description}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<Timeline>
|
||||
{timelineEntries.map((te, i) => {
|
||||
return <TimelineEntryComponent te={te} key={i}></TimelineEntryComponent>
|
||||
})}
|
||||
</Timeline>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,17 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects/view/$id/equipment')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Equipment',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_sidebar/projects/view/$id/equipment"!</div>
|
||||
return (
|
||||
<div className="bg-emerald-500 h-full">
|
||||
Hello "/_sidebar/projects/view/$id/equipment"!
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects/view/$id/finance')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Finanzen',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
|
||||
@@ -27,9 +27,19 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { useSuspenseQuery } from '@connectrpc/connect-query'
|
||||
import { getProject } from '@/gen/project/v1/project-ProjectService_connectquery'
|
||||
import { Breadcrumb } from '@/components/ui/breadcrumb'
|
||||
import { Map, MapControls, MapMarker, MapRef, MarkerContent, MarkerPopup, MarkerTooltip } from '@/components/ui/map'
|
||||
import { useRef } from 'react'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects/view/$id/')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'General',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const managers = [
|
||||
@@ -100,6 +110,21 @@ const clients = [
|
||||
},
|
||||
]
|
||||
|
||||
const locations = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'SZU',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Stadthalle',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Gasometer',
|
||||
},
|
||||
]
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
@@ -113,11 +138,12 @@ const formSchema = z.object({
|
||||
type: z.string(),
|
||||
status: z.string(),
|
||||
client: z.number(),
|
||||
location: z.number(),
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useParams()
|
||||
const { data } = useProject(Number(id))
|
||||
const { data } = useSuspenseQuery(getProject, { id: Number(id) })
|
||||
const { mutate } = useProjectEdit(Number(id))
|
||||
|
||||
const form = useForm({
|
||||
@@ -128,6 +154,7 @@ function RouteComponent() {
|
||||
type: 'Tour',
|
||||
status: 'Confirmed',
|
||||
client: 1,
|
||||
location: 1,
|
||||
},
|
||||
validators: {
|
||||
onSubmit: formSchema,
|
||||
@@ -138,91 +165,222 @@ function RouteComponent() {
|
||||
name: value.title!,
|
||||
description: value.description!,
|
||||
icon: '',
|
||||
MandantID: data?.MandantID,
|
||||
MandantID: 1,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
id="project-esther-graf-general-form"
|
||||
className="w-2xl"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<div className='w-full mt-2 grid grid-cols-6 lg:grid-cols-12 gap-6'>
|
||||
<div className="col-span-6 rounded-md bg-sidebar p-2 overflow-hidden">
|
||||
<form
|
||||
id="project-esther-graf-general-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<FieldLegend>Projekt Info</FieldLegend>
|
||||
<FieldDescription>
|
||||
Allgemeine Infos über das Projekt
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="title"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Projektname</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
placeholder="Login button not working on mobile"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="description"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Beschreibung</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
<FieldLegend>Projekt Info</FieldLegend>
|
||||
<FieldDescription>
|
||||
Allgemeine Infos über das Projekt
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="title"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Projektname</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
placeholder="I'm having an issue with the login button on mobile."
|
||||
rows={6}
|
||||
className="min-h-24 resize-none"
|
||||
aria-invalid={isInvalid}
|
||||
placeholder="Login button not working on mobile"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupText className="tabular-nums">
|
||||
{field.state.value.length}/100 characters
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Allgemeine Infos zum Projekt
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="description"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Beschreibung</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
placeholder="I'm having an issue with the login button on mobile."
|
||||
rows={6}
|
||||
className="min-h-24 resize-none"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupText className="tabular-nums">
|
||||
{field.state.value.length}/100 characters
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Allgemeine Infos zum Projekt
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="manager"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="responsive" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-tanstack-select-language">
|
||||
Manager
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Zuständiger für dieses Projekt
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value.toString()}
|
||||
onValueChange={(v) => field.handleChange(Number(v))}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-tanstack-select-language"
|
||||
aria-invalid={isInvalid}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
{managers.map((manager) => (
|
||||
<SelectItem
|
||||
key={manager.id}
|
||||
value={manager.id.toString()}
|
||||
>
|
||||
{manager.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="type"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="responsive" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-tanstack-select-language">
|
||||
Projekttyp
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Art des Projektes (Gibt voreinstellungen für Felder
|
||||
wie zum Beispiel: Zahlungskonditionen vor)
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-tanstack-select-language"
|
||||
aria-invalid={isInvalid}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
{projectTypes.map((prj) => (
|
||||
<SelectItem key={prj.name} value={prj.name}>
|
||||
{prj.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="status"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="responsive" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-tanstack-select-language">
|
||||
Status
|
||||
</FieldLabel>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-tanstack-select-language"
|
||||
aria-invalid={isInvalid}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
{statusse.map((stat) => (
|
||||
<SelectItem key={stat.name} value={stat.name}>
|
||||
{stat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</FieldGroup>
|
||||
<FieldSeparator />
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="manager"
|
||||
name="client"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
@@ -230,11 +388,8 @@ function RouteComponent() {
|
||||
<Field orientation="responsive" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-tanstack-select-language">
|
||||
Manager
|
||||
Client
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Zuständiger für dieses Projekt
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
@@ -252,12 +407,9 @@ function RouteComponent() {
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
{managers.map((manager) => (
|
||||
<SelectItem
|
||||
key={manager.id}
|
||||
value={manager.id.toString()}
|
||||
>
|
||||
{manager.name}
|
||||
{clients.map((stat) => (
|
||||
<SelectItem key={stat.id} value={stat.id.toString()}>
|
||||
{stat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -266,8 +418,10 @@ function RouteComponent() {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="type"
|
||||
name="location"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
@@ -275,20 +429,16 @@ function RouteComponent() {
|
||||
<Field orientation="responsive" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-tanstack-select-language">
|
||||
Projekttyp
|
||||
Location
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Art des Projektes (Gibt voreinstellungen für Felder
|
||||
wie zum Beispiel: Zahlungskonditionen vor)
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
value={field.state.value.toString()}
|
||||
onValueChange={(v) => field.handleChange(Number(v))}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-tanstack-select-language"
|
||||
@@ -298,47 +448,8 @@ function RouteComponent() {
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
{projectTypes.map((prj) => (
|
||||
<SelectItem key={prj.name} value={prj.name}>
|
||||
{prj.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="status"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="responsive" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-tanstack-select-language">
|
||||
Status
|
||||
</FieldLabel>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-tanstack-select-language"
|
||||
aria-invalid={isInvalid}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
{statusse.map((stat) => (
|
||||
<SelectItem key={stat.name} value={stat.name}>
|
||||
{locations.map((stat) => (
|
||||
<SelectItem key={stat.id} value={stat.id.toString()}>
|
||||
{stat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -350,58 +461,66 @@ function RouteComponent() {
|
||||
/>
|
||||
</FieldGroup>
|
||||
</FieldGroup>
|
||||
<FieldSeparator />
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="client"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="responsive" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-tanstack-select-language">
|
||||
Client
|
||||
</FieldLabel>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value.toString()}
|
||||
onValueChange={(v) => field.handleChange(Number(v))}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-tanstack-select-language"
|
||||
aria-invalid={isInvalid}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
{clients.map((stat) => (
|
||||
<SelectItem key={stat.id} value={stat.id.toString()}>
|
||||
{stat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<Field orientation="horizontal" className="mt-4">
|
||||
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="project-esther-graf-general-form">
|
||||
Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</>
|
||||
</form>
|
||||
<Field orientation="horizontal" className="mt-4">
|
||||
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="project-esther-graf-general-form">
|
||||
Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="col-span-6 rounded-md overflow-hidden h-128 max-h-128 bg-sidebar flex flex-col p-3">
|
||||
<ShowEventLocation lat={48.202373} lon={16.332889} label='Wiener Stadthalle D'/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function ShowEventLocation({lat, lon, label}: {lat: number, lon: number, label: string}) {
|
||||
const map = useRef<MapRef>(null);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className='text-xl mb-1'>Location</h2>
|
||||
<div className="flex-grow rounded-md overflow-hidden">
|
||||
<Map
|
||||
center={{
|
||||
lat,
|
||||
lon,
|
||||
}}
|
||||
zoom={15}
|
||||
ref={map}
|
||||
>
|
||||
<MapControls
|
||||
position='bottom-right'
|
||||
showFullscreen
|
||||
showLocate
|
||||
showCompass
|
||||
showZoom
|
||||
/>
|
||||
<MapMarker
|
||||
latitude={lat}
|
||||
longitude={lon}
|
||||
>
|
||||
<MarkerContent>
|
||||
<div className="size-4 rounded-full bg-emerald-500 border-2 border-white shadow-lg" />
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>{label}</MarkerTooltip>
|
||||
<MarkerPopup>
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-foreground">{label}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{lat.toFixed(4)}, {lon.toFixed(4)}
|
||||
</p>
|
||||
</div>
|
||||
</MarkerPopup>
|
||||
</MapMarker>
|
||||
</Map>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,11 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects/view/$id/personal')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Personal',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import TimelineTable from '@/features/Projects/components/timelinetable'
|
||||
import { Scheduler } from "@bitnoi.se/react-scheduler"
|
||||
|
||||
export const Route = createFileRoute('/_sidebar/projects/view/$id/timeline')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Timeline',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_sidebar/projects/view/$id/timeline"!</div>
|
||||
return <div className='h-full max-h-full relative'>
|
||||
<Scheduler data={[{
|
||||
id: '1',
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
startDate: new Date(2026, 0, 11, 16, 0, 0),
|
||||
endDate: new Date(2026, 0, 11, 18, 0, 0),
|
||||
title: "TEST",
|
||||
occupancy: 1
|
||||
}
|
||||
],
|
||||
label: {
|
||||
title: "Hello World",
|
||||
icon: "NZLL",
|
||||
subtitle: "TEST"
|
||||
}
|
||||
}]}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,43 @@
|
||||
import TodoTable from '@/features/Projects/components/todotable'
|
||||
import { getTodo, listTodos } from '@/gen/todo/v1/todo-TodoService_connectquery'
|
||||
import {
|
||||
createInfiniteQueryOptions,
|
||||
createQueryOptions,
|
||||
} from '@connectrpc/connect-query'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
const fetchSize = 25
|
||||
export const Route = createFileRoute('/_sidebar/projects/view/$id/todos')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumb: 'Todos',
|
||||
}
|
||||
},
|
||||
loader: async ({ params: { id }, context }) => {
|
||||
await context.queryClient.ensureInfiniteQueryData(
|
||||
createInfiniteQueryOptions(
|
||||
listTodos,
|
||||
{
|
||||
page: 0,
|
||||
perPage: fetchSize,
|
||||
orberBy: 'id',
|
||||
asc: false,
|
||||
},
|
||||
{
|
||||
transport: context.transport,
|
||||
getNextPageParam: (_, p) => {
|
||||
console.log(p.length)
|
||||
return p.length * fetchSize
|
||||
// return ()
|
||||
},
|
||||
pageParamKey: 'page',
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_sidebar/projects/view/$id/todos"!</div>
|
||||
return <TodoTable />
|
||||
}
|
||||
|
||||
12
src/routes/image[.]png.ts
Normal file
12
src/routes/image[.]png.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { generateSeoImageForContent } from '@/lib/ogimage'
|
||||
|
||||
export const Route = createFileRoute('/image.png')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: async ({ request }) => {
|
||||
return await generateSeoImageForContent()
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,19 +1,23 @@
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { LoginForm } from '@/components/login-form'
|
||||
import { useProfile } from '@/features/Auth/queries'
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
|
||||
export const getBackendURI = createServerFn({ method: 'GET' }).handler(() => {
|
||||
return process.env.BACKEND_URI
|
||||
})
|
||||
|
||||
export const getRuntimeRPCURI = createServerFn({ method: 'GET' }).handler(
|
||||
() => {
|
||||
// return process.env.RPC_URI
|
||||
return process.env.RPC_URI
|
||||
},
|
||||
)
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: App,
|
||||
})
|
||||
|
||||
function App() {
|
||||
const { data, isLoading } = useProfile()
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (data?.ID != null) {
|
||||
navigate({ href: '/dashboard' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm md:max-w-4xl">
|
||||
|
||||
5
src/start.tsx
Normal file
5
src/start.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createStart } from '@tanstack/react-start'
|
||||
|
||||
export const startInstance = createStart(() => ({
|
||||
defaultSsr: false,
|
||||
}))
|
||||
@@ -135,6 +135,16 @@ code {
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
.maplibregl-popup-content {
|
||||
@apply bg-transparent! shadow-none! p-0! rounded-none!;
|
||||
@apply bg-transparent! shadow-none! p-0! rounded-none!;
|
||||
@apply bg-transparent! shadow-none! p-0! rounded-none!;
|
||||
}
|
||||
.maplibregl-popup-tip {
|
||||
@apply hidden!;
|
||||
@apply hidden!;
|
||||
@apply hidden!;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-width {
|
||||
@@ -155,9 +165,13 @@ code {
|
||||
|
||||
.full-h {
|
||||
/* max-height: calc(100% - var(--spacing) * 16); */
|
||||
height: calc(100% - var(--spacing) * 16);
|
||||
max-height: calc(100% - var(--spacing) * 16);
|
||||
}
|
||||
|
||||
.table-height {
|
||||
height: calc(100% - calc(var(--spacing) * 30));
|
||||
}
|
||||
|
||||
.left-timeline-line {
|
||||
left: calc((var(--spacing) * 6) - 1px);
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1
|
||||
exit status 1exit status 1
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"include": ["**/*.ts", "**/*.tsx", "eslint.config.js", "prettier.config.js", "vite.config.js"],
|
||||
"include": ["**/*.ts", "**/*.tsx", "eslint.config.js", "prettier.config.js", "vite.config.js", "server.js", "src/fetch-polyfill.js"],
|
||||
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
|
||||
@@ -3,22 +3,37 @@ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
||||
import viteReact from '@vitejs/plugin-react'
|
||||
import viteTsConfigPaths from 'vite-tsconfig-paths'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { oidcSpa } from 'oidc-spa/vite-plugin'
|
||||
import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin'
|
||||
|
||||
const config = defineConfig({
|
||||
plugins: [
|
||||
// this is the plugin that enables path aliases
|
||||
viteTsConfigPaths({
|
||||
projects: ['./tsconfig.json'],
|
||||
}),
|
||||
tailwindcss(),
|
||||
tanstackStart({
|
||||
customViteReactPlugin: true,
|
||||
}),
|
||||
viteReact(),
|
||||
],
|
||||
server: {
|
||||
port: 3001,
|
||||
},
|
||||
// import { nitro } from 'nitro/vite'
|
||||
|
||||
const config = defineConfig(() => {
|
||||
return {
|
||||
plugins: [
|
||||
nitroV2Plugin(),
|
||||
viteTsConfigPaths({
|
||||
projects: ['./tsconfig.json'],
|
||||
}),
|
||||
tailwindcss(),
|
||||
// nitro({ preset: 'node-server' }),
|
||||
tanstackStart({
|
||||
// spa: {
|
||||
// enabled: true,
|
||||
// },
|
||||
}),
|
||||
oidcSpa({
|
||||
freezeFetch: true,
|
||||
freezeXMLHttpRequest: true,
|
||||
freezeWebSocket: true,
|
||||
}),
|
||||
viteReact(),
|
||||
],
|
||||
server: {
|
||||
port: 3001,
|
||||
},
|
||||
// build: isSsrBuild ? ssrBuildConfig : clientBuildConfig,
|
||||
}
|
||||
})
|
||||
|
||||
export default config
|
||||
|
||||
Reference in New Issue
Block a user