diff --git a/src/apps/officer-web/Screens/LandingPage/LandingPage.tsx b/src/apps/officer-web/Screens/LandingPage/LandingPage.tsx deleted file mode 100644 index 7b3b5065..00000000 --- a/src/apps/officer-web/Screens/LandingPage/LandingPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { AuthLayout } from './Layout'; -import { Hero } from './components/Hero'; - -export function LandingPage() { - return ( - - - - ); -} diff --git a/src/apps/officer-web/Screens/LandingPage/Layout.tsx b/src/apps/officer-web/Screens/LandingPage/Layout.tsx deleted file mode 100644 index 5024fd65..00000000 --- a/src/apps/officer-web/Screens/LandingPage/Layout.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { ReactNode } from 'react'; -import { useLocation, useNavigate } from 'react-router'; -import { useAuth } from 'hooks/useAuth'; -import { PixelGrid } from '../../../../workspaces/components/PixelGrid'; -import { DuckAvatar } from './components/DuckAvatar'; -import { SignupModal } from './components/SignupModal'; -import { LoginModal } from './components/LoginModal'; -import { ForgotPasswordModal } from './components/ForgotPasswordModal'; -import { VerifyModal } from './components/VerifyModal'; -import { ResetPasswordModal } from './components/ResetPasswordModal'; - -type AuthLayoutProps = { - children?: ReactNode; -}; - -export function AuthLayout({ children }: AuthLayoutProps) { - const location = useLocation(); - const navigate = useNavigate(); - const { registrationOpen } = useAuth(); - - const signupOpen = registrationOpen && location.pathname === '/auth/register'; - const loginOpen = location.pathname === '/auth/login'; - const forgotPasswordOpen = location.pathname === '/auth/forgot-password'; - const verifyOpen = location.pathname === '/auth/verify'; - const resetPasswordOpen = location.pathname === '/auth/reset-password'; - - const closeModal = (open: boolean) => !open && navigate('/'); - - return ( -
- - - -
- {/* Background layer */} -
- - {/* Content layer - above duck and pixel grid */} -
{children}
-
- - - - - - -
- ); -} diff --git a/src/apps/officer-web/Screens/LandingPage/components/ForgotPasswordModal.tsx b/src/apps/officer-web/Screens/LandingPage/components/ForgotPasswordModal.tsx deleted file mode 100644 index e8268c08..00000000 --- a/src/apps/officer-web/Screens/LandingPage/components/ForgotPasswordModal.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useState } from 'react'; -import { toast } from 'sonner'; -import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -import { Link } from 'react-router'; -import { PaperDialog } from '@/components/Dialogs'; -import { useForm } from 'hooks/useForm'; -import { useAuth } from 'hooks/useAuth'; - -type ForgotPasswordFormState = { - email?: string; -}; - -const validateForm = (state: Partial) => { - return !!state.email; -}; - -type ForgotPasswordModalProps = { - open: boolean; - onOpenChange: (open: boolean) => void; -}; - -export const ForgotPasswordModal = ({ open, onOpenChange }: ForgotPasswordModalProps) => { - const [isSubmitting, setIsSubmitting] = useState(false); - const [sent, setSent] = useState(false); - const { state, formRef, update, isValid } = useForm({}, validateForm); - const { forgotPassword } = useAuth(); - - const handleSubmit = async (ev: React.FormEvent) => { - ev.preventDefault(); - if (!isValid || isSubmitting) return; - - setIsSubmitting(true); - try { - await forgotPassword({ email: state.email! }); - setSent(true); - } catch (ex) { - const error = ex as { message?: string }; - toast.error(error.message || 'Failed to send recovery email. Please try again.'); - } finally { - setIsSubmitting(false); - } - }; - - const handleClose = (isOpen: boolean) => { - if (!isOpen) { - setSent(false); - if (formRef.current) { - update({ email: '' }); - } - } - onOpenChange(isOpen); - }; - - const isDisabled = !isValid || isSubmitting; - - return ( - { - if (!sent) { - const firstInput = (ev.currentTarget as HTMLElement | null)?.querySelector('input'); - firstInput?.focus(); - } - }} - > - {sent ? ( -
-

Email Sent

-

Please check your email for further instructions.

- -
- ) : ( - <> - - Forgot Password - - Enter your email and we'll send you a recovery link - - - -
- - - - - - -

- Remember your password?{' '} - - Sign In - -

-
- - )} -
- ); -}; diff --git a/src/apps/officer-web/Screens/LandingPage/components/Hero/Hero.tsx b/src/apps/officer-web/Screens/LandingPage/components/Hero/Hero.tsx deleted file mode 100644 index c1832814..00000000 --- a/src/apps/officer-web/Screens/LandingPage/components/Hero/Hero.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router'; -import { toast } from 'sonner'; -import { useAuth } from 'hooks/useAuth'; -import { Button } from '@/components/ui/button'; -import { OfficerSvgLogo } from '../Logos'; - -export function Hero() { - const navigate = useNavigate(); - const { registrationOpen, signup } = useAuth(); - const [email, setEmail] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); - const [done, setDone] = useState(false); - - const handleBootstrap = async () => { - const trimmed = email.trim(); - if (!trimmed || isSubmitting) return; - - setIsSubmitting(true); - try { - await signup({ email: trimmed }); - setDone(true); - } catch (ex) { - const error = ex as { message?: string }; - toast.error(error.message || 'Bootstrap failed. Please try again.'); - } finally { - setIsSubmitting(false); - } - }; - - return ( - <> -
-
- -
-
- -
-
- - {registrationOpen ? ( -
-
- {done ? ( -
-

Check your email

-

- A verification link has been sent to {email}. Click it to activate your account. -

-
- ) : ( - <> -

Welcome, Admin

-

Enter your email to create your administrator account.

-
- setEmail(ev.target.value)} - onKeyDown={(ev) => { - if (ev.key === 'Enter') { - ev.preventDefault(); - handleBootstrap(); - } - }} - placeholder="admin@example.com" - className="flex-1 px-4 py-2.5 rounded-lg border-2 border-duck-dark/20 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:border-duck-teal/50" - /> - -
- - )} -
-
- ) : ( -
- -
- )} - - ); -} diff --git a/src/apps/officer-web/Screens/LandingPage/components/Hero/index.tsx b/src/apps/officer-web/Screens/LandingPage/components/Hero/index.tsx deleted file mode 100644 index 729bca95..00000000 --- a/src/apps/officer-web/Screens/LandingPage/components/Hero/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './Hero'; diff --git a/src/apps/officer-web/Screens/LandingPage/components/LoginModal.tsx b/src/apps/officer-web/Screens/LandingPage/components/LoginModal.tsx deleted file mode 100644 index 1f2f036b..00000000 --- a/src/apps/officer-web/Screens/LandingPage/components/LoginModal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useState } from 'react'; -import { toast } from 'sonner'; -import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -import { Link } from 'react-router'; -import { PaperDialog } from '@/components/Dialogs'; -import { useForm } from 'hooks/useForm'; -import { useAuth } from 'hooks/useAuth'; - -type LoginFormState = { - email?: string; - password?: string; -}; - -const validateForm = (state: Partial) => { - const { email, password } = state; - if (!email || !password) return false; - return true; -}; - -type LoginModalProps = { - open: boolean; - onOpenChange: (open: boolean) => void; -}; - -export const LoginModal = ({ open, onOpenChange }: LoginModalProps) => { - const [isSubmitting, setIsSubmitting] = useState(false); - const { state, formRef, update, isValid } = useForm({}, validateForm); - const { signin } = useAuth(); - - const handleSubmit = async (ev: React.FormEvent) => { - ev.preventDefault(); - if (!isValid || isSubmitting) return; - - setIsSubmitting(true); - try { - await signin({ email: state.email!, password: state.password! }); - window.location.href = '/'; - } catch (ex) { - const error = ex as { message?: string }; - toast.error(error.message || 'Login failed. Please try again.'); - update({ ...state, password: '' }); - } finally { - setIsSubmitting(false); - } - }; - - const handleClose = (isOpen: boolean) => { - if (!isOpen) { - if (formRef.current) { - update({ email: '', password: '' }); - } - } - onOpenChange(isOpen); - }; - - const isDisabled = !isValid || isSubmitting; - - return ( - { - const firstInput = (ev.currentTarget as HTMLElement | null)?.querySelector('input'); - firstInput?.focus(); - }} - > - - Welcome Back - Sign in to your account - - -
- - - - - - - - -
-

- Don't have an account?{' '} - - Register - -

-

- - Forgot password? - -

-
-
-
- ); -}; diff --git a/src/apps/officer-web/Screens/LandingPage/components/ResetPasswordModal.tsx b/src/apps/officer-web/Screens/LandingPage/components/ResetPasswordModal.tsx deleted file mode 100644 index c8e1a533..00000000 --- a/src/apps/officer-web/Screens/LandingPage/components/ResetPasswordModal.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useState } from 'react'; -import { toast } from 'sonner'; -import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -import { Link } from 'react-router'; -import { PaperDialog } from '@/components/Dialogs'; -import { useForm } from 'hooks/useForm'; -import { useAuth } from 'hooks/useAuth'; - -type ResetPasswordFormState = { - password?: string; - confirmPassword?: string; -}; - -const validateForm = (state: Partial) => { - const { password, confirmPassword } = state; - if (!password || !confirmPassword) return false; - if (password !== confirmPassword) return false; - return true; -}; - -type ResetPasswordModalProps = { - open: boolean; - onOpenChange: (open: boolean) => void; -}; - -export const ResetPasswordModal = ({ open, onOpenChange }: ResetPasswordModalProps) => { - const [isSubmitting, setIsSubmitting] = useState(false); - const [reset, setReset] = useState(false); - const { state, formRef, update, isValid } = useForm({}, validateForm); - const { resetPassword } = useAuth(); - - const handleSubmit = async (ev: React.FormEvent) => { - ev.preventDefault(); - if (!isValid || isSubmitting) return; - - setIsSubmitting(true); - try { - const verificationCode = new URL(window.location.href).searchParams.get('verificationCode'); - if (!verificationCode) throw new Error('Invalid verification code'); - await resetPassword({ password: state.password!, verificationCode }); - setReset(true); - } catch (ex) { - const error = ex as { message?: string }; - toast.error(error.message || 'Password reset failed. Please try again.'); - update({ password: '', confirmPassword: '' }); - } finally { - setIsSubmitting(false); - } - }; - - const handleClose = (isOpen: boolean) => { - if (!isOpen) { - setReset(false); - if (formRef.current) { - update({ password: '', confirmPassword: '' }); - } - } - onOpenChange(isOpen); - }; - - const isDisabled = !isValid || isSubmitting; - - return ( - { - if (!reset) { - const firstInput = (ev.currentTarget as HTMLElement | null)?.querySelector('input'); - firstInput?.focus(); - } - }} - > - {reset ? ( -
-

Password Reset

-

Your password has been reset. You can now sign in.

- -
- ) : ( - <> - - Reset Password - Enter your new password - - -
- - - - - - - - -

- - Back to Sign In - -

-
- - )} -
- ); -}; diff --git a/src/apps/officer-web/Screens/LandingPage/components/SignupModal.tsx b/src/apps/officer-web/Screens/LandingPage/components/SignupModal.tsx deleted file mode 100644 index 4273d4af..00000000 --- a/src/apps/officer-web/Screens/LandingPage/components/SignupModal.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useState } from 'react'; -import { toast } from 'sonner'; -import { useLocation, Link } from 'react-router'; -import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -import { PaperDialog } from '@/components/Dialogs'; -import { useForm } from 'hooks/useForm'; -import { useAuth } from 'hooks/useAuth'; - -type SignupFormState = { - name?: string; - email?: string; - password?: string; - confirmPassword?: string; -}; - -const validateForm = (state: Partial) => { - const { name, email, password, confirmPassword } = state; - if (!name || !email || !password || !confirmPassword) return false; - if (password !== confirmPassword) return false; - return true; -}; - -type SignupModalProps = { - open: boolean; - onOpenChange: (open: boolean) => void; -}; - -export const SignupModal = ({ open, onOpenChange }: SignupModalProps) => { - const location = useLocation(); - const bootstrapEmail = (location.state as { email?: string } | null)?.email ?? ''; - const [isSubmitting, setIsSubmitting] = useState(false); - const [registered, setRegistered] = useState(false); - const { state, formRef, update, isValid } = useForm({ email: bootstrapEmail }, validateForm); - const { signup } = useAuth(); - - const handleSubmit = async (ev: React.FormEvent) => { - ev.preventDefault(); - if (!isValid || isSubmitting) return; - - setIsSubmitting(true); - try { - await signup({ name: state.name, email: state.email, password: state.password }); - setRegistered(true); - } catch (ex) { - const error = ex as { message?: string }; - toast.error(error.message || 'Signup failed. Please try again.'); - update({ ...state, password: '', confirmPassword: '' }); - } finally { - setIsSubmitting(false); - } - }; - - const handleClose = (isOpen: boolean) => { - if (!isOpen) { - setRegistered(false); - if (formRef.current) { - update({ name: '', email: '', password: '', confirmPassword: '' }); - } - } - onOpenChange(isOpen); - }; - - const isDisabled = !isValid || isSubmitting; - - return ( - { - if (!registered) { - const firstInput = (ev.currentTarget as HTMLElement).querySelector('input'); - firstInput?.focus(); - } - }} - > - {registered ? ( -
-

Registration Successful

-

Please check your email to verify your account.

- -
- ) : ( - <> - - Create Account - Create a new account to get started - - -
- - - - - - - - - - - - -

- Already have an account?{' '} - - Sign In - -

-
- - )} -
- ); -}; diff --git a/src/apps/officer-web/Screens/LandingPage/components/VerifyModal.tsx b/src/apps/officer-web/Screens/LandingPage/components/VerifyModal.tsx deleted file mode 100644 index 797146ca..00000000 --- a/src/apps/officer-web/Screens/LandingPage/components/VerifyModal.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router'; -import { toast } from 'sonner'; -import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -import { PaperDialog } from '@/components/Dialogs'; -import { useForm } from 'hooks/useForm'; -import { useAuth } from 'hooks/useAuth'; -import { useClient } from 'hooks/useClient'; - -type VerifyFormState = { - name?: string; - password?: string; - confirmPassword?: string; -}; - -const validateForm = (state: Partial) => { - const { name, password, confirmPassword } = state; - if (!name || !password || !confirmPassword) return false; - if (password !== confirmPassword) return false; - return true; -}; - -type VerifyModalProps = { - open: boolean; - onOpenChange: (open: boolean) => void; -}; - -type TokenStatus = 'loading' | 'valid' | 'invalid'; - -export const VerifyModal = ({ open, onOpenChange }: VerifyModalProps) => { - const navigate = useNavigate(); - const client = useClient('/api/auth'); - const { verify } = useAuth(); - const { state, formRef, update, isValid } = useForm({}, validateForm); - - const [verificationCode] = useState(() => new URL(window.location.href).searchParams.get('verificationCode') ?? ''); - const [tokenStatus, setTokenStatus] = useState('loading'); - const [email, setEmail] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); - const [verified, setVerified] = useState(false); - const [resending, setResending] = useState(false); - const [resent, setResent] = useState(false); - - // Validate token on mount and strip it from the URL - useEffect(() => { - if (!open || !verificationCode) { - if (!verificationCode) setTokenStatus('invalid'); - return; - } - - window.history.replaceState(null, '', '/auth/verify'); - - client - .post<{ email: string }>('/verify-token', { verificationCode }) - .then((data) => { - setEmail(data.email); - setTokenStatus('valid'); - }) - .catch(() => { - // Try to decode email from expired token for resend - try { - const payload = JSON.parse(atob(verificationCode.split('.')[1]!)); - if (payload.email) setEmail(payload.email); - } catch { - // ignore - } - setTokenStatus('invalid'); - }); - }, [open]); - - // Redirect after successful verification - useEffect(() => { - if (!verified) return; - const timer = setTimeout(() => navigate('/'), 3000); - return () => clearTimeout(timer); - }, [verified]); - - const handleSubmit = async (ev: React.FormEvent) => { - ev.preventDefault(); - if (!isValid || isSubmitting || !verificationCode) return; - - setIsSubmitting(true); - try { - await verify({ - verificationCode, - name: state.name, - password: state.password, - confirmPassword: state.confirmPassword, - }); - setVerified(true); - } catch (ex) { - const error = ex as { message?: string }; - toast.error(error.message || 'Verification failed. Please try again.'); - update({ ...state, password: '', confirmPassword: '' }); - } finally { - setIsSubmitting(false); - } - }; - - const handleResend = async () => { - if (!email || resending) return; - setResending(true); - try { - await client.post('/resend-verification', { email }); - setResent(true); - } catch (ex) { - const error = ex as { message?: string }; - toast.error(error.message || 'Failed to resend. Please try again.'); - } finally { - setResending(false); - } - }; - - // Loading state - if (tokenStatus === 'loading') { - return ( - -
- - Validating... - Please wait - -
-
- ); - } - - // Invalid / expired token - if (tokenStatus === 'invalid') { - return ( - -
-

