Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ html.a11y-high-contrast body {
}
}

html.dark {
--background: #111827;
--foreground: #f9fafb;
}

body {
background: var(--background);
color: var(--foreground);
Expand Down
13 changes: 9 additions & 4 deletions src/app/profile/__tests__/ProfileTabs.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';
import { ThemeProvider } from '@/lib/theme-provider';
import ProfileTabs from '../components/ProfileTabs';

function renderWithTheme(ui: React.ReactElement) {
return render(<ThemeProvider defaultTheme="light">{ui}</ThemeProvider>);
}

describe('ProfileTabs', () => {
it('renders the profile panel first to keep initial work minimal', () => {
render(<ProfileTabs />);
renderWithTheme(<ProfileTabs />);

expect(screen.getByRole('tab', { name: 'Profile' })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByRole('tabpanel', { name: 'Profile' })).toBeInTheDocument();
Expand All @@ -17,7 +22,7 @@ describe('ProfileTabs', () => {
it('loads settings only when the settings tab is selected', async () => {
const user = userEvent.setup();

render(<ProfileTabs />);
renderWithTheme(<ProfileTabs />);
await user.click(screen.getByRole('tab', { name: 'Settings' }));

await waitFor(() =>
Expand All @@ -33,7 +38,7 @@ describe('ProfileTabs', () => {
it('loads achievements only when the achievements tab is selected', async () => {
const user = userEvent.setup();

render(<ProfileTabs />);
renderWithTheme(<ProfileTabs />);
await user.click(screen.getByRole('tab', { name: 'Achievements' }));

await waitFor(() =>
Expand All @@ -50,7 +55,7 @@ describe('ProfileTabs', () => {
it('loads certificates only when the certificates tab is selected', async () => {
const user = userEvent.setup();

render(<ProfileTabs />);
renderWithTheme(<ProfileTabs />);
await user.click(screen.getByRole('tab', { name: 'Certification Program' }));

await waitFor(() =>
Expand Down
12 changes: 6 additions & 6 deletions src/app/profile/components/AchievementsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ interface AchievementCardProps {

const AchievementCard = memo(function AchievementCard({ achievement }: AchievementCardProps) {
return (
<article className="rounded-lg border border-gray-200 p-4 text-center">
<article className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-750 p-4 text-center transition-colors duration-200">
<div className="mb-2 text-4xl" aria-hidden="true">
{achievement.icon}
</div>
<h3 className="font-semibold text-gray-900">{achievement.title}</h3>
<p className="text-sm text-gray-500">{achievement.description}</p>
<p className="mt-1 text-xs text-gray-400">{achievement.earnedAt}</p>
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{achievement.title}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">{achievement.description}</p>
<p className="mt-1 text-xs text-gray-400 dark:text-gray-500">{achievement.earnedAt}</p>
</article>
);
});
Expand All @@ -27,9 +27,9 @@ function AchievementsPanel() {
id="achievements-panel"
role="tabpanel"
aria-labelledby="achievements-tab"
className="rounded-lg bg-white p-6 shadow"
className="rounded-lg bg-white dark:bg-gray-800 p-6 shadow transition-colors duration-200"
>
<h2 className="mb-6 text-xl font-semibold text-gray-900">Achievements</h2>
<h2 className="mb-6 text-xl font-semibold text-gray-900 dark:text-gray-100">Achievements</h2>

<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{achievements.map((achievement) => (
Expand Down
2 changes: 1 addition & 1 deletion src/app/profile/components/CertificatesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function CertificatesPanel() {
id="certificates-panel"
role="tabpanel"
aria-labelledby="certificates-tab"
className="rounded-lg bg-white p-6 shadow dark:bg-gray-900"
className="rounded-lg bg-white dark:bg-gray-800 p-6 shadow transition-colors duration-200"
>
<div className="mb-6">
<p className="text-sm font-semibold uppercase tracking-wide text-blue-600 dark:text-blue-400">
Expand Down
68 changes: 44 additions & 24 deletions src/app/profile/components/CustomerSupportPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,29 @@ function FaqItem({ id, question, answer }: FaqItemProps) {
const toggle = useCallback(() => setIsOpen((prev) => !prev), []);

return (
<div className="border border-gray-200 rounded-lg overflow-hidden">
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<h3>
<button
type="button"
id={headingId}
aria-expanded={isOpen}
aria-controls={panelId}
onClick={toggle}
className="flex w-full items-center justify-between px-4 py-4 text-left font-medium text-gray-900 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 transition-colors"
className="flex w-full items-center justify-between px-4 py-4 text-left font-medium text-gray-900 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 transition-colors"
>
<span>{question}</span>
{isOpen ? (
<ChevronUp size={18} className="shrink-0 text-gray-500" aria-hidden="true" />
<ChevronUp
size={18}
className="shrink-0 text-gray-500 dark:text-gray-400"
aria-hidden="true"
/>
) : (
<ChevronDown size={18} className="shrink-0 text-gray-500" aria-hidden="true" />
<ChevronDown
size={18}
className="shrink-0 text-gray-500 dark:text-gray-400"
aria-hidden="true"
/>
)}
</button>
</h3>
Expand All @@ -44,7 +52,7 @@ function FaqItem({ id, question, answer }: FaqItemProps) {
role="region"
aria-labelledby={headingId}
hidden={!isOpen}
className="px-4 pb-4 text-sm text-gray-600 leading-relaxed"
className="px-4 pb-4 text-sm text-gray-600 dark:text-gray-300 leading-relaxed"
>
{answer}
</div>
Expand Down Expand Up @@ -78,15 +86,15 @@ function ContactForm() {
<div
role="status"
aria-live="polite"
className="rounded-lg bg-green-50 border border-green-200 p-6 text-center"
className="rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-6 text-center"
>
<p className="text-green-800 font-medium">
<p className="text-green-800 dark:text-green-300 font-medium">
✅ Your message has been sent. We&apos;ll get back to you within 24 hours.
</p>
<button
type="button"
onClick={() => setSubmitState('idle')}
className="mt-4 text-sm text-green-700 underline hover:text-green-900 focus:outline-none focus:ring-2 focus:ring-green-500 rounded"
className="mt-4 text-sm text-green-700 dark:text-green-400 underline hover:text-green-900 dark:hover:text-green-200 focus:outline-none focus:ring-2 focus:ring-green-500 rounded"
>
Send another message
</button>
Expand All @@ -97,7 +105,10 @@ function ContactForm() {
return (
<form onSubmit={handleSubmit} noValidate className="space-y-4">
<div>
<label htmlFor="support-subject" className="mb-1 block text-sm font-medium text-gray-700">
<label
htmlFor="support-subject"
className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Subject
</label>
<input
Expand All @@ -107,12 +118,15 @@ function ContactForm() {
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Briefly describe your issue"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500"
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500 dark:placeholder-gray-400"
/>
</div>

<div>
<label htmlFor="support-message" className="mb-1 block text-sm font-medium text-gray-700">
<label
htmlFor="support-message"
className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Message
</label>
<textarea
Expand All @@ -122,20 +136,20 @@ function ContactForm() {
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Describe your issue in detail…"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500 resize-none"
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500 resize-none dark:placeholder-gray-400"
/>
</div>

{submitState === 'error' && (
<p role="alert" className="text-sm text-red-600">
<p role="alert" className="text-sm text-red-600 dark:text-red-400">
Something went wrong. Please try again.
</p>
)}

<button
type="submit"
disabled={submitState === 'submitting' || !subject.trim() || !message.trim()}
className="rounded-lg bg-blue-500 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
className="rounded-lg bg-blue-500 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:cursor-not-allowed disabled:opacity-50"
>
{submitState === 'submitting' ? 'Sending…' : 'Send Message'}
</button>
Expand All @@ -157,8 +171,8 @@ function CustomerSupportPanel() {
return (
<section id="support-panel" role="tabpanel" aria-labelledby="support-tab" className="space-y-8">
{/* Contact Options */}
<div className="rounded-lg bg-white p-6 shadow">
<h2 className="mb-6 text-xl font-semibold text-gray-900">Contact Us</h2>
<div className="rounded-lg bg-white dark:bg-gray-800 p-6 shadow transition-colors duration-200">
<h2 className="mb-6 text-xl font-semibold text-gray-900 dark:text-gray-100">Contact Us</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{supportContactOptions.map((option) => {
const Icon = contactIcons[option.icon as keyof typeof contactIcons] ?? Mail;
Expand All @@ -168,21 +182,25 @@ function CustomerSupportPanel() {
href={option.href}
target={option.href.startsWith('http') ? '_blank' : undefined}
rel={option.href.startsWith('http') ? 'noopener noreferrer' : undefined}
className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 text-center transition-colors hover:border-blue-300 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 dark:border-gray-700 p-4 text-center transition-colors hover:border-blue-300 hover:bg-blue-50 dark:hover:border-blue-600 dark:hover:bg-blue-900/20 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label={option.ariaLabel}
>
<Icon size={24} className="text-blue-500" aria-hidden="true" />
<span className="font-medium text-gray-900">{option.label}</span>
<span className="text-xs text-gray-500">{option.description}</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{option.label}</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
{option.description}
</span>
</a>
);
})}
</div>
</div>

{/* FAQ */}
<div className="rounded-lg bg-white p-6 shadow">
<h2 className="mb-6 text-xl font-semibold text-gray-900">Frequently Asked Questions</h2>
<div className="rounded-lg bg-white dark:bg-gray-800 p-6 shadow transition-colors duration-200">
<h2 className="mb-6 text-xl font-semibold text-gray-900 dark:text-gray-100">
Frequently Asked Questions
</h2>
<div className="space-y-3">
{supportFaqs.map((faq) => (
<FaqItem key={faq.id} id={faq.id} question={faq.question} answer={faq.answer} />
Expand All @@ -191,9 +209,11 @@ function CustomerSupportPanel() {
</div>

{/* Contact Form */}
<div className="rounded-lg bg-white p-6 shadow">
<h2 className="mb-2 text-xl font-semibold text-gray-900">Send Us a Message</h2>
<p className="mb-6 text-sm text-gray-500">
<div className="rounded-lg bg-white dark:bg-gray-800 p-6 shadow transition-colors duration-200">
<h2 className="mb-2 text-xl font-semibold text-gray-900 dark:text-gray-100">
Send Us a Message
</h2>
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400">
Can&apos;t find what you&apos;re looking for? Fill out the form below and our support team
will respond within 24 hours.
</p>
Expand Down
6 changes: 3 additions & 3 deletions src/app/profile/components/ProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ interface ProfileHeaderProps {

export default function ProfileHeader({ user }: ProfileHeaderProps) {
return (
<header className="border-b bg-white shadow-sm">
<header className="border-b bg-white dark:bg-gray-800 dark:border-gray-700 shadow-sm">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">Profile</h1>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Profile</h1>
<div className="flex items-center space-x-2" aria-label={`Signed in as ${user.name}`}>
<div
className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 font-semibold text-white"
aria-hidden="true"
>
{user.initials}
</div>
<span className="text-gray-700">{user.name}</span>
<span className="text-gray-700 dark:text-gray-300">{user.name}</span>
</div>
</div>
</div>
Expand Down
34 changes: 21 additions & 13 deletions src/app/profile/components/ProfileInfoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ function ProfileInfoPanel() {
id="profile-panel"
role="tabpanel"
aria-labelledby="profile-tab"
className="rounded-lg bg-white p-6 shadow"
className="rounded-lg bg-white dark:bg-gray-800 p-6 shadow transition-colors duration-200"
>
<h2 className="mb-6 text-xl font-semibold text-gray-900">Personal Information</h2>
<h2 className="mb-6 text-xl font-semibold text-gray-900 dark:text-gray-100">
Personal Information
</h2>

<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label
htmlFor="profile-full-name"
className="mb-2 block text-sm font-medium text-gray-700"
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Full Name
</label>
Expand All @@ -26,34 +28,37 @@ function ProfileInfoPanel() {
type="text"
defaultValue={profileUser.name}
autoComplete="name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500"
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500 dark:placeholder-gray-400"
/>
</div>

<div>
<label htmlFor="profile-email" className="mb-2 block text-sm font-medium text-gray-700">
<label
htmlFor="profile-email"
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Email
</label>
<input
id="profile-email"
type="email"
defaultValue={profileUser.email}
autoComplete="email"
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500"
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500 dark:placeholder-gray-400"
/>
</div>

<div>
<label
htmlFor="profile-learning-goal"
className="mb-2 block text-sm font-medium text-gray-700"
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Learning Goal
</label>
<select
id="profile-learning-goal"
defaultValue="monthly-course"
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500"
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500"
>
{learningGoalOptions.map((option) => (
<option key={option.value} value={option.value}>
Expand All @@ -66,14 +71,14 @@ function ProfileInfoPanel() {
<div>
<label
htmlFor="profile-daily-time"
className="mb-2 block text-sm font-medium text-gray-700"
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Daily Learning Time
</label>
<select
id="profile-daily-time"
defaultValue="30-minutes"
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500"
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500"
>
{dailyLearningTimeOptions.map((option) => (
<option key={option.value} value={option.value}>
Expand All @@ -85,21 +90,24 @@ function ProfileInfoPanel() {
</div>

<div className="mt-6">
<label htmlFor="profile-bio" className="mb-2 block text-sm font-medium text-gray-700">
<label
htmlFor="profile-bio"
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Bio
</label>
<textarea
id="profile-bio"
rows={4}
defaultValue={profileUser.bio}
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500"
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500 dark:placeholder-gray-400"
/>
</div>

<div className="mt-6">
<button
type="button"
className="rounded-lg bg-blue-500 px-6 py-2 text-white transition-colors hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
className="rounded-lg bg-blue-500 px-6 py-2 text-white transition-colors hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
>
Save Changes
</button>
Expand Down
Loading
Loading