Files
cameleer-saas/ui/sign-in/src/SignInPage.tsx
hsiegeln 2f7d4bd71c
All checks were successful
CI / build (push) Successful in 1m7s
CI / docker (push) Successful in 3m34s
feat: use cameleer3-logo.svg from design-system v0.1.36 everywhere
- Sidebar, sign-in page, and favicons all use the single SVG
- Postinstall copies SVG for SaaS HTML favicon (gitignored)
- Sign-in favicon committed (baked into Logto Docker image)
- Remove old PNG favicon references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:03:18 +02:00

111 lines
3.8 KiB
TypeScript

import { type FormEvent, useMemo, useState } from 'react';
import { Card, Input, Button, Alert, FormField } from '@cameleer/design-system';
import cameleerLogo from '@cameleer/design-system/assets/cameleer3-logo.svg';
import { signIn } from './experience-api';
import styles from './SignInPage.module.css';
const SUBTITLES = [
"Prove you're not a mirage",
"Only authorized cameleers beyond this dune",
"Halt, traveler — state your business",
"The caravan doesn't move without credentials",
"No hitchhikers on this caravan",
"This oasis requires a password",
"Camels remember faces. We use passwords.",
"You shall not pass... without logging in",
"The desert is vast. Your session has expired.",
"Another day, another dune to authenticate",
"Papers, please. The caravan master is watching.",
"Trust, but verify — ancient cameleer proverb",
"Even the Silk Road had checkpoints",
"Your camel is parked outside. Now identify yourself.",
"One does not simply walk into the dashboard",
"The sands shift, but your password shouldn't",
"Unauthorized access? In this economy?",
"Welcome back, weary traveler",
"The dashboard awaits on the other side of this dune",
"Keep calm and authenticate",
"Who goes there? Friend or rogue exchange?",
"Access denied looks the same in every desert",
"May your routes be green and your tokens valid",
"Forgot your password? That's between you and the dunes.",
"No ticket, no caravan",
];
export function SignInPage() {
const subtitle = useMemo(() => SUBTITLES[Math.floor(Math.random() * SUBTITLES.length)], []);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const redirectTo = await signIn(username, password);
window.location.replace(redirectTo);
} catch (err) {
setError(err instanceof Error ? err.message : 'Sign-in failed');
setLoading(false);
}
};
return (
<div className={styles.page}>
<Card className={styles.card}>
<div className={styles.loginForm}>
<div className={styles.logo}>
<img src={cameleerLogo} alt="" className={styles.logoImg} />
cameleer3
</div>
<p className={styles.subtitle}>{subtitle}</p>
{error && (
<div className={styles.error}>
<Alert variant="error">{error}</Alert>
</div>
)}
<form className={styles.fields} onSubmit={handleSubmit} aria-label="Sign in" noValidate>
<FormField label="Username" htmlFor="login-username">
<Input
id="login-username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
autoFocus
autoComplete="username"
disabled={loading}
/>
</FormField>
<FormField label="Password" htmlFor="login-password">
<Input
id="login-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
autoComplete="current-password"
disabled={loading}
/>
</FormField>
<Button
variant="primary"
type="submit"
loading={loading}
disabled={loading || !username || !password}
className={styles.submitButton}
>
Sign in
</Button>
</form>
</div>
</Card>
</div>
);
}