Link Expired

-

- This verification link is no longer valid.{' '} - {email ? 'Click below to receive a new one.' : 'Please request a new one.'} -

- {resent ? ( -

- A new verification email has been sent to {email}. -

- ) : ( - email && ( - - ) - )} -
-
- ); - } - - // Valid token — show form or success - return ( - { - const firstInput = (ev.currentTarget as HTMLElement).querySelector('input:not([disabled])'); - (firstInput as HTMLElement)?.focus(); - }} - > - {verified ? ( -
-

Account Verified

-

- Congratulations! Your account has been set up successfully. Redirecting... -

-
- ) : ( - <> - - Set Up Your Account - Complete your account details - - -
- - - - - - - - - - - -
- - )} -
- ); -}; diff --git a/src/apps/officer-web/Screens/LandingPage/index.tsx b/src/apps/officer-web/Screens/LandingPage/index.tsx deleted file mode 100644 index 13809481..00000000 --- a/src/apps/officer-web/Screens/LandingPage/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { LandingPage } from './LandingPage'; -export { AuthLayout } from './Layout'; diff --git a/src/workspaces/types/assets.d.ts b/src/workspaces/types/assets.d.ts new file mode 100644 index 00000000..b288f318 --- /dev/null +++ b/src/workspaces/types/assets.d.ts @@ -0,0 +1,19 @@ +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.svg' { + const src: string; + export default src; +} diff --git a/src/workspaces/types/globals.d.ts b/src/workspaces/types/globals.d.ts index a643b763..60bb51dc 100644 --- a/src/workspaces/types/globals.d.ts +++ b/src/workspaces/types/globals.d.ts @@ -1,23 +1,3 @@ -declare module '*.jpg' { - const src: string; - export default src; -} - -declare module '*.jpeg' { - const src: string; - export default src; -} - -declare module '*.png' { - const src: string; - export default src; -} - -declare module '*.svg' { - const src: string; - export default src; -} - declare global { type SelectOption = { value: number | string; label?: string; href?: string; hidden?: boolean };