initial commit - setup nextjs template

This commit is contained in:
biersoeckli
2024-10-17 08:48:00 +00:00
commit 0e8f9c96de
92 changed files with 4530 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
{
"name": "quick-kube",
"dockerComposeFile": "./docker.compose.yml",
"service": "vscode-container",
"workspaceFolder": "/workspace",
"shutdownAction": "stopCompose",
"customizations": {
"vscode": {
"extensions": [
"Orta.vscode-jest",
"Prisma.prisma",
"pflannery.vscode-versionlens",
"github.vscode-github-actions",
"bradlc.vscode-tailwindcss",
"GitHub.copilot",
"GitHub.copilot-chat",
"oven.bun-vscode",
"christian-kohler.path-intellisense",
"esbenp.prettier-vscode",
"VisualStudioExptTeam.vscodeintellicode",
"mhutchie.git-graph",
"donjayamanne.githistory"
]
}
},
"postCreateCommand": "curl -fsSL https://bun.sh/install | bash"
}

View File

@@ -0,0 +1,11 @@
version: "3"
services:
vscode-container:
image: mcr.microsoft.com/devcontainers/typescript-node
#image: mcr.microsoft.com/devcontainers/base:debian
#image: mcr.microsoft.com/devcontainers/base:alpine
command: /bin/sh -c "while sleep 1000; do :; done"
volumes:
- ..:/workspace
- ~/.ssh:/home/vscode/.ssh
env_file: devcontainer.env

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
!.next/static
!.next/standalone
.git
db

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.devcontainer/devcontainer.env
db

36
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,36 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Debug File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Run File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"noDebug": true,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "attach",
"name": "Attach Bun",
"url": "ws://localhost:6499/",
"stopOnEntry": false
}
]
}

17
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
// The path to the `bun` executable.
"bun.runtime": "/home/node/.bun/bin/bun",
// If support for Bun should be added to the default "JavaScript Debug Terminal".
"bun.debugTerminal.enabled": true,
// If the debugger should stop on the first line of the program.
"bun.debugTerminal.stopOnEntry": false,
"java.compile.nullAnalysis.mode": "automatic",
"typescript.tsdk": "node_modules/typescript/lib",
"files.exclude": {
"**/.DS_Store": true,
},
"search.exclude": {
},
"files.trimTrailingWhitespace": true,
"prettier.singleQuote": true,
}

57
Dockerfile Normal file
View File

@@ -0,0 +1,57 @@
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY bun.lockb package.json ./
RUN yarn install
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn run prisma-generate-build
RUN yarn run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js

36
README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

BIN
bun.lockb Executable file

Binary file not shown.

17
components.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

41
fix-wrong-zod-imports.js Normal file
View File

@@ -0,0 +1,41 @@
const fs = require('fs');
const path = require('path');
// Define the directory to search in
const DIRECTORY = './src/model/generated-zod/';
// Define the string to search for and the replacement string
const SEARCH_STRING = 'import * as imports from "../../../prisma/null"';
const REPLACEMENT_STRING = '';
// Function to recursively find all .ts files in the directory
function findFiles(dir, ext, files = []) {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
findFiles(fullPath, ext, files);
} else if (path.extname(fullPath) === ext) {
files.push(fullPath);
}
}
return files;
}
// Function to replace content in the files
function replaceInFile(filePath, searchString, replacementString) {
const data = fs.readFileSync(filePath, 'utf8');
const result = data.replace(searchString, replacementString);
fs.writeFileSync(filePath, result, 'utf8');
}
// Find all .ts files in the directory
const tsFiles = findFiles(DIRECTORY, '.ts');
// Replace the specified content in each file
tsFiles.forEach((file) => {
replaceInFile(file, SEARCH_STRING, REPLACEMENT_STRING);
});
console.log('Replacement complete.');

9
next.config.mjs Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
/* experimental: {
instrumentationHook: true
}*/
};
export default nextConfig;

72
package.json Normal file
View File

@@ -0,0 +1,72 @@
{
"name": "quick-kube",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"prisma-generate": "bunx prisma generate && bun ./fix-wrong-zod-imports.js",
"prisma-generate-build": "npx prisma generate && node ./fix-wrong-zod-imports.js",
"prisma-migrate": "bunx prisma migrate dev --name migration",
"prisma-deploy": "bunx prisma migrate deploy",
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.21.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"@tanstack/react-table": "^8.20.5",
"@types/bcrypt": "^5.0.2",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"lucide-react": "^0.453.0",
"moment": "^2.30.1",
"next": "14.2.15",
"next-auth": "^4.24.8",
"next-themes": "^0.3.0",
"nodemailer": "^6.9.15",
"prisma": "^5.21.0",
"react": "^18.3.1",
"react-day-picker": "8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"reflect-metadata": "^0.2.2",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.2",
"typedi": "^0.10.0",
"vaul": "^1.1.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.7.6",
"@types/nodemailer": "^6.4.16",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@types/xml2js": "^0.4.14",
"eslint": "^9.12.0",
"eslint-config-next": "14.2.15",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"zod-prisma": "^0.5.4"
}
}

