first commit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Felix FaerberandClaude Opus 4.7 committed 2026-05-27 12:22:05 +03:00
commit 974250b357
15 files changed
+4247

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
@import 'tailwindcss';
@theme {
--font-sans:
'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
html,
body {
background: #fafafa;
color: #0a0a0a;
font-family: var(--font-sans);
}
+86
View File
@@ -0,0 +1,86 @@
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from 'react-router';
import type { Route } from './+types/root';
import './app.css';
export function meta() {
return [
{ charset: 'utf-8' },
{ title: 't4t Gateway' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
name: 'description',
content:
'OpenAI-compatible API on Token4Token. 50 xBZZ welcome credit, no wallet needed.',
},
];
}
export const links: Route.LinksFunction = () => [
{
rel: 'preload',
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
as: 'style',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
crossOrigin: 'anonymous',
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang='en' className='scroll-smooth'>
<head>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = 'Oops!';
let details = 'An unexpected error occurred.';
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? '404' : 'Error';
details =
error.status === 404
? 'The requested page could not be found.'
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main className='container mx-auto p-4 pt-16'>
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className='w-full overflow-x-auto p-4'>
<code>{stack}</code>
</pre>
)}
</main>
);
}
+7
View File
@@ -0,0 +1,7 @@
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/_index.tsx'),
route('signup', 'routes/signup.tsx'),
route('verify', 'routes/verify.tsx'),
] satisfies RouteConfig;
+27
View File
@@ -0,0 +1,27 @@
export default function Index() {
return (
<main className='flex min-h-screen flex-col items-center justify-center px-6 text-center'>
<h1 className='text-4xl font-semibold tracking-tight sm:text-5xl'>
t4t Gateway
</h1>
<p className='mt-4 max-w-xl text-base text-neutral-600'>
OpenAI-compatible API on Token4Token. 50 xBZZ welcome credit, no wallet
needed.
</p>
<div className='mt-8 flex gap-3'>
<a
href='/signup'
className='inline-flex h-10 items-center rounded-md bg-neutral-900 px-5 text-sm font-medium text-white hover:bg-neutral-700'
>
Sign up
</a>
<a
href='/login'
className='inline-flex h-10 items-center rounded-md border border-neutral-300 bg-white px-5 text-sm font-medium hover:bg-neutral-100'
>
Log in
</a>
</div>
</main>
);
}
+141
View File
@@ -0,0 +1,141 @@
import { useState } from 'react';
const API_URL = import.meta.env.VITE_API_URL ?? 'https://api.t4t-gateway.com';
type SignupResponse = {
token: string;
user: { id: string; email: string; role: string; emailVerified: boolean };
verificationToken?: string;
};
export function meta() {
return [{ title: 'Sign up · t4t Gateway' }];
}
export default function Signup() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<SignupResponse | null>(null);
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
setSubmitting(true);
try {
const res = await fetch(`${API_URL}/auth/email/signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const body = await res.json();
if (!res.ok) {
throw new Error(body?.message ?? body?.error ?? `Signup failed (${res.status})`);
}
localStorage.setItem('jwt', body.token);
setResult(body);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setSubmitting(false);
}
}
if (result) {
return (
<main className='flex min-h-screen items-center justify-center px-6'>
<div className='w-full max-w-md rounded-lg border border-neutral-200 bg-white p-8 shadow-sm'>
<h1 className='text-2xl font-semibold'>Check your email</h1>
<p className='mt-2 text-sm text-neutral-600'>
We sent a verification link to <strong>{result.user.email}</strong>.
Open it to activate your 50 xBZZ welcome credit.
</p>
{result.verificationToken && (
<div className='mt-6 rounded-md bg-amber-50 p-4 text-sm text-amber-900'>
<p className='font-medium'>Dev mode — verification token:</p>
<code className='mt-1 block break-all font-mono text-xs'>
{result.verificationToken}
</code>
<a
href={`/verify?token=${result.verificationToken}`}
className='mt-3 inline-block text-amber-900 underline'
>
Verify now →
</a>
</div>
)}
</div>
</main>
);
}
return (
<main className='flex min-h-screen items-center justify-center px-6'>
<form
onSubmit={onSubmit}
className='w-full max-w-md rounded-lg border border-neutral-200 bg-white p-8 shadow-sm'
>
<h1 className='text-2xl font-semibold'>Sign up</h1>
<p className='mt-2 text-sm text-neutral-600'>
50 xBZZ welcome credit. No wallet needed.
</p>
<div className='mt-6 space-y-4'>
<div>
<label htmlFor='email' className='block text-sm font-medium'>
Email
</label>
<input
id='email'
type='email'
autoComplete='email'
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className='mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 text-sm shadow-sm focus:border-neutral-900 focus:outline-none focus:ring-1 focus:ring-neutral-900'
/>
</div>
<div>
<label htmlFor='password' className='block text-sm font-medium'>
Password
</label>
<input
id='password'
type='password'
autoComplete='new-password'
required
minLength={8}
value={password}
onChange={(e) => setPassword(e.target.value)}
className='mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 text-sm shadow-sm focus:border-neutral-900 focus:outline-none focus:ring-1 focus:ring-neutral-900'
/>
<p className='mt-1 text-xs text-neutral-500'>At least 8 characters.</p>
</div>
</div>
{error && (
<p className='mt-4 rounded-md bg-red-50 px-3 py-2 text-sm text-red-700'>
{error}
</p>
)}
<button
type='submit'
disabled={submitting}
className='mt-6 inline-flex h-10 w-full items-center justify-center rounded-md bg-neutral-900 px-5 text-sm font-medium text-white hover:bg-neutral-700 disabled:opacity-50'
>
{submitting ? 'Creating account…' : 'Create account'}
</button>
<p className='mt-4 text-center text-sm text-neutral-600'>
Already have an account?{' '}
<a href='/login' className='font-medium text-neutral-900 underline'>
Log in
</a>
</p>
</form>
</main>
);
}
+98
View File
@@ -0,0 +1,98 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router';
const API_URL = import.meta.env.VITE_API_URL ?? 'https://api.t4t-gateway.com';
type Status = 'pending' | 'success' | 'already' | 'error';
export function meta() {
return [{ title: 'Verify email · t4t Gateway' }];
}
export default function Verify() {
const [params] = useSearchParams();
const token = params.get('token');
const [status, setStatus] = useState<Status>('pending');
const [message, setMessage] = useState<string | null>(null);
useEffect(() => {
if (!token) {
setStatus('error');
setMessage('Missing verification token in URL.');
return;
}
(async () => {
try {
const res = await fetch(`${API_URL}/auth/email/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
const body = await res.json();
if (!res.ok) {
throw new Error(body?.message ?? body?.error ?? `Verify failed (${res.status})`);
}
setStatus(body.alreadyVerified ? 'already' : 'success');
} catch (err) {
setStatus('error');
setMessage(err instanceof Error ? err.message : 'Unknown error');
}
})();
}, [token]);
return (
<main className='flex min-h-screen items-center justify-center px-6'>
<div className='w-full max-w-md rounded-lg border border-neutral-200 bg-white p-8 shadow-sm'>
{status === 'pending' && (
<>
<h1 className='text-2xl font-semibold'>Verifying…</h1>
<p className='mt-2 text-sm text-neutral-600'>One moment.</p>
</>
)}
{status === 'success' && (
<>
<h1 className='text-2xl font-semibold'>Email verified</h1>
<p className='mt-2 text-sm text-neutral-600'>
Your 50 xBZZ welcome credit is being applied.
</p>
<a
href='/login'
className='mt-6 inline-flex h-10 items-center rounded-md bg-neutral-900 px-5 text-sm font-medium text-white hover:bg-neutral-700'
>
Continue to log in
</a>
</>
)}
{status === 'already' && (
<>
<h1 className='text-2xl font-semibold'>Already verified</h1>
<p className='mt-2 text-sm text-neutral-600'>
This email was verified previously. Nothing to do.
</p>
<a
href='/login'
className='mt-6 inline-flex h-10 items-center rounded-md bg-neutral-900 px-5 text-sm font-medium text-white hover:bg-neutral-700'
>
Log in
</a>
</>
)}
{status === 'error' && (
<>
<h1 className='text-2xl font-semibold'>Verification failed</h1>
<p className='mt-2 text-sm text-red-700'>{message}</p>
<a
href='/signup'
className='mt-6 inline-flex h-10 items-center rounded-md border border-neutral-300 bg-white px-5 text-sm font-medium hover:bg-neutral-100'
>
Back to sign up
</a>
</>
)}
</div>
</main>
);
}