Før grafikk

This commit is contained in:
Erol Haagenrud 2026-07-17 21:57:44 +02:00
parent 0ca9151ed6
commit be8da63df6
7 changed files with 57 additions and 49 deletions

View file

@ -155,7 +155,13 @@
"Bash(python3)", "Bash(python3)",
"Bash(docker --version)", "Bash(docker --version)",
"Bash(docker run --rm -v /opt/teecup/frontend:/app -w /app node:22-slim bash -c ' *)", "Bash(docker run --rm -v /opt/teecup/frontend:/app -w /app node:22-slim bash -c ' *)",
"Bash(git -C /opt/teecup status --short frontend/)" "Bash(git -C /opt/teecup status --short frontend/)",
"Bash(grep -n \"CORS\\\\|cors\\\\|allow_origin\" /opt/teecup/app/main.py *)",
"Bash(git -C /opt/teecup ls-files frontend/.gitignore)",
"Bash(git -C /opt/teecup rev-parse --show-toplevel)",
"Bash(git -C /opt/teecup config --get core.excludesfile)",
"Bash(git -C /opt/teecup ls-files .claude/)",
"Bash(git -C /opt/teecup status --porcelain)"
], ],
"additionalDirectories": [ "additionalDirectories": [
"/opt/teeoff/deploy", "/opt/teeoff/deploy",

3
frontend/.gitignore vendored
View file

@ -12,4 +12,7 @@ __v0_jsx-dev-runtime.ts
# Common ignores # Common ignores
node_modules node_modules
.next/ .next/
.pnpm-store/
next-env.d.ts
pnpm-workspace.yaml
.DS_Store .DS_Store

View file

@ -1,4 +1,3 @@
import { Analytics } from '@vercel/analytics/next'
import type { Metadata, Viewport } from 'next' import type { Metadata, Viewport } from 'next'
import { Nunito } from 'next/font/google' import { Nunito } from 'next/font/google'
import './globals.css' import './globals.css'
@ -47,10 +46,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="no" className={`${nunito.variable} bg-background`}> <html lang="no" className={`${nunito.variable} bg-background`}>
<body className="font-sans antialiased"> <body className="font-sans antialiased">{children}</body>
{children}
{process.env.NODE_ENV === 'production' && <Analytics />}
</body>
</html> </html>
) )
} }

View file

