diff --git a/frontend/e2e/auth-fixes.spec.ts b/frontend/e2e/auth-fixes.spec.ts index e745906..ff851b7 100644 --- a/frontend/e2e/auth-fixes.spec.ts +++ b/frontend/e2e/auth-fixes.spec.ts @@ -2,14 +2,7 @@ import { test, expect, dismissCookieBanner } from './fixtures'; /** * auth-fixes.spec.ts - * E2E tests that specifically cover the auth bugs fixed in the last PR: - * - * 1. Login form is visible on dark background (correct text/input colors) - * 2. Empty-field validation shows inline error (no navigation away) - * 3. Wrong-credentials shows inline error message (not a blank screen) - * 4. Guest login "Continue as Guest" button works end-to-end - * 5. "Sign up" link navigates to /register - * 6. "Sign in" link on register page navigates to /login + * E2E tests for auth pages in light mode — Login & Register. */ test.describe('Login Page — Visual & Validation', () => { @@ -18,20 +11,19 @@ test.describe('Login Page — Visual & Validation', () => { await dismissCookieBanner(page); }); - test('h1 heading is visible against dark background', async ({ page }) => { + test('h1 heading is visible and reads "Log in"', async ({ page }) => { const heading = page.locator('h1'); await expect(heading).toBeVisible(); - await expect(heading).toContainText('Welcome Back'); + await expect(heading).toContainText('Log in'); }); - test('email label is visible (dark-mode text color)', async ({ page }) => { - // Label must be visible — previously used text-gray-700 on dark bg + test('email label is visible', async ({ page }) => { const emailLabel = page.locator('label[for="email"]'); await expect(emailLabel).toBeVisible(); await expect(emailLabel).toContainText('Email'); }); - test('password label is visible (dark-mode text color)', async ({ page }) => { + test('password label is visible', async ({ page }) => { const passLabel = page.locator('label[for="password"]'); await expect(passLabel).toBeVisible(); await expect(passLabel).toContainText('Password'); @@ -50,31 +42,19 @@ test.describe('Login Page — Visual & Validation', () => { test('eye toggle reveals and hides password', async ({ page }) => { const input = page.locator('#password'); await input.fill('secret123'); - - // Click the toggle button inside the password wrapper const toggle = page.locator('#password').locator('..').locator('button'); await toggle.click(); await expect(input).toHaveAttribute('type', 'text'); - await toggle.click(); await expect(input).toHaveAttribute('type', 'password'); }); - test('remember me checkbox is visible and toggleable', async ({ page }) => { - const checkbox = page.locator('input[type="checkbox"]'); - await expect(checkbox).toBeVisible(); - await checkbox.check(); - await expect(checkbox).toBeChecked(); - await checkbox.uncheck(); - await expect(checkbox).not.toBeChecked(); - }); - test('forgot password link is visible', async ({ page }) => { - await expect(page.locator('text=Forgot password?')).toBeVisible(); + await expect(page.locator('text=Forgot password')).toBeVisible(); }); - test('"Don\'t have an account? Sign up" link is visible', async ({ page }) => { - await expect(page.getByRole('link', { name: /sign up/i })).toBeVisible(); + test('"Register Now" link is visible', async ({ page }) => { + await expect(page.getByRole('link', { name: /register now/i })).toBeVisible(); }); test('"Continue as Guest" button is visible', async ({ page }) => { @@ -82,68 +62,52 @@ test.describe('Login Page — Visual & Validation', () => { }); }); -test.describe('Login Page — Validation errors (inline, no toast spam)', () => { +test.describe('Login Page — Validation errors', () => { test.beforeEach(async ({ page }) => { await page.goto('/login'); await dismissCookieBanner(page); }); test('shows inline error when both fields are empty', async ({ page }) => { - await page.getByRole('button', { name: /sign in$/i }).click(); - - // Inline error banner should appear inside the form - const errorBanner = page.locator('div.bg-red-900\\/50, [class*="bg-red"]').first(); + await page.getByRole('button', { name: /sign in/i }).click(); + const errorBanner = page.locator('[class*="bg-red"]').first(); await expect(errorBanner).toBeVisible({ timeout: 5000 }); await expect(errorBanner).toContainText(/fill in all fields/i); - - // Must NOT have navigated away await expect(page).toHaveURL('/login'); }); test('shows inline error when password field is empty', async ({ page }) => { await page.locator('#email').fill('user@test.com'); - await page.getByRole('button', { name: /sign in$/i }).click(); - - const errorBanner = page.locator('div.bg-red-900\\/50, [class*="bg-red"]').first(); + await page.getByRole('button', { name: /sign in/i }).click(); + const errorBanner = page.locator('[class*="bg-red"]').first(); await expect(errorBanner).toBeVisible({ timeout: 5000 }); await expect(page).toHaveURL('/login'); }); - test('shows inline error (not just a toast) on wrong credentials', async ({ page }) => { + test('shows inline error on wrong credentials', async ({ page }) => { await page.locator('#email').fill('wrong@test.com'); await page.locator('#password').fill('WrongPass123'); - await page.getByRole('button', { name: /sign in$/i }).click(); - - // The inline red error box should appear - const errorBanner = page.locator('div.bg-red-900\\/50, [class*="bg-red"]').first(); + await page.getByRole('button', { name: /sign in/i }).click(); + const errorBanner = page.locator('[class*="bg-red"]').first(); await expect(errorBanner).toBeVisible({ timeout: 10000 }); - - // Page should still be /login (no redirect) await expect(page).toHaveURL('/login'); }); test('sign-in button shows spinner while loading', async ({ page }) => { await page.locator('#email').fill('test@example.com'); await page.locator('#password').fill('password123'); - - // Click and immediately check for spinner - await page.getByRole('button', { name: /sign in$/i }).click(); - - // The loader / spinner should appear briefly - const spinner = page.locator('.animate-spin'); - // If the network is fast the spinner may disappear immediately — use a - // soft check rather than strict assertion - await spinner.isVisible().catch(() => false); - // Just assert the page stays on /login or moves to /dashboard — no crash + await page.getByRole('button', { name: /sign in/i }).click(); + // Soft check — spinner may disappear fast on fast connections + await page.locator('.animate-spin').isVisible().catch(() => false); await expect(page.locator('body')).toBeVisible(); }); }); test.describe('Login → Register navigation', () => { - test('clicking Sign up navigates to /register', async ({ page }) => { + test('clicking Register Now navigates to /register', async ({ page }) => { await page.goto('/login'); await dismissCookieBanner(page); - await page.getByRole('link', { name: /sign up/i }).click(); + await page.getByRole('link', { name: /register now/i }).click(); await expect(page).toHaveURL('/register'); }); @@ -157,7 +121,6 @@ test.describe('Login → Register navigation', () => { test.describe('Guest Login Flow', () => { test.beforeEach(async ({ page }) => { - // Start from a clean state await page.goto('/privacy-policy'); await page.evaluate(() => localStorage.clear()); }); @@ -174,11 +137,8 @@ test.describe('Guest Login Flow', () => { await dismissCookieBanner(page); await page.getByRole('button', { name: /continue as guest/i }).click(); await expect(page).toHaveURL('/dashboard', { timeout: 15000 }); - - // Navigate to a protected page await page.goto('/tasks'); await expect(page).toHaveURL('/tasks'); - await page.goto('/pomodoro'); await expect(page).toHaveURL('/pomodoro'); }); @@ -188,8 +148,6 @@ test.describe('Guest Login Flow', () => { await dismissCookieBanner(page); await page.getByRole('button', { name: /continue as guest/i }).click(); await expect(page).toHaveURL('/dashboard', { timeout: 15000 }); - - // Try to go back to login — should be redirected await page.goto('/login'); await expect(page).toHaveURL('/dashboard'); }); diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index 79e829a..0b672bf 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -8,25 +8,35 @@ test.describe('Registration Page', () => { await dismissCookieBanner(page); }); - test('should display all form fields and buttons', async ({ page }) => { - await expect(page.locator('h1')).toContainText('Create an Account'); + test('should display register heading', async ({ page }) => { + await expect(page.locator('h1')).toContainText('Register'); + }); + + test('should show step 1 fields by default (name + email)', async ({ page }) => { await expect(page.locator('#name')).toBeVisible(); await expect(page.locator('#email')).toBeVisible(); - await expect(page.locator('#password')).toBeVisible(); - await expect(page.locator('#confirmPassword')).toBeVisible(); - await expect(page.getByRole('button', { name: /create account/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /next/i })).toBeVisible(); await expect(page.getByRole('button', { name: /start as guest/i })).toBeVisible(); await expect(page.getByRole('link', { name: /sign in/i })).toBeVisible(); }); - test('should reject empty form submission', async ({ page }) => { - await page.getByRole('button', { name: /create account/i }).click(); - await expect(page.locator('text=Please fill in all fields')).toBeVisible(); + test('should reject empty step 1 submission', async ({ page }) => { + await page.getByRole('button', { name: /next/i }).click(); + await expect(page.locator('[class*="bg-red"]').first()).toBeVisible({ timeout: 5000 }); + }); + + test('should advance to step 2 with valid name + email', async ({ page }) => { + await page.locator('#name').fill('Test User'); + await page.locator('#email').fill('test@example.com'); + await page.getByRole('button', { name: /next/i }).click(); + await expect(page.locator('#password')).toBeVisible(); + await expect(page.locator('#confirmPassword')).toBeVisible(); }); test('should reject password shorter than 8 characters', async ({ page }) => { await page.locator('#name').fill('Test User'); await page.locator('#email').fill('short@test.com'); + await page.getByRole('button', { name: /next/i }).click(); await page.locator('#password').fill('abc12'); await page.locator('#confirmPassword').fill('abc12'); await page.getByRole('button', { name: /create account/i }).click(); @@ -36,6 +46,7 @@ test.describe('Registration Page', () => { test('should reject password without a letter', async ({ page }) => { await page.locator('#name').fill('Test User'); await page.locator('#email').fill('noletter@test.com'); + await page.getByRole('button', { name: /next/i }).click(); await page.locator('#password').fill('12345678'); await page.locator('#confirmPassword').fill('12345678'); await page.getByRole('button', { name: /create account/i }).click(); @@ -45,6 +56,7 @@ test.describe('Registration Page', () => { test('should reject password without a number', async ({ page }) => { await page.locator('#name').fill('Test User'); await page.locator('#email').fill('nonumber@test.com'); + await page.getByRole('button', { name: /next/i }).click(); await page.locator('#password').fill('abcdefgh'); await page.locator('#confirmPassword').fill('abcdefgh'); await page.getByRole('button', { name: /create account/i }).click(); @@ -54,6 +66,7 @@ test.describe('Registration Page', () => { test('should reject mismatched passwords', async ({ page }) => { await page.locator('#name').fill('Test User'); await page.locator('#email').fill('mismatch@test.com'); + await page.getByRole('button', { name: /next/i }).click(); await page.locator('#password').fill('TestPass123'); await page.locator('#confirmPassword').fill('Different456'); await page.getByRole('button', { name: /create account/i }).click(); @@ -61,29 +74,32 @@ test.describe('Registration Page', () => { }); test('should show password strength indicator', async ({ page }) => { + await page.locator('#name').fill('Test User'); + await page.locator('#email').fill('pw@test.com'); + await page.getByRole('button', { name: /next/i }).click(); await page.locator('#password').fill('abc'); - // The PasswordStrengthIndicator should render await expect(page.locator('text=weak')).toBeVisible(); - await page.locator('#password').fill('abcdefgh'); await expect(page.locator('text=fair')).toBeVisible(); }); test('should show passwords match indicator', async ({ page }) => { + await page.locator('#name').fill('Test User'); + await page.locator('#email').fill('match@test.com'); + await page.getByRole('button', { name: /next/i }).click(); await page.locator('#password').fill('TestPass123'); await page.locator('#confirmPassword').fill('TestPass123'); await expect(page.locator('text=Passwords match')).toBeVisible(); }); test('should toggle password visibility', async ({ page }) => { + await page.locator('#name').fill('Test User'); + await page.locator('#email').fill('toggle@test.com'); + await page.getByRole('button', { name: /next/i }).click(); const passwordInput = page.locator('#password'); await passwordInput.fill('TestPass123'); - - // Initially hidden await expect(passwordInput).toHaveAttribute('type', 'password'); - - // Click eye button to show - await page.locator('#password ~ button, [id="password"] + button, #password').locator('..').locator('button').click(); + await page.locator('#password').locator('..').locator('button').click(); await expect(passwordInput).toHaveAttribute('type', 'text'); }); @@ -100,51 +116,38 @@ test.describe('Login Page', () => { await dismissCookieBanner(page); }); + test('should display login heading', async ({ page }) => { + await expect(page.locator('h1')).toContainText('Log in'); + }); + test('should display all form fields and buttons', async ({ page }) => { - await expect(page.locator('h1')).toContainText('Welcome Back'); await expect(page.locator('#email')).toBeVisible(); await expect(page.locator('#password')).toBeVisible(); - await expect(page.getByRole('button', { name: /sign in$/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible(); await expect(page.getByRole('button', { name: /continue as guest/i })).toBeVisible(); - await expect(page.getByRole('link', { name: /sign up/i })).toBeVisible(); - }); - - test('should reject empty password on submit', async ({ page }) => { - await page.locator('#email').fill('user@test.com'); - await page.getByRole('button', { name: /sign in$/i }).click(); - await expect(page.locator('text=Please fill in all fields')).toBeVisible(); + await expect(page.getByRole('link', { name: /register now/i })).toBeVisible(); }); - test('should reject empty email on submit', async ({ page }) => { - await page.locator('#password').fill('password123'); - // Browser HTML5 validation on type="email" will prevent submission, - // so we need to remove the type attribute first - await page.locator('#email').evaluate((el) => el.removeAttribute('type')); - await page.getByRole('button', { name: /sign in$/i }).click(); - await expect(page.locator('text=Please fill in all fields')).toBeVisible(); + test('should reject empty form submission', async ({ page }) => { + await page.getByRole('button', { name: /sign in/i }).click(); + await expect(page.locator('[class*="bg-red"]').first()).toBeVisible({ timeout: 5000 }); + await expect(page).toHaveURL('/login'); }); test('should toggle password visibility', async ({ page }) => { const passwordInput = page.locator('#password'); await passwordInput.fill('TestPass123'); await expect(passwordInput).toHaveAttribute('type', 'password'); - - // Click the eye toggle button await page.locator('#password').locator('..').locator('button').click(); await expect(passwordInput).toHaveAttribute('type', 'text'); }); - test('should have remember me checkbox', async ({ page }) => { - const checkbox = page.locator('input[type="checkbox"]'); - await expect(checkbox).toBeVisible(); - }); - test('should have forgot password link', async ({ page }) => { - await expect(page.locator('text=Forgot password?')).toBeVisible(); + await expect(page.locator('text=Forgot password')).toBeVisible(); }); test('should navigate to register page', async ({ page }) => { - await page.getByRole('link', { name: /sign up/i }).click(); + await page.getByRole('link', { name: /register now/i }).click(); await expect(page).toHaveURL('/register'); }); }); @@ -165,11 +168,8 @@ test.describe('Guest Login', () => { test('should login as guest and reach dashboard', async ({ page }) => { await page.goto('/login'); await dismissCookieBanner(page); - const guestBtn = page.getByRole('button', { name: /continue as guest/i }); await guestBtn.click(); - - // Should navigate to dashboard (may take a moment for API call) await expect(page).toHaveURL('/dashboard', { timeout: 15000 }); }); }); @@ -183,35 +183,34 @@ test.describe('Full Registration + Login Flow', () => { }; test('should register → land on dashboard → logout → login again', async ({ page }) => { - // 1. Register + // 1. Register - step 1 await page.goto('/register'); await dismissCookieBanner(page); - await page.locator('#name').fill(testUser.name); await page.locator('#email').fill(testUser.email); + await page.getByRole('button', { name: /next/i }).click(); + + // 2. Register - step 2 await page.locator('#password').fill(testUser.password); await page.locator('#confirmPassword').fill(testUser.password); await page.getByRole('button', { name: /create account/i }).click(); - // 2. Should land on dashboard + // 3. Should land on dashboard await expect(page).toHaveURL('/dashboard', { timeout: 15000 }); - // 3. Logout — navigate to login - await page.goto('/login'); + // 4. Logout — clear auth and navigate to login await page.evaluate(() => { localStorage.removeItem('isAuthenticated'); localStorage.removeItem('isGuest'); }); - await page.reload(); - - // 4. Should be on login page + await page.goto('/login'); await expect(page).toHaveURL('/login'); // 5. Login with same credentials await dismissCookieBanner(page); await page.locator('#email').fill(testUser.email); await page.locator('#password').fill(testUser.password); - await page.getByRole('button', { name: /sign in$/i }).click(); + await page.getByRole('button', { name: /sign in/i }).click(); // 6. Should land on dashboard again await expect(page).toHaveURL('/dashboard', { timeout: 15000 }); diff --git a/frontend/public/ai_elements.png b/frontend/public/ai_elements.png new file mode 100644 index 0000000..0c19151 Binary files /dev/null and b/frontend/public/ai_elements.png differ diff --git a/frontend/public/deep_work_elements.png b/frontend/public/deep_work_elements.png new file mode 100644 index 0000000..5723695 Binary files /dev/null and b/frontend/public/deep_work_elements.png differ diff --git a/frontend/public/hero_light.png b/frontend/public/hero_light.png new file mode 100644 index 0000000..aeb7d88 Binary files /dev/null and b/frontend/public/hero_light.png differ diff --git a/frontend/public/plan_focus.png b/frontend/public/plan_focus.png new file mode 100644 index 0000000..f9e314b Binary files /dev/null and b/frontend/public/plan_focus.png differ diff --git a/frontend/public/productivity_element.png b/frontend/public/productivity_element.png new file mode 100644 index 0000000..89104de Binary files /dev/null and b/frontend/public/productivity_element.png differ diff --git a/frontend/public/sportify_elements.png b/frontend/public/sportify_elements.png new file mode 100644 index 0000000..61928dc Binary files /dev/null and b/frontend/public/sportify_elements.png differ diff --git a/frontend/public/time_tracker_elements.png b/frontend/public/time_tracker_elements.png new file mode 100644 index 0000000..6b8f738 Binary files /dev/null and b/frontend/public/time_tracker_elements.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c39752c..92bfc23 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -95,7 +95,7 @@ const App = () => { return ( - + diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx index a8f67e8..367753e 100644 --- a/frontend/src/components/ProtectedRoute.tsx +++ b/frontend/src/components/ProtectedRoute.tsx @@ -14,7 +14,7 @@ export const ProtectedRoute = () => { } if (!isAuthenticated) { - return ; + return ; } return ; diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx index 29351f8..2b0d746 100644 --- a/frontend/src/components/TopBar.tsx +++ b/frontend/src/components/TopBar.tsx @@ -72,8 +72,8 @@ export function TopBar({ onMenuClick }: TopBarProps) { }; const handleLogout = () => { + navigate('/'); logout(); - navigate('/login'); }; const themes = [ diff --git a/frontend/src/components/landing-page-modern/cta/FinalCTASection.tsx b/frontend/src/components/landing-page-modern/cta/FinalCTASection.tsx index 7d17d3f..483366f 100644 --- a/frontend/src/components/landing-page-modern/cta/FinalCTASection.tsx +++ b/frontend/src/components/landing-page-modern/cta/FinalCTASection.tsx @@ -6,28 +6,70 @@ export const FinalCTASection = () => { const navigate = useNavigate(); return ( -
-
- {/* Main CTA */} +
+ {/* Premium gradient background */} +
+ {/* Top border accent */} +
+ {/* Decorative blobs */} +
+
+ +
-

- Ready to Transform Your Workflow? + +

+ Ready to do your
+ + best work ever? +

-

- Join thousands of professionals who have reclaimed their focus and - transformed how they work. Start your free trial today. +

+ Join thousands of people who have reclaimed their focus and + transformed how they work — completely free, no credit card required.

{/* CTA Buttons */} { > navigate('/register')} - className="group relative px-8 py-4 bg-orange-500 hover:bg-orange-600 text-white font-semibold rounded-lg overflow-hidden transition-colors" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} + className="group relative px-8 py-4 text-white font-bold rounded-xl overflow-hidden transition-all shadow-lg shadow-indigo-500/25 hover:shadow-indigo-500/40" + style={{ + background: 'linear-gradient(135deg, #6D5EF9 0%, #8B7CF6 100%)', + }} + whileHover={{ scale: 1.04 }} + whileTap={{ scale: 0.97 }} > +
- Start Free Trial - + Get Started Free + window.open('https://demo.focusmaster.dev', '_blank')} - className="px-8 py-4 border border-slate-700 hover:border-indigo-500/50 text-slate-300 hover:text-indigo-400 font-semibold rounded-lg transition-all hover:bg-indigo-500/5" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} + onClick={() => navigate('/login')} + className="px-8 py-4 border border-slate-200 hover:border-indigo-300 text-slate-600 hover:text-indigo-700 font-semibold rounded-xl transition-all hover:bg-indigo-50/60 bg-white/80 backdrop-blur-sm" + whileHover={{ scale: 1.04 }} + whileTap={{ scale: 0.97 }} > - Schedule a Demo + Sign In - {/* Trust statement */} - - • Trusted YourSelf - + {['No credit card', 'Free forever', 'Open source'].map((item) => ( + + + + + {item} + + ))} +
); diff --git a/frontend/src/components/landing-page-modern/detailed-features/DetailedFeaturesSection.tsx b/frontend/src/components/landing-page-modern/detailed-features/DetailedFeaturesSection.tsx index a49c8b8..322159d 100644 --- a/frontend/src/components/landing-page-modern/detailed-features/DetailedFeaturesSection.tsx +++ b/frontend/src/components/landing-page-modern/detailed-features/DetailedFeaturesSection.tsx @@ -5,13 +5,6 @@ import { BarChart2, Music2, Clock, - Play, - Pause, - SkipBack, - SkipForward, - Coffee, - LogOut, - MessageSquare, Bot, } from 'lucide-react'; @@ -117,7 +110,7 @@ export const DetailedFeaturesSection = () => { {/* Text Content */}
{/* Badge */} -
+
@@ -127,7 +120,7 @@ export const DetailedFeaturesSection = () => {
{/* Title */} -

+

{feature.title}

@@ -145,7 +138,7 @@ export const DetailedFeaturesSection = () => {
- + {feat} @@ -161,222 +154,49 @@ export const DetailedFeaturesSection = () => { transition={{ duration: 0.6 }} viewport={{ once: true }} > -
- -
- {/* Focus Engine (Timer) */} - {feature.subtitle === 'Focus Engine' && ( -
-
- {/* Glowing circular progress mask */} - - - -
- - 25:00 - - - Focus - -
-
-
- - -
-
- )} - - {/* Task Manager (Kanban) */} - {feature.subtitle === 'Task Manager' && ( -
- {/* Column 1 */} -
- To Do -
-
-

Database Migration

- High -
-
-

Update API docs

-
-
-
- {/* Column 2 */} -
-
- In Progress - -
-
-
-

Design Landing Page

-
-
-
-
-
-
-
- )} - - {/* Productivity Analytics */} - {feature.subtitle === 'Productivity Analytics' && ( -
-
- Weekly Performance - Peak Focus: 4.8h -
-
- {[ - { h: 30, day: 'Mon' }, - { h: 60, day: 'Tue' }, - { h: 45, day: 'Wed' }, - { h: 90, day: 'Thu' }, - { h: 70, day: 'Fri' } - ].map((item, i) => ( -
-
- -
- {item.day} -
- ))} -
-
- )} - - {/* Spotify Control */} - {feature.subtitle === 'Spotify Control' && ( -
-
- {/* album cover art placeholder */} -
- -
-
-

Lofi Focus Beats

-

FocusMaster Radio

-
- - Connected -
-
-
- {/* Seek bar */} -
-
-
-
-
- 1:24 - 3:45 -
-
- {/* Controls */} -
- - - -
-
- )} - - {/* Time Tracking */} - {feature.subtitle === 'Time Tracking' && ( -
-
- Shift Tracker -
- - Clocked In -
-
-
- Time Elapsed - 04:32:18 -
-
- - -
-
- )} - - {/* AI Coach */} - {feature.subtitle === 'FocusMaster AI' && ( -
- {/* Shimmer effect */} -
- -
-
- - FocusMaster AI -
- Online -
- -
-
-

- Insight: I noticed your focus drops off around the 40-minute mark. I've adjusted your timer to 35 minutes for optimal retention. -

-
- -
-
- - You -
-

Generate a quiz from my physics notes.

-
- -
-

- Generating 5 MCQs on Quantum Mechanics.pdf... -

-
-
-
-
-
-
- )} -
+
+ {feature.subtitle === 'Focus Engine' && ( + Deep Work Focus Session + )} + {feature.subtitle === 'Task Manager' && ( + Plan & Focus Task Manager + )} + {feature.subtitle === 'Productivity Analytics' && ( + Productivity Analytics + )} + {feature.subtitle === 'FocusMaster AI' && ( + FocusMaster AI + )} + {feature.subtitle === 'Spotify Control' && ( + Spotify Control + )} + {feature.subtitle === 'Time Tracking' && ( + Time Tracking + )}
diff --git a/frontend/src/components/landing-page-modern/features/FeatureShowcaseSection.tsx b/frontend/src/components/landing-page-modern/features/FeatureShowcaseSection.tsx new file mode 100644 index 0000000..bba77a2 --- /dev/null +++ b/frontend/src/components/landing-page-modern/features/FeatureShowcaseSection.tsx @@ -0,0 +1,193 @@ +import { useRef } from 'react'; +import { motion, useInView } from 'framer-motion'; +import { Brain, CalendarCheck, BarChart3, ArrowRight } from 'lucide-react'; + +const cards = [ + { + tag: 'Deep Work', + tagColor: '#6D5EF9', + tagBg: 'rgba(109,94,249,0.08)', + tagBorder: 'rgba(109,94,249,0.2)', + title: 'Enter a state of unbreakable focus', + description: + 'Block distractions and immerse yourself in deep work sessions that actually move the needle. Proven Pomodoro intervals keep you locked in.', + image: '/deep_work_elements.png', + icon: Brain, + accent: '#6D5EF9', + imagePosition: 'center top', + wide: true, + }, + { + tag: 'Focus Planning', + tagColor: '#10b981', + tagBg: 'rgba(16,185,129,0.08)', + tagBorder: 'rgba(16,185,129,0.2)', + title: 'Plan your sessions, own your day', + description: + 'Structure your week with intelligent focus plans. Set intentions before each session and review progress as you go.', + image: '/plan_focus.png', + icon: CalendarCheck, + accent: '#10b981', + imagePosition: 'center center', + wide: false, + }, + { + tag: 'Productivity', + tagColor: '#f59e0b', + tagBg: 'rgba(245,158,11,0.08)', + tagBorder: 'rgba(245,158,11,0.2)', + title: "Track what you've actually done", + description: + 'Visual analytics show your productive hours, streaks, and patterns — so you can improve what matters most.', + image: '/productivity_element.png', + icon: BarChart3, + accent: '#f59e0b', + imagePosition: 'center center', + wide: false, + }, +]; + +const FeatureCard = ({ + card, + index, +}: { + card: (typeof cards)[0]; + index: number; +}) => { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: '-80px' }); + const Icon = card.icon; + + return ( + + {/* Image area */} +
+ {card.title} + {/* Subtle bottom scrim so text doesn't clash */} +
+
+ + {/* Text area */} +
+ {/* Tag */} + + + {card.tag} + + +

+ {card.title} +

+ +

+ {card.description} +

+ + +
+ + {/* Hover accent border */} +
+ + ); +}; + +export const FeatureShowcaseSection = () => { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: '-60px' }); + + return ( +
+ {/* Subtle background pattern */} +
+ +
+ {/* Section header */} + + + Built for performance + +

+ Everything you need to
+ do your best work +

+

+ FocusMaster brings together the tools that high performers rely on — + packaged in a single, distraction-free experience. +

+
+ + {/* Bento grid — wide card + 2 stacked */} +
+ {cards.map((card, i) => ( + + ))} +
+
+
+ ); +}; diff --git a/frontend/src/components/landing-page-modern/footer/FooterSection.tsx b/frontend/src/components/landing-page-modern/footer/FooterSection.tsx index 051892c..f50f753 100644 --- a/frontend/src/components/landing-page-modern/footer/FooterSection.tsx +++ b/frontend/src/components/landing-page-modern/footer/FooterSection.tsx @@ -2,7 +2,7 @@ import { Github, Linkedin, Twitter } from 'lucide-react'; export const FooterSection = () => { return ( -