Files
skillhub/apps/web/components/EarlyAccessForm.tsx
airano 97b427831a Initial release v1.0.0
Open-source marketplace for AI Agent skills.

Features:
- Next.js 15 web app with i18n (en/fa)
- CLI tool for skill installation (npx skillhub)
- GitHub crawler/indexer with multi-strategy discovery
- Security scanning for all indexed skills
- Self-hostable with Docker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 06:42:07 +03:30

108 lines
3.5 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useTranslations } from 'next-intl';
import { Mail, CheckCircle, Loader2 } from 'lucide-react';
interface EarlyAccessFormProps {
variant: 'a' | 'b';
locale: string;
}
export function EarlyAccessForm({ variant, locale }: EarlyAccessFormProps) {
const t = useTranslations('claudePlugin');
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email || !email.includes('@')) {
setErrorMessage(t('form.invalidEmail'));
setStatus('error');
return;
}
setStatus('loading');
try {
const response = await fetch('/api/early-access', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
variant,
locale,
source: 'claude-plugin-landing',
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to submit');
}
setStatus('success');
setEmail('');
} catch (err) {
setErrorMessage(err instanceof Error ? err.message : t('form.genericError'));
setStatus('error');
}
};
if (status === 'success') {
return (
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
<CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
<p className="text-green-800 dark:text-green-300 font-medium text-center">
{t('form.successMessage')}
</p>
<p className="text-green-600 dark:text-green-400 text-sm text-center">
{t('form.successDescription')}
</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-secondary" />
<input
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (status === 'error') setStatus('idle');
}}
placeholder={t('form.emailPlaceholder')}
className="w-full pl-10 pr-4 py-3 rounded-lg border border-border bg-surface text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
disabled={status === 'loading'}
/>
</div>
<button
type="submit"
disabled={status === 'loading'}
className="px-6 py-3 rounded-lg bg-primary text-white font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{status === 'loading' ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : null}
{variant === 'a' ? t('form.ctaA') : t('form.ctaB')}
</button>
</div>
{status === 'error' && (
<p className="text-red-500 text-sm text-center">{errorMessage}</p>
)}
<p className="text-text-secondary text-xs text-center">
{t('form.privacyNote')}
</p>
</form>
);
}