Initial BizOne portal setup

This commit is contained in:
2026-05-11 11:36:33 +07:00
commit 57017dd397
249 changed files with 41305 additions and 0 deletions

View File

@ -0,0 +1,177 @@
'use client';
import Link from 'next/link';
import type { ReactNode } from 'react';
import { useMemo, useState } from 'react';
type Props = {
token: string;
languageSwitcher: ReactNode;
resetRequest: {
email: string;
name: string;
expiresAt: string | null;
};
};
function passwordChecks(password: string) {
return {
minLength: password.length >= 8,
hasNumber: /\d/.test(password),
hasSpecial: /[^A-Za-z0-9]/.test(password),
};
}
function formatExpiry(value: string | null) {
if (!value) return 'Unavailable';
return new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(value));
}
export function ResetPasswordCard({ token, resetRequest, languageSwitcher }: Props) {
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const [pending, setPending] = useState(false);
const checks = useMemo(() => passwordChecks(password), [password]);
const strengthCount = [checks.minLength, checks.hasNumber, checks.hasSpecial].filter(Boolean).length;
const strengthLabel = strengthCount <= 1 ? 'Weak' : strengthCount === 2 ? 'Medium' : 'Strong';
async function submit() {
setError('');
setMessage('');
if (password !== confirmPassword) {
setError('Password confirmation does not match.');
return;
}
setPending(true);
try {
const response = await fetch(`/api/auth/password-reset/${token}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
const payload = await response.json();
if (!response.ok) {
setError(payload.message || 'Failed to reset password');
return;
}
setMessage('Password updated. You can now log in with your new password.');
} finally {
setPending(false);
}
}
return (
<main className="auth-page auth-page-enterprise auth-page-login">
<div className="auth-enterprise-glow auth-enterprise-glow-right" />
<div className="auth-enterprise-glow auth-enterprise-glow-left" />
<div className="auth-page-symbol auth-page-symbol-top">
<span className="material-symbols-outlined">password</span>
</div>
<div className="auth-page-symbol auth-page-symbol-bottom">
<span className="material-symbols-outlined">verified_user</span>
</div>
<section className="auth-container auth-container-login">
<div className="auth-login-toolbar">
<div className="auth-login-locale">{languageSwitcher}</div>
</div>
<header className="auth-brand">
<div className="auth-brand-mark">
<span className="material-symbols-outlined">vpn_key</span>
</div>
<div className="auth-brand-copy">
<h1>BizOne</h1>
<p>Create a new password and secure the admin account before returning to the dashboard.</p>
</div>
</header>
<section className="auth-card auth-card-enterprise">
<div className="two-factor-copy auth-public-copy">
<h2>Reset Your Password</h2>
<p>
Hello, {resetRequest.name}. Choose a new password for <strong>{resetRequest.email}</strong>.
</p>
<p className="auth-public-meta">Reset link expires: {formatExpiry(resetRequest.expiresAt)}</p>
</div>
<div className="invite-form">
<label className="invite-field">
<span>New Password</span>
<div className="invite-password-wrap">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="Enter your password"
/>
<button type="button" onClick={() => setShowPassword((current) => !current)}>
<span className="material-symbols-outlined">{showPassword ? 'visibility_off' : 'visibility'}</span>
</button>
</div>
</label>
<label className="invite-field">
<span>Confirm New Password</span>
<input
type="password"
value={confirmPassword}
onChange={(event) => setConfirmPassword(event.target.value)}
placeholder="Repeat your password"
/>
</label>
<div className="invite-strength">
<div className="invite-strength-head">
<span>Password Strength</span>
<strong>{strengthLabel}</strong>
</div>
<div className="invite-strength-bars">
{[0, 1, 2, 3].map((index) => (
<span key={index} className={index < strengthCount + 1 ? 'is-active' : ''} />
))}
</div>
</div>
<div className="invite-checklist">
<div className={checks.minLength ? 'is-done' : ''}>
<span className="material-symbols-outlined">{checks.minLength ? 'check_circle' : 'circle'}</span>
Minimum 8 characters
</div>
<div className={checks.hasNumber ? 'is-done' : ''}>
<span className="material-symbols-outlined">{checks.hasNumber ? 'check_circle' : 'circle'}</span>
At least one number
</div>
<div className={checks.hasSpecial ? 'is-done' : ''}>
<span className="material-symbols-outlined">{checks.hasSpecial ? 'check_circle' : 'circle'}</span>
One special character (@, #, $, etc.)
</div>
</div>
<button type="button" className="invite-submit-button" onClick={submit} disabled={pending}>
{pending ? 'Updating...' : 'Update Password'}
</button>
{error ? <p className="form-error">{error}</p> : null}
{message ? <p className="form-success">{message}</p> : null}
</div>
<div className="invite-footer auth-inline-footer">
<span>Need help?</span>
<Link href="/login">Back to login</Link>
</div>
</section>
</section>
</main>
);
}