Bulk commit: Stand ende 22.01.

This commit is contained in:
2026-01-22 17:39:38 +01:00
parent cca316daf2
commit f8940b5dcc
75 changed files with 12419 additions and 6719 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
.env.local

11
.env.production Normal file
View 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"

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
legacy-peer-deps=true

17
Dockerfile Normal file
View 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" ]

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -1,5 +0,0 @@
import { registerGlobalMiddleware } from '@tanstack/react-start'
registerGlobalMiddleware({
middleware: [],
})

27
src/components/404.tsx Normal file
View 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 couldnt find the page youre 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">&larr;</span> Back to home
</Link>
</div>
</main>
)
}
export default NotFound

View File

@@ -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>
)

View File

@@ -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>
)

View File

@@ -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>

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

View 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

View File

@@ -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">

View File

@@ -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&apos;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

File diff suppressed because it is too large Load Diff

1305
src/components/ui/map.tsx Normal file

File diff suppressed because it is too large Load Diff

View 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 }

View File

@@ -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)

View File

@@ -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>

View File

@@ -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()
},

View File

@@ -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',

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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: [] });
},
})
}

View File

@@ -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>

View File

@@ -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

View 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

View 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

View File

@@ -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
View 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
}

View 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;

View 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);

View 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);

View 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;

View 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);

View 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;

View 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
View 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);

View File

@@ -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
View 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
View 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 }
// }

View File

@@ -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])
}

View File

@@ -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>>
}
}

View File

@@ -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>
}
}

View File

@@ -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',
}}

View File

@@ -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>

View 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 />
}

View File

@@ -1,7 +1,11 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_sidebar/dashboard')({
component: RouteComponent,
beforeLoad: () => {
return {
breadcrumb: 'Dashboard',
}
},
})
function RouteComponent() {

View File

@@ -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>
}

View File

@@ -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>
)

View File

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

View File

@@ -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>
}

View 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 />
}

View File

@@ -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>
}

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

@@ -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>
</>
)
}

View File

@@ -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() {

View File

@@ -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>
}

View File

@@ -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
View 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()
},
},
},
})

View File

@@ -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
View File

@@ -0,0 +1,5 @@
import { createStart } from '@tanstack/react-start'
export const startInstance = createStart(() => ({
defaultSsr: false,
}))

View File

@@ -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);
}

View File

@@ -1 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1
exit status 1exit status 1

View File

@@ -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",

View File

@@ -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