Initial project setup with Next.js, TypeScript, and Tailwind CSS
This commit is contained in:
parent
050a0c708b
commit
9572dbc901
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "test-fixes",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^14.2.0",
|
||||||
|
"react": "^18.3.0",
|
||||||
|
"react-dom": "^18.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/react": "^18.3.0",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"typescript": "^5.4.0",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"postcss": "^8.4.0",
|
||||||
|
"autoprefixer": "^10.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import SettingsPage from '../../components/SettingsPage';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Settings',
|
||||||
|
description: 'Manage your account settings and preferences.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsRoute() {
|
||||||
|
return <SettingsPage />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,554 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import ToggleSwitch from './ToggleSwitch';
|
||||||
|
import type {
|
||||||
|
ProfileSettings,
|
||||||
|
SecuritySettings,
|
||||||
|
NotificationSettings,
|
||||||
|
SettingsTab,
|
||||||
|
} from '../types/settings';
|
||||||
|
|
||||||
|
/* ────────────────────────── Tab Button ────────────────────────── */
|
||||||
|
|
||||||
|
interface TabButtonProps {
|
||||||
|
tab: SettingsTab;
|
||||||
|
activeTab: SettingsTab;
|
||||||
|
label: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
onClick: (tab: SettingsTab) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabButton: React.FC<TabButtonProps> = ({
|
||||||
|
tab,
|
||||||
|
activeTab,
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const isActive = tab === activeTab;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onClick(tab)}
|
||||||
|
className={`
|
||||||
|
flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg
|
||||||
|
transition-all duration-200 whitespace-nowrap
|
||||||
|
${
|
||||||
|
isActive
|
||||||
|
? 'bg-blue-600 text-white shadow-sm'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ────────────────────────── Form Input ────────────────────────── */
|
||||||
|
|
||||||
|
interface FormInputProps {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
type?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormInput: React.FC<FormInputProps> = ({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
type = 'text',
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
disabled = false,
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
className="
|
||||||
|
w-full px-3.5 py-2.5 rounded-lg border border-gray-300 bg-white
|
||||||
|
text-sm text-gray-900 placeholder-gray-400
|
||||||
|
transition-colors duration-200
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||||
|
disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ────────────────────────── Textarea ────────────────────────── */
|
||||||
|
|
||||||
|
interface FormTextareaProps {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
rows?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormTextarea: React.FC<FormTextareaProps> = ({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
rows = 3,
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
rows={rows}
|
||||||
|
className="
|
||||||
|
w-full px-3.5 py-2.5 rounded-lg border border-gray-300 bg-white
|
||||||
|
text-sm text-gray-900 placeholder-gray-400 resize-none
|
||||||
|
transition-colors duration-200
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ────────────────────────── Section Card ────────────────────────── */
|
||||||
|
|
||||||
|
const SectionCard: React.FC<{
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ title, description, children }) => (
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm">
|
||||||
|
<div className="px-6 py-5 border-b border-gray-100">
|
||||||
|
<h3 className="text-base font-semibold text-gray-900">{title}</h3>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-1 text-sm text-gray-500">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="px-6 py-5">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ────────────────────────── SVG Icons ────────────────────────── */
|
||||||
|
|
||||||
|
const UserIcon = () => (
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ShieldIcon = () => (
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BellIcon = () => (
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ────────────────────────── Profile Tab ────────────────────────── */
|
||||||
|
|
||||||
|
const ProfileTab: React.FC<{
|
||||||
|
profile: ProfileSettings;
|
||||||
|
onChange: (updates: Partial<ProfileSettings>) => void;
|
||||||
|
}> = ({ profile, onChange }) => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<SectionCard
|
||||||
|
title="Personal Information"
|
||||||
|
description="Update your personal details and public profile."
|
||||||
|
>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||||
|
<FormInput
|
||||||
|
id="firstName"
|
||||||
|
label="First Name"
|
||||||
|
value={profile.firstName}
|
||||||
|
onChange={(v) => onChange({ firstName: v })}
|
||||||
|
placeholder="John"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
id="lastName"
|
||||||
|
label="Last Name"
|
||||||
|
value={profile.lastName}
|
||||||
|
onChange={(v) => onChange({ lastName: v })}
|
||||||
|
placeholder="Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormInput
|
||||||
|
id="email"
|
||||||
|
label="Email Address"
|
||||||
|
type="email"
|
||||||
|
value={profile.email}
|
||||||
|
onChange={(v) => onChange({ email: v })}
|
||||||
|
placeholder="john@example.com"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
id="username"
|
||||||
|
label="Username"
|
||||||
|
value={profile.username}
|
||||||
|
onChange={(v) => onChange({ username: v })}
|
||||||
|
placeholder="johndoe"
|
||||||
|
/>
|
||||||
|
<FormTextarea
|
||||||
|
id="bio"
|
||||||
|
label="Bio"
|
||||||
|
value={profile.bio}
|
||||||
|
onChange={(v) => onChange({ bio: v })}
|
||||||
|
placeholder="Tell us about yourself…"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard title="Avatar" description="Upload a profile picture.">
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="h-16 w-16 rounded-full bg-gray-200 flex items-center justify-center text-gray-400">
|
||||||
|
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0 0 22.5 18.75V5.25A2.25 2.25 0 0 0 20.25 3H3.75A2.25 2.25 0 0 0 1.5 5.25v13.5A2.25 2.25 0 0 0 3.75 21Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Change Avatar
|
||||||
|
</button>
|
||||||
|
<p className="mt-1.5 text-xs text-gray-500">JPG, PNG or GIF. Max 2MB.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ────────────────────────── Security Tab ────────────────────────── */
|
||||||
|
|
||||||
|
const SecurityTab: React.FC<{
|
||||||
|
security: SecuritySettings;
|
||||||
|
onChange: (updates: Partial<SecuritySettings>) => void;
|
||||||
|
}> = ({ security, onChange }) => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<SectionCard
|
||||||
|
title="Change Password"
|
||||||
|
description="Update your password to keep your account secure."
|
||||||
|
>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<FormInput
|
||||||
|
id="currentPassword"
|
||||||
|
label="Current Password"
|
||||||
|
type="password"
|
||||||
|
value={security.currentPassword}
|
||||||
|
onChange={(v) => onChange({ currentPassword: v })}
|
||||||
|
placeholder="Enter current password"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
id="newPassword"
|
||||||
|
label="New Password"
|
||||||
|
type="password"
|
||||||
|
value={security.newPassword}
|
||||||
|
onChange={(v) => onChange({ newPassword: v })}
|
||||||
|
placeholder="Enter new password"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
id="confirmPassword"
|
||||||
|
label="Confirm New Password"
|
||||||
|
type="password"
|
||||||
|
value={security.confirmPassword}
|
||||||
|
onChange={(v) => onChange({ confirmPassword: v })}
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
title="Security Preferences"
|
||||||
|
description="Configure additional security options for your account."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="twoFactor"
|
||||||
|
label="Two-Factor Authentication"
|
||||||
|
description="Add an extra layer of security with 2FA via authenticator app."
|
||||||
|
checked={security.twoFactorEnabled}
|
||||||
|
onChange={(v) => onChange({ twoFactorEnabled: v })}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="sessionTimeout"
|
||||||
|
label="Auto Session Timeout"
|
||||||
|
description="Automatically log out after 30 minutes of inactivity."
|
||||||
|
checked={security.sessionTimeout}
|
||||||
|
onChange={(v) => onChange({ sessionTimeout: v })}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="loginNotifications"
|
||||||
|
label="Login Notifications"
|
||||||
|
description="Receive an email when a new device signs into your account."
|
||||||
|
checked={security.loginNotifications}
|
||||||
|
onChange={(v) => onChange({ loginNotifications: v })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ────────────────────────── Notifications Tab ────────────────────────── */
|
||||||
|
|
||||||
|
const NotificationsTab: React.FC<{
|
||||||
|
notifications: NotificationSettings;
|
||||||
|
onChange: (updates: Partial<NotificationSettings>) => void;
|
||||||
|
}> = ({ notifications, onChange }) => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<SectionCard
|
||||||
|
title="Notification Channels"
|
||||||
|
description="Choose how you want to receive notifications."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="emailNotifications"
|
||||||
|
label="Email Notifications"
|
||||||
|
description="Receive notifications via email."
|
||||||
|
checked={notifications.emailNotifications}
|
||||||
|
onChange={(v) => onChange({ emailNotifications: v })}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="pushNotifications"
|
||||||
|
label="Push Notifications"
|
||||||
|
description="Receive push notifications in your browser."
|
||||||
|
checked={notifications.pushNotifications}
|
||||||
|
onChange={(v) => onChange({ pushNotifications: v })}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="smsNotifications"
|
||||||
|
label="SMS Notifications"
|
||||||
|
description="Receive important alerts via text message."
|
||||||
|
checked={notifications.smsNotifications}
|
||||||
|
onChange={(v) => onChange({ smsNotifications: v })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
title="Notification Types"
|
||||||
|
description="Control which types of notifications you receive."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="securityAlerts"
|
||||||
|
label="Security Alerts"
|
||||||
|
description="Critical security-related notifications."
|
||||||
|
checked={notifications.securityAlerts}
|
||||||
|
onChange={(v) => onChange({ securityAlerts: v })}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="mentionNotifications"
|
||||||
|
label="Mentions"
|
||||||
|
description="When someone mentions you in a comment or post."
|
||||||
|
checked={notifications.mentionNotifications}
|
||||||
|
onChange={(v) => onChange({ mentionNotifications: v })}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="commentNotifications"
|
||||||
|
label="Comments"
|
||||||
|
description="When someone comments on your posts or tasks."
|
||||||
|
checked={notifications.commentNotifications}
|
||||||
|
onChange={(v) => onChange({ commentNotifications: v })}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="weeklyDigest"
|
||||||
|
label="Weekly Digest"
|
||||||
|
description="Receive a weekly summary of activity."
|
||||||
|
checked={notifications.weeklyDigest}
|
||||||
|
onChange={(v) => onChange({ weeklyDigest: v })}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
id="marketingEmails"
|
||||||
|
label="Marketing Emails"
|
||||||
|
description="Product updates, tips, and promotional content."
|
||||||
|
checked={notifications.marketingEmails}
|
||||||
|
onChange={(v) => onChange({ marketingEmails: v })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ────────────────────────── Main Settings Page ────────────────────────── */
|
||||||
|
|
||||||
|
const SettingsPage: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<SettingsTab>('profile');
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
|
||||||
|
const [profile, setProfile] = useState<ProfileSettings>({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
username: '',
|
||||||
|
bio: '',
|
||||||
|
avatarUrl: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [security, setSecurity] = useState<SecuritySettings>({
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
twoFactorEnabled: false,
|
||||||
|
sessionTimeout: true,
|
||||||
|
loginNotifications: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [notifications, setNotifications] = useState<NotificationSettings>({
|
||||||
|
emailNotifications: true,
|
||||||
|
pushNotifications: true,
|
||||||
|
smsNotifications: false,
|
||||||
|
marketingEmails: false,
|
||||||
|
securityAlerts: true,
|
||||||
|
weeklyDigest: true,
|
||||||
|
mentionNotifications: true,
|
||||||
|
commentNotifications: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateProfile = useCallback(
|
||||||
|
(updates: Partial<ProfileSettings>) =>
|
||||||
|
setProfile((prev) => ({ ...prev, ...updates })),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateSecurity = useCallback(
|
||||||
|
(updates: Partial<SecuritySettings>) =>
|
||||||
|
setSecurity((prev) => ({ ...prev, ...updates })),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateNotifications = useCallback(
|
||||||
|
(updates: Partial<NotificationSettings>) =>
|
||||||
|
setNotifications((prev) => ({ ...prev, ...updates })),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setSaved(false);
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
setSaving(false);
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 sm:text-3xl">
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
|
Manage your account settings and preferences.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="mb-8 flex gap-2 overflow-x-auto pb-1">
|
||||||
|
<TabButton
|
||||||
|
tab="profile"
|
||||||
|
activeTab={activeTab}
|
||||||
|
label="Profile"
|
||||||
|
icon={<UserIcon />}
|
||||||
|
onClick={setActiveTab}
|
||||||
|
/>
|
||||||
|
<TabButton
|
||||||
|
tab="security"
|
||||||
|
activeTab={activeTab}
|
||||||
|
label="Security"
|
||||||
|
icon={<ShieldIcon />}
|
||||||
|
onClick={setActiveTab}
|
||||||
|
/>
|
||||||
|
<TabButton
|
||||||
|
tab="notifications"
|
||||||
|
activeTab={activeTab}
|
||||||
|
label="Notifications"
|
||||||
|
icon={<BellIcon />}
|
||||||
|
onClick={setActiveTab}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="mb-8">
|
||||||
|
{activeTab === 'profile' && (
|
||||||
|
<ProfileTab profile={profile} onChange={updateProfile} />
|
||||||
|
)}
|
||||||
|
{activeTab === 'security' && (
|
||||||
|
<SecurityTab security={security} onChange={updateSecurity} />
|
||||||
|
)}
|
||||||
|
{activeTab === 'notifications' && (
|
||||||
|
<NotificationsTab
|
||||||
|
notifications={notifications}
|
||||||
|
onChange={updateNotifications}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<div className="flex items-center justify-end gap-3 border-t border-gray-200 pt-6">
|
||||||
|
{saved && (
|
||||||
|
<span className="flex items-center gap-1.5 text-sm text-green-600">
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
Changes saved
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
className="
|
||||||
|
inline-flex items-center justify-center gap-2 px-6 py-2.5
|
||||||
|
text-sm font-semibold text-white bg-blue-600 rounded-lg
|
||||||
|
shadow-sm hover:bg-blue-700
|
||||||
|
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
|
||||||
|
disabled:opacity-60 disabled:cursor-not-allowed
|
||||||
|
transition-all duration-200
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin h-4 w-4" 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…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Save Changes'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsPage;
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ToggleSwitchProps {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-4 border-b border-gray-100 last:border-0">
|
||||||
|
<div className="flex-1 pr-4">
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="text-sm font-medium text-gray-900 cursor-pointer"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-1 text-sm text-gray-500">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id={id}
|
||||||
|
role="switch"
|
||||||
|
type="button"
|
||||||
|
aria-checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => onChange(!checked)}
|
||||||
|
className={`
|
||||||
|
relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full
|
||||||
|
border-2 border-transparent transition-colors duration-200 ease-in-out
|
||||||
|
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
|
||||||
|
${checked ? 'bg-blue-600' : 'bg-gray-200'}
|
||||||
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`
|
||||||
|
pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow-lg
|
||||||
|
ring-0 transition-transform duration-200 ease-in-out
|
||||||
|
${checked ? 'translate-x-5' : 'translate-x-0'}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToggleSwitch;
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
export interface ProfileSettings {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
bio: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecuritySettings {
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
twoFactorEnabled: boolean;
|
||||||
|
sessionTimeout: boolean;
|
||||||
|
loginNotifications: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationSettings {
|
||||||
|
emailNotifications: boolean;
|
||||||
|
pushNotifications: boolean;
|
||||||
|
smsNotifications: boolean;
|
||||||
|
marketingEmails: boolean;
|
||||||
|
securityAlerts: boolean;
|
||||||
|
weeklyDigest: boolean;
|
||||||
|
mentionNotifications: boolean;
|
||||||
|
commentNotifications: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SettingsTab = 'profile' | 'security' | 'notifications';
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue