diff --git a/frontend/cmmty/pages/documents/[id]/settings/ChangePassword.tsx b/frontend/cmmty/pages/documents/[id]/settings/ChangePassword.tsx new file mode 100644 index 0000000..47c57db --- /dev/null +++ b/frontend/cmmty/pages/documents/[id]/settings/ChangePassword.tsx @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; + +const ChangePassword = () => { + const [oldPass, setOldPass] = useState(''); + const [newPass, setNewPass] = useState(''); + + const handleChange = async () => { + try { + await fetch('/api/user/change-password', { + method: 'POST', + body: JSON.stringify({ oldPass, newPass }), + }); + alert('Password changed successfully!'); + } catch { + alert('Error changing password.'); + } + }; + + return ( +
+

Change Password

+ setOldPass(e.target.value)} + /> + setNewPass(e.target.value)} + /> + +
+ ); +}; + +export default ChangePassword; diff --git a/frontend/cmmty/pages/documents/[id]/settings/NotificationToggles.tsx b/frontend/cmmty/pages/documents/[id]/settings/NotificationToggles.tsx new file mode 100644 index 0000000..f0f6206 --- /dev/null +++ b/frontend/cmmty/pages/documents/[id]/settings/NotificationToggles.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; + +const NotificationToggles = () => { + const [prefs, setPrefs] = useState({ + riskAlerts: true, + verificationEmails: true, + weeklyDigest: false, + }); + + const handleToggle = (key: keyof typeof prefs) => { + setPrefs({ ...prefs, [key]: !prefs[key] }); + }; + + const handleSave = async () => { + try { + await fetch('/api/user/notifications', { + method: 'POST', + body: JSON.stringify(prefs), + }); + alert('Notification preferences saved!'); + } catch { + alert('Error saving preferences.'); + } + }; + + return ( +
+

Notifications

+ {Object.keys(prefs).map((key) => ( + + ))} + +
+ ); +}; + +export default NotificationToggles; diff --git a/frontend/cmmty/pages/documents/[id]/settings/SettingsForm.tsx b/frontend/cmmty/pages/documents/[id]/settings/SettingsForm.tsx new file mode 100644 index 0000000..033d004 --- /dev/null +++ b/frontend/cmmty/pages/documents/[id]/settings/SettingsForm.tsx @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; + +const SettingsForm = () => { + const [fullName, setFullName] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSave = async () => { + setLoading(true); + try { + // API call to save profile + await fetch('/api/user/update', { + method: 'POST', + body: JSON.stringify({ fullName }), + }); + alert('Profile updated successfully!'); + } catch (err) { + alert('Error updating profile.'); + } finally { + setLoading(false); + } + }; + + return ( +
+

Personal Information

+ setFullName(e.target.value)} + /> + +
+ ); +}; + +export default SettingsForm; diff --git a/frontend/cmmty/pages/documents/[id]/settings/index.tsx b/frontend/cmmty/pages/documents/[id]/settings/index.tsx new file mode 100644 index 0000000..ca46b7e --- /dev/null +++ b/frontend/cmmty/pages/documents/[id]/settings/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import SettingsForm from './SettingsForm'; +import NotificationToggles from './NotificationToggles'; +import ChangePassword from './ChangePassword'; + +const SettingsPage = () => { + return ( +
+

User Profile Settings

+ + + +
+ ); +}; + +export default SettingsPage; diff --git a/frontend/cmmty/pages/forgot-password/ForgotPasswordForm.tsx b/frontend/cmmty/pages/forgot-password/ForgotPasswordForm.tsx new file mode 100644 index 0000000..181729e --- /dev/null +++ b/frontend/cmmty/pages/forgot-password/ForgotPasswordForm.tsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; + +const ForgotPasswordForm = () => { + const [email, setEmail] = useState(''); + const [submitted, setSubmitted] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!email || !/\S+@\S+\.\S+/.test(email)) { + alert('Please enter a valid email.'); + return; + } + try { + await fetch('/api/auth/forgot-password', { + method: 'POST', + body: JSON.stringify({ email }), + }); + } catch (err) { + // swallow errors to avoid enumeration + } finally { + setSubmitted(true); + } + }; + + return ( +
+ + setEmail(e.target.value)} + required + /> + + {submitted &&

If an account exists, a reset link has been sent.

} +
+ ); +}; + +export default ForgotPasswordForm; diff --git a/frontend/cmmty/pages/forgot-password/index.tsx b/frontend/cmmty/pages/forgot-password/index.tsx new file mode 100644 index 0000000..a0f9965 --- /dev/null +++ b/frontend/cmmty/pages/forgot-password/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ForgotPasswordForm from './ForgotPasswordForm'; + +const ForgotPasswordPage = () => { + return ( +
+

Forgot Password

+ + Back to Login +
+ ); +}; + +export default ForgotPasswordPage; diff --git a/frontend/cmmty/pages/reset-password/ResetPasswordForm.tsx b/frontend/cmmty/pages/reset-password/ResetPasswordForm.tsx new file mode 100644 index 0000000..4eef556 --- /dev/null +++ b/frontend/cmmty/pages/reset-password/ResetPasswordForm.tsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import { useRouter } from 'next/router'; + +interface Props { + token: string; +} + +const ResetPasswordForm: React.FC = ({ token }) => { + const [newPass, setNewPass] = useState(''); + const [confirmPass, setConfirmPass] = useState(''); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (newPass.length < 8) { + setError('Password must be at least 8 characters.'); + return; + } + if (newPass !== confirmPass) { + setError('Passwords do not match.'); + return; + } + setError(''); + try { + const res = await fetch('/api/auth/reset-password', { + method: 'POST', + body: JSON.stringify({ token, newPass }), + }); + if (res.ok) { + setSuccess(true); + setTimeout(() => router.push('/login'), 3000); + } else { + setError('Invalid or expired token.'); + } + } catch { + setError('Something went wrong. Please try again.'); + } + }; + + return ( +
+ + setNewPass(e.target.value)} + required + /> + + setConfirmPass(e.target.value)} + required + /> + + {error &&

{error}

} + {success &&

Password reset successful! Redirecting to login...

} +
+ ); +}; + +export default ResetPasswordForm; diff --git a/frontend/cmmty/pages/reset-password/index.tsx b/frontend/cmmty/pages/reset-password/index.tsx new file mode 100644 index 0000000..949b019 --- /dev/null +++ b/frontend/cmmty/pages/reset-password/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useRouter } from 'next/router'; +import ResetPasswordForm from './ResetPasswordForm'; + +const ResetPasswordPage = () => { + const router = useRouter(); + const { token } = router.query; + + return ( +
+

Reset Password

+ {token ? ( + + ) : ( +

Invalid or missing reset token.

+ )} +
+ ); +}; + +export default ResetPasswordPage; diff --git a/frontend/cmmty/pages/verify-email/Confirmed.tsx b/frontend/cmmty/pages/verify-email/Confirmed.tsx new file mode 100644 index 0000000..51a44ea --- /dev/null +++ b/frontend/cmmty/pages/verify-email/Confirmed.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; + +interface Props { + token: string; +} + +const Confirmed: React.FC = ({ token }) => { + const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); + const router = useRouter(); + + useEffect(() => { + const verifyEmail = async () => { + try { + const res = await fetch('/api/auth/verify-email', { + method: 'POST', + body: JSON.stringify({ token }), + }); + if (res.ok) { + setStatus('success'); + setTimeout(() => router.push('/dashboard'), 3000); + } else { + setStatus('error'); + } + } catch { + setStatus('error'); + } + }; + verifyEmail(); + }, [token, router]); + + return ( +
+ {status === 'loading' &&

Verifying your email...

} + {status === 'success' &&

Email verified! Redirecting to dashboard...

} + {status === 'error' &&

Verification failed. Please try again.

} +
+ ); +}; + +export default Confirmed; diff --git a/frontend/cmmty/pages/verify-email/Pending.tsx b/frontend/cmmty/pages/verify-email/Pending.tsx new file mode 100644 index 0000000..23c432e --- /dev/null +++ b/frontend/cmmty/pages/verify-email/Pending.tsx @@ -0,0 +1,29 @@ +import React, { useState } from 'react'; + +const Pending = () => { + const [loading, setLoading] = useState(false); + + const handleResend = async () => { + setLoading(true); + try { + await fetch('/api/auth/resend-verification', { method: 'POST' }); + alert('Verification email resent!'); + } catch { + alert('Error resending email.'); + } finally { + setLoading(false); + } + }; + + return ( +
+

Verify Your Email

+

Please check your inbox for a verification link.

+ +
+ ); +}; + +export default Pending; diff --git a/frontend/cmmty/pages/verify-email/index.tsx b/frontend/cmmty/pages/verify-email/index.tsx new file mode 100644 index 0000000..aa999d6 --- /dev/null +++ b/frontend/cmmty/pages/verify-email/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { useRouter } from 'next/router'; +import Pending from './Pending'; +import Confirmed from './Confirmed'; + +const VerifyEmailPage = () => { + const router = useRouter(); + const { token } = router.query; + + if (token) { + return ; + } + return ; +}; + +export default VerifyEmailPage; diff --git a/frontend/cmmty/styles/forgot-password.css b/frontend/cmmty/styles/forgot-password.css new file mode 100644 index 0000000..d0cf60d --- /dev/null +++ b/frontend/cmmty/styles/forgot-password.css @@ -0,0 +1,35 @@ +.forgot-container { + max-width: 400px; + margin: auto; + padding: 1rem; + text-align: center; +} + +.forgot-form { + display: flex; + flex-direction: column; +} + +.forgot-form input, +.forgot-form button { + margin: 0.5rem 0; + padding: 0.75rem; + width: 100%; +} + +.success-msg { + color: green; + margin-top: 1rem; +} + +.back-link { + display: block; + margin-top: 1rem; + color: #0070f3; +} + +@media (max-width: 600px) { + .forgot-container { + padding: 0.5rem; + } +} diff --git a/frontend/cmmty/styles/reset-password.css b/frontend/cmmty/styles/reset-password.css new file mode 100644 index 0000000..e913435 --- /dev/null +++ b/frontend/cmmty/styles/reset-password.css @@ -0,0 +1,34 @@ +.reset-container { + max-width: 400px; + margin: auto; + padding: 1rem; + text-align: center; +} + +.reset-form { + display: flex; + flex-direction: column; +} + +.reset-form input, +.reset-form button { + margin: 0.5rem 0; + padding: 0.75rem; + width: 100%; +} + +.error-msg { + color: red; + margin-top: 0.5rem; +} + +.success-msg { + color: green; + margin-top: 0.5rem; +} + +@media (max-width: 600px) { + .reset-container { + padding: 0.5rem; + } +} diff --git a/frontend/cmmty/styles/settings.css b/frontend/cmmty/styles/settings.css new file mode 100644 index 0000000..3d2e5f0 --- /dev/null +++ b/frontend/cmmty/styles/settings.css @@ -0,0 +1,21 @@ +.settings-container { + max-width: 600px; + margin: auto; + padding: 1rem; +} + +.settings-section { + margin-bottom: 2rem; +} + +input, button { + display: block; + width: 100%; + margin: 0.5rem 0; +} + +@media (max-width: 600px) { + .settings-container { + padding: 0.5rem; + } +} diff --git a/frontend/cmmty/styles/verify-email.css b/frontend/cmmty/styles/verify-email.css new file mode 100644 index 0000000..b8add1f --- /dev/null +++ b/frontend/cmmty/styles/verify-email.css @@ -0,0 +1,18 @@ +.verify-container { + max-width: 500px; + margin: auto; + padding: 1rem; + text-align: center; +} + +button { + margin-top: 1rem; + padding: 0.75rem; + width: 100%; +} + +@media (max-width: 600px) { + .verify-container { + padding: 0.5rem; + } +}