244 lines
13 KiB
TypeScript
244 lines
13 KiB
TypeScript
import React, { useState } from 'react';
|
|
import axios from 'axios';
|
|
import { useAuth } from '../AuthContext';
|
|
import { useNavigate, Link } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Tooltip } from '../components/Tooltip';
|
|
import { LegalModal } from '../components/LegalModal';
|
|
import { ShieldCheck, FileText, AlertTriangle, Key } from 'lucide-react';
|
|
import { GoogleLogin } from '@react-oauth/google';
|
|
|
|
export default function Signup() {
|
|
const { t } = useTranslation();
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [etsyShopName, setEtsyShopName] = useState('');
|
|
const [etsyShopLink, setEtsyShopLink] = useState('');
|
|
// New Fields for Phase 6
|
|
const [apiKey, setApiKey] = useState('');
|
|
const [termsAccepted, setTermsAccepted] = useState(false);
|
|
const [kvkkAccepted, setKvkkAccepted] = useState(false);
|
|
|
|
// UI State
|
|
const [error, setError] = useState<{ message: string, code?: string } | null>(null);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [modalType, setModalType] = useState<'terms' | 'kvkk' | null>(null);
|
|
|
|
const { login } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError(null);
|
|
|
|
// Frontend Verification
|
|
if (!termsAccepted || !kvkkAccepted) {
|
|
setError({ message: "You must accept both the User Agreement and KVKK to register." });
|
|
return;
|
|
}
|
|
if (apiKey.length < 20) {
|
|
setError({ message: "Please enter a valid Gemini API Key." });
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
const res = await axios.post('/api/auth/register', {
|
|
email,
|
|
password,
|
|
apiKey,
|
|
etsyShopName,
|
|
etsyShopLink,
|
|
termsAccepted: true // Backend checks this
|
|
});
|
|
|
|
login(res.data.token, res.data.user);
|
|
navigate('/');
|
|
} catch (err: any) {
|
|
const data = err.response?.data;
|
|
const code = data?.code || 'UNKNOWN_ERROR';
|
|
const msg = data?.error || 'Registration failed. Please try again.';
|
|
setError({ message: msg, code });
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center bg-stone-50 p-4">
|
|
<LegalModal
|
|
isOpen={!!modalType}
|
|
onClose={() => setModalType(null)}
|
|
type={modalType}
|
|
/>
|
|
|
|
<div className="w-full max-w-lg rounded-2xl bg-white p-8 shadow-xl border border-stone-100">
|
|
<div className="text-center mb-8">
|
|
<h2 className="text-3xl font-black text-stone-900 tracking-tight">Create Account</h2>
|
|
<p className="text-stone-500 mt-2 text-sm">Join the Beta access program</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className={`mb-6 rounded-xl p-4 flex gap-3 items-start ${error.code === 'QUOTA_EXCEEDED' ? 'bg-amber-50 text-amber-800 border-amber-200 border' : 'bg-red-50 text-red-800 border-red-200 border'}`}>
|
|
<AlertTriangle className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<h4 className="font-bold text-sm uppercase tracking-wide mb-1">
|
|
{error.code === 'QUOTA_EXCEEDED' ? 'Quota Exceeded' : 'Registration Failed'}
|
|
</h4>
|
|
<p className="text-sm opacity-90">{error.message}</p>
|
|
{error.code === 'INVALID_KEY' && (
|
|
<p className="text-xs mt-2 font-mono bg-white/50 p-1 rounded inline-block">
|
|
Tip: Keys usually start with AIza...
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
{/* Security Note */}
|
|
<div className="bg-blue-50 border border-blue-100 rounded-lg p-3 flex gap-2 items-center text-xs text-blue-700">
|
|
<ShieldCheck className="w-4 h-4" />
|
|
<span>Your data is encrypted. We strictly follow KVKK/GDPR.</span>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-sm font-bold text-stone-700">Email Address</label>
|
|
<input
|
|
type="email"
|
|
className="w-full rounded-lg border border-stone-300 p-3 text-sm focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
placeholder="you@example.com"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-sm font-bold text-stone-700">Password</label>
|
|
<input
|
|
type="password"
|
|
className="w-full rounded-lg border border-stone-300 p-3 text-sm focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
placeholder="••••••••"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="mb-1 block text-sm font-bold text-stone-700">Etsy Shop Name</label>
|
|
<input
|
|
type="text"
|
|
className="w-full rounded-lg border border-stone-300 p-3 text-sm focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
|
|
value={etsyShopName}
|
|
onChange={(e) => setEtsyShopName(e.target.value)}
|
|
placeholder="e.g. PixelParadise"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="mb-1 block text-sm font-bold text-stone-700">Etsy Shop Link</label>
|
|
<input
|
|
type="url"
|
|
className="w-full rounded-lg border border-stone-300 p-3 text-sm focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
|
|
value={etsyShopLink}
|
|
onChange={(e) => setEtsyShopLink(e.target.value)}
|
|
placeholder="https://etsy.com/shop/..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex justify-between items-center mb-1">
|
|
<label className="block text-sm font-bold text-stone-700">Gemini API Key (Required)</label>
|
|
<a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noreferrer" className="text-[10px] font-bold text-blue-600 hover:underline uppercase">
|
|
Get Key ↗
|
|
</a>
|
|
</div>
|
|
<div className="relative">
|
|
<Key className="absolute left-3 top-3 w-4 h-4 text-stone-400" />
|
|
<input
|
|
type="password"
|
|
className="w-full rounded-lg border border-stone-300 p-3 pl-10 text-sm font-mono focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
|
|
value={apiKey}
|
|
onChange={(e) => setApiKey(e.target.value)}
|
|
required
|
|
placeholder="AIzaSy..."
|
|
/>
|
|
</div>
|
|
<p className="text-[10px] text-stone-400 mt-1">
|
|
The system will send a test request to validate this key immediately.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Legal Checkboxes */}
|
|
<div className="space-y-3 pt-2">
|
|
<label className="flex items-start gap-3 cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
className="mt-1 w-4 h-4 rounded border-stone-300 text-stone-900 focus:ring-stone-900"
|
|
checked={termsAccepted}
|
|
onChange={(e) => setTermsAccepted(e.target.checked)}
|
|
/>
|
|
<span className="text-sm text-stone-600 group-hover:text-stone-800 transition-colors">
|
|
I accept the <button type="button" onClick={(e) => { e.preventDefault(); setModalType('terms'); }} className="font-bold underline">User Agreement & IP Rights</button>.
|
|
</span>
|
|
</label>
|
|
|
|
<label className="flex items-start gap-3 cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
className="mt-1 w-4 h-4 rounded border-stone-300 text-stone-900 focus:ring-stone-900"
|
|
checked={kvkkAccepted}
|
|
onChange={(e) => setKvkkAccepted(e.target.checked)}
|
|
/>
|
|
<span className="text-sm text-stone-600 group-hover:text-stone-800 transition-colors">
|
|
I have read and accept the <button type="button" onClick={(e) => { e.preventDefault(); setModalType('kvkk'); }} className="font-bold underline">KVKK (PDPL) & Privacy Policy</button>.
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting || !termsAccepted || !kvkkAccepted || !apiKey}
|
|
className="w-full rounded-xl bg-stone-900 py-3.5 font-bold text-white uppercase tracking-widest hover:bg-stone-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg hover:shadow-xl transform active:scale-[0.98]"
|
|
>
|
|
{isSubmitting ? 'Verifying Key...' : 'Create Account'}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="mt-8">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<div className="w-full border-t border-stone-200"></div>
|
|
</div>
|
|
<div className="relative flex justify-center text-sm">
|
|
<span className="bg-white px-2 text-stone-500 font-bold uppercase text-[10px] tracking-widest">Or continue with</span>
|
|
</div>
|
|
</div>
|
|
<div className="mt-6 flex justify-center">
|
|
<GoogleLogin
|
|
onSuccess={async (credentialResponse) => {
|
|
try {
|
|
const res = await axios.post('/api/auth/google', { credential: credentialResponse.credential });
|
|
login(res.data.token, res.data.user);
|
|
navigate('/');
|
|
} catch (err) {
|
|
setError({ message: 'Google Signup Failed' });
|
|
}
|
|
}}
|
|
onError={() => setError({ message: 'Google Signup Failed' })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 text-center pt-6 border-t border-stone-100">
|
|
<p className="text-stone-500 text-sm">Already have an account? <Link to="/login" className="font-bold text-stone-900 hover:underline">Log in</Link></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|