Alt rent, ingen byggeartefakter liggende igjen, dokumentasjonen oppdatert. Oppsummering:

Live og verifisert ende-til-ende:

/verify-siden bygget og live, e-postmalen sender nå en ekte klikkbar lenke (app/email.py/app/config.py)
Frontend containerisert (frontend/Dockerfile, standalone Next.js) og rullet ut som egen teecup_frontend-tjeneste
Caddy peker nå teecup.teeoff.no på frontend-en, som selv proxyer API-kall server-side — same-origin, ingen CORS, cookie uendret (dokumentert som ny ADR-016)
Fant og fikset en reell fallgruve: Next.js sin rewrites() bakes inn ved build-tid for standalone-output, ikke lest ved kjøretid — løst med en Docker build-time ARG
Ekte e-post sendt, ekte lenke klikket, sesjon opprettet — bekreftet av deg
Én ting du må huske: Caddy-endringen ligger i det separate /opt/teeoff-repoet (deploy/Caddyfile), ikke i teecup-repoet — den er ikke committet ennå. Lett å glemme siden resten av denne økten kun har jobbet i /opt/teecup. Vil du at jeg minner deg, eller committer du den nå selv i teeoff-repoet?

CLAUDE.md, FEATURE_BACKLOG.md og ARCHITECTURE_DECISIONS.md (ny ADR-016) er oppdatert. Klar for commit i teecup-repoet når du vil.
This commit is contained in:
Erol Haagenrud 2026-07-17 22:28:43 +02:00
parent be8da63df6
commit 59517b9290
13 changed files with 384 additions and 18 deletions

View file

