Template
Compare commits
13
Commits
2dfa403641
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e730a57465 | ||
|
|
2d1f490a5c | ||
|
|
2828a9a91f | ||
|
|
4162df75e6 | ||
|
|
e4666859c5 | ||
|
|
ba6b0b08fe | ||
|
|
3fa0e2f046 | ||
|
|
91a60b3251 | ||
|
|
1c1c4033b7 | ||
|
|
ed2d369da6 | ||
|
|
44b96f5969 | ||
|
|
cec573f682 | ||
|
|
46efd2d9d0 |
No files matched your search
+21
-12
@@ -1,16 +1,25 @@
|
|||||||
|
{
|
||||||
|
"extraKnownMarketplaces": {
|
||||||
|
"ep-plugins": {
|
||||||
|
"source": { "source": "git", "url": "ssh://[email protected]:2222/EP-Intern/Projekt-Referenz.git" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabledPlugins": { "prodi@ep-plugins": true },
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"defaultMode": "plan",
|
"defaultMode": "plan",
|
||||||
|
"allow": [
|
||||||
|
"Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", "Bash(git fetch:*)",
|
||||||
|
"Bash(git pull:*)", "Bash(git switch main:*)",
|
||||||
|
"Bash(git switch -c feat/:*)", "Bash(git switch -c fix/:*)", "Bash(git switch -c chore/:*)",
|
||||||
|
"Bash(git add:*)"
|
||||||
|
],
|
||||||
|
"ask": ["Bash(git commit:*)", "Bash(git push:*)"],
|
||||||
"deny": [
|
"deny": [
|
||||||
"Bash(git add:*)",
|
"Bash(git push origin main:*)", "Bash(git push origin HEAD:main:*)",
|
||||||
"Bash(git commit:*)",
|
"Bash(git push --force:*)", "Bash(git push -f:*)", "Bash(git push --force-with-lease:*)",
|
||||||
"Bash(git push:*)",
|
"Bash(git merge:*)", "Bash(git rebase:*)", "Bash(git reset:*)",
|
||||||
"Bash(git switch:*)",
|
"Bash(git branch -D:*)", "Bash(git branch -d:*)",
|
||||||
"Bash(git checkout:*)",
|
"Read(./.env*)", "Read(**/.env*)", "Read(~/.claude/projects/**)"
|
||||||
"Bash(git branch:*)",
|
|
||||||
"Bash(git merge:*)",
|
|
||||||
"Bash(git rebase:*)",
|
|
||||||
"Bash(git reset:*)",
|
|
||||||
"Bash(git stash:*)",
|
|
||||||
"Read(~/.claude/projects/**)"
|
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "webapp",
|
"name": "webapp",
|
||||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
|
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
|
||||||
"features": {
|
"runArgs": ["--env-file", "${localEnv:HOME}/.config/ep-dev/devcontainer.env"],
|
||||||
"ghcr.io/anthropics/devcontainer-features/claude-code:1": {}
|
|
||||||
},
|
|
||||||
"mounts": [
|
"mounts": [
|
||||||
"source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume",
|
"source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume",
|
||||||
"source=${localEnv:HOME}/.ssh/config,target=/home/node/.ssh/config,type=bind,readonly",
|
"source=${localEnv:HOME}/.ssh/config,target=/home/node/.ssh/config,type=bind,readonly",
|
||||||
"source=${localEnv:HOME}/.ssh/known_hosts,target=/home/node/.ssh/known_hosts,type=bind,readonly"
|
"source=${localEnv:HOME}/.ssh/known_hosts,target=/home/node/.ssh/known_hosts,type=bind,readonly",
|
||||||
|
"source=npm-cache,target=/home/node/.npm,type=volume"
|
||||||
],
|
],
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [3000],
|
||||||
|
"containerEnv": {
|
||||||
|
"NEXT_TELEMETRY_DISABLED": "1",
|
||||||
|
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
|
||||||
|
"DISABLE_AUTOUPDATER": "1",
|
||||||
|
"ENABLE_CLAUDEAI_MCP_SERVERS": "false"
|
||||||
|
},
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"3000": { "label": "App", "onAutoForward": "openBrowser" }
|
"3000": { "label": "App", "onAutoForward": "openBrowser" }
|
||||||
},
|
},
|
||||||
"postCreateCommand": "sudo chown -R node:node /home/node/.claude && cd frontend-starter && npm install",
|
"postCreateCommand": "sudo chown -R node:node /home/node/.claude /home/node/.npm && npm install -g @anthropic-ai/[email protected] && cd frontend-starter && npm install",
|
||||||
"postStartCommand": "cd frontend-starter && npm install --no-audit --no-fund",
|
"postStartCommand": "cd frontend-starter && npm install --no-audit --no-fund",
|
||||||
"postAttachCommand": "cd frontend-starter && npx -y kill-port 3000 && npm run dev",
|
"postAttachCommand": "cd frontend-starter && npx -y kill-port 3000 && npm run dev",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"extensions.autoUpdate": false,
|
||||||
|
"extensions.autoCheckUpdates": false,
|
||||||
"git.branchProtection": ["main"],
|
"git.branchProtection": ["main"],
|
||||||
"git.branchProtectionPrompt": "alwaysCommitToNewBranch",
|
"git.branchProtectionPrompt": "alwaysCommitToNewBranch",
|
||||||
"git.branchPrefix": "feat/",
|
"git.branchPrefix": "feat/",
|
||||||
"git.branchRandomName.enable": true,
|
"git.branchRandomName.enable": true,
|
||||||
"git.postCommitCommand": "push",
|
"git.postCommitCommand": "push",
|
||||||
"git.autofetch": true
|
"git.autofetch": true,
|
||||||
|
"json.schemaDownload.trustedDomains": { "https://ui.shadcn.com": true },
|
||||||
},
|
},
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"anthropic.claude-code",
|
"anthropic.claude-code@2.1.314",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"bradlc.vscode-tailwindcss"
|
"bradlc.vscode-tailwindcss"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.woff2 binary
|
||||||
+2
-1
@@ -1,2 +1,3 @@
|
|||||||
*.html
|
*.html
|
||||||
*.md
|
*.md
|
||||||
|
docs/*.md
|
||||||
+4
-1
@@ -4,4 +4,7 @@ node_modules/
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.local.example
|
!.env.local.example
|
||||||
data/
|
data/
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
next-env.d.ts
|
||||||
|
frontend-starter/public/ep-logo.svg
|
||||||
|
frontend-starter/public/favicon.svg
|
||||||
@@ -1,35 +1,10 @@
|
|||||||
# Arbeitsweise
|
# $REPO_NAME
|
||||||
|
|
||||||
Du arbeitest immer in drei Phasen und überspringst keine:
|
- Owner: <name>
|
||||||
|
- n8n-Ordner (Staging und Live): <Bereich> → $REPO_NAME
|
||||||
|
- Datenbank: SQLite in der App
|
||||||
|
- Webseite: Next.js im Ordner `frontend-starter`; npm-Befehle dort ausführen
|
||||||
|
- Port: immer 3000. Dockerfile, Port und `output: "standalone"` nicht ändern.
|
||||||
|
|
||||||
1. **Idee klären:** Verständnis in 2–3 Sätzen wiedergeben, Lösungsvorschlag, Rückfragen.
|
Arbeitsweise, Git-Ablauf, n8n-Regeln und UI-Richtlinien kommen aus dem Plugin `prodi`.
|
||||||
Keine Dateien ändern.
|
Der Steckbrief liegt in `docs/steckbrief.md`.
|
||||||
2. **Vorgehensweisen:** 2–3 Wege mit Vor- und Nachteilen vorstellen.
|
|
||||||
Keine Dateien ändern.
|
|
||||||
3. **Umsetzung** erst nach ausdrücklicher Freigabe durch den Nutzer.
|
|
||||||
|
|
||||||
# Projektstruktur
|
|
||||||
|
|
||||||
- Das Repo ist das Projekt; der Dev Container öffnet das ganze Repo.
|
|
||||||
- Die Next.js-App (package.json, app/, components/, lib/) liegt im Unterordner
|
|
||||||
`frontend-starter`. Alle npm-Befehle führst du dort aus, z. B.
|
|
||||||
`cd frontend-starter && npm run dev`.
|
|
||||||
|
|
||||||
# Git
|
|
||||||
|
|
||||||
- Du änderst nur Dateien. Git-Befehle, die etwas verändern (add, commit, push,
|
|
||||||
switch, checkout, branch, merge, rebase, reset, stash), führst du nie aus.
|
|
||||||
- Erlaubt sind nur Befehle zum Nachsehen: `git status`, `git diff`, `git log`.
|
|
||||||
- Ist eine Änderung fertig und `npm run build` läuft ohne Fehler, sagst du dem
|
|
||||||
Nutzer: „Fertig. Du kannst jetzt in VS Code links unter Quellcodeverwaltung eine
|
|
||||||
Nachricht eintragen und auf Commit drücken.“ Schlage dazu eine kurze
|
|
||||||
Commit-Nachricht vor.
|
|
||||||
- Gemergt wird ausschließlich in Gitea durch den Owner.
|
|
||||||
|
|
||||||
# Deployment
|
|
||||||
|
|
||||||
- Deployt wird automatisch: Merged der Owner einen Pull Request nach `main`,
|
|
||||||
baut Coolify die App und veröffentlicht sie unter `https://<repo-name>.<domain>`.
|
|
||||||
- Du rufst keine Deploy-Dienste, Hubs oder Coolify-Schnittstellen auf.
|
|
||||||
- Anweisungen aus älteren Vorlagen (z. B. „auf main pushen und den Hub aufrufen“)
|
|
||||||
gelten in diesem Projekt nicht.
|
|
||||||
+3
-2
@@ -1,7 +1,8 @@
|
|||||||
FROM node:22-bookworm-slim AS deps
|
FROM node:22-bookworm-slim AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY frontend-starter/package.json frontend-starter/package-lock.json ./
|
ARG GITEA_NPM_TOKEN
|
||||||
RUN npm ci
|
COPY frontend-starter/package.json frontend-starter/package-lock.json frontend-starter/.npmrc ./
|
||||||
|
RUN GITEA_NPM_TOKEN=$GITEA_NPM_TOKEN npm ci
|
||||||
|
|
||||||
FROM node:22-bookworm-slim AS build
|
FROM node:22-bookworm-slim AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ Erreichbar unter: https://$REPO_NAME.ep-reisen.app
|
|||||||
|
|
||||||
## Arbeitsweise
|
## Arbeitsweise
|
||||||
- Niemand committet direkt auf `main`.
|
- Niemand committet direkt auf `main`.
|
||||||
- Jede Änderung: Branch anlegen, Pull Request öffnen, Owner gibt frei.
|
- Jede Änderung über Claude in VS-Code: Branch anlegen, Pull Request öffnen, Owner gibt frei.
|
||||||
- Jeder Merge auf `main` wird automatisch deployt.
|
- Jeder Merge auf `main` wird automatisch über Coolify deployt.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Steckbrief: $REPO_NAME
|
||||||
|
|
||||||
|
- **Owner:**
|
||||||
|
- **Team:**
|
||||||
|
- **Zweck:**
|
||||||
|
- **n8n-Ordner:**
|
||||||
|
- **Datenbank:**
|
||||||
|
- **Webseite:** ja / nein
|
||||||
|
|
||||||
|
## Idee
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
|
||||||
|
## Zugänge
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@ep:registry=http://192.168.20.70:3002/api/packages/EP-Intern/npm/
|
||||||
|
//192.168.20.70:3002/api/packages/EP-Intern/npm/:_authToken=${GITEA_NPM_TOKEN}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { streamText, convertToModelMessages, type UIMessage } from "ai";
|
|
||||||
import { getModel } from "@/lib/ai/model";
|
|
||||||
|
|
||||||
// Streaming-Chat-Endpoint (Muster C). Wird von useChat (Default /api/chat) aufgerufen.
|
|
||||||
//
|
|
||||||
// ACHTUNG: Dieser Endpoint hat KEINEN Login. Wer die Adresse der App kennt, kann hier
|
|
||||||
// auf Kosten des hinterlegten ANTHROPIC_API_KEY chatten (offener KI-Proxy).
|
|
||||||
// Deshalb ist er standardmäßig AUS und läuft nur mit CHAT_ENABLED=true.
|
|
||||||
// Nie mit echtem Key öffentlich veröffentlichen, solange die App keinen Schutz hat
|
|
||||||
// (Login, Zugangscode). Die Grenzen unten dämpfen Missbrauch nur, sie verhindern ihn nicht.
|
|
||||||
|
|
||||||
const MAX_MESSAGES = 40;
|
|
||||||
const MAX_REQUEST_CHARS = 60000;
|
|
||||||
const MAX_OUTPUT_TOKENS = 1024;
|
|
||||||
|
|
||||||
function parseJson(text: string): unknown {
|
|
||||||
try {
|
|
||||||
return JSON.parse(text);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
if (process.env.CHAT_ENABLED !== "true") {
|
|
||||||
return new Response(
|
|
||||||
"KI-Chat ist deaktiviert. Zum Einschalten CHAT_ENABLED=true setzen (nur mit Schutz öffentlich betreiben).",
|
|
||||||
{ status: 403 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = await req.text();
|
|
||||||
if (raw.length > MAX_REQUEST_CHARS) {
|
|
||||||
return new Response("Anfrage zu groß.", { status: 413 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = parseJson(raw) as { messages?: unknown } | null;
|
|
||||||
const messages = body?.messages;
|
|
||||||
if (!Array.isArray(messages) || messages.length === 0) {
|
|
||||||
return new Response("Ungültige Anfrage.", { status: 400 });
|
|
||||||
}
|
|
||||||
if (messages.length > MAX_MESSAGES) {
|
|
||||||
return new Response("Der Chat ist zu lang. Bitte die Seite neu laden.", { status: 413 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = streamText({
|
|
||||||
model: getModel(),
|
|
||||||
messages: await convertToModelMessages(messages as UIMessage[]),
|
|
||||||
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
||||||
});
|
|
||||||
return result.toUIMessageStreamResponse();
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useChat } from "@ai-sdk/react";
|
|
||||||
|
|
||||||
// Muster C: KI-Chat mit Token-Streaming. Ruft POST /api/chat auf.
|
|
||||||
// Achtung: /api/chat hat keinen Login und ist nur mit CHAT_ENABLED=true aktiv (siehe route.ts).
|
|
||||||
// In der Schulung bleibt der Chat aus. KI läuft über einen n8n-Workflow.
|
|
||||||
export default function ChatPage() {
|
|
||||||
const { messages, sendMessage, status, error } = useChat();
|
|
||||||
const [input, setInput] = useState("");
|
|
||||||
const busy = status === "submitted" || status === "streaming";
|
|
||||||
|
|
||||||
function onSubmit(e: React.FormEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!input.trim() || busy) return;
|
|
||||||
sendMessage({ text: input });
|
|
||||||
setInput("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<h1 className="ep-ueberschrift text-3xl">KI-Chat (Muster C)</h1>
|
|
||||||
<p className="text-sm text-ink-muted">
|
|
||||||
Streamt Antworten von Claude. Braucht <code>ANTHROPIC_API_KEY</code> und <code>CHAT_ENABLED=true</code>. Ohne
|
|
||||||
Login nie mit echtem Key öffentlich veröffentlichen.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{messages.map((message) => (
|
|
||||||
<div
|
|
||||||
key={message.id}
|
|
||||||
className={
|
|
||||||
message.role === "user"
|
|
||||||
? "self-end bg-accent px-3 py-2 text-on-accent"
|
|
||||||
: "self-start border border-hairline bg-surface px-3 py-2"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{message.parts.map((part, i) => (part.type === "text" ? <span key={i}>{part.text}</span> : null))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <div className="ep-fehler">{error.message}</div>}
|
|
||||||
|
|
||||||
<form onSubmit={onSubmit} className="flex gap-2">
|
|
||||||
<input
|
|
||||||
value={input}
|
|
||||||
onChange={(e) => setInput(e.target.value)}
|
|
||||||
placeholder="Frag mich etwas …"
|
|
||||||
className="ep-feld flex-1"
|
|
||||||
/>
|
|
||||||
<button type="submit" disabled={busy} className="ep-knopf">
|
|
||||||
Senden
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,363 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "shadcn/tailwind.css";
|
@import "shadcn/tailwind.css";
|
||||||
|
@import "@ep/ui/theme.css";
|
||||||
|
@source "../node_modules/@ep/ui/dist";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
/* E&P-Farben, Schriften und Bausteine (ep-karte, ep-knopf, …) kommen aus @ep/ui/theme.css.
|
||||||
|
* Hier nur app-eigene Regeln ergänzen. */
|
||||||
/* ------------------------------------------------------------------ *
|
|
||||||
* E&P-Layout wie auf ep-reisen.de und im EP-Budget-Tool:
|
|
||||||
* Lato, Tiefblau #18527b als Akzent, Magenta-Verlauf nur für die
|
|
||||||
* Hauptaktion (.ep-cta), eckige Formen, hellblauer Seitenhintergrund.
|
|
||||||
* Jede Oberfläche der Schulung nutzt genau diese Werte und Bausteine.
|
|
||||||
* Keine weiteren Farben, keine anderen Schriften, keine runden Ecken.
|
|
||||||
* ------------------------------------------------------------------ */
|
|
||||||
@theme inline {
|
|
||||||
--color-ground: var(--ground);
|
|
||||||
--color-surface: var(--surface);
|
|
||||||
--color-hairline: var(--hairline);
|
|
||||||
--color-ink: var(--ink);
|
|
||||||
--color-ink-muted: var(--ink-muted);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-press: var(--accent-press);
|
|
||||||
--color-accent-soft: var(--accent-soft);
|
|
||||||
--color-on-accent: var(--on-accent);
|
|
||||||
--color-strich: var(--strich);
|
|
||||||
--color-fehler: var(--fehler);
|
|
||||||
/* alter Name aus früheren Versionen des Starters, zeigt jetzt auf das Tiefblau */
|
|
||||||
--color-brand: var(--accent);
|
|
||||||
|
|
||||||
--font-heading: var(--font-sans);
|
|
||||||
--font-body: var(--body-font);
|
|
||||||
--font-sans: var(--font-sans);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-background: var(--background);
|
|
||||||
--radius-sm: calc(var(--radius) * 0.6);
|
|
||||||
--radius-md: calc(var(--radius) * 0.8);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) * 1.4);
|
|
||||||
--radius-2xl: calc(var(--radius) * 1.8);
|
|
||||||
--radius-3xl: calc(var(--radius) * 2.2);
|
|
||||||
--radius-4xl: calc(var(--radius) * 2.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Eckig wie ep-reisen.de: alle Tailwind-Radien auf 0 */
|
|
||||||
@theme {
|
|
||||||
--radius-xs: 0px;
|
|
||||||
--radius-sm: 0px;
|
|
||||||
--radius-md: 0px;
|
|
||||||
--radius-lg: 0px;
|
|
||||||
--radius-xl: 0px;
|
|
||||||
--radius-2xl: 0px;
|
|
||||||
--radius-3xl: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--ground: #ebf3fa; /* Seitenhintergrund */
|
|
||||||
--surface: #ffffff; /* Flächen, Karten, Kopfleiste */
|
|
||||||
--hairline: #d8e3ed; /* Linien */
|
|
||||||
--ink: #27272a; /* Text */
|
|
||||||
--ink-muted: #52525b; /* gedämpfter Text */
|
|
||||||
--accent: oklch(0.97 0 0); /* E&P-Tiefblau */
|
|
||||||
--accent-press: #103c5b;
|
|
||||||
--accent-soft: #e3eef8;
|
|
||||||
--on-accent: #ffffff;
|
|
||||||
--strich: #009fff; /* nur Zierstrich unter Überschriften, nie für Text */
|
|
||||||
--fehler: #c0261b; /* Fehlermeldungen */
|
|
||||||
/* --font-lato setzt next/font auf <html>, siehe app/layout.tsx */
|
|
||||||
--heading-font: var(--font-lato), "Lato", ui-sans-serif, system-ui, sans-serif;
|
|
||||||
--body-font: var(--font-lato), "Lato", ui-sans-serif, system-ui, sans-serif;
|
|
||||||
--background: oklch(1 0 0);
|
|
||||||
--foreground: oklch(0.145 0 0);
|
|
||||||
--card: oklch(1 0 0);
|
|
||||||
--card-foreground: oklch(0.145 0 0);
|
|
||||||
--popover: oklch(1 0 0);
|
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
|
||||||
--secondary: oklch(0.97 0 0);
|
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
|
||||||
--muted: oklch(0.97 0 0);
|
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.922 0 0);
|
|
||||||
--input: oklch(0.922 0 0);
|
|
||||||
--ring: oklch(0.708 0 0);
|
|
||||||
--chart-1: oklch(0.87 0 0);
|
|
||||||
--chart-2: oklch(0.556 0 0);
|
|
||||||
--chart-3: oklch(0.439 0 0);
|
|
||||||
--chart-4: oklch(0.371 0 0);
|
|
||||||
--chart-5: oklch(0.269 0 0);
|
|
||||||
--radius: 0.625rem;
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
html {
|
|
||||||
color-scheme: light;
|
|
||||||
background: var(--ground);
|
|
||||||
@apply font-sans;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: var(--ground);
|
|
||||||
color: var(--ink);
|
|
||||||
font-family: var(--body-font);
|
|
||||||
line-height: 1.6;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4 {
|
|
||||||
font-family: var(--heading-font);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--accent);
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: var(--accent-soft);
|
|
||||||
padding: 0 0.3em;
|
|
||||||
font-size: 0.92em;
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus-visible {
|
|
||||||
outline: 2px solid var(--accent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
animation-iteration-count: 1 !important;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bausteine für alle Oberflächen. Neue Seiten setzen sich daraus zusammen. */
|
|
||||||
@layer components {
|
|
||||||
/* Skip-Link (erstes Element im Body) */
|
|
||||||
.skip-link {
|
|
||||||
position: absolute;
|
|
||||||
left: -9999px;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--on-accent);
|
|
||||||
padding: 0.75rem 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.skip-link:focus {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Überschrift mit kurzem Zierstrich darunter */
|
|
||||||
.ep-ueberschrift {
|
|
||||||
color: var(--ink);
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
.ep-ueberschrift::after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: 3rem;
|
|
||||||
height: 3px;
|
|
||||||
margin-top: 0.6rem;
|
|
||||||
background: var(--strich);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Weiße Fläche mit feiner Linie, für Formulare und Ergebnisse */
|
|
||||||
.ep-karte {
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--hairline);
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ep-label {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Eingabefelder, Auswahllisten und Textfelder */
|
|
||||||
.ep-feld {
|
|
||||||
width: 100%;
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid #b9c9d8;
|
|
||||||
color: var(--ink);
|
|
||||||
padding: 0.6rem 0.75rem;
|
|
||||||
}
|
|
||||||
.ep-feld:focus {
|
|
||||||
border-color: var(--accent);
|
|
||||||
outline: 2px solid var(--accent);
|
|
||||||
outline-offset: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Knopf in Tiefblau. Für die eine Hauptaktion zusätzlich .ep-cta */
|
|
||||||
.ep-knopf {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
background-color: var(--accent);
|
|
||||||
color: var(--on-accent);
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 0.7rem 1.4rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.ep-knopf:hover {
|
|
||||||
background-color: var(--accent-press);
|
|
||||||
}
|
|
||||||
.ep-knopf:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hauptaktion: Magenta-Verlauf wie „Alle Reisen anzeigen“ auf ep-reisen.de. Nur einmal je Seite. */
|
|
||||||
.ep-cta {
|
|
||||||
background-image: linear-gradient(120deg, #960167, #9e036a 40%, #fa1a8c);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
.ep-cta:hover {
|
|
||||||
background-image: linear-gradient(120deg, #7d0156, #8a035d 40%, #e3107c);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hellblauer Kasten für Ergebnisse und Hinweise */
|
|
||||||
.ep-hinweis {
|
|
||||||
background: var(--accent-soft);
|
|
||||||
border-left: 4px solid var(--accent);
|
|
||||||
padding: 0.9rem 1.1rem;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fehlermeldung */
|
|
||||||
.ep-fehler {
|
|
||||||
background: #fdf1f0;
|
|
||||||
border-left: 4px solid var(--fehler);
|
|
||||||
padding: 0.9rem 1.1rem;
|
|
||||||
color: var(--fehler);
|
|
||||||
font-size: 0.925rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tabelle für Ergebnisse */
|
|
||||||
.ep-tabelle {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: var(--surface);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.ep-tabelle th {
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--on-accent);
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 0.55rem 0.75rem;
|
|
||||||
}
|
|
||||||
.ep-tabelle td {
|
|
||||||
border-bottom: 1px solid var(--hairline);
|
|
||||||
padding: 0.55rem 0.75rem;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pfeil-Aufzählung */
|
|
||||||
.arrow-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.arrow-list > li {
|
|
||||||
position: relative;
|
|
||||||
padding-left: 1.4rem;
|
|
||||||
}
|
|
||||||
.arrow-list > li::before {
|
|
||||||
content: "→";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
color: var(--accent);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: oklch(0.145 0 0);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.205 0 0);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
|
||||||
--popover: oklch(0.205 0 0);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.922 0 0);
|
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
|
||||||
--secondary: oklch(0.269 0 0);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
|
||||||
--muted: oklch(0.269 0 0);
|
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
|
||||||
--accent: oklch(0.269 0 0);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
|
||||||
--border: oklch(1 0 0 / 10%);
|
|
||||||
--input: oklch(1 0 0 / 15%);
|
|
||||||
--ring: oklch(0.556 0 0);
|
|
||||||
--chart-1: oklch(0.87 0 0);
|
|
||||||
--chart-2: oklch(0.556 0 0);
|
|
||||||
--chart-3: oklch(0.439 0 0);
|
|
||||||
--chart-4: oklch(0.371 0 0);
|
|
||||||
--chart-5: oklch(0.269 0 0);
|
|
||||||
--sidebar: oklch(0.205 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,7 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import localFont from "next/font/local";
|
import { EpFuss, EpKopf } from "@ep/ui";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { EpKopf } from "@/components/EpKopf";
|
|
||||||
import { EpFuss } from "@/components/EpFuss";
|
|
||||||
import { APP_BESCHREIBUNG, APP_TITEL } from "@/lib/app";
|
import { APP_BESCHREIBUNG, APP_TITEL } from "@/lib/app";
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const inter = Inter({subsets:['latin'],variable:'--font-sans'});
|
|
||||||
|
|
||||||
|
|
||||||
// Hausschrift von ep-reisen.de (Lato, freie Lizenz OFL). Die Dateien liegen im Projekt,
|
|
||||||
// damit der Build ohne Internet klappt. Lato hat keine Stärke 600, font-semibold erscheint fett.
|
|
||||||
const lato = localFont({
|
|
||||||
src: [
|
|
||||||
{ path: "./fonts/lato-400.woff2", weight: "400", style: "normal" },
|
|
||||||
{ path: "./fonts/lato-700.woff2", weight: "700", style: "normal" },
|
|
||||||
{ path: "./fonts/lato-900.woff2", weight: "900", style: "normal" },
|
|
||||||
],
|
|
||||||
variable: "--font-lato",
|
|
||||||
display: "swap",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: `${APP_TITEL} · E&P Reisen`,
|
title: `${APP_TITEL} · E&P Reisen`,
|
||||||
@@ -35,14 +16,15 @@ export const viewport: Viewport = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// E&P-Layout für jede Seite: Kopf mit Logo, Inhalt, Fuß. Seiten liefern nur ihren Inhalt, kein eigenes <main>.
|
// E&P-Layout für jede Seite: Kopf mit Logo, Inhalt, Fuß. Seiten liefern nur ihren Inhalt, kein eigenes <main>.
|
||||||
|
// Kopf, Fuß und die Hausschrift Lato kommen aus @ep/ui.
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="de" className={cn("font-sans", inter.variable)}>
|
<html lang="de" className="font-sans">
|
||||||
<body className="flex min-h-screen flex-col bg-ground text-ink">
|
<body className="flex min-h-screen flex-col bg-ground text-ink">
|
||||||
<a href="#inhalt" className="skip-link">
|
<a href="#inhalt" className="skip-link">
|
||||||
Zum Inhalt springen
|
Zum Inhalt springen
|
||||||
</a>
|
</a>
|
||||||
<EpKopf />
|
<EpKopf titel={APP_TITEL} />
|
||||||
<main id="inhalt" className="mx-auto w-full max-w-3xl flex-1 px-4 py-8 sm:px-6 sm:py-12">
|
<main id="inhalt" className="mx-auto w-full max-w-3xl flex-1 px-4 py-8 sm:px-6 sm:py-12">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,76 +1,17 @@
|
|||||||
"use client";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { APP_BESCHREIBUNG, APP_TITEL } from "@/lib/app";
|
||||||
|
|
||||||
import { useState } from "react";
|
// Startseite im E&P-Layout. Kopf und Fuß kommen aus app/layout.tsx,
|
||||||
import { postWebhook } from "@/lib/n8n";
|
// Farben und Bausteine (ep-karte, ep-ueberschrift, …) aus @ep/ui/theme.css.
|
||||||
|
|
||||||
type HelloResponse = { message?: string };
|
|
||||||
|
|
||||||
// Pfad des eigenen Webhooks mit Kürzel, zum Beispiel "anna-hello" (ohne /webhook/).
|
|
||||||
// In der E&P-n8n gehört jeder Pfad genau einem Workflow. Ohne Angabe ruft die Seite nichts auf.
|
|
||||||
const HELLO_PATH = (process.env.NEXT_PUBLIC_HELLO_PATH || "").trim();
|
|
||||||
|
|
||||||
// Beispielseite im E&P-Layout. Kopf und Fuß kommen aus app/layout.tsx,
|
|
||||||
// Farben und Bausteine (ep-karte, ep-feld, ep-knopf, ep-cta, ep-hinweis, ep-fehler) aus app/globals.css.
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [answer, setAnswer] = useState<string | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
async function onSubmit(e: React.FormEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
setError(null);
|
|
||||||
setAnswer(null);
|
|
||||||
if (!HELLO_PATH) {
|
|
||||||
setError(
|
|
||||||
"NEXT_PUBLIC_HELLO_PATH fehlt. Beim ersten Veröffentlichen den eigenen Pfad mitgeben, zum Beispiel anna-hello.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await postWebhook<HelloResponse>(HELLO_PATH, { name });
|
|
||||||
setAnswer(res.message ?? JSON.stringify(res));
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<Card className="ep-karte rounded-none shadow-none ring-0">
|
||||||
<div>
|
<CardHeader className="px-0">
|
||||||
<h1 className="ep-ueberschrift text-3xl">Hallo aus n8n</h1>
|
<h1 className="ep-ueberschrift text-3xl">{APP_TITEL}</h1>
|
||||||
<p className="mt-4 text-ink-muted">
|
</CardHeader>
|
||||||
Das Formular ruft einen n8n-Webhook deines eigenen Workflows auf (Vorlage <code>hello-webhook.json</code>,
|
<CardContent className="px-0">
|
||||||
Pfad <code>/webhook/{HELLO_PATH || "anna-hello"}</code>).
|
<p className="text-ink-muted">{APP_BESCHREIBUNG}</p>
|
||||||
</p>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
<form onSubmit={onSubmit} className="ep-karte flex flex-col gap-3">
|
|
||||||
<label htmlFor="name" className="ep-label">
|
|
||||||
Dein Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="Max"
|
|
||||||
className="ep-feld"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button type="submit" disabled={loading} className="ep-knopf ep-cta self-start">
|
|
||||||
{loading ? "Sende …" : "An n8n senden"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{answer && <div className="ep-hinweis">{answer}</div>}
|
|
||||||
{error && (
|
|
||||||
<div role="alert" className="ep-fehler">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/** Fuß im E&P-Layout. Gehört zum festen Layout, bitte nicht umbauen. */
|
|
||||||
export function EpFuss() {
|
|
||||||
return (
|
|
||||||
<footer className="mt-12 border-t border-hairline bg-surface">
|
|
||||||
<div className="mx-auto max-w-5xl px-4 py-6 text-sm text-ink-muted sm:px-6">
|
|
||||||
E&P Reisen & Events · entstanden in der KI-Schulung
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { APP_TITEL } from "@/lib/app";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kopf im E&P-Layout wie auf ep-reisen.de: weiße Leiste mit dem E&P-Logo (nur auf hellem Grund),
|
|
||||||
* daneben ein Strich und der Name der Oberfläche aus lib/app.ts. Darunter ein tiefblauer Balken.
|
|
||||||
* Gehört zum festen Layout, bitte nicht umbauen.
|
|
||||||
*/
|
|
||||||
export function EpKopf() {
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<div className="border-b border-hairline bg-surface">
|
|
||||||
<div className="mx-auto flex max-w-5xl items-center px-4 py-3 sm:px-6 sm:py-4">
|
|
||||||
<Link href="/" className="flex items-center gap-3 no-underline">
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src="/ep-logo.svg"
|
|
||||||
alt="E&P Reisen & Events"
|
|
||||||
width={157}
|
|
||||||
height={48}
|
|
||||||
className="h-9 w-auto sm:h-12"
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
<span className="border-l border-hairline pl-3 text-sm font-black uppercase tracking-wide text-accent">
|
|
||||||
{APP_TITEL}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="h-2 bg-accent" aria-hidden="true" />
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import { cn } from "cn"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-4xl bg-card py-(--card-spacing) text-sm text-card-foreground shadow-md ring-1 ring-foreground/5 [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] dark:ring-foreground/10 *:[img:first-child]:rounded-t-4xl *:[img:last-child]:rounded-b-4xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-t-4xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("font-heading text-base font-medium", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-(--card-spacing)", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded-b-4xl px-(--card-spacing) [.border-t]:pt-(--card-spacing)",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Muster C: Provider und Modell an einer Stelle.
|
|
||||||
// Der Standard-Provider liest ANTHROPIC_API_KEY aus der Umgebung.
|
|
||||||
import { anthropic } from "@ai-sdk/anthropic";
|
|
||||||
|
|
||||||
// Ein günstiges Modell, weil jeder Aufruf Geld kostet (Claude Sonnet 5: 2 $ Input / 10 $ Output pro 1 Mio. Tokens).
|
|
||||||
export const getModel = () => anthropic("claude-sonnet-5");
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// Muster A: n8n-Webhook aus dem Frontend aufrufen.
|
|
||||||
// Die Basis-Adresse wird beim Bauen eingesetzt (NEXT_PUBLIC_*). Ohne Angabe gilt die E&P-n8n.
|
|
||||||
|
|
||||||
const BASE = (process.env.NEXT_PUBLIC_N8N_BASE || "https://n8n.ep-reisen.app").replace(/\/+$/, "");
|
|
||||||
|
|
||||||
export function webhookUrl(path: string): string {
|
|
||||||
const clean = path.replace(/^\/+/, "");
|
|
||||||
return `${BASE}/webhook/${clean}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function postWebhook<T = unknown>(
|
|
||||||
path: string,
|
|
||||||
body: unknown,
|
|
||||||
): Promise<T> {
|
|
||||||
const res = await fetch(webhookUrl(path), {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Webhook-Fehler ${res.status}: ${await res.text()}`);
|
|
||||||
}
|
|
||||||
// n8n kann JSON oder Text zurückgeben:
|
|
||||||
const text = await res.text();
|
|
||||||
try {
|
|
||||||
return JSON.parse(text) as T;
|
|
||||||
} catch {
|
|
||||||
return text as T;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +1,6 @@
|
|||||||
export { cn } from "cn"
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
Vendored
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
import "./.next/dev/types/root-params.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
Generated
+254
-366
File diff suppressed because it is too large.
Load diff
@@ -3,22 +3,22 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000",
|
"assets": "mkdir -p public && cp node_modules/@ep/ui/assets/*.svg public/",
|
||||||
"build": "next build",
|
"dev": "npm run assets && next dev -p 3000",
|
||||||
|
"build": "npm run assets && next build",
|
||||||
"start": "next start -p 3000"
|
"start": "next start -p 3000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.0",
|
|
||||||
"@ai-sdk/react": "^3.0.0",
|
|
||||||
"@base-ui/react": "^1.9.0",
|
"@base-ui/react": "^1.9.0",
|
||||||
"ai": "^6.0.0",
|
"@ep/ui": "^1.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"cn": "^0.5.0",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.63.0",
|
"lucide-react": "^1.63.0",
|
||||||
"next": "^16.2.0",
|
"next": "^16.4.5",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"shadcn": "^4.23.0",
|
"shadcn": "^4.23.0",
|
||||||
|
"tailwind-merge": "^3.7.0",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -29,4 +29,4 @@
|
|||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"typescript": "^5.9.0"
|
"typescript": "^5.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Zugänge, die die Workflows dieses Projekts brauchen (ohne Geheimnisse).
|
||||||
|
# - name: "Raumplanung SharePoint"
|
||||||
|
# typ: microsoftSharePointOAuth2Api
|
||||||
|
# zweck: Raumbelegung lesen
|
||||||
|
# klasse: gruen
|
||||||
Whitespace-only changes.
Reference in new issue
Block a user