feat: inisialisasi project kit v2
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import GuestLayout from '@/Layouts/GuestLayout';
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
|
||||
export default function ConfirmPassword() {
|
||||
const { data, setData, post, processing, errors, reset } = useForm({ password: '' });
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post(route('password.confirm'), { onFinish: () => reset('password') });
|
||||
};
|
||||
|
||||
return (
|
||||
<GuestLayout>
|
||||
<Head title="Confirm password" />
|
||||
|
||||
<div className="mb-8 anim-down">
|
||||
<div className="w-12 h-12 bg-[#3D4E4B]/5 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg className="w-5 h-5 text-[#3D4E4B]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-[#1A2421] tracking-tight">Confirm your password</h1>
|
||||
<p className="mt-1.5 text-sm text-gray-400 font-medium leading-relaxed">
|
||||
For your security, please confirm your password to continue.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} className="anim-up" style={{ animationDelay: '0.1s' }}>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-semibold text-gray-600 mb-1.5">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
autoFocus
|
||||
value={data.password}
|
||||
onChange={e => setData('password', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className={`auth-input${errors.password ? ' !border-red-300 !bg-red-50/50' : ''}`}
|
||||
/>
|
||||
{errors.password && <p className="mt-1.5 text-xs font-semibold text-red-500">{errors.password}</p>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="mt-6 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 animate-spin text-white/60" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Confirming…
|
||||
</>
|
||||
) : 'Confirm password'}
|
||||
</button>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import GuestLayout from '@/Layouts/GuestLayout';
|
||||
import { Head, useForm, Link } from '@inertiajs/react';
|
||||
|
||||
export default function ForgotPassword({ status }: { status?: string }) {
|
||||
const { data, setData, post, processing, errors } = useForm({ email: '' });
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post(route('password.email'));
|
||||
};
|
||||
|
||||
return (
|
||||
<GuestLayout>
|
||||
<Head title="Forgot password" />
|
||||
|
||||
<div className="mb-8 anim-down">
|
||||
<h1 className="text-2xl font-bold text-[#1A2421] tracking-tight">Forgot password?</h1>
|
||||
<p className="mt-1.5 text-sm text-gray-400 font-medium">
|
||||
Enter your email and we'll send a reset link.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status && (
|
||||
<div className="mb-6 px-4 py-3 rounded-xl bg-emerald-50 border border-emerald-100 text-sm font-semibold text-emerald-700 anim-fade">
|
||||
{status}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={submit} className="anim-up" style={{ animationDelay: '0.1s' }}>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-semibold text-gray-600 mb-1.5">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
placeholder="you@company.com"
|
||||
className={`auth-input${errors.email ? ' !border-red-300 !bg-red-50/50' : ''}`}
|
||||
/>
|
||||
{errors.email && <p className="mt-1.5 text-xs font-semibold text-red-500">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="mt-6 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 animate-spin text-white/60" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Sending…
|
||||
</>
|
||||
) : 'Send reset link'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-7 text-center text-sm text-gray-400 font-medium anim-fade" style={{ animationDelay: '0.18s' }}>
|
||||
<Link href={route('login')} className="text-[#3D4E4B] font-semibold hover:text-[#D4A017] transition-colors duration-200">
|
||||
← Back to sign in
|
||||
</Link>
|
||||
</p>
|
||||
</GuestLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import React from 'react';
|
||||
import GuestLayout from '@/Layouts/GuestLayout';
|
||||
import { Head, Link, useForm, usePage } from '@inertiajs/react';
|
||||
|
||||
interface LoginProps {
|
||||
status?: string;
|
||||
canResetPassword: boolean;
|
||||
}
|
||||
|
||||
export default function Login({ status, canResetPassword }: LoginProps) {
|
||||
const { system_settings } = usePage().props as any;
|
||||
const isGoogleEnabled = system_settings?.oauth_google_enabled === '1' || system_settings?.oauth_google_enabled === true;
|
||||
const isGithubEnabled = system_settings?.oauth_github_enabled === '1' || system_settings?.oauth_github_enabled === true;
|
||||
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post(route('login'), { onFinish: () => reset('password') });
|
||||
};
|
||||
|
||||
return (
|
||||
<GuestLayout>
|
||||
<Head title="Sign in" />
|
||||
|
||||
{/* Heading */}
|
||||
<div className="mb-8 anim-down">
|
||||
<h1 className="text-2xl font-bold text-[#1A2421] tracking-tight">Sign in</h1>
|
||||
<p className="mt-1.5 text-sm text-gray-400 font-medium">
|
||||
Enter your credentials to access the dashboard.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status message */}
|
||||
{status && (
|
||||
<div className="mb-6 px-4 py-3 rounded-xl bg-emerald-50 border border-emerald-100 text-sm font-semibold text-emerald-700 anim-fade">
|
||||
{status}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OAuth */}
|
||||
{(isGoogleEnabled || isGithubEnabled) && (
|
||||
<div className="mb-6 anim-up" style={{ animationDelay: '0.08s' }}>
|
||||
<div className={`grid gap-3 ${isGoogleEnabled && isGithubEnabled ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
||||
{isGoogleEnabled && (
|
||||
<a
|
||||
href="/auth/google/redirect"
|
||||
className="flex items-center justify-center gap-2.5 px-4 py-2.5 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-gray-300 transition-colors duration-200 text-sm font-semibold text-gray-700"
|
||||
>
|
||||
<svg className="w-4 h-4 shrink-0" viewBox="0 0 24 24">
|
||||
<path fill="#EA4335" d="M12 5c1.61 0 3.09.59 4.23 1.57l3.12-3.12C17.35 1.67 14.85 1 12 1 7.73 1 4.14 3.48 2.46 7.1l3.71 2.87C7.04 7.09 9.34 5 12 5z"/>
|
||||
<path fill="#4285F4" d="M23.49 12.27c0-.79-.07-1.54-.19-2.27H12v4.51h6.47c-.29 1.48-1.14 2.73-2.4 3.58l3.7 2.87c2.16-2 3.72-4.94 3.72-8.69z"/>
|
||||
<path fill="#FBBC05" d="M6.17 14.77l-3.71 2.87C4.14 21.27 7.73 23 12 23c2.97 0 5.48-1 7.37-2.69l-3.7-2.87c-1.03.69-2.35 1.11-3.67 1.11-2.66 0-4.96-2.09-5.83-4.78z"/>
|
||||
<path fill="#34A853" d="M12 19.45c1.32 0 2.64-.42 3.67-1.11l3.7 2.87C17.48 22 14.97 23 12 23 7.73 23 4.14 21.27 2.47 17.64l3.71-2.87c.86 2.69 3.16 4.68 5.82 4.68z"/>
|
||||
</svg>
|
||||
Google
|
||||
</a>
|
||||
)}
|
||||
{isGithubEnabled && (
|
||||
<a
|
||||
href="/auth/github/redirect"
|
||||
className="flex items-center justify-center gap-2.5 px-4 py-2.5 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-gray-300 transition-colors duration-200 text-sm font-semibold text-gray-700"
|
||||
>
|
||||
<svg className="w-4 h-4 shrink-0" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-100" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="px-3 bg-white text-xs font-semibold text-gray-300 uppercase tracking-widest">or</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={submit} className="anim-up" style={{ animationDelay: '0.14s' }}>
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-semibold text-gray-600 mb-1.5">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
placeholder="you@company.com"
|
||||
className={`auth-input${errors.email ? ' !border-red-300 !bg-red-50/50' : ''}`}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1.5 text-xs font-semibold text-red-500">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label htmlFor="password" className="text-sm font-semibold text-gray-600">
|
||||
Password
|
||||
</label>
|
||||
{canResetPassword && (
|
||||
<Link
|
||||
href={route('password.request')}
|
||||
className="text-xs font-semibold text-[#D4A017] hover:text-[#B88B14] transition-colors duration-200"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className={`auth-input${errors.password ? ' !border-red-300 !bg-red-50/50' : ''}`}
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="mt-1.5 text-xs font-semibold text-red-500">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remember me */}
|
||||
<label className="mt-4 flex items-center gap-2.5 cursor-pointer select-none group w-fit">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data.remember}
|
||||
onChange={(e) => setData('remember', e.target.checked)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 rounded-[5px] border flex items-center justify-center transition-colors duration-200 shrink-0 ${data.remember ? 'bg-[#3D4E4B] border-[#3D4E4B]' : 'bg-white border-gray-300'}`}>
|
||||
{data.remember && (
|
||||
<svg className="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-500 group-hover:text-gray-700 transition-colors duration-200">
|
||||
Keep me signed in
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="mt-6 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 animate-spin text-white/60" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Signing in…
|
||||
</>
|
||||
) : (
|
||||
'Sign in'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Register link */}
|
||||
<p className="mt-7 text-center text-sm text-gray-400 font-medium anim-fade" style={{ animationDelay: '0.22s' }}>
|
||||
Don't have an account?{' '}
|
||||
<Link href={route('register')} className="text-[#3D4E4B] font-semibold hover:text-[#D4A017] transition-colors duration-200">
|
||||
Create one
|
||||
</Link>
|
||||
</p>
|
||||
</GuestLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import React from 'react';
|
||||
import GuestLayout from '@/Layouts/GuestLayout';
|
||||
import { Head, Link, useForm, usePage } from '@inertiajs/react';
|
||||
|
||||
export default function Register() {
|
||||
const { system_settings } = usePage().props as any;
|
||||
const isRegistrationEnabled = system_settings?.allow_registration === '1' || system_settings?.allow_registration === true;
|
||||
const isGoogleEnabled = system_settings?.oauth_google_enabled === '1' || system_settings?.oauth_google_enabled === true;
|
||||
const isGithubEnabled = system_settings?.oauth_github_enabled === '1' || system_settings?.oauth_github_enabled === true;
|
||||
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post(route('register'), { onFinish: () => reset('password', 'password_confirmation') });
|
||||
};
|
||||
|
||||
if (!isRegistrationEnabled) {
|
||||
return (
|
||||
<GuestLayout>
|
||||
<Head title="Registration Closed" />
|
||||
<div className="anim-fade">
|
||||
<div className="w-12 h-12 bg-amber-50 border border-amber-100 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg className="w-5 h-5 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-[#1A2421] tracking-tight">Registration closed</h1>
|
||||
<p className="mt-2 text-sm text-gray-400 font-medium">New account registration is currently disabled by the administrator.</p>
|
||||
<div className="mt-8 pt-6 border-t border-gray-100">
|
||||
<Link href={route('login')} className="text-sm font-semibold text-[#3D4E4B] hover:text-[#D4A017] transition-colors duration-200">
|
||||
← Back to sign in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</GuestLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GuestLayout>
|
||||
<Head title="Create account" />
|
||||
|
||||
{/* Heading */}
|
||||
<div className="mb-8 anim-down">
|
||||
<h1 className="text-2xl font-bold text-[#1A2421] tracking-tight">Create account</h1>
|
||||
<p className="mt-1.5 text-sm text-gray-400 font-medium">Fill in your details to get started.</p>
|
||||
</div>
|
||||
|
||||
{/* OAuth */}
|
||||
{(isGoogleEnabled || isGithubEnabled) && (
|
||||
<div className="mb-6 anim-up" style={{ animationDelay: '0.08s' }}>
|
||||
<div className={`grid gap-3 ${isGoogleEnabled && isGithubEnabled ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
||||
{isGoogleEnabled && (
|
||||
<a href="/auth/google/redirect" className="flex items-center justify-center gap-2.5 px-4 py-2.5 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-gray-300 transition-colors duration-200 text-sm font-semibold text-gray-700">
|
||||
<svg className="w-4 h-4 shrink-0" viewBox="0 0 24 24">
|
||||
<path fill="#EA4335" d="M12 5c1.61 0 3.09.59 4.23 1.57l3.12-3.12C17.35 1.67 14.85 1 12 1 7.73 1 4.14 3.48 2.46 7.1l3.71 2.87C7.04 7.09 9.34 5 12 5z"/>
|
||||
<path fill="#4285F4" d="M23.49 12.27c0-.79-.07-1.54-.19-2.27H12v4.51h6.47c-.29 1.48-1.14 2.73-2.4 3.58l3.7 2.87c2.16-2 3.72-4.94 3.72-8.69z"/>
|
||||
<path fill="#FBBC05" d="M6.17 14.77l-3.71 2.87C4.14 21.27 7.73 23 12 23c2.97 0 5.48-1 7.37-2.69l-3.7-2.87c-1.03.69-2.35 1.11-3.67 1.11-2.66 0-4.96-2.09-5.83-4.78z"/>
|
||||
<path fill="#34A853" d="M12 19.45c1.32 0 2.64-.42 3.67-1.11l3.7 2.87C17.48 22 14.97 23 12 23 7.73 23 4.14 21.27 2.47 17.64l3.71-2.87c.86 2.69 3.16 4.68 5.82 4.68z"/>
|
||||
</svg>
|
||||
Google
|
||||
</a>
|
||||
)}
|
||||
{isGithubEnabled && (
|
||||
<a href="/auth/github/redirect" className="flex items-center justify-center gap-2.5 px-4 py-2.5 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-gray-300 transition-colors duration-200 text-sm font-semibold text-gray-700">
|
||||
<svg className="w-4 h-4 shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center"><div className="w-full border-t border-gray-100" /></div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="px-3 bg-white text-xs font-semibold text-gray-300 uppercase tracking-widest">or</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={submit} className="anim-up" style={{ animationDelay: '0.14s' }}>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label htmlFor="first_name" className="block text-sm font-semibold text-gray-600 mb-1.5">First name</label>
|
||||
<input id="first_name" type="text" autoComplete="given-name" autoFocus value={data.first_name}
|
||||
onChange={e => setData('first_name', e.target.value)}
|
||||
placeholder="Alex"
|
||||
className={`auth-input${errors.first_name ? ' !border-red-300 !bg-red-50/50' : ''}`} />
|
||||
{errors.first_name && <p className="mt-1.5 text-xs font-semibold text-red-500">{errors.first_name}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="last_name" className="block text-sm font-semibold text-gray-600 mb-1.5">Last name</label>
|
||||
<input id="last_name" type="text" autoComplete="family-name" value={data.last_name}
|
||||
onChange={e => setData('last_name', e.target.value)}
|
||||
placeholder="Johnson"
|
||||
className={`auth-input${errors.last_name ? ' !border-red-300 !bg-red-50/50' : ''}`} />
|
||||
{errors.last_name && <p className="mt-1.5 text-xs font-semibold text-red-500">{errors.last_name}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-semibold text-gray-600 mb-1.5">Email address</label>
|
||||
<input id="email" type="email" autoComplete="email" value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
placeholder="you@company.com"
|
||||
className={`auth-input${errors.email ? ' !border-red-300 !bg-red-50/50' : ''}`} />
|
||||
{errors.email && <p className="mt-1.5 text-xs font-semibold text-red-500">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-semibold text-gray-600 mb-1.5">Password</label>
|
||||
<input id="password" type="password" autoComplete="new-password" value={data.password}
|
||||
onChange={e => setData('password', e.target.value)}
|
||||
placeholder="Min. 8 characters"
|
||||
className={`auth-input${errors.password ? ' !border-red-300 !bg-red-50/50' : ''}`} />
|
||||
{errors.password && <p className="mt-1.5 text-xs font-semibold text-red-500">{errors.password}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password_confirmation" className="block text-sm font-semibold text-gray-600 mb-1.5">Confirm password</label>
|
||||
<input id="password_confirmation" type="password" autoComplete="new-password" value={data.password_confirmation}
|
||||
onChange={e => setData('password_confirmation', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className={`auth-input${errors.password_confirmation ? ' !border-red-300 !bg-red-50/50' : ''}`} />
|
||||
{errors.password_confirmation && <p className="mt-1.5 text-xs font-semibold text-red-500">{errors.password_confirmation}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={processing}
|
||||
className="mt-6 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed">
|
||||
{processing ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 animate-spin text-white/60" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Creating account…
|
||||
</>
|
||||
) : 'Create account'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-7 text-center text-sm text-gray-400 font-medium anim-fade" style={{ animationDelay: '0.22s' }}>
|
||||
Already have an account?{' '}
|
||||
<Link href={route('login')} className="text-[#3D4E4B] font-semibold hover:text-[#D4A017] transition-colors duration-200">Sign in</Link>
|
||||
</p>
|
||||
</GuestLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import GuestLayout from '@/Layouts/GuestLayout';
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
|
||||
interface ResetPasswordProps {
|
||||
token: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export default function ResetPassword({ token, email }: ResetPasswordProps) {
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
token,
|
||||
email,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post(route('password.store'), { onFinish: () => reset('password', 'password_confirmation') });
|
||||
};
|
||||
|
||||
return (
|
||||
<GuestLayout>
|
||||
<Head title="Reset password" />
|
||||
|
||||
<div className="mb-8 anim-down">
|
||||
<h1 className="text-2xl font-bold text-[#1A2421] tracking-tight">Set new password</h1>
|
||||
<p className="mt-1.5 text-sm text-gray-400 font-medium">
|
||||
Choose a strong password for <span className="text-[#3D4E4B] font-semibold">{email}</span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} className="space-y-4 anim-up" style={{ animationDelay: '0.1s' }}>
|
||||
{/* Email readonly — needed for form submission, not shown */}
|
||||
<input type="hidden" value={data.email} />
|
||||
<input type="hidden" value={data.token} />
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-semibold text-gray-600 mb-1.5">
|
||||
New password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
autoFocus
|
||||
value={data.password}
|
||||
onChange={e => setData('password', e.target.value)}
|
||||
placeholder="Min. 8 characters"
|
||||
className={`auth-input${errors.password ? ' !border-red-300 !bg-red-50/50' : ''}`}
|
||||
/>
|
||||
{errors.password && <p className="mt-1.5 text-xs font-semibold text-red-500">{errors.password}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password_confirmation" className="block text-sm font-semibold text-gray-600 mb-1.5">
|
||||
Confirm new password
|
||||
</label>
|
||||
<input
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={data.password_confirmation}
|
||||
onChange={e => setData('password_confirmation', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className={`auth-input${errors.password_confirmation ? ' !border-red-300 !bg-red-50/50' : ''}`}
|
||||
/>
|
||||
{errors.password_confirmation && <p className="mt-1.5 text-xs font-semibold text-red-500">{errors.password_confirmation}</p>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="mt-2 w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 animate-spin text-white/60" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Saving…
|
||||
</>
|
||||
) : 'Reset password'}
|
||||
</button>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import GuestLayout from '@/Layouts/GuestLayout';
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
|
||||
export default function VerifyEmail({ status }: { status?: string }) {
|
||||
const { post, processing } = useForm({});
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post(route('verification.send'));
|
||||
};
|
||||
|
||||
return (
|
||||
<GuestLayout>
|
||||
<Head title="Verify email" />
|
||||
|
||||
<div className="mb-8 anim-down">
|
||||
<div className="w-12 h-12 bg-[#3D4E4B]/5 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg className="w-5 h-5 text-[#3D4E4B]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-[#1A2421] tracking-tight">Check your email</h1>
|
||||
<p className="mt-1.5 text-sm text-gray-400 font-medium leading-relaxed">
|
||||
We sent a verification link to your email address. Click the link to activate your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status === 'verification-link-sent' && (
|
||||
<div className="mb-6 px-4 py-3 rounded-xl bg-emerald-50 border border-emerald-100 text-sm font-semibold text-emerald-700 anim-fade">
|
||||
A new verification link has been sent to your email.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={submit} className="anim-up" style={{ animationDelay: '0.1s' }}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full h-11 rounded-xl bg-[#3D4E4B] hover:bg-[#2D3A38] text-white text-sm font-bold tracking-tight transition-colors duration-200 flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 animate-spin text-white/60" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Sending…
|
||||
</>
|
||||
) : 'Resend verification email'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-5 text-center anim-fade" style={{ animationDelay: '0.18s' }}>
|
||||
<Link
|
||||
href={route('logout')}
|
||||
method="post"
|
||||
as="button"
|
||||
className="text-sm font-semibold text-gray-400 hover:text-[#3D4E4B] transition-colors duration-200"
|
||||
>
|
||||
Sign out
|
||||
</Link>
|
||||
</div>
|
||||
</GuestLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user