@ -161,7 +161,20 @@
"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)"
"Bash(git -C /opt/teecup status --porcelain)",
"Bash(grep -rn -A2 '@router\\\\.\\\\\\(get\\\\|post\\\\|patch\\\\|delete\\\\|put\\\\\\)\\($' /opt/teecup/app/routers/matches.py /opt/teecup/app/routers/scoring.py /opt/teecup/app/routers/tournaments.py)",
"Bash(grep -rn -A1 '@router\\\\.\\\\\\(get\\\\|post\\\\|patch\\\\|delete\\\\|put\\\\\\)\\($' /opt/teecup/app/routers/matches.py /opt/teecup/app/routers/scoring.py /opt/teecup/app/routers/tournaments.py)",
"Bash(python3 -m py_compile /opt/teecup/app/config.py /opt/teecup/app/email.py)",
"Bash(docker build *)",
"Bash(docker rmi *)",
"Bash(curl -s -o /dev/null -w 'teecup.teeoff.no/: %{http_code}\\\\n' https://teecup.teeoff.no/)",
"Bash(curl -s https://teecup.teeoff.no/health)",
"Bash(curl -s https://teecup.teeoff.no/)",
"Bash(curl -s -o /dev/null -w '%{http_code}\\\\n' https://teecup.teeoff.no/)",
"Bash(curl -s -o /dev/null -w '%{http_code}\\\\n' https://teecup.teeoff.no/verify)",
"Bash(curl -s -X POST https://teecup.teeoff.no/auth/request-link -H 'Content-Type: application/json' -d '{\"email\":\"erol.haagenrud@envide.no\",\"locale\":\"nb\"}')",
"Bash(git -C /opt/teecup status --short frontend/pnpm-lock.yaml)",
"Bash(git -C /opt/teecup diff --stat frontend/pnpm-lock.yaml)"
],
"additionalDirectories": [
"/opt/teeoff/deploy",

View file

@ -363,6 +363,48 @@ brukerens varige profilinnstilling.
---
## ADR-016 — Frontend som eneste offentlige overflate, API-et server-side proxyet
**Beslutning:** `teecup.teeoff.no` peker (Caddy) på Next.js-frontenden
(`teecup_frontend`), IKKE direkte på FastAPI-et. Frontenden proxyer selv
kjente API-sti-prefikser (`/auth/*`, `/orgs/*`, `/health` — som til sammen
dekker HELE dagens API-overflate, verifisert med et grep av samtlige
rutedefinisjoner) videre til `teecup_api:8000` server-side, via Next.js sin
egen `rewrites()`-mekanisme (`frontend/next.config.mjs`). API-et er ikke
lenger separat Caddy-rutet eller offentlig eksponert under eget navn.
**Begrunnelse:** Alt kjører dermed under samme opprinnelse (origin) sett fra
nettleseren — ingen CORS-konfigurasjon trengs, og HttpOnly-sesjonscookien
(ADR-009) fungerer helt uendret enten kallet "egentlig" går til frontend
eller API. Alternativet (eget subdomene for API-et, `SameSite=None`-cookie
eller CORS-hull) ville svekket cookie-sikkerhetsmodellen som allerede var
bevisst bygget stram. Samme mønster generaliserer til alle fremtidige
skjermer uten videre Caddy-endringer — nye API-ruter under `/auth`, `/orgs`
trenger ingen ny proxy-regel, kun nye Next.js-sider som kaller dem med
relative URL-er.
**Reell fallgruve funnet og fikset ved bygging (2026-07-17):** Next.js sin
`rewrites()` løses ved BUILD-tid for `output: "standalone"` (bakes inn i
server-bunten), ikke ved container-oppstart. En `docker run -e
TEECUP_API_ORIGIN=...` ved kjøretid ble derfor stille ignorert (falt tilbake
til default `localhost:8000`, som ikke fantes i containeren — proxy-kall
feilet med `ECONNREFUSED`). Løst med en Docker build-time `ARG
TEECUP_API_ORIGIN` (default `http://teecup_api:8000`, matcher alltid det
delte nettverkets tjenestenavn) i `frontend/Dockerfile`, satt via
`docker-compose.yml` sin `build.args`. Generell lærdom for fremtidige
Next.js/Docker-oppsett i dette prosjektet: alt som brukes inne i
`next.config.mjs` er en BUILD-tids verdi, ikke en runtime-verdi, med mindre
det eksplisitt leses på nytt et sted som faktisk kjører per request (en
route handler, ikke selve config-filen).
**Konsekvens:** Enhver fremtidig ny API-sti-prefiks (utenfor `/auth`,
`/orgs`, `/health`) MÅ legges til i `frontend/next.config.mjs` sin
`rewrites()`-liste, ellers blir den utilgjengelig fra nettleseren selv om
API-et selv fungerer (kun nåbar internt på Docker-nettverket). `teecup_api`
sin port er ikke lenger tenkt nåbar direkte utenfra i prod.
---
## Åpne spørsmål (ikke besluttet ennå)
Disse må avklares før eller under de relevante fasene:

View file

@ -3,7 +3,7 @@
Les dette først i hver økt. Det koder hva vi har bestemt og hvordan vi jobber.
## Autoritative kilder (les før du gjør noe)
- `ARCHITECTURE_DECISIONS.md` — hva som er bestemt og hvorfor (ADR-001…015). Fasit.
- `ARCHITECTURE_DECISIONS.md` — hva som er bestemt og hvorfor (ADR-001…016). Fasit.
- `FEATURE_BACKLOG.md` — hva som gjenstår, hva som er utsatt, hva som mangler.
- Endres en beslutning: legg til en ny ADR, ikke slett historikk. Hold begge
filene oppdatert når noe avgjøres.
@ -253,8 +253,54 @@ Ferdig og verifisert:
delt Caddy-instans, ikke samme risikoklasse som Caddy-hendelsen fra
containeriseringsrunden).
- **Frontend startet, innlogging LIVE (2026-07-17, ADR-016):** første
frontend-skjerm i prosjektet. Designet i V0 (Next.js + Tailwind +
shadcn/ui), hentet inn som `frontend/` — merkevare-form/farge fra
Teeoff-logoen, IKKE navn/logo (egne, separate produkter, se ADR-009).
**Kvalitetsrunde før bruk:** V0s fargetokens var OKLCH-TILNÆRMINGER, ikke
eksakte — regnet ut presise verdier fra `#8bc24a`/`#ff5722` og rettet alle
6 forekomster i `globals.css`. Fjernet `@vercel/analytics` (unødvendig på
egen-hostet infra), fjernet `typescript: { ignoreBuildErrors: true }`
(ekte typesjekk kjører nå), fjernet dødt `pnpm.overrides`-felt.
`frontend/.gitignore` manglet `.pnpm-store/` — årsaken til at brukerens
VSCode viste 10 000 "ukjente" filer ved første commit-forsøk; rettet.
**Kablet mot ekte API:** `next.config.mjs` sin `rewrites()` proxyer
`/auth/*`/`/orgs/*`/`/health` server-side til `teecup_api` — same-origin,
ingen CORS, cookie uendret (full begrunnelse i ADR-016). Login-skjermet
sender ekte `POST /auth/request-link`; ny `/verify`-side mottar
`?token=...` fra e-postlenken (auto-verifiserer) eller viser et manuelt
"lim inn koden"-felt. `app/email.py` fikk en ny `PUBLIC_BASE_URL`-
innstilling og sender nå en EKTE klikkbar lenke (koden beholdes som
fallback).
**Reell fallgruve funnet og fikset ved containerisering:** Next.js sin
`rewrites()` løses ved BUILD-tid for `output: "standalone"`, ikke ved
container-oppstart — en runtime `-e TEECUP_API_ORIGIN=...` ble stille
ignorert (proxy-kall feilet med `ECONNREFUSED` mot `localhost:8000`).
Løst med en Docker build-time `ARG TEECUP_API_ORIGIN` i
`frontend/Dockerfile`, satt via `docker-compose.yml` sin `build.args`.
**Rullet ut live:** ny `teecup_frontend`-tjeneste i `docker-compose.yml`.
Caddy (`teecup.teeoff.no`, i det SEPARATE `teeoff`-repoet,
`/opt/teeoff/deploy/Caddyfile`) endret fra å peke direkte på `teecup_api`
til å peke på `teecup_frontend` — samme stale-inode-oppførsel som
containeriseringsrunden (graceful `reload` plukket IKKE opp endringen,
`/` fortsatte å gi `teecup_api` sin egen 404 i stedet for innloggingssiden
til reload faktisk skjedde). Løst likt: full `docker restart
teeoff_caddy`, brukeren bekreftet eksplisitt på forhånd. `teeoff.no`
upåvirket gjennom hele omstarten.
**Verifisert med FAKTISK e-postlevering:** ekte magic-link sendt til
brukerens egen adresse over `https://teecup.teeoff.no`, ekte e-post
mottatt med en ekte klikkbar lenke, åpnet i nettleser, landet på en
fungerende `/verify`-side, sesjon opprettet — brukeren bekreftet innlogget
status. Første gang en hel bruker-vendt flyt er bevist ende-til-ende i
produksjon, ikke bare API-et isolert.
**Merk for neste økt:** `deploy/Caddyfile`-endringen ligger uncommitted i
det SEPARATE `/opt/teeoff`-repoet, ikke i `teecup`-repoet — lett å glemme
siden denne økten ellers kun har jobbet i `/opt/teecup`.
Neste steg:
1. Frontend (PWA, offline-first) og kommunikasjon. Frontend er fortsatt IKKE
startet (⬜ i utviklingsplanen i ARCHITECTURE_DECISIONS.md) — helt frem til
nå har API-et vært nåbart, men uten noe grensesnitt en sluttbruker kan
bruke.
1. Flere V0-skjermer (turneringsoversikt, lag/roster, økt/program, blind
draw, scorekort, leaderboard) — samme mønster: design i V0, kvalitetsrunde
(farger/typesjekk/analytics), kable mot ekte API via samme
rewrites-proxy, ingen nye Caddy-endringer nødvendig (ADR-016).
2. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
3. Kommunikasjon (chat/feed) — ikke startet.

View file

@ -35,6 +35,53 @@
| Dato/klokkeslett på turnering/økt/match | ✅ | ADR-015. `session.scheduled_at` + `tee_interval_minutes` → utledet `match.tee_time`. Se egen seksjon under. |
| Maskinlesbar feilkode-kontrakt (`{"code",...}`) | ✅ | ADR-015. Full retrofit, alle 39 tidligere steder. Se egen seksjon under. |
| i18n-forberedelse (nb/en) | ✅ | ADR-015. `preferred_locale` + ekte engelsk e-postmal. Se egen seksjon under. |
| Frontend: innlogging + verifisering, LIVE | ✅ | ADR-016. Next.js på `teecup.teeoff.no`, ekte magic-link-flyt bevist med reell e-post. Se egen seksjon under. |
---
### Frontend: innlogging + verifisering — ✅ BYGGET OG VERIFISERT LIVE 2026-07-17
- Første frontend-skjerm i prosjektet. Designet i V0 (login-skjerm, merkevare
form/farge hentet fra Teeoff-logoen — IKKE navn/logo, se ADR-016s
begrunnelse og tidligere samtale), hentet inn som `frontend/` (Next.js +
Tailwind + shadcn/ui).
- **Kvalitetsrunde på V0-output før bruk:** fargetokens i `globals.css` var
OKLCH-TILNÆRMINGER av de forespurte hex-fargene (`#8bc24a`/`#ff5722`), ikke
eksakte — regnet ut presise OKLCH-ekvivalenter og rettet alle 6
forekomster (lys/mørk/system-mørk). Fjernet `@vercel/analytics` helt
(ga null verdi på egen-hostet infrastruktur, kun en unødvendig
tredjeparts nettverkskall). Fjernet `typescript: { ignoreBuildErrors:
true }` (bygget kjører nå ekte typesjekk). Fjernet dødt
`pnpm.overrides`-felt. `frontend/.gitignore` manglet `.pnpm-store/`
årsaken til at VSCode viste 10 000 "ukjente" filer ved første
commit-forsøk; rettet.
- **Kablet mot ekte API** (`app/routers/auth.py` sine `/auth/request-link`
og `/auth/verify-link`): `frontend/next.config.mjs` sin `rewrites()`
proxyer `/auth/*`/`/orgs/*`/`/health` server-side til API-et — se
ADR-016 for hele begrunnelsen (same-origin, ingen CORS, cookie uendret).
`components/login-form.tsx` sender ekte `POST /auth/request-link`;
ny `app/verify/page.tsx` + `components/verify-form.tsx` mottar
`?token=...` fra e-postlenken (automatisk verifisering) ELLER viser et
manuelt "lim inn koden"-felt (nødvendig fallback, ikke overflødig — se
under).
- **Backend-endring i samme runde:** `app/email.py`/`app/config.py` fikk en
ny `PUBLIC_BASE_URL`-innstilling (default `https://teecup.teeoff.no`,
valgfri) — e-postmalen sender nå en EKTE klikkbar lenke
(`{base}/verify?token=...`) i tillegg til den rå koden som fallback
(samme mal-mekanisme som i18n-runden, ADR-015).
- **Containerisert og rullet ut LIVE** (ny `frontend/Dockerfile`,
multi-stage, Next.js `output: "standalone"`; ny `teecup_frontend`-tjeneste
i `docker-compose.yml`). Caddy (`teecup.teeoff.no`, i det SEPARATE
`teeoff`-repoet) peker nå på `teecup_frontend` i stedet for `teecup_api`
direkte — se ADR-016 for hvorfor, og CLAUDE.md-status for
driftsdetaljene (samme stale-Caddy-inode-hendelse som
containeriseringsrunden, løst likt).
- **Verifisert med FAKTISK e-postlevering, ikke bare curl:** ekte
`POST /auth/request-link` sendt til brukerens egen adresse over
`https://teecup.teeoff.no`, ekte e-post mottatt med en ekte klikkbar
lenke, lenken åpnet i nettleser, landet på en fungerende `/verify`-side,
sesjon opprettet — brukeren bekreftet "jeg er tilsynelatende innlogget."
Første gang en hel bruker-vendt flyt (ikke bare API-et isolert) er bevist
ende-til-ende i produksjon.
---

