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