chore: seminarhof customer project
This commit is contained in:
18
apps/app/src/lib/auth.ts
Normal file
18
apps/app/src/lib/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createAuthClient } from 'better-auth/client'
|
||||
import { apiUrl } from './env'
|
||||
|
||||
// Better Auth mounts at /api/auth (default basePath); the client appends the
|
||||
// endpoint verbatim to baseURL, so the base must include /auth — otherwise it
|
||||
// emits /api/sign-in/email and 404s on both sandbox and deploy.
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: `${apiUrl()}/auth`,
|
||||
})
|
||||
|
||||
export async function signIn(email: string, password: string): Promise<void> {
|
||||
const { error } = await authClient.signIn.email({ email, password })
|
||||
if (error) throw new Error('Invalid credentials')
|
||||
}
|
||||
|
||||
export async function signOut(): Promise<void> {
|
||||
await authClient.signOut()
|
||||
}
|
||||
24
apps/app/src/lib/dates.ts
Normal file
24
apps/app/src/lib/dates.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Placeholder shown for missing dates (an enquiry may not have dates yet).
|
||||
const NO_DATE = '—'
|
||||
|
||||
export function fmtDate(d: Date | null | undefined, locale: string): string {
|
||||
if (!d) return NO_DATE
|
||||
return d.toLocaleDateString(locale.startsWith('de') ? 'de-DE' : 'en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function fmtDateShort(d: Date | null | undefined, locale: string): string {
|
||||
if (!d) return NO_DATE
|
||||
return d.toLocaleDateString(locale.startsWith('de') ? 'de-DE' : 'en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
})
|
||||
}
|
||||
|
||||
export function toDateInputStr(d: Date | null | undefined): string {
|
||||
if (!d) return ''
|
||||
return d.toISOString().slice(0, 10)
|
||||
}
|
||||
24
apps/app/src/lib/env.ts
Normal file
24
apps/app/src/lib/env.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Endpoints come from env, never hardcoded.
|
||||
//
|
||||
// In local dev VITE_API_URL is set at build time and Vite replaces the
|
||||
// import.meta.env reference inline. In deployed/sandbox bundles VITE_API_URL is
|
||||
// a runtime Worker binding — invisible to Vite at build time — so it's undefined
|
||||
// in the bundle. We fall back to the current origin + /api, which is correct
|
||||
// because the dispatcher routes every /api/* path to the API on the same host.
|
||||
export function apiUrl(): string {
|
||||
if (typeof window !== 'undefined' && (window as any).__E2E_API_URL) {
|
||||
// E2E talks directly to the backend (no vite proxy). Better Auth is mounted
|
||||
// at /api/auth there, while pikku routes live at the root — so the auth
|
||||
// client must add the /api prefix that __E2E_API_URL (bare host) lacks.
|
||||
return `${(window as any).__E2E_API_URL as string}/api`
|
||||
}
|
||||
if (import.meta.env.SSR) {
|
||||
// SSR context: the auth client is only consumed in the browser. Return the
|
||||
// build-time var when available or a placeholder so SSR never throws.
|
||||
return import.meta.env.VITE_API_URL ?? '/__api'
|
||||
}
|
||||
// Client: build-time var (local dev) or derive from the current origin.
|
||||
// Better Auth's client requires an absolute base URL, so never return a bare
|
||||
// relative '/api' here — '/api/auth' throws "Invalid base URL".
|
||||
return import.meta.env.VITE_API_URL ?? `${window.location.origin}/api`
|
||||
}
|
||||
35
apps/app/src/lib/status.ts
Normal file
35
apps/app/src/lib/status.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Brand-aligned status palettes. Single source of truth for booking, invoice
|
||||
// and room state colours. Each entry: bg (badge background), fg (text colour)
|
||||
// and dot (8 px leading dot). Hex codes are checked for ≥4.5:1 on bg.
|
||||
|
||||
export type BookingStatus =
|
||||
| 'enquiry'
|
||||
| 'reserved'
|
||||
| 'confirmed'
|
||||
| 'ended'
|
||||
| 'cancelled'
|
||||
|
||||
export const BOOKING_STATUS: Record<BookingStatus, { bg: string; fg: string; dot: string }> = {
|
||||
enquiry: { bg: '#F7EEF5', fg: '#583651', dot: '#BC7AA6' },
|
||||
reserved: { bg: '#FFEDDC', fg: '#8A4A1A', dot: '#FFBC7D' },
|
||||
confirmed: { bg: '#E9D4E2', fg: '#4E3149', dot: '#88557F' },
|
||||
ended: { bg: '#EDEDED', fg: '#4A4A4A', dot: '#989499' },
|
||||
cancelled: { bg: '#F2F2F2', fg: '#6B6B6B', dot: '#B5B5B5' },
|
||||
}
|
||||
|
||||
/** Valid transitions per status (client-side, for CTA buttons). Enforced server-side too. */
|
||||
export const BOOKING_TRANSITIONS: Record<BookingStatus, BookingStatus[]> = {
|
||||
enquiry: ['reserved', 'cancelled'],
|
||||
reserved: ['confirmed', 'cancelled'],
|
||||
confirmed: ['ended', 'cancelled'],
|
||||
ended: [],
|
||||
cancelled: [],
|
||||
}
|
||||
|
||||
export type InvoiceStatus = 'outstanding' | 'paid' | 'cancelled'
|
||||
|
||||
export const INVOICE_STATUS: Record<InvoiceStatus, { bg: string; fg: string; dot: string }> = {
|
||||
outstanding: { bg: '#FFF8E0', fg: '#806600', dot: '#FFCE00' },
|
||||
paid: { bg: '#E9D4E2', fg: '#4E3149', dot: '#88557F' },
|
||||
cancelled: { bg: '#F2F2F2', fg: '#6B6B6B', dot: '#B5B5B5' },
|
||||
}
|
||||
Reference in New Issue
Block a user