View file

@ -67,5 +67,10 @@ class Settings:
FROM_EMAIL: str | None = os.environ.get("TEECUP_FROM_EMAIL", "").strip() or None
SMTP_CONFIGURED: bool = bool(SMTP_SERVER and SMTP_PORT and SMTP_USER and SMTP_PASS and FROM_EMAIL)
# Offentlig URL-base for lenker i utsendt e-post (frontend, ikke API-et
# direkte). Ikke hemmelig -- valgfri med prod-riktig standard slik at
# scratch-/dev-testing kan overstyre uten kodeendring.
PUBLIC_BASE_URL: str = os.getenv("TEECUP_PUBLIC_BASE_URL", "https://teecup.teeoff.no").rstrip("/")
settings = Settings()

View file

@ -42,7 +42,9 @@ _TEMPLATES = {
"subject": "Logg inn på TeeCup",
"body": (
"Hei,\n\n"
"Bruk denne koden for å logge inn på TeeCup innen {minutes} minutter:\n\n"
"Åpne denne lenken for å logge inn på TeeCup innen {minutes} minutter:\n\n"
"{link}\n\n"
"Fungerer ikke lenken? Lim inn denne koden manuelt i stedet:\n"
"{token}\n\n"
"Ba du ikke om dette, kan du se bort fra e-posten.\n"
),
@ -51,7 +53,9 @@ _TEMPLATES = {
"subject": "Log in to TeeCup",
"body": (
"Hi,\n\n"
"Use this code to log in to TeeCup within {minutes} minutes:\n\n"
"Open this link to log in to TeeCup within {minutes} minutes:\n\n"
"{link}\n\n"
"Link not working? Paste this code in manually instead:\n"
"{token}\n\n"
"If you didn't request this, you can ignore this email.\n"
),
@ -60,15 +64,16 @@ _TEMPLATES = {
async def send_magic_link_email(to_email: str, raw_token: str, locale: str = "nb") -> None:
"""Sender selve magic-link-tokenet som klartekst, på angitt språk.
"""Sender en klikkbar innloggingslenke (pluss selve koden som fallback).
MERK: ingen klikkbar lenke ennå -- TeeCup har ikke noe frontend å peke
(se plan). Når frontend bygges, oppdater malen til en ekte URL.
`locale` valideres allerede av Pydantic (Literal["nb","en"]) vei inn i
routeren, men faller trygt tilbake til "nb" her også hvis den skulle
komme fra et annet kall uten den garantien.
"""
template = _TEMPLATES.get(locale, _TEMPLATES["nb"])
subject = template["subject"]
body = template["body"].format(minutes=settings.MAGIC_LINK_MAX_AGE_MINUTES, token=raw_token)
link = f"{settings.PUBLIC_BASE_URL}/verify?token={raw_token}"
body = template["body"].format(
minutes=settings.MAGIC_LINK_MAX_AGE_MINUTES, token=raw_token, link=link
)
await to_thread(_send_sync, to_email, subject, body)

View file

@ -18,6 +18,18 @@ services:
- teeoff_default
restart: unless-stopped
teecup_frontend:
build:
context: ./frontend
args:
TEECUP_API_ORIGIN: http://teecup_api:8000
container_name: teecup_frontend
networks:
- teeoff_default
restart: unless-stopped
depends_on:
- teecup_api
networks:
teeoff_default:
external: true

6
frontend/.dockerignore Normal file
View file

@ -0,0 +1,6 @@
node_modules
.next
.pnpm-store
next-env.d.ts
pnpm-workspace.yaml
.git

28
frontend/Dockerfile Normal file
View file

@ -0,0 +1,28 @@
FROM node:22-slim AS deps
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --ignore-scripts
FROM node:22-slim AS builder
WORKDIR /app
RUN corepack enable
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# next.config.mjs sin rewrites() leses ved BUILD-tid for standalone-output,
# ikke ved container-oppstart -- API_ORIGIN må derfor være kjent her, ikke
# bare som en runtime env-var på "docker run".
ARG TEECUP_API_ORIGIN=http://teecup_api:8000
ENV TEECUP_API_ORIGIN=$TEECUP_API_ORIGIN
RUN pnpm build
FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]

View file

@ -0,0 +1,28 @@
import { Suspense } from "react"
import { Wordmark } from "@/components/login-form"
import { VerifyForm } from "@/components/verify-form"
export default function VerifyPage() {
return (
<main className="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-5 py-10">
<div className="flex w-full max-w-sm flex-col gap-8">
<header className="flex flex-col items-center gap-3 text-center">
<Wordmark />
</header>
<Suspense
fallback={
<div className="flex w-full flex-col items-center gap-4 rounded-3xl border border-border bg-card p-8 text-center shadow-lg shadow-black/5">
<div
aria-hidden="true"
className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
/>
</div>
}
>
<VerifyForm />
</Suspense>
</div>
</main>
)
}

View file

@ -0,0 +1,138 @@
"use client"
import type React from "react"
import { useEffect, useState } from "react"
import { useSearchParams } from "next/navigation"
import { CheckCircle2, KeyRound } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
type Status = "checking" | "verifying" | "manual" | "success" | "error"
export function VerifyForm() {
const searchParams = useSearchParams()
const urlToken = searchParams.get("token")
const [status, setStatus] = useState<Status>(urlToken ? "checking" : "manual")
const [manualToken, setManualToken] = useState("")
const [email, setEmail] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
async function verify(token: string) {
setStatus("verifying")
setError(null)
try {
const res = await fetch("/auth/verify-link", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ token }),
})
if (!res.ok) {
const body = await res.json().catch(() => null)
throw new Error(body?.detail?.message ?? "Lenken er ugyldig eller utløpt.")
}
const user = await res.json()
setEmail(user.email)
setStatus("success")
} catch (err) {
setError(err instanceof Error ? err.message : "Noe gikk galt. Prøv igjen.")
setStatus("error")
}
}
useEffect(() => {
if (urlToken) void verify(urlToken)
// Kjør kun ved mount / når URL-tokenet faktisk endres.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [urlToken])
function handleManualSubmit(e: React.FormEvent) {
e.preventDefault()
if (!manualToken.trim()) return
void verify(manualToken.trim())
}
if (status === "checking" || status === "verifying") {
return (
<div className="flex w-full flex-col items-center gap-4 rounded-3xl border border-border bg-card p-8 text-center shadow-lg shadow-black/5">
<div
aria-hidden="true"
className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
/>
<p className="text-sm text-muted-foreground" role="status">
Logger deg inn
</p>
</div>
)
}
if (status === "success") {
return (
<div className="flex w-full flex-col items-center gap-5 rounded-3xl border border-border bg-card p-6 text-center shadow-lg shadow-black/5 sm:p-8">
<div className="flex size-16 items-center justify-center rounded-2xl bg-primary/15">
<CheckCircle2 aria-hidden="true" className="size-8 text-primary" />
</div>
<div className="flex flex-col gap-2">
<h2 className="text-2xl font-extrabold tracking-tight text-balance">Du er innlogget</h2>
{email && (
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
Logget inn som <span className="font-semibold text-foreground">{email}</span>.
</p>
)}
</div>
</div>
)
}
// "manual" eller "error" -- samme skjema, ev. med en feilmelding over knappen.
return (
<div className="w-full rounded-3xl border border-border bg-card p-6 shadow-lg shadow-black/5 sm:p-8">
<form onSubmit={handleManualSubmit} className="flex flex-col gap-6" noValidate>
<div className="flex flex-col gap-2">
<Label htmlFor="token" className="text-sm font-semibold">
Kode fra e-posten
</Label>
<div className="relative">
<KeyRound
aria-hidden="true"
className="pointer-events-none absolute left-3.5 top-1/2 size-5 -translate-y-1/2 text-muted-foreground"
/>
<Input
id="token"
type="text"
autoComplete="one-time-code"
autoFocus
placeholder="Lim inn koden"
value={manualToken}
onChange={(e) => setManualToken(e.target.value)}
className="h-14 rounded-2xl pl-11 text-base"
/>
</div>
</div>
{error && (
<p role="alert" className="text-center text-sm font-medium text-destructive">
{error}
</p>
)}
<Button
type="submit"
disabled={!manualToken.trim()}
className="h-14 rounded-2xl text-base font-bold shadow-sm"
>
Logg inn
</Button>
<a
href="/"
className="text-center text-sm font-semibold text-muted-foreground transition-colors hover:text-foreground"
>
Be om en ny lenke
</a>
</form>
</div>
)
}

View file

@ -5,6 +5,7 @@ const API_ORIGIN = process.env.TEECUP_API_ORIGIN || "http://localhost:8000"
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
images: {
unoptimized: true,
},

View file

@ -1,5 +1,5 @@
{
"name": "my-project",
"name": "teecup-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
@ -28,10 +28,5 @@
"postcss": "^8.5",
"tailwindcss": "^4.3.3",
"typescript": "5.7.3"
},
"pnpm": {
"overrides": {
"hono": "4.12.25"
}
}
}