commit 0e8f9c96de381fd128946eb9f05ce42af10288c3 Author: biersoeckli Date: Thu Oct 17 08:48:00 2024 +0000 initial commit - setup nextjs template diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..72aeccb --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} \ No newline at end of file diff --git a/.devcontainer/docker.compose.yml b/.devcontainer/docker.compose.yml new file mode 100644 index 0000000..9aeef5f --- /dev/null +++ b/.devcontainer/docker.compose.yml @@ -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 \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3ac30b8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.next +!.next/static +!.next/standalone +.git +db \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6fa3df5 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..333f9fb --- /dev/null +++ b/.vscode/launch.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..964df12 --- /dev/null +++ b/.vscode/settings.json @@ -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, +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4faf1f2 --- /dev/null +++ b/Dockerfile @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c403366 --- /dev/null +++ b/README.md @@ -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. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..e3bc2ec Binary files /dev/null and b/bun.lockb differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..8c574b7 --- /dev/null +++ b/components.json @@ -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" + } +} \ No newline at end of file diff --git a/fix-wrong-zod-imports.js b/fix-wrong-zod-imports.js new file mode 100644 index 0000000..994b141 --- /dev/null +++ b/fix-wrong-zod-imports.js @@ -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.'); diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..e9a68ab --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,9 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + /* experimental: { + instrumentationHook: true + }*/ +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..6f1f157 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/prisma/migrations/20241017061426_migration/migration.sql b/prisma/migrations/20241017061426_migration/migration.sql new file mode 100644 index 0000000..64bb103 --- /dev/null +++ b/prisma/migrations/20241017061426_migration/migration.sql @@ -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"); diff --git a/prisma/migrations/20241017064349_migration/migration.sql b/prisma/migrations/20241017064349_migration/migration.sql new file mode 100644 index 0000000..ae9845a --- /dev/null +++ b/prisma/migrations/20241017064349_migration/migration.sql @@ -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; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -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" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..f69f6e9 --- /dev/null +++ b/prisma/schema.prisma @@ -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 diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..44afcda --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -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 } \ No newline at end of file diff --git a/src/app/error/page.tsx b/src/app/error/page.tsx new file mode 100644 index 0000000..73b3479 --- /dev/null +++ b/src/app/error/page.tsx @@ -0,0 +1,9 @@ + +export default function ErrorPage() { + + return ( +
+ Error Page +
+ ) +} \ No newline at end of file diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..8f6bfe9 --- /dev/null +++ b/src/app/globals.css @@ -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]; +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..2a1f3de --- /dev/null +++ b/src/app/layout.tsx @@ -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 ( + + +
{children}
+ + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..06250e0 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,10 @@ +import { Button } from "@/components/ui/button"; +import Image from "next/image"; + +export default function Home() { + return ( +
+ +
+ ); +} diff --git a/src/app/unauthorized/page.tsx b/src/app/unauthorized/page.tsx new file mode 100644 index 0000000..9cd9a6f --- /dev/null +++ b/src/app/unauthorized/page.tsx @@ -0,0 +1,9 @@ + +export default function UnauthorizedPage() { + + return ( +
+ Unauthorized +
+ ) +} \ No newline at end of file diff --git a/src/components/custom/bottom-bar-menu.tsx b/src/components/custom/bottom-bar-menu.tsx new file mode 100644 index 0000000..b506073 --- /dev/null +++ b/src/components/custom/bottom-bar-menu.tsx @@ -0,0 +1,13 @@ +export default function BottomBarMenu({ children }: { children: React.ReactNode }) { + return (<> +
+
+
+ {children} +
+
+
+
+ + ) +} \ No newline at end of file diff --git a/src/components/custom/checkbox-form-field.tsx b/src/components/custom/checkbox-form-field.tsx new file mode 100644 index 0000000..f84baa8 --- /dev/null +++ b/src/components/custom/checkbox-form-field.tsx @@ -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( + { + form, + label, + name + }: { + form: UseFormReturn, + label: string + name: keyof TFormType + } +) { + + return (<> +
+ ( + + {name as any} + + + + + + )} + /> +
+ ( + + + { + form.setValue(name as any, (checkboxState === true) as any) + }} + /> + +
+ + {label} + +
+
+ )} + /> + ) +} \ No newline at end of file diff --git a/src/components/custom/code.tsx b/src/components/custom/code.tsx new file mode 100644 index 0000000..d54fa67 --- /dev/null +++ b/src/components/custom/code.tsx @@ -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 && + { + if (!copieable) return; + navigator.clipboard.writeText(children || ''); + toast.success('In die Zwischenablage kopiert'); + }}> + {children} + + ) +} \ No newline at end of file diff --git a/src/components/custom/default-data-table.tsx b/src/components/custom/default-data-table.tsx new file mode 100644 index 0000000..1aa815a --- /dev/null +++ b/src/components/custom/default-data-table.tsx @@ -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 { + columns: ColumnDef[] + data: TData[] +} + +export function DefaultDataTable({ + columns, + data, + globalFilterFn, + hideSearchBar = false, + onColumnVisabilityUpdate +}: DataTableProps & { + hideSearchBar?: boolean, + onColumnVisabilityUpdate?: (visabilityConfig: [string, boolean][]) => void + globalFilterFn?: FilterFnOption | undefined +}) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [globalFilter, setGlobalFilter] = React.useState([]) + + 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(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 ( +
+
+ {!hideSearchBar && + table.setGlobalFilter(String(event.target.value)) + } + className="max-w-sm" + />} + + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + Keine Suchergebnisse + + + )} + +
+
+
+ +
+
+ ) +} diff --git a/src/components/custom/form-label-with-question.tsx b/src/components/custom/form-label-with-question.tsx new file mode 100644 index 0000000..b7524d7 --- /dev/null +++ b/src/components/custom/form-label-with-question.tsx @@ -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
+ {children} +
+ + + + +

