Initial project setup with Next.js, TypeScript, and Tailwind CSS

This commit is contained in:
tanjdev7614 2026-04-15 19:32:19 +06:00
parent 050a0c708b
commit 9572dbc901
8 changed files with 726 additions and 0 deletions

25
package.json Normal file
View File

@ -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"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

10
src/app/settings/page.tsx Normal file
View File

@ -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 />;
}

View File

@ -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;

View File

@ -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;

30
src/types/settings.ts Normal file
View File

@ -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';

15
tailwind.config.ts Normal file
View File

@ -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;

23
tsconfig.json Normal file
View File

@ -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"]
}