8
postcss.config.mjs Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@@ -0,0 +1,73 @@
-- CreateTable
CREATE TABLE "Account" (
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
PRIMARY KEY ("provider", "providerAccountId"),
CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Session" (
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" DATETIME NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" DATETIME,
"image" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" DATETIME NOT NULL,
PRIMARY KEY ("identifier", "token")
);
-- CreateTable
CREATE TABLE "Authenticator" (
"credentialID" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"credentialPublicKey" TEXT NOT NULL,
"counter" INTEGER NOT NULL,
"credentialDeviceType" TEXT NOT NULL,
"credentialBackedUp" BOOLEAN NOT NULL,
"transports" TEXT,
PRIMARY KEY ("userId", "credentialID"),
CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID");

View File

@@ -0,0 +1,25 @@
/*
Warnings:
- Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_User" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" DATETIME,
"password" TEXT NOT NULL,
"image" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_User" ("createdAt", "email", "emailVerified", "id", "image", "name", "updatedAt") SELECT "createdAt", "email", "emailVerified", "id", "image", "name", "updatedAt" FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

113
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,113 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
generator zod {
provider = "zod-prisma"
output = "../src/model/generated-zod" // (default) the directory where generated zod schemas will be saved
relationModel = true // (default) Create and export both plain and related models.
// relationModel = "default" // Do not export model without relations.
// relationModel = false // Do not generate related model
modelCase = "PascalCase" // (default) Output models using pascal case (ex. UserModel, PostModel)
// modelCase = "camelCase" // Output models using camel case (ex. userModel, postModel)
modelSuffix = "Model" // (default) Suffix to apply to your prisma models when naming Zod schemas
// useDecimalJs = false // (default) represent the prisma Decimal type using as a JS number
useDecimalJs = true // represent the prisma Decimal type using Decimal.js (as Prisma does)
imports = null // (default) will import the referenced file in generated schemas to be used via imports.someExportedVariable
// https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-by-null-values
prismaJsonNullability = true // (default) uses prisma's scheme for JSON field nullability
// prismaJsonNullability = false // allows null assignment to optional JSON fields
}
datasource db {
provider = "sqlite"
url = "file:../db/data.db"
}
// *** The following code is for the default NextAuth.js schema
model Account {
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
}
model Session {
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
password String
image String?
accounts Account[]
sessions Session[]
// Optional for WebAuthn support
Authenticator Authenticator[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model VerificationToken {
identifier String
token String
expires DateTime
@@id([identifier, token])
}
// Optional for WebAuthn support
model Authenticator {
credentialID String @unique
userId String
providerAccountId String
credentialPublicKey String
counter Int
credentialDeviceType String
credentialBackedUp Boolean
transports String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([userId, credentialID])
}
// *** FROM HERE CUSTOM CLASSES

View File

@@ -0,0 +1,7 @@
import NextAuth, { } from "next-auth"
import { authOptions } from "@/lib/auth-options";
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

9
src/app/error/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
export default function ErrorPage() {
return (
<div>
Error Page
</div>
)
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

73
src/app/globals.css Normal file
View File

@@ -0,0 +1,73 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
.max-w-8xl {
@apply max-w-[90rem];
}

33
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { cn } from "@/lib/utils"
import { Toaster } from "@/components/ui/sonner"
import "./globals.css";
const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
});
export const metadata: Metadata = {
title: "QuickKube",
description: "", // todo
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={cn(
"min-h-screen bg-background font-sans antialiased",
inter.variable
)}>
<main>{children}</main>
<Toaster/>
</body>
</html>
);
}

10
src/app/page.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { Button } from "@/components/ui/button";
import Image from "next/image";
export default function Home() {
return (
<main className="">
<Button>Hello World</Button>
</main>
);
}

View File

@@ -0,0 +1,9 @@
export default function UnauthorizedPage() {
return (
<div>
Unauthorized
</div>
)
}

View File

@@ -0,0 +1,13 @@
export default function BottomBarMenu({ children }: { children: React.ReactNode }) {
return (<>
<div className="flex w-full flex-col items-center left-0 bottom-0 fixed bg-white border-t z-50">
<div className="w-full max-w-8xl px-4 lg:px-20">
<div className="flex p-4 gap-4 items-center">
{children}
</div>
</div>
</div>
<div className="h-20"></div>
</>
)
}

View File

@@ -0,0 +1,66 @@
'use client'
import { FieldValues, UseFormReturn } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input";
import { Checkbox } from "../ui/checkbox";
export default function CheckboxFormField<TFormType extends FieldValues>(
{
form,
label,
name
}: {
form: UseFormReturn<TFormType, any, undefined>,
label: string
name: keyof TFormType
}
) {
return (<>
<div className="hidden">
<FormField
control={form.control}
name={name as any}
render={({ field }) => (
<FormItem>
<FormLabel>{name as any}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name={name as any}
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
disabled={field.disabled}
checked={field.value}
onCheckedChange={(checkboxState) => {
form.setValue(name as any, (checkboxState === true) as any)
}}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
{label}
</FormLabel>
</div>
</FormItem>
)}
/>
</>)
}

View File

@@ -0,0 +1,17 @@
'use client'
import { Toast } from "@/lib/toast.utils";
import { toast } from "sonner";
export function Code({ children, copieable = true }: { children: string | null | undefined, copieable?: boolean }) {
return (children &&
<code className={'relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold ' + (copieable ? 'cursor-pointer' : '')}
onClick={() => {
if (!copieable) return;
navigator.clipboard.writeText(children || '');
toast.success('In die Zwischenablage kopiert');
}}>
{children}
</code>
)
}

View File

@@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
useReactTable,
getPaginationRowModel,
VisibilityState,
getSortedRowModel,
filterFns,
FilterFnOption
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { DataTablePagination } from "@/components/ui/pagignation"
import { DataTableViewOptions } from "@/components/ui/column-toggle"
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export function DefaultDataTable<TData, TValue>({
columns,
data,
globalFilterFn,
hideSearchBar = false,
onColumnVisabilityUpdate
}: DataTableProps<TData, TValue> & {
hideSearchBar?: boolean,
onColumnVisabilityUpdate?: (visabilityConfig: [string, boolean][]) => void
globalFilterFn?: FilterFnOption<any> | undefined
}) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [globalFilter, setGlobalFilter] = React.useState<any>([])
const initialVisabilityState = columns.filter(col => (col as any).isVisible === false).reduce((acc, col) => {
acc[(col as any).accessorKey] = false;
return acc;
}, {} as VisibilityState);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>(initialVisabilityState);
React.useEffect(() => {
if (onColumnVisabilityUpdate) {
onColumnVisabilityUpdate(table.getAllColumns().filter(x => (x.columnDef as any).accessorKey).map(x => [(x.columnDef as any).accessorKey, x.getIsVisible()]));
}
}, [columnVisibility]);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onGlobalFilterChange: setGlobalFilter,
enableGlobalFilter: true,
globalFilterFn: globalFilterFn ?? filterFns.includesString,
state: {
sorting,
columnFilters,
columnVisibility,
globalFilter
},
})
return (
<div>
<div className="flex items-center py-4">
{!hideSearchBar && <Input
placeholder="Suchen..."
value={globalFilter ?? ""}
onChange={(event: any) =>
table.setGlobalFilter(String(event.target.value))
}
className="max-w-sm"
/>}
<DataTableViewOptions table={table} />
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Keine Suchergebnisse
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="mt-4">
<DataTablePagination table={table} />
</div>
</div>
)
}

View File

@@ -0,0 +1,22 @@
import { QuestionMarkCircledIcon } from "@radix-ui/react-icons";
import { FormLabel } from "../ui/form";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
export default function FormLabelWithQuestion(
{ children, hint }: { children: React.ReactNode, hint: string | React.ReactNode }
) {
return <div className="flex gap-1 mt-1 pb-1">
<FormLabel>{children}</FormLabel>
<div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild><QuestionMarkCircledIcon /></TooltipTrigger>
<TooltipContent>
<p className="max-w-[350px]">{hint}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
}

View File

@@ -0,0 +1,26 @@
import Link from "next/link";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
import { Button } from "../ui/button";
import { QuestionMarkIcon } from "@radix-ui/react-icons";
export function HintBoxUrl({ url }: { url: string }) {
const uri = new URL(url);
return <TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link href={url} target="_blank">
<Button type="button" variant="outline" className="h-8 w-8 p-0"><QuestionMarkIcon /></Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Absprung zu {uri.hostname}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
}

View File

@@ -0,0 +1,20 @@
'use client'
import { useState } from "react";
import { AlertDialogAction } from "../ui/alert-dialog";
import LoadingSpinner from "../ui/loading-spinner";
export default function LoadingAlertDialogAction({ onClick, children }: { onClick: () => Promise<void>, children: React.ReactNode }) {
const [buttonIsLoading, setButtonIsLoading] = useState(false);
return (
<AlertDialogAction onClick={async () => {
setButtonIsLoading(true);
try {
await onClick();
} finally {
setButtonIsLoading(false);
}
}} disabled={buttonIsLoading}>{buttonIsLoading ? <LoadingSpinner></LoadingSpinner> : children}</AlertDialogAction>
)
}

View File

@@ -0,0 +1,92 @@
"use client"
import * as React from "react"
import { DropdownMenuCheckboxItemProps } from "@radix-ui/react-dropdown-menu"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { ControllerRenderProps, FieldValues, UseFormReturn } from "react-hook-form"
import { FormControl, FormField, FormItem, FormLabel } from "../ui/form"
import { Input } from "../ui/input"
import { ScrollArea } from "../ui/scroll-area"
type Checked = DropdownMenuCheckboxItemProps["checked"]
export function MultiselectDropdownField<TFormType extends FieldValues>(
{
form,
label,
name,
options
}: {
form: UseFormReturn<TFormType, any, undefined>,
label: string | React.ReactNode,
name: keyof TFormType
options: string[]
}
) {
const [values, setValues] = React.useState<Map<string, boolean>>(new Map<string, boolean>())
React.useEffect(() => {
const values = form.getValues();
const fieldValue = values[name] as string;
const selectedValue = (fieldValue || "").split(",");
const selectedOptions = new Map<string, boolean>()
for (const option of options) {
selectedOptions.set(option, selectedValue.includes(option))
}
setValues(selectedOptions);
}, [form, options, name]);
return <>
<div className="hidden">
<FormField
control={form.control}
name={name as any}
render={({ field }) => (
<FormItem>
<FormLabel>id</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">{!(form.getValues()[name] as string) || (form.getValues()[name] as string).length === 0 ? label : (form.getValues()[name] as string)?.replaceAll(',', ', ')}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Kantone</DropdownMenuLabel>
<DropdownMenuSeparator />
<ScrollArea className="h-60">
{Array.from(values.entries()).map(([key, value]) => {
return <DropdownMenuCheckboxItem
key={key}
checked={values.get(key)}
onCheckedChange={(checked) => {
setValues(new Map(values.set(key, checked)));
const value = Array.from(values.entries()).filter(([_, value]) => value).map(([key]) => key).join(",");
form.setValue(name as any, value as any);
}}
>
{key}
</DropdownMenuCheckboxItem>
})}
</ScrollArea>
</DropdownMenuContent>
</DropdownMenu>
</>;
}

View File

@@ -0,0 +1,213 @@
import { CaretSortIcon, CheckIcon, Cross2Icon } from '@radix-ui/react-icons'
import * as React from 'react'
import { cn } from '@/lib/utils'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger
} from '@/components/ui/popover'
import { ScrollArea } from '@/components/ui/scroll-area'
interface Option {
value: string
label: string
}
interface SelectBoxProps {
options: Option[]
value?: string[] | string
onChange?: (values: string[] | string) => void
placeholder?: string
inputPlaceholder?: string
emptyPlaceholder?: string
className?: string
multiple?: boolean
}
const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
(
{
inputPlaceholder,
emptyPlaceholder,
placeholder,
className,
options,
value,
onChange,
multiple
},
ref
) => {
const [searchTerm, setSearchTerm] = React.useState<string>('')
const [isOpen, setIsOpen] = React.useState(false)
const handleSelect = (selectedValue: string) => {
if (multiple) {
const newValue =
value?.includes(selectedValue) && Array.isArray(value)
? value.filter((v) => v !== selectedValue)
: [...((value as any) ?? []), selectedValue]
onChange?.(newValue)
} else {
onChange?.(selectedValue)
setIsOpen(false)
}
}
const handleClear = () => {
onChange?.(multiple ? [] : '')
}
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<div
className={cn(
'flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring',
className
)}
>
<div
className={cn(
'items-center gap-1 overflow-hidden text-sm',
multiple
? 'flex flex-grow flex-wrap '
: 'inline-flex whitespace-nowrap'
)}
>
{value && value.length > 0 ? (
multiple ? (
options
.filter(
(option) =>
Array.isArray(value) && value.includes(option.value)
)
.map((option) => (
<span
key={option.value}
className="inline-flex items-center gap-1 rounded-md border py-0.5 pl-2 pr-1 text-xs font-medium text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<span>{option.label}</span>
<span
onClick={(e) => {
e.preventDefault()
handleSelect(option.value)
}}
className="flex items-center rounded-sm px-[1px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground"
>
<Cross2Icon />
</span>
</span>
))
) : (
options.find((opt) => opt.value === value)?.label
)
) : (
<span className="mr-auto text-muted-foreground">
{placeholder}
</span>
)}
</div>
<div className="flex items-center self-stretch pl-1 text-muted-foreground/60 hover:text-foreground [&>div]:flex [&>div]:items-center [&>div]:self-stretch">
{value && value.length > 0 ? (
<div
onClick={(e) => {
e.preventDefault()
handleClear()
}}
>
<Cross2Icon className="size-4" />
</div>
) : (
<div>
<CaretSortIcon className="size-4" />
</div>
)}
</div>
</div>
</PopoverTrigger>
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<div className="relative">
<CommandInput
value={searchTerm}
onValueChange={(e) => setSearchTerm(e)}
ref={ref}
placeholder={inputPlaceholder ?? 'Search...'}
className="h-9"
/>
{searchTerm && (
<div
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-muted-foreground hover:text-foreground"
onClick={() => setSearchTerm('')}
>
<Cross2Icon className="size-4" />
</div>
)}
</div>
<CommandEmpty>
{emptyPlaceholder ?? 'No results found.'}
</CommandEmpty>
<CommandGroup>
<ScrollArea>
<div className="max-h-64">
{options.map((option) => {
const isSelected =
Array.isArray(value) && value.includes(option.value)
return (
<CommandItem
key={option.value}
// value={option.value}
onSelect={() => handleSelect(option.value)}
>
{multiple && (
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
<CheckIcon />
</div>
)}
<span>{option.label}</span>
{!multiple && option.value === value && (
<CheckIcon
className={cn(
'ml-auto',
option.value === value
? 'opacity-100'
: 'opacity-0'
)}
/>
)}
</CommandItem>
)
})}
</div>
</ScrollArea>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
)
}
)
SelectBox.displayName = 'SelectBox'
export default SelectBox

View File

@@ -0,0 +1,8 @@
'use client'
import { ArrowLeft } from "lucide-react";
import { Button } from "../ui/button";
export default function NavigateBackButton() {
return <Button variant="ghost" onClick={() => window.history.back()}><ArrowLeft /></Button>;
}

View File

@@ -0,0 +1,86 @@
'use client'
import { FieldValues, UseFormReturn } from "react-hook-form";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
export default function SelectFormField<TFormType extends FieldValues>(
{
form,
label,
name,
values,
placeholder,
formDescription,
onValueChange
}: {
form: UseFormReturn<TFormType, any, undefined>;
label: string | React.ReactNode;
name: keyof TFormType;
values: [string, string][];
placeholder?: string;
formDescription?: string;
onValueChange?: (value: string) => void;
}
) {
return (<>
<div className="hidden">
<FormField
control={form.control}
name={name as any}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name={name as any}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<Select disabled={field.disabled}
onValueChange={(val) => {
if (val) {
form.setValue(name as any, val as any);
if (onValueChange) {
onValueChange(val);
}
}
}} defaultValue={field.value ?? undefined}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
</FormControl>
<SelectContent>
{values.map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectContent>
</Select>
{formDescription && <FormDescription>
{formDescription}
</FormDescription>}
<FormMessage />
</FormItem>
)}
/>
</>)
}

View File

@@ -0,0 +1,164 @@
"use client"
import { ColumnDef, Row } from "@tanstack/react-table"
import { DataTableColumnHeader } from "@/components/ui/column-header"
import { ReactNode, useEffect, useState } from "react"
import { DefaultDataTable } from "./default-data-table"
import { usePathname, useRouter } from "next/navigation"
import FullLoadingSpinner from "../ui/full-loading-spinnter"
export function SimpleDataTable<TData>({
tableIdentifier,
columns,
data,
actionCol,
onItemClick,
onItemClickLink,
hideSearchBar = false,
}: {
tableIdentifier?: string,
columns: ([string, string, boolean, (item: TData) => ReactNode] | [string, string, boolean])[],
data: TData[],
hideSearchBar?: boolean,
onItemClick?: (selectedItem: TData) => void,
onItemClickLink?: (selectedItem: TData) => string,
actionCol?: (selectedItem: TData) => ReactNode
}) {
const router = useRouter();
const pathName = usePathname();
const [columnsWithVisability, setColumnsWithVisability] = useState<(([string, string, boolean, (item: TData) => ReactNode] | [string, string, boolean])[]) | undefined>(undefined);
const [columnInputData, setColumnInputData] = useState<TData[] | undefined>(undefined);
const setUserVisabilityForColumns = function <TData>(columns: ([string, string, boolean, (item: TData) => ReactNode] | [string, string, boolean])[]) {
if (!columns) {
return;
}
const configFromLocalstorage = window.localStorage.getItem(`tableConfig-${tableIdentifier ?? pathName}`) || undefined;
let parsedConfig: [string, boolean][] = [];
if (!!configFromLocalstorage) {
parsedConfig = JSON.parse(configFromLocalstorage);
}
for (const col of columns) {
const [accessorKey, header, isVisible] = col;
const storedConfig = parsedConfig.find(([key]) => key === accessorKey);
if (storedConfig) {
col[2] = storedConfig[1];
}
}
}
const updateVisabilityConfig = (visabilityConfig: [string, boolean][]) => {
window.localStorage.setItem(`tableConfig-${tableIdentifier ?? pathName}`, JSON.stringify(visabilityConfig));
}
useEffect(() => {
setUserVisabilityForColumns(columns);
setColumnsWithVisability(columns);
}, [columns]);
useEffect(() => {
const outData = data.map((item) => {
for (const [accessorKey, headerName, isVisible, customRowDefinition] of columns) {
if (!customRowDefinition) {
continue;
}
(item as any)[accessorKey + '_generated'] = customRowDefinition(item);
}
return item;
});
setColumnInputData(outData);
}, [data, columns]);
if (!columnsWithVisability || !columnInputData) {
return <FullLoadingSpinner />;
}
const globalFilterFn = (row: Row<TData>, columnHeaderNameNotWorking: string, searchTerm: string) => {
if (!searchTerm || Array.isArray(searchTerm)) {
return true;
}
const allCellValues = row.getAllCells().map(cell => {
const headerName = cell.column.id;
// if there is a custom column definition --> use it for filtering
const columnDefinitionForFilter = columns.find(col => col[0] === headerName);
if (columnDefinitionForFilter && columnDefinitionForFilter[3]) {
const columnValue = columnDefinitionForFilter[3](row.original);
if (typeof columnValue === 'string') {
return columnValue.toLowerCase();
}
return '';
}
// use default column value for filtering
return String(cell.getValue() ?? '').toLowerCase();
});
return allCellValues.join(' ').includes(searchTerm.toLowerCase());
};
const indexOfFirstVisibleColumn = columnsWithVisability.findIndex(([_, __, isVisible]) => isVisible);
const dataColumns = columnsWithVisability.map(([accessorKey, header, isVisible, customRowDefinition], columnIndex) => {
const dataCol = {
accessorKey,
isVisible,
headerName: header,
filterFn: (row, searchTerm) => {
const columnValue = ((customRowDefinition ? customRowDefinition(row.original) : (row.original as any)[accessorKey] as unknown as string) ?? '');
console.log(columnValue)
if (typeof columnValue === 'string') {
return columnValue.toLowerCase().includes(searchTerm.toLowerCase());
}
return false;
},
header: ({ column }: { column: any }) => header && (
<DataTableColumnHeader column={column} title={header} />
)
} as ColumnDef<TData>;
if (customRowDefinition) {
dataCol.cell = ({ row }) => customRowDefinition(row.original);
}
if (onItemClick && columnIndex === indexOfFirstVisibleColumn) {
dataCol.cell = ({ row }) => {
const item = row.original;
return (
<div className="cursor-pointer" onClick={() => onItemClick(item)}>
{customRowDefinition ? customRowDefinition(item) : (row.original as any)[accessorKey] as unknown as string}
</div>
);
};
}
if (onItemClickLink && columnIndex === indexOfFirstVisibleColumn) {
dataCol.cell = ({ row }) => {
const item = row.original;
return (
<div className="cursor-pointer" onClick={() => router.push(onItemClickLink(item))}>
{customRowDefinition ? customRowDefinition(item) : (row.original as any)[accessorKey] as unknown as string}
</div>
);
};
}
return dataCol;
});
const finalCols: ColumnDef<TData>[] = [
...dataColumns
];
if (actionCol) {
finalCols.push({
id: "actions",
cell: ({ row }) => {
const property = row.original;
return actionCol(property);
},
});
}
return <DefaultDataTable globalFilterFn={globalFilterFn} columns={finalCols} data={columnInputData} hideSearchBar={hideSearchBar} onColumnVisabilityUpdate={updateVisabilityConfig} />
}

View File

@@ -0,0 +1,10 @@
'use client'
import { useFormStatus } from "react-dom";
import LoadingSpinner from "../ui/loading-spinner";
import { Button } from "../ui/button";
export function SubmitButton(props: { children: React.ReactNode }) {
const { pending, data, method, action } = useFormStatus();
return <Button type="submit" disabled={pending}>{pending ?<LoadingSpinner></LoadingSpinner> : props.children}</Button>
}

View File

@@ -0,0 +1,13 @@
import { ExternalLink } from "lucide-react";
import Link from "next/link";
export default function TextLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<Link href={href} target="_blank">
<div className="flex gap-1 items-center pt-1">
<p className="text-blue-500 hover:underline flex-1">{children}</p>
<div className="text-blue-500 hover:underline"> <ExternalLink size={15} /></div>
</div>
</Link>
)
}

View File

@@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
} as any}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -0,0 +1,64 @@
import { cn } from "@/lib/utils"
import { Button } from "./button"
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "./dropdown-menu"
import {
ArrowDownIcon,
ArrowUpIcon,
CaretSortIcon,
EyeNoneIcon,
} from "@radix-ui/react-icons"
import { Column } from "@tanstack/react-table"
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
return (
<div className={cn("flex items-center space-x-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
) : column.getIsSorted() === "asc" ? (
<ArrowUpIcon className="ml-2 h-4 w-4" />
) : (
<CaretSortIcon className="ml-2 h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}

View File

@@ -0,0 +1,53 @@
"use client"
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
import { MixerHorizontalIcon } from "@radix-ui/react-icons"
import { Table } from "@tanstack/react-table"
import { Button } from "./button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuCheckboxItem } from "./dropdown-menu"
interface DataTableViewOptionsProps<TData> {
table: Table<TData>
}
export function DataTableViewOptions<TData>({
table,
}: DataTableViewOptionsProps<TData>) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex"
>
<MixerHorizontalIcon className="mr-2 h-4 w-4" />
Ansicht
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
<DropdownMenuLabel>Spalten Ein-/Ausblenden</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{(column.columnDef as any).headerName}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -0,0 +1,155 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

178
src/components/ui/form.tsx Normal file
View File

@@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -0,0 +1,8 @@
import { cn } from "@/lib/utils"
import LoadingSpinner from "./loading-spinner";
export default function FullLoadingSpinner() {
return <div className={cn("flex", "items-center", "justify-center", "h-full", "py-4")}>
<LoadingSpinner></LoadingSpinner>
</div>;
}

View File

@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,18 @@
import { cn } from "@/lib/utils"
export default function LoadingSpinner() {
return <svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={cn("animate-spin")}
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
}

View File

@@ -0,0 +1,91 @@
import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon,
} from "@radix-ui/react-icons"
import { Table } from "@tanstack/react-table"
import { Button } from "./button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
interface DataTablePaginationProps<TData> {
table: Table<TData>
}
export function DataTablePagination<TData>({
table,
}: DataTablePaginationProps<TData>) {
return (
<div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground">
{/*table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.*/}
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Zeilen pro Seite</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50, 100].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Seite {table.getState().pagination.pageIndex + 1} von{" "}
{table.getPageCount()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<DoubleArrowLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<DoubleArrowRightIcon className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

117
src/components/ui/table.tsx Normal file
View File

@@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View File

@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

69
src/lib/auth-options.ts Normal file
View File

@@ -0,0 +1,69 @@
import { PrismaClient, User } from "@prisma/client";
import NextAuth, { NextAuthOptions, Session } from "next-auth"
import EmailProvider from "next-auth/providers/email";
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { JWT } from "next-auth/jwt";
import { UserSession } from "@/model/sim-session.model";
import dataAccess from "@/server/data-access/data-access.client";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcrypt";
import userService from "@/server/services/user.service";
/*
const response: any = await signIn("credentials", {
email,
password,
redirect: false,
});
*/
const saltRounds = 10;
export const authOptions: NextAuthOptions = {
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
// The name to display on the sign in form (e.g. "Sign in with...")
name: "Credentials",
// `credentials` is used to generate a form on the sign in page.
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
return await userService.authorize(credentials);
}
})
],
callbacks: {
/* async jwt({ token, user }) {
// Initial sign in
if (user) {
token.id = user.id;
token.role = 'tenant'; //user.role;
}
return token;
},*/
/*async session({ session, token, user }) {
// Add the user's role to the session
const dbUser = user as User;
const simSession = session as SimSession;
simSession.userId = dbUser.id;
return simSession;
},*/
},
adapter: PrismaAdapter(dataAccess.client),
};
function mapUser(user: User) {
return {
id: user.id,
username: user.email
};
}

