diff --git a/apps/hub/app/(app)/admin/station/[id]/page.tsx b/apps/hub/app/(app)/admin/station/[id]/page.tsx
new file mode 100644
index 00000000..399adf90
--- /dev/null
+++ b/apps/hub/app/(app)/admin/station/[id]/page.tsx
@@ -0,0 +1,13 @@
+import { prisma } from '@repo/db';
+import { StationForm } from '../_components/Form';
+
+export default async ({ params }: { params: Promise<{ id: string }> }) => {
+ const { id } = await params;
+ const station = await prisma.station.findUnique({
+ where: {
+ id: parseInt(id),
+ },
+ });
+ if (!station) return
Station not found
;
+ return ;
+};
diff --git a/apps/hub/app/(app)/admin/station/_components/Form.tsx b/apps/hub/app/(app)/admin/station/_components/Form.tsx
new file mode 100644
index 00000000..c8bfe068
--- /dev/null
+++ b/apps/hub/app/(app)/admin/station/_components/Form.tsx
@@ -0,0 +1,246 @@
+'use client';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { StationOptionalDefaultsSchema } from '@repo/db/zod';
+import { set, useForm } from 'react-hook-form';
+import { z } from 'zod';
+import { BosUse, Country, Station } from '@repo/db';
+import { FileText, LocateIcon, PlaneIcon } from 'lucide-react';
+import { Input } from '../../../../_components/ui/Input';
+import { useState } from 'react';
+import { deleteStation, upsertStation } from '../action';
+import { Button } from '../../../../_components/ui/Button';
+import { redirect } from 'next/navigation';
+
+export const StationForm = ({ station }: { station?: Station }) => {
+ const form = useForm>({
+ resolver: zodResolver(StationOptionalDefaultsSchema),
+ defaultValues: station,
+ });
+ const [loading, setLoading] = useState(false);
+ const [deleteLoading, setDeleteLoading] = useState(false);
+ console.log(form.formState.errors);
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/apps/hub/app/(app)/admin/station/action.ts b/apps/hub/app/(app)/admin/station/action.ts
new file mode 100644
index 00000000..34ffa68d
--- /dev/null
+++ b/apps/hub/app/(app)/admin/station/action.ts
@@ -0,0 +1,20 @@
+'use server';
+
+import { prisma, Prisma, Station } from '@repo/db';
+
+export const upsertStation = async (
+ station: Prisma.StationCreateInput,
+ id?: Station['id']
+) => {
+ const newStation = id
+ ? await prisma.station.update({
+ where: { id: id },
+ data: station,
+ })
+ : await prisma.station.create({ data: station });
+ return newStation;
+};
+
+export const deleteStation = async (id: Station['id']) => {
+ await prisma.station.delete({ where: { id: id } });
+};
diff --git a/apps/hub/app/(app)/admin/station/new/page.tsx b/apps/hub/app/(app)/admin/station/new/page.tsx
new file mode 100644
index 00000000..a7d5f4de
--- /dev/null
+++ b/apps/hub/app/(app)/admin/station/new/page.tsx
@@ -0,0 +1,5 @@
+import { StationForm } from '../_components/Form';
+
+export default () => {
+ return ;
+};
diff --git a/apps/hub/app/(app)/admin/station/page.tsx b/apps/hub/app/(app)/admin/station/page.tsx
new file mode 100644
index 00000000..70c3c91c
--- /dev/null
+++ b/apps/hub/app/(app)/admin/station/page.tsx
@@ -0,0 +1,42 @@
+import { DatabaseBackupIcon } from 'lucide-react';
+import { PaginatedTable } from '../../../_components/PaginatedTable';
+import Link from 'next/link';
+
+export default () => {
+ return (
+ <>
+
+
+ Stationen
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/apps/hub/app/(app)/admin/user/loading.tsx b/apps/hub/app/(app)/admin/user/loading.tsx
new file mode 100644
index 00000000..dfffa537
--- /dev/null
+++ b/apps/hub/app/(app)/admin/user/loading.tsx
@@ -0,0 +1,5 @@
+import { Loading } from '../../../_components/ui/Loading';
+
+export default () => {
+ ;
+};
diff --git a/apps/hub/app/(app)/admin/user/page.tsx b/apps/hub/app/(app)/admin/user/page.tsx
index b6a9b4a2..d386dff1 100644
--- a/apps/hub/app/(app)/admin/user/page.tsx
+++ b/apps/hub/app/(app)/admin/user/page.tsx
@@ -1,29 +1,34 @@
-import Link from 'next/link';
+import { User2 } from 'lucide-react';
import { PaginatedTable } from '../../../_components/PaginatedTable';
export default async () => {
return (
-
+ <>
+
+ Benutzer
+
+
+ >
);
};
diff --git a/apps/hub/app/(app)/page.tsx b/apps/hub/app/(app)/page.tsx
index b37a4be8..4a6b72b8 100644
--- a/apps/hub/app/(app)/page.tsx
+++ b/apps/hub/app/(app)/page.tsx
@@ -1,7 +1,6 @@
import Link from 'next/link';
import { PaginatedTable } from '../_components/PaginatedTable';
import { Header } from '../_components/ui/Header';
-import { PrismaClient } from '@repo/db';
export default async function Home() {
return (
diff --git a/apps/hub/app/(app)/settings/account/_components/forms.tsx b/apps/hub/app/(app)/settings/account/_components/forms.tsx
index 537d6108..b4701a2c 100644
--- a/apps/hub/app/(app)/settings/account/_components/forms.tsx
+++ b/apps/hub/app/(app)/settings/account/_components/forms.tsx
@@ -227,6 +227,7 @@ export const SocialForm = ({
type="number"
className="input input-bordered w-full"
placeholder="1445241"
+ defaultValue={user.vatsimCid as number | undefined}
{...form.register('vatsimCid', {
valueAsNumber: true,
})}
diff --git a/apps/hub/app/_components/Table.tsx b/apps/hub/app/_components/Table.tsx
index e63373d0..aaa16372 100644
--- a/apps/hub/app/_components/Table.tsx
+++ b/apps/hub/app/_components/Table.tsx
@@ -39,7 +39,7 @@ export default function SortableTable({
-
+
),
diff --git a/apps/hub/app/_components/ui/Input.tsx b/apps/hub/app/_components/ui/Input.tsx
new file mode 100644
index 00000000..1bb89d3e
--- /dev/null
+++ b/apps/hub/app/_components/ui/Input.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import {
+ FieldValues,
+ Path,
+ RegisterOptions,
+ UseFormReturn,
+} from 'react-hook-form';
+import { cn } from '../../../helper/cn';
+
+interface InputProps
+ extends Omit, 'form'> {
+ name: Path;
+ form: UseFormReturn;
+ formOptions?: RegisterOptions;
+ label?: string;
+ placeholder?: string;
+}
+
+export const Input = ({
+ name,
+ label = name,
+ placeholder = label,
+ form,
+ formOptions,
+ className,
+ ...inputProps
+}: InputProps) => {
+ return (
+
+ );
+};
diff --git a/apps/hub/app/_components/ui/Loading.tsx b/apps/hub/app/_components/ui/Loading.tsx
new file mode 100644
index 00000000..9003604f
--- /dev/null
+++ b/apps/hub/app/_components/ui/Loading.tsx
@@ -0,0 +1,8 @@
+export const Loading = () => {
+ return (
+
+ );
+};
diff --git a/apps/hub/app/_components/ui/Nav.tsx b/apps/hub/app/_components/ui/Nav.tsx
index 1e800aad..17090ff3 100644
--- a/apps/hub/app/_components/ui/Nav.tsx
+++ b/apps/hub/app/_components/ui/Nav.tsx
@@ -29,7 +29,10 @@ export const VerticalNav = () => {
-
- User
+ Benutzer
+
+ -
+ Stationen
diff --git a/package-lock.json b/package-lock.json
index 3c7cf4cb..42c89ea2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -107,6 +107,7 @@
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@next-auth/prisma-adapter": "^1.0.7",
+ "@repo/db": "*",
"@repo/ui": "*",
"@tanstack/react-table": "^8.20.6",
"axios": "^1.7.9",
@@ -1175,6 +1176,58 @@
}
}
},
+ "node_modules/@prisma/debug": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.3.1.tgz",
+ "integrity": "sha512-RrEBkd+HLZx+ydfmYT0jUj7wjLiS95wfTOSQ+8FQbvb6vHh5AeKfEPt/XUQ5+Buljj8hltEfOslEW57/wQIVeA=="
+ },
+ "node_modules/@prisma/engines": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.3.1.tgz",
+ "integrity": "sha512-sXdqEVLyGAJ5/iUoG/Ea5AdHMN71m6PzMBWRQnLmhhOejzqAaEr8rUd623ql6OJpED4s/U4vIn4dg1qkF7vGag==",
+ "hasInstallScript": true,
+ "peer": true,
+ "dependencies": {
+ "@prisma/debug": "6.3.1",
+ "@prisma/engines-version": "6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0",
+ "@prisma/fetch-engine": "6.3.1",
+ "@prisma/get-platform": "6.3.1"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0.tgz",
+ "integrity": "sha512-R/ZcMuaWZT2UBmgX3Ko6PAV3f8//ZzsjRIG1eKqp3f2rqEqVtCv+mtzuH2rBPUC9ujJ5kCb9wwpxeyCkLcHVyA==",
+ "peer": true
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.3.1.tgz",
+ "integrity": "sha512-HOf/0umOgt+/S2xtZze+FHKoxpVg4YpVxROr6g2YG09VsI3Ipyb+rGvD6QGbCqkq5NTWAAZoOGNL+oy7t+IhaQ==",
+ "peer": true,
+ "dependencies": {
+ "@prisma/debug": "6.3.1",
+ "@prisma/engines-version": "6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0",
+ "@prisma/get-platform": "6.3.1"
+ }
+ },
+ "node_modules/@prisma/generator-helper": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.3.1.tgz",
+ "integrity": "sha512-hX2fxjMksyAWAS0OcDi7GVmRUqsZ35ZY3Zla1EfO+uDYW6BY+om8kuKHyKkIvvRcUlTmL+xccl+nJwNToqP/aA==",
+ "dependencies": {
+ "@prisma/debug": "6.3.1"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.3.1.tgz",
+ "integrity": "sha512-AYLq6Hk9xG73JdLWJ3Ip9Wg/vlP7xPvftGBalsPzKDOHr/ImhwJ09eS8xC2vNT12DlzGxhfk8BkL0ve2OriNhQ==",
+ "peer": true,
+ "dependencies": {
+ "@prisma/debug": "6.3.1"
+ }
+ },
"node_modules/@radix-ui/react-icons": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
@@ -2666,6 +2719,11 @@
"node": ">=6"
}
},
+ "node_modules/code-block-writer": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz",
+ "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w=="
+ },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -4274,7 +4332,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -7314,6 +7371,33 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
},
+ "node_modules/prisma": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.3.1.tgz",
+ "integrity": "sha512-JKCZWvBC3enxk51tY4TWzS4b5iRt4sSU1uHn2I183giZTvonXaQonzVtjLzpOHE7qu9MxY510kAtFGJwryKe3Q==",
+ "hasInstallScript": true,
+ "peer": true,
+ "dependencies": {
+ "@prisma/engines": "6.3.1"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": ">=18.18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.3"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -8869,7 +8953,7 @@
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
- "dev": true,
+ "devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -9282,6 +9366,24 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
+ "node_modules/zod-prisma-types": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/zod-prisma-types/-/zod-prisma-types-3.2.4.tgz",
+ "integrity": "sha512-S4spVBMJAmecLv+aLyRhXK26qW9nWwcsOf1H1fRcEmiI8DPbftZ99u0fhqHlymuTpmMcUpuKxcNNOIqNY0ScSQ==",
+ "dependencies": {
+ "@prisma/generator-helper": "^6.3.0",
+ "code-block-writer": "^12.0.0",
+ "lodash": "^4.17.21",
+ "zod": "^3.24.1"
+ },
+ "bin": {
+ "zod-prisma-types": "dist/bin.js"
+ },
+ "peerDependencies": {
+ "@prisma/client": "^4.x.x || ^5.x.x || ^6.x.x",
+ "prisma": "^4.x.x || ^5.x.x || ^6.x.x"
+ }
+ },
"node_modules/zustand": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
@@ -9315,7 +9417,8 @@
"version": "0.0.0",
"license": "ISC",
"dependencies": {
- "@prisma/client": "^6.2.1"
+ "@prisma/client": "^6.2.1",
+ "zod-prisma-types": "^3.2.4"
}
},
"packages/eslint-config": {
diff --git a/packages/database/index.ts b/packages/database/index.ts
index c2256878..263476f3 100644
--- a/packages/database/index.ts
+++ b/packages/database/index.ts
@@ -1,2 +1,6 @@
export { prisma } from './prisma/client'; // exports instance of prisma
export * from './generated/client'; // exports generated types from prisma
+
+import * as zodTypes from './generated/zod';
+
+export const zod = zodTypes;
diff --git a/packages/database/package.json b/packages/database/package.json
index 939f3e60..0c445ddd 100644
--- a/packages/database/package.json
+++ b/packages/database/package.json
@@ -5,16 +5,18 @@
"main": "generated/client/index.js",
"types": "generated/client/index.d.ts",
"scripts": {
- "db:generate": "npx prisma generate",
+ "db:generate": "npx prisma generate && npx prisma generate zod",
"db:migrate": "npx prisma migrate dev",
"db:deploy": "npx prisma migrate deploy"
},
"exports": {
- ".": "./index.ts"
+ ".": "./index.ts",
+ "./zod": "./zod.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
- "@prisma/client": "^6.2.1"
+ "@prisma/client": "^6.2.1",
+ "zod-prisma-types": "^3.2.4"
}
}
diff --git a/packages/database/prisma/migrations/20250216163541_/migration.sql b/packages/database/prisma/migrations/20250216163541_/migration.sql
new file mode 100644
index 00000000..1d19d6e0
--- /dev/null
+++ b/packages/database/prisma/migrations/20250216163541_/migration.sql
@@ -0,0 +1,32 @@
+-- CreateEnum
+CREATE TYPE "BosUse" AS ENUM ('PRIMARY', 'SECONDARY', 'DUAL_USE');
+
+-- CreateEnum
+CREATE TYPE "Country" AS ENUM ('DE', 'AT', 'CH');
+
+-- CreateTable
+CREATE TABLE "Station" (
+ "id" SERIAL NOT NULL,
+ "bosUse" "BosUse" NOT NULL,
+ "bosCallsign" TEXT NOT NULL,
+ "bosCallsignShort" TEXT NOT NULL,
+ "bosRadioArea" TEXT NOT NULL,
+ "country" "Country" NOT NULL,
+ "operator" TEXT NOT NULL,
+ "aircraft" TEXT NOT NULL,
+ "aircraftRegistration" TEXT NOT NULL,
+ "aircraftSpeed" INTEGER NOT NULL,
+ "hasWinch" BOOLEAN NOT NULL,
+ "is24h" BOOLEAN NOT NULL,
+ "hasNvg" BOOLEAN NOT NULL,
+ "locationState" TEXT NOT NULL,
+ "locationStateShort" TEXT NOT NULL,
+ "hasRope" BOOLEAN NOT NULL,
+ "fir" TEXT NOT NULL,
+ "latitude" DOUBLE PRECISION NOT NULL,
+ "longitude" DOUBLE PRECISION NOT NULL,
+ "atcCallsign" TEXT NOT NULL,
+ "hideRangeRings" BOOLEAN NOT NULL,
+
+ CONSTRAINT "Station_pkey" PRIMARY KEY ("id")
+);
diff --git a/packages/database/prisma/schema/schema.prisma b/packages/database/prisma/schema/schema.prisma
index 17820e12..5a9a79c5 100644
--- a/packages/database/prisma/schema/schema.prisma
+++ b/packages/database/prisma/schema/schema.prisma
@@ -7,7 +7,13 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["prismaSchemaFolder"]
- output = "../../generated/client"
+ output = "../../generated/client"
+}
+
+generator zod {
+ provider = "zod-prisma-types"
+ output = "../../generated/zod"
+ createOptionalDefaultValuesTypes = true
}
datasource db {
diff --git a/packages/database/prisma/schema/station.prisma b/packages/database/prisma/schema/station.prisma
new file mode 100644
index 00000000..cc429682
--- /dev/null
+++ b/packages/database/prisma/schema/station.prisma
@@ -0,0 +1,35 @@
+enum BosUse {
+ PRIMARY
+ SECONDARY
+ DUAL_USE
+}
+
+enum Country {
+ DE
+ AT
+ CH
+}
+
+model Station {
+ id Int @id @default(autoincrement())
+ bosUse BosUse
+ bosCallsign String
+ bosCallsignShort String
+ bosRadioArea String
+ country Country
+ operator String
+ aircraft String
+ aircraftRegistration String
+ aircraftSpeed Int
+ hasWinch Boolean
+ is24h Boolean
+ hasNvg Boolean
+ locationState String
+ locationStateShort String
+ hasRope Boolean
+ fir String
+ latitude Float
+ longitude Float
+ atcCallsign String
+ hideRangeRings Boolean
+}
diff --git a/packages/database/zod.ts b/packages/database/zod.ts
new file mode 100644
index 00000000..ca41570e
--- /dev/null
+++ b/packages/database/zod.ts
@@ -0,0 +1 @@
+export * from './generated/zod';