@ -19,6 +19,7 @@ export function LoginForm() {
const [sent, setSent] = useState(false) const [sent, setSent] = useState(false)
const [sending, setSending] = useState(false) const [sending, setSending] = useState(false)
const [cooldown, setCooldown] = useState(0) const [cooldown, setCooldown] = useState(0)
const [error, setError] = useState<string | null>(null)
const emailValid = isValidEmail(email) const emailValid = isValidEmail(email)
const showError = touched && email.length > 0 && !emailValid const showError = touched && email.length > 0 && !emailValid
@ -33,12 +34,25 @@ export function LoginForm() {
}, [cooldown]) }, [cooldown])
async function sendLink() { async function sendLink() {
// Mock only — pretend to dispatch a magic link.
setSending(true) setSending(true)
await new Promise((r) => setTimeout(r, 700)) setError(null)
setSending(false) try {
setSent(true) // Alltid samme suksess-respons uansett om e-posten finnes (anti-
setCooldown(RESEND_COOLDOWN) // enumerering, se ADR-009) -- kun nettverks-/serverfeil havner i catch.
const res = await fetch("/auth/request-link", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ email: email.trim(), locale: "nb" }),
})
if (!res.ok) throw new Error(`request-link: ${res.status}`)
setSent(true)
setCooldown(RESEND_COOLDOWN)
} catch {
setError("Klarte ikke å sende lenken. Sjekk tilkoblingen og prøv igjen.")
} finally {
setSending(false)
}
} }
function handleSubmit(e: React.FormEvent) { function handleSubmit(e: React.FormEvent) {
@ -65,6 +79,7 @@ export function LoginForm() {
email={email} email={email}
cooldown={cooldown} cooldown={cooldown}
sending={sending} sending={sending}
error={error}
onResend={handleResend} onResend={handleResend}
onReset={handleReset} onReset={handleReset}
/> />
@ -101,6 +116,12 @@ export function LoginForm() {
)} )}
</div> </div>
{error && (
<p role="alert" className="text-center text-sm font-medium text-destructive">
{error}
</p>
)}
<Button <Button
type="submit" type="submit"
disabled={sending} disabled={sending}
@ -122,12 +143,14 @@ function ConfirmationState({
email, email,
cooldown, cooldown,
sending, sending,
error,
onResend, onResend,
onReset, onReset,
}: { }: {
email: string email: string
cooldown: number cooldown: number
sending: boolean sending: boolean
error: string | null
onResend: () => void onResend: () => void
onReset: () => void onReset: () => void
}) { }) {
@ -157,6 +180,12 @@ function ConfirmationState({
</p> </p>
</div> </div>
{error && (
<p role="alert" className="text-sm font-medium text-destructive">
{error}
</p>
)}
<div className="mt-1 w-full"> <div className="mt-1 w-full">
{cooldown > 0 ? ( {cooldown > 0 ? (
<p className="text-sm text-muted-foreground" aria-live="polite"> <p className="text-sm text-muted-foreground" aria-live="polite">

View file

@ -1,11 +1,20 @@
// API-et proxyes server-side under samme opprinnelse (ingen CORS, cookien
// fungerer uendret) -- se ADR-009/015. TEECUP_API_ORIGIN peker mot en lokal
// backend i dev; i prod peker den mot teecup_api på det delte Docker-nettverket.
const API_ORIGIN = process.env.TEECUP_API_ORIGIN || "http://localhost:8000"
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
typescript: {
ignoreBuildErrors: true,
},
images: { images: {
unoptimized: true, unoptimized: true,
}, },
async rewrites() {
return [
{ source: "/auth/:path*", destination: `${API_ORIGIN}/auth/:path*` },
{ source: "/orgs/:path*", destination: `${API_ORIGIN}/orgs/:path*` },
{ source: "/health", destination: `${API_ORIGIN}/health` },
]
},
} }
export default nextConfig export default nextConfig

View file

@ -10,7 +10,6 @@
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.5.0", "@base-ui/react": "^1.5.0",
"@vercel/analytics": "1.6.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.16.0", "lucide-react": "^1.16.0",

View file

@ -11,9 +11,6 @@ importers:
'@base-ui/react': '@base-ui/react':
specifier: ^1.5.0 specifier: ^1.5.0
version: 1.5.0(@date-fns/tz@1.4.1)(@types/react@19.2.14)(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.5.0(@date-fns/tz@1.4.1)(@types/react@19.2.14)(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@vercel/analytics':
specifier: 1.6.1
version: 1.6.1(next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
@ -701,32 +698,6 @@ packages:
'@types/validate-npm-package-name@4.0.2': '@types/validate-npm-package-name@4.0.2':
resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
'@vercel/analytics@1.6.1':
resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==}
peerDependencies:
'@remix-run/react': ^2
'@sveltejs/kit': ^1 || ^2
next: '>= 13'
react: ^18 || ^19 || ^19.0.0-rc
svelte: '>= 4'
vue: ^3
vue-router: ^4
peerDependenciesMeta:
'@remix-run/react':
optional: true
'@sveltejs/kit':
optional: true
next:
optional: true
react:
optional: true
svelte:
optional: true
vue:
optional: true
vue-router:
optional: true
accepts@2.0.0: accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -2646,11 +2617,6 @@ snapshots:
'@types/validate-npm-package-name@4.0.2': {} '@types/validate-npm-package-name@4.0.2': {}
'@vercel/analytics@1.6.1(next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
optionalDependencies:
next: 16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
accepts@2.0.0: accepts@2.0.0:
dependencies: dependencies:
mime-types: 3.0.2 mime-types: 3.0.2