31
src/lib/form.utilts.ts Normal file
View File

@@ -0,0 +1,31 @@
import { ServerActionResult, SuccessActionResult } from "@/model/server-action-error-return.model";
import { UseFormReturn } from "react-hook-form";
import { z, ZodType } from "zod";
export type FormZodErrorValidationCallback<T> = {
[K in keyof T]?: string[] | undefined;
};
export class FormUtils {
static mapValidationErrorsToForm<T extends ZodType<any, any, any>>(
state: ServerActionResult<z.infer<T>, undefined>,
form: UseFormReturn<z.infer<T>, any, undefined>) {
form.clearErrors();
if (state && state.errors) {
for (const [key, value] of Object.entries(state.errors)) {
if (!value || value.length === 0) {
continue;
}
form.setError(key as keyof z.infer<T> as any, { type: 'manual', message: value.join(', ') });
}
}
}
static getInitialFormState<T extends ZodType<any, any, any>>(): ServerActionResult<z.infer<T>, undefined> {
return {
status: '',
message: undefined
} as any;
}
}

23
src/lib/format.utils.ts Normal file
View File

@@ -0,0 +1,23 @@
import { formatInTimeZone } from 'date-fns-tz';
export function formatDate(date: Date | undefined | null): string {
if (!date) {
return '';
}
return formatInTimeZone(date, 'Europe/Zurich', 'dd.MM.yyyy');
}
export function formatDateTime(date: Date | undefined | null): string {
if (!date) {
return '';
}
return formatInTimeZone(date, 'Europe/Zurich', 'dd.MM.yyyy HH:mm');
}
export function formatTime(date: Date | undefined | null): string {
if (!date) {
return '';
}
return formatInTimeZone(date, 'Europe/Zurich', 'HH:mm');
}

47
src/lib/toast.utils.ts Normal file
View File

@@ -0,0 +1,47 @@
import { ServerActionResult } from "@/model/server-action-error-return.model";
import { toast } from "sonner";
export class Toast {
static async fromAction<A, B>(action: () => Promise<ServerActionResult<A, B>>) {
return new Promise<ServerActionResult<A, B>>(async (resolve, reject) => {
toast.promise(async () => {
return await action();
}, {
loading: 'laden...',
success: (result) => {
resolve(result);
if (result.status === 'success') {
return result.message;
}
},
error: (error) => {
reject(error);
if (error.message) {
return 'Fehler: ' + error.message;
}
return 'Ein unbekannter Fehler ist aufgetreten';
}
});
});
/*
try {
const result = await action();
if (result && result.message && result.status === 'error') {
toast.error('Fehler: ' + result.message);
}
if (result && result.status === 'error') {
toast.error('Ein unbekannter Fehler ist aufgetreten');
}
if (result && result.message && result.status === 'success') {
toast.success(result.message);
}
return result;
} catch (ex) {
toast.error('Ein unbekannter Fehler ist aufgetreten');
}*/
}
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

79
src/lib/zod.utils.ts Normal file
View File

@@ -0,0 +1,79 @@
import { z } from "zod";
export const stringToNumber = z.union([z.string(), z.number()])
.transform((val) => {
if (typeof val === 'string') {
const parsed = parseFloat(val);
if (isNaN(parsed)) {
return null;
}
return parsed;
}
return val;
})
.refine((val) => typeof val === 'number', {
message: 'Der Eingegebene Wert muss eine Zahl sein.',
});
export const stringToOptionalNumber = z.preprocess((val) => {
if (val === null || val === undefined) {
return null;
}
if (typeof val === 'string') {
const parsed = parseFloat(val);
if (isNaN(parsed)) {
return null;
}
return parsed;
}
return val;
}, z.number().positive().nullish());
export const stringToOptionalDate = z.preprocess((val) => {
if (val === null || val === undefined) {
return null;
}
if (typeof val === 'string') {
const parsed = new Date(val);
if (isNaN(parsed.getTime())) {
return null;
}
return parsed;
}
return val;
}, z.date().nullish());
export const stringToDate = z.union([z.string(), z.date()])
.transform((val) => {
if (typeof val === 'string') {
const parsed = new Date(val);
if (isNaN(parsed.getTime())) {
return null;
}
return parsed;
}
return val;
})
.refine((val) => val instanceof Date, {
message: 'Der Eingegebene Wert muss ein Datum sein.',
});
/*z.union([z.string(), z.number(), z.null(), z.undefined()])
.transform((val) => {
if (val === null || val === undefined) {
return null;
}
if (typeof val === 'string') {
const parsed = parseFloat(val);
if (isNaN(parsed)) {
return null;
}
return parsed;
}
return val;
})
.refine((val) => val === null || typeof val === 'number', {
message: 'Der Eingegebene Wert muss eine Zahl oder leer sein.',
});
*/

3
src/middleware.ts Normal file
View File

@@ -0,0 +1,3 @@
export { default } from "next-auth/middleware"
export const config = { matcher: ["/"] }

View File

@@ -0,0 +1,12 @@
import { FormZodErrorValidationCallback } from "@/lib/form.utilts";
import { ServiceException } from "./service.exception.model";
import { z, ZodType } from "zod";
export class FormValidationException<T extends ZodType<any, any, any>> extends ServiceException {
constructor(message: string, public readonly errors: FormZodErrorValidationCallback<z.infer<T>> | null) {
super(message);
this.name = FormValidationException.name;
// Optionally, you can capture the stack trace here if needed
Error.captureStackTrace(this, this.constructor);
}
}

View File

@@ -0,0 +1,32 @@
import * as z from "zod"
import { CompleteUser, RelatedUserModel } from "./index"
export const AccountModel = z.object({
userId: z.string(),
type: z.string(),
provider: z.string(),
providerAccountId: z.string(),
refresh_token: z.string().nullish(),
access_token: z.string().nullish(),
expires_at: z.number().int().nullish(),
token_type: z.string().nullish(),
scope: z.string().nullish(),
id_token: z.string().nullish(),
session_state: z.string().nullish(),
createdAt: z.date(),
updatedAt: z.date(),
})
export interface CompleteAccount extends z.infer<typeof AccountModel> {
user: CompleteUser
}
/**
* RelatedAccountModel contains all relations on your model in addition to the scalars
*
* NOTE: Lazy required in case of potential circular dependencies within schema
*/
export const RelatedAccountModel: z.ZodSchema<CompleteAccount> = z.lazy(() => AccountModel.extend({
user: RelatedUserModel,
}))

View File

@@ -0,0 +1,27 @@
import * as z from "zod"
import { CompleteUser, RelatedUserModel } from "./index"
export const AuthenticatorModel = z.object({
credentialID: z.string(),
userId: z.string(),
providerAccountId: z.string(),
credentialPublicKey: z.string(),
counter: z.number().int(),
credentialDeviceType: z.string(),
credentialBackedUp: z.boolean(),
transports: z.string().nullish(),
})
export interface CompleteAuthenticator extends z.infer<typeof AuthenticatorModel> {
user: CompleteUser
}
/**
* RelatedAuthenticatorModel contains all relations on your model in addition to the scalars
*
* NOTE: Lazy required in case of potential circular dependencies within schema
*/
export const RelatedAuthenticatorModel: z.ZodSchema<CompleteAuthenticator> = z.lazy(() => AuthenticatorModel.extend({
user: RelatedUserModel,
}))

View File

@@ -0,0 +1,5 @@
export * from "./account"
export * from "./session"
export * from "./user"
export * from "./verificationtoken"
export * from "./authenticator"

View File

@@ -0,0 +1,24 @@
import * as z from "zod"
import { CompleteUser, RelatedUserModel } from "./index"
export const SessionModel = z.object({
sessionToken: z.string(),
userId: z.string(),
expires: z.date(),
createdAt: z.date(),
updatedAt: z.date(),
})
export interface CompleteSession extends z.infer<typeof SessionModel> {
user: CompleteUser
}
/**
* RelatedSessionModel contains all relations on your model in addition to the scalars
*
* NOTE: Lazy required in case of potential circular dependencies within schema
*/
export const RelatedSessionModel: z.ZodSchema<CompleteSession> = z.lazy(() => SessionModel.extend({
user: RelatedUserModel,
}))

View File

@@ -0,0 +1,31 @@
import * as z from "zod"
import { CompleteAccount, RelatedAccountModel, CompleteSession, RelatedSessionModel, CompleteAuthenticator, RelatedAuthenticatorModel } from "./index"
export const UserModel = z.object({
id: z.string(),
name: z.string().nullish(),
email: z.string(),
emailVerified: z.date().nullish(),
password: z.string(),
image: z.string().nullish(),
createdAt: z.date(),
updatedAt: z.date(),
})
export interface CompleteUser extends z.infer<typeof UserModel> {
accounts: CompleteAccount[]
sessions: CompleteSession[]
Authenticator: CompleteAuthenticator[]
}
/**
* RelatedUserModel contains all relations on your model in addition to the scalars
*
* NOTE: Lazy required in case of potential circular dependencies within schema
*/
export const RelatedUserModel: z.ZodSchema<CompleteUser> = z.lazy(() => UserModel.extend({
accounts: RelatedAccountModel.array(),
sessions: RelatedSessionModel.array(),
Authenticator: RelatedAuthenticatorModel.array(),
}))

View File

@@ -0,0 +1,8 @@
import * as z from "zod"
export const VerificationTokenModel = z.object({
identifier: z.string(),
token: z.string(),
expires: z.date(),
})

View File

@@ -0,0 +1,24 @@
import { FormZodErrorValidationCallback } from "@/lib/form.utilts";
import { z, ZodType } from "zod";
export class ServerActionResult<TErrorData, TReturnData> {
constructor(public readonly status: 'error' | 'success',
public readonly data?: TReturnData | void,
public readonly message?: string,
public readonly errors?: FormZodErrorValidationCallback<TErrorData>) {
}
}
export class SuccessActionResult<T> extends ServerActionResult<undefined, T> {
constructor(data?: T, message?: string) {
super('success', data, message);
}
}
export class ErrorActionResult<TErrorData> extends ServerActionResult<TErrorData, undefined> {
constructor(errors: FormZodErrorValidationCallback<TErrorData>, message?: string) {
super('error', undefined, message, errors);
}
}

View File

@@ -0,0 +1,8 @@
export class ServiceException extends Error {
constructor(message: string) {
super(message);
this.name = ServiceException.name;
// Optionally, you can capture the stack trace here if needed
Error.captureStackTrace(this, this.constructor);
}
}

View File

@@ -0,0 +1,5 @@
import { Session } from "next-auth";
export interface UserSession extends Session {
id?: string;
}

View File

@@ -0,0 +1,98 @@
import { Prisma, PrismaClient } from "@prisma/client";
import { Service } from "typedi";
import { DefaultArgs } from "@prisma/client/runtime/library";
import { ListUtils } from "../utils/list.utils";
type clientType = keyof PrismaClient<Prisma.PrismaClientOptions, never | undefined>;
const prismaClientSingleton = () => {
return new PrismaClient()
}
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
type PrismaTransactionType = Omit<PrismaClient<Prisma.PrismaClientOptions, never, DefaultArgs>, "$on" | "$connect" | "$disconnect" | "$use" | "$transaction" | "$extends">;
export class DataAccessClient {
client = prisma;
async updateManyItems<TEntityType, TKey>(tableName: keyof PrismaTransactionType, itemsToUpdate: TEntityType[],
primaryKeySelector: (item: TEntityType) => TKey) {
this.client.$transaction(async (tx) => {
await this.updateManyItemsWithExistingTransaction(tableName, itemsToUpdate, primaryKeySelector, tx);
});
}
async updateManyItemsWithExistingTransaction<TEntityType, TKey>(tableName: keyof PrismaTransactionType, itemsToUpdate: TEntityType[],
primaryKeySelector: (item: TEntityType) => TKey, transaction: PrismaTransactionType, primaryKeyField: keyof TEntityType = 'id' as keyof TEntityType) {
for (const chunk of ListUtils.chunk(itemsToUpdate, 50)) {
await Promise.all(chunk.map(async dataItem => {
await (transaction[tableName] as any).update({
where: {
[primaryKeyField]: primaryKeySelector(dataItem)
},
data: dataItem
});
}));
}
}
async getById(clientType: clientType, id: any, idKey = 'id') {
return await (this.client[clientType] as any).findFirstOrThrow({
where: {
[idKey as string]: id
}
});
}
async getByIdOrNull(clientType: clientType, id: any, idKey = 'id') {
return await (this.client[clientType] as any).findFirst({
where: {
[idKey as string]: id
}
});
}
async save<TEntityType>(clientType: clientType, item: TEntityType, idKey = 'id'): Promise<TEntityType> {
if (!(item as any)[idKey]) {
return (await this.client[clientType] as any).create({
data: item,
});
}
return await (this.client[clientType] as any).update({
where: {
[idKey as string]: (item as any)[idKey]
},
data: item
});
}
async saveIfNotExists<TEntityType>(clientType: clientType, item: TEntityType, idKey = 'id'): Promise<TEntityType> {
const existing = await this.getByIdOrNull(clientType, (item as any)[idKey], idKey);
if (!existing) {
return (await this.client[clientType] as any).create({
data: item,
});
}
return await (this.client[clientType] as any).update({
where: {
[idKey as string]: (item as any)[idKey]
},
data: item
});
}
}
const dataAccess = new DataAccessClient();
export default dataAccess;

View File

@@ -0,0 +1,79 @@
/*
import { Prisma, RealEstate } from "@prisma/client";
import dataAccess from "../data-access/data-access.client";
import { revalidateTag, unstable_cache } from "next/cache";
import { Tags } from "../utils/cache-tag-generator.utils";
import { DefaultArgs } from "@prisma/client/runtime/library";
export class RealEstateService {
async deleteRealEstate(id: string) {
const existingItem = await this.getRealEstateById(id);
if (!existingItem) {
return;
}
await dataAccess.client.realEstate.delete({
where: {
id
}
});
revalidateTag(Tags.realEstates(existingItem.landlordId));
}
async getRealEstatesForLandlord(landlordId: string) {
return await unstable_cache(async (landlordId: string) => await dataAccess.client.realEstate.findMany({
where: {
landlordId
}
}),
[Tags.realEstates(landlordId)], {
tags: [Tags.realEstates(landlordId)]
})(landlordId);
}
async getRealEstateById(id: string) {
return dataAccess.client.realEstate.findUnique({
where: {
id
}
});
}
async getCurrentLandlordIdFromRealEstate(realEstateId: string) {
const property = await dataAccess.client.realEstate.findUnique({
select: {
landlordId: true
},
where: {
id: realEstateId
}
});
return property?.landlordId;
}
async saveRealEstate(property: Prisma.RealEstateUncheckedCreateInput | Prisma.RealEstateUncheckedUpdateInput) {
let savedProperty: Prisma.Prisma__RealEstateClient<RealEstate, never, DefaultArgs>;
if (property.id) {
savedProperty = dataAccess.client.realEstate.update({
where: {
id: property.id as string
},
data: property
});
} else {
savedProperty = dataAccess.client.realEstate.create({
data: property as Prisma.RealEstateUncheckedCreateInput
});
}
revalidateTag(Tags.realEstates(property.landlordId as string));
return savedProperty;
}
}
const realEstateService = new RealEstateService();
export default realEstateService;*/

View File

@@ -0,0 +1,64 @@
import { Prisma, User } from "@prisma/client";
import dataAccess from "../data-access/data-access.client";
import { revalidateTag, unstable_cache } from "next/cache";
import { Tags } from "../utils/cache-tag-generator.utils";
import bcrypt from "bcrypt";
const saltRounds = 10;
export class UserService {
async maptoDtoUser(user: User) {
return {
id: user.id,
email: user.email
};
}
async authorize(credentials: Record<"password" | "username", string> | undefined) {
if (!credentials || !credentials.username || !credentials.password) {
return null;
}
const dbUser = await dataAccess.client.user.findFirst({
where: {
email: credentials.username
}
});
if (!dbUser) {
return null;
}
const isPasswordValid = await bcrypt.compare(credentials.password, dbUser.password);
if (!isPasswordValid) {
return null;
}
return this.maptoDtoUser(dbUser);
}
async registerUser(email: string, password: string) {
const hashedPassword = await bcrypt.hash(password, saltRounds);
try {
const user = await dataAccess.client.user.create({
data: {
email,
password: hashedPassword
}
});
return user;
} finally {
revalidateTag(Tags.users());
}
}
async getUsers() {
return await unstable_cache(async () => await dataAccess.client.user.findMany(),
[Tags.users()], {
tags: [Tags.users()]
})();
}
}
const userService = new UserService();
export default userService;

View File

@@ -0,0 +1,113 @@
import { ServiceException } from "@/model/service.exception.model";
import { UserSession } from "@/model/sim-session.model";
import { getServerSession } from "next-auth";
import { ZodRawShape, ZodObject, objectUtil, baseObjectOutputType, z, ZodType } from "zod";
import { redirect } from "next/navigation";
import { ServerActionResult, SuccessActionResult } from "@/model/server-action-error-return.model";
import { FormValidationException } from "@/model/form-validation-exception.model";
import { authOptions } from "@/lib/auth-options";
export async function getUserSession(): Promise<UserSession | null> {
const session = await getServerSession(authOptions) as UserSession | null;
return session;
}
/*
export async function checkIfCurrentUserHasAccessToContract(contractId: string | null | undefined) {
const session = await getLandlordSession();
if (!contractId) {
return { ...session };
}
const currentLandlordIdIfExists = await rentalContractService.getCurrentLandlordIdForContract(contractId);
if (!currentLandlordIdIfExists) {
throw new ServiceException('Objekt nicht gefunden.');
}
if (currentLandlordIdIfExists !== session.landlordId) {
throw new ServiceException('Sie haben keine Berechtigung, dieses Objekt zu bearbeiten.');
}
return { ...session };
}
*/
export async function saveFormAction<ReturnType, ZodType extends ZodRawShape>(formData: FormData,
validationModel: ZodObject<ZodType>,
func: (validateData: { [k in keyof objectUtil.addQuestionMarks<baseObjectOutputType<ZodType>, any>]: objectUtil.addQuestionMarks<baseObjectOutputType<ZodType>, any>[k]; }) => Promise<ReturnType>,
redirectOnSuccessPath?: string,
ignoredFields: (keyof ZodType)[] = []) {
return simpleAction<ReturnType, z.infer<typeof validationModel>>(async () => {
const inputData = convertFormDataToJson(formData);
// Omit ignored fields from validation model
const omitBody = {};
const allIgnoreFiels = ['createdAt', 'updatedAt', ...ignoredFields];
allIgnoreFiels.forEach(field => (omitBody as any)[field] = true);
const schemaWithoutIgnoredFields = validationModel.omit(omitBody);
const validatedFields = schemaWithoutIgnoredFields.safeParse(inputData);
if (!validatedFields.success) {
console.error('Validation failed for input:', inputData, 'with errors:', validatedFields.error.flatten().fieldErrors);
throw new FormValidationException('Bitte überprüfen Sie Ihre eingaben.', validatedFields.error.flatten().fieldErrors);
}
if (!validatedFields.data) {
console.error('No data available after validation of input:', validatedFields.data);
throw new ServiceException('Ein unbekannter Fehler ist aufgetreten.');
}
return await func(validatedFields.data);
}, redirectOnSuccessPath);
}
function convertFormDataToJson(formData: FormData) {
const jsonObject: { [key: string]: any } = {};
formData.forEach((value, key) => {
if (key.startsWith('$ACTION')) {
return;
}
if (value === '') {
jsonObject[key] = null;
} else {
jsonObject[key] = value;
}
});
return jsonObject;
}
export async function simpleAction<ReturnType, ValidationCallbackType>(
func: () => Promise<ReturnType>,
redirectOnSuccessPath?: string) {
let funcResult: ReturnType;
try {
funcResult = await func();
} catch (ex) {
if (ex instanceof FormValidationException) {
return {
status: 'error',
message: ex.message,
errors: ex.errors ?? undefined
} as ServerActionResult<ValidationCallbackType, ReturnType>;
} else if (ex instanceof ServiceException) {
return {
status: 'error',
message: ex.message
} as ServerActionResult<ValidationCallbackType, ReturnType>;
} else {
console.error(ex)
return {
status: 'error',
message: 'Ein unbekannter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.'
} as ServerActionResult<ValidationCallbackType, ReturnType>;
}
}
if (redirectOnSuccessPath) redirect(redirectOnSuccessPath);
if (funcResult instanceof ServerActionResult) {
return {
status: funcResult.status,
message: funcResult.message,
errors: funcResult.errors,
data: funcResult.data
} as ServerActionResult<ValidationCallbackType, typeof funcResult.data>;
}
return {
status: 'success',
data: funcResult ?? undefined
} as ServerActionResult<ValidationCallbackType, ReturnType>;
}

View File

@@ -0,0 +1,6 @@
export class Tags {
static users() {
return `users`;
}
}

View File

@@ -0,0 +1,21 @@
import util from 'util';
import child_process from 'child_process';
export class CommandExecutorUtils {
static async runCommand(command: string) {
if (!command) {
throw new Error('cannot run an empty command');
}
try {
//console.log('Running command: "' + command + '"...');
const exec = util.promisify(child_process.exec);
const { stdout, stderr } = await exec(command);
console.log('stdout:\n', stdout);
console.log('stderr:\n', stderr);
} catch (err) {
console.error('Error while running command: +' + command + '":');
console.error(err);
};
}
}

View File

@@ -0,0 +1,43 @@
export class ListUtils {
static distinctBy<T, TKey>(array: T[], keySelector: (item: T) => TKey): T[] {
const keys = new Set<TKey>();
const result = new Array<T>();
for (const item of array) {
const key = keySelector(item);
if (!keys.has(key)) {
keys.add(key);
result.push(item);
}
}
return result;
}
static groupBy<T, TKey>(array: T[], keySelector: (item: T) => TKey): Map<TKey, T[]> {
return array.reduce((map, item) => {
const key = keySelector(item);
const collection = map.get(key);
if (!collection) {
map.set(key, [item]);
} else {
collection.push(item);
}
return map;
}, new Map<TKey, T[]>());
}
static chunk<T>(array: T[], chunkSize: number): T[][] {
return array.reduce((resultArray, item, index) => {
const chunkIndex = Math.floor(index / chunkSize);
if (!resultArray[chunkIndex]) {
resultArray[chunkIndex] = [];
}
resultArray[chunkIndex].push(item);
return resultArray;
}, [] as T[][]);
}
static removeNulls<T>(array: (T | null)[]): T[] {
return array.filter(x => x !== null) as T[];
}
}

84
tailwind.config.ts Normal file
View File

@@ -0,0 +1,84 @@
import type { Config } from "tailwindcss"
const { fontFamily } = require("tailwindcss/defaultTheme")
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
},
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default config

26
tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "fix-wrong-zod-imports.js"]
}