{hint}

+
+
+
+
+
+} \ No newline at end of file diff --git a/src/components/custom/hint-box-url.tsx b/src/components/custom/hint-box-url.tsx new file mode 100644 index 0000000..3bd1748 --- /dev/null +++ b/src/components/custom/hint-box-url.tsx @@ -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 + + + + + + + +

Absprung zu {uri.hostname}

+
+
+
+} \ No newline at end of file diff --git a/src/components/custom/loading-alert-dialog-action.tsx b/src/components/custom/loading-alert-dialog-action.tsx new file mode 100644 index 0000000..9dfbfe8 --- /dev/null +++ b/src/components/custom/loading-alert-dialog-action.tsx @@ -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, children: React.ReactNode }) { + + const [buttonIsLoading, setButtonIsLoading] = useState(false); + return ( + { + setButtonIsLoading(true); + try { + await onClick(); + } finally { + setButtonIsLoading(false); + } + }} disabled={buttonIsLoading}>{buttonIsLoading ? : children} + ) +} \ No newline at end of file diff --git a/src/components/custom/multiselect-dropdorw-field.tsx b/src/components/custom/multiselect-dropdorw-field.tsx new file mode 100644 index 0000000..3e47803 --- /dev/null +++ b/src/components/custom/multiselect-dropdorw-field.tsx @@ -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( + { + form, + label, + name, + options + }: { + form: UseFormReturn, + label: string | React.ReactNode, + name: keyof TFormType + options: string[] + } +) { + + const [values, setValues] = React.useState>(new Map()) + + + React.useEffect(() => { + const values = form.getValues(); + const fieldValue = values[name] as string; + const selectedValue = (fieldValue || "").split(","); + const selectedOptions = new Map() + for (const option of options) { + selectedOptions.set(option, selectedValue.includes(option)) + } + setValues(selectedOptions); + }, [form, options, name]); + + + + return <> +
+ ( + + id + + + + + )} + /> +
+ + + + + + Kantone + + + {Array.from(values.entries()).map(([key, value]) => { + return { + 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} + + })} + + + + ; +} \ No newline at end of file diff --git a/src/components/custom/multiselect-field.tsx b/src/components/custom/multiselect-field.tsx new file mode 100644 index 0000000..f767f14 --- /dev/null +++ b/src/components/custom/multiselect-field.tsx @@ -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( + ( + { + inputPlaceholder, + emptyPlaceholder, + placeholder, + className, + options, + value, + onChange, + multiple + }, + ref + ) => { + const [searchTerm, setSearchTerm] = React.useState('') + 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 ( + + +
+
+ {value && value.length > 0 ? ( + multiple ? ( + options + .filter( + (option) => + Array.isArray(value) && value.includes(option.value) + ) + .map((option) => ( + + {option.label} + { + e.preventDefault() + handleSelect(option.value) + }} + className="flex items-center rounded-sm px-[1px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground" + > + + + + )) + ) : ( + options.find((opt) => opt.value === value)?.label + ) + ) : ( + + {placeholder} + + )} +
+
+ {value && value.length > 0 ? ( +
{ + e.preventDefault() + handleClear() + }} + > + +
+ ) : ( +
+ +
+ )} +
+
+
+ + +
+ setSearchTerm(e)} + ref={ref} + placeholder={inputPlaceholder ?? 'Search...'} + className="h-9" + /> + {searchTerm && ( +
setSearchTerm('')} + > + +
+ )} +
+ + {emptyPlaceholder ?? 'No results found.'} + + + +
+ {options.map((option) => { + const isSelected = + Array.isArray(value) && value.includes(option.value) + return ( + handleSelect(option.value)} + > + {multiple && ( +
+ +
+ )} + {option.label} + {!multiple && option.value === value && ( + + )} +
+ ) + })} +
+
+
+
+
+
+ ) + } +) + +SelectBox.displayName = 'SelectBox' + +export default SelectBox \ No newline at end of file diff --git a/src/components/custom/navigate-back.tsx b/src/components/custom/navigate-back.tsx new file mode 100644 index 0000000..899af1b --- /dev/null +++ b/src/components/custom/navigate-back.tsx @@ -0,0 +1,8 @@ +'use client' + +import { ArrowLeft } from "lucide-react"; +import { Button } from "../ui/button"; + +export default function NavigateBackButton() { + return ; +} \ No newline at end of file diff --git a/src/components/custom/select-form-field.tsx b/src/components/custom/select-form-field.tsx new file mode 100644 index 0000000..f9ed387 --- /dev/null +++ b/src/components/custom/select-form-field.tsx @@ -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( + { + form, + label, + name, + values, + placeholder, + formDescription, + onValueChange + }: { + form: UseFormReturn; + label: string | React.ReactNode; + name: keyof TFormType; + values: [string, string][]; + placeholder?: string; + formDescription?: string; + onValueChange?: (value: string) => void; + } +) { + + return (<> +
+ ( + + {label} + + + + + + )} + /> +
+ ( + + {label} + + {formDescription && + {formDescription} + } + + + )} + /> + ) +} \ No newline at end of file diff --git a/src/components/custom/simple-data-table.tsx b/src/components/custom/simple-data-table.tsx new file mode 100644 index 0000000..15f0ef8 --- /dev/null +++ b/src/components/custom/simple-data-table.tsx @@ -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({ + 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(undefined); + + const setUserVisabilityForColumns = function (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 ; + } + + const globalFilterFn = (row: Row, 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 && ( + + ) + } as ColumnDef; + + if (customRowDefinition) { + dataCol.cell = ({ row }) => customRowDefinition(row.original); + } + + if (onItemClick && columnIndex === indexOfFirstVisibleColumn) { + dataCol.cell = ({ row }) => { + const item = row.original; + return ( +
onItemClick(item)}> + {customRowDefinition ? customRowDefinition(item) : (row.original as any)[accessorKey] as unknown as string} +
+ ); + }; + } + + if (onItemClickLink && columnIndex === indexOfFirstVisibleColumn) { + dataCol.cell = ({ row }) => { + const item = row.original; + return ( +
router.push(onItemClickLink(item))}> + {customRowDefinition ? customRowDefinition(item) : (row.original as any)[accessorKey] as unknown as string} +
+ ); + }; + } + + return dataCol; + }); + + const finalCols: ColumnDef[] = [ + ...dataColumns + ]; + + if (actionCol) { + finalCols.push({ + id: "actions", + cell: ({ row }) => { + const property = row.original; + return actionCol(property); + }, + }); + } + + return +} diff --git a/src/components/custom/submit-button.tsx b/src/components/custom/submit-button.tsx new file mode 100644 index 0000000..7a40709 --- /dev/null +++ b/src/components/custom/submit-button.tsx @@ -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 +} \ No newline at end of file diff --git a/src/components/custom/text-link.tsx b/src/components/custom/text-link.tsx new file mode 100644 index 0000000..76fda37 --- /dev/null +++ b/src/components/custom/text-link.tsx @@ -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 ( + +
+

{children}

+
+
+ + ) +} \ No newline at end of file diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..25e7b47 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..36496a2 --- /dev/null +++ b/src/components/ui/button.tsx @@ -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, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..83c173a --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -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 + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + , + IconRight: ({ ...props }) => , + } as any} + {...props} + /> + ) +} +Calendar.displayName = "Calendar" + +export { Calendar } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..df61a13 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/column-header.tsx b/src/components/ui/column-header.tsx new file mode 100644 index 0000000..89a62a3 --- /dev/null +++ b/src/components/ui/column-header.tsx @@ -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 + extends React.HTMLAttributes { + column: Column + title: string +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + if (!column.getCanSort()) { + return
{title}
+ } + + return ( +
+ + + + + + column.toggleSorting(false)}> + + Asc + + column.toggleSorting(true)}> + + Desc + + + column.toggleVisibility(false)}> + + Hide + + + +
+ ) +} diff --git a/src/components/ui/column-toggle.tsx b/src/components/ui/column-toggle.tsx new file mode 100644 index 0000000..3f216a5 --- /dev/null +++ b/src/components/ui/column-toggle.tsx @@ -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 { + table: Table +} + +export function DataTableViewOptions({ + table, +}: DataTableViewOptionsProps) { + return ( + + + + + + Spalten Ein-/Ausblenden + + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {(column.columnDef as any).headerName} + + ) + })} + + + ) +} diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..1a37e67 --- /dev/null +++ b/src/components/ui/command.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..01ff19c --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx new file mode 100644 index 0000000..6a0ef53 --- /dev/null +++ b/src/components/ui/drawer.tsx @@ -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) => ( + +) +Drawer.displayName = "Drawer" + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)) +DrawerContent.displayName = "DrawerContent" + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerHeader.displayName = "DrawerHeader" + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerFooter.displayName = "DrawerFooter" + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f69a0d6 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -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, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..ce264ae --- /dev/null +++ b/src/components/ui/form.tsx @@ -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 = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +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 ") + } + + 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( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +