From 3ae9fd149b75a2ca25bc54d58631c43dec858a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Fri, 5 Dec 2025 21:59:56 +0100 Subject: [PATCH] feat(frontend): add unified header and footer to public pages Implemented consistent navigation across all public-facing pages with a reusable layout system. Created PublicLayout component that wraps pages with a header containing the logo and a footer with navigation links. Changes: - Created PublicHeader component with logo linking to homepage - Created PublicFooter component with Product, Account, and Support sections - Created PublicLayout wrapper component using flex layout - Updated all public pages to use PublicLayout: - LoginPage, RegisterPage, ForgotPasswordPage, ResetPasswordPage - VerifyEmailPage, ContactPage, AboutUsPage, HowItWorksPage - NotFoundPage - Fixed gradient background pages to use min-h-full for proper height - Fixed content pages to avoid min-h-screen conflicts with flex-grow - Updated About Us content --- frontend/public/content/about-us.md | 25 +- .../src/components/layout/PublicFooter.jsx | 71 ++++++ .../src/components/layout/PublicHeader.jsx | 17 ++ .../src/components/layout/PublicLayout.jsx | 16 ++ frontend/src/pages/AboutUsPage.jsx | 20 +- frontend/src/pages/ContactPage.jsx | 10 +- frontend/src/pages/ForgotPasswordPage.jsx | 17 +- frontend/src/pages/HowItWorksPage.jsx | 20 +- frontend/src/pages/LoginPage.jsx | 19 +- frontend/src/pages/NotFoundPage.jsx | 17 +- frontend/src/pages/RegisterPage.jsx | 17 +- frontend/src/pages/ResetPasswordPage.jsx | 235 +++++++++--------- frontend/src/pages/VerifyEmailPage.jsx | 235 +++++++++--------- 13 files changed, 412 insertions(+), 307 deletions(-) create mode 100644 frontend/src/components/layout/PublicFooter.jsx create mode 100644 frontend/src/components/layout/PublicHeader.jsx create mode 100644 frontend/src/components/layout/PublicLayout.jsx diff --git a/frontend/public/content/about-us.md b/frontend/public/content/about-us.md index 076c008..5c5ba71 100644 --- a/frontend/public/content/about-us.md +++ b/frontend/public/content/about-us.md @@ -1,26 +1,9 @@ -# About Us +Hi, I’m Radek – a software engineer, a West Coast Swing dancer and the person behind **spotlight.cam**. -## Lorem Ipsum -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Spotlight.cam is a project built by someone who actually stands in the same registration lines, dances in the same heats and scrolls through the same event pages as you :P -## Dolor Sit Amet -Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat: +If we ever meet at an event, I’ll probably be somewhere near the dance floor, probably pressing “record” on someone’s spotlight. -- **Duis aute irure** - Dolor in reprehenderit in voluptate velit esse cillum dolore -- **Eu fugiat nulla** - Pariatur excepteur sint occaecat cupidatat non proident -- **Sunt in culpa** - Qui officia deserunt mollit anim id est laborum -- **Sed ut perspiciatis** - Unde omnis iste natus error sit voluptatem - -## Consectetur Adipiscing - -Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. - -## Nemo Enim - -Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. - ---- - -*For questions or feedback, [contact us](/contact).* +To be fair, the original idea for this service was actually dropped on me by a friend who was tired of hunting for someone to film her dances – I just did what any backend developer / DevOps / Linux admin would do: said “okay, that shouldn’t be that hard… right?”, opened my editor, set up a few servers and scripts… and suddenly we had a new project on our hands 😅 I also had a not-so-secret teammate: AI. Without it, this would probably still be stuck in my “one day” folder for about a year 😄 \ No newline at end of file diff --git a/frontend/src/components/layout/PublicFooter.jsx b/frontend/src/components/layout/PublicFooter.jsx new file mode 100644 index 0000000..79f36b5 --- /dev/null +++ b/frontend/src/components/layout/PublicFooter.jsx @@ -0,0 +1,71 @@ +import { Link } from 'react-router-dom'; + +const PublicFooter = () => { + return ( + + ); +}; + +export default PublicFooter; diff --git a/frontend/src/components/layout/PublicHeader.jsx b/frontend/src/components/layout/PublicHeader.jsx new file mode 100644 index 0000000..f3c72eb --- /dev/null +++ b/frontend/src/components/layout/PublicHeader.jsx @@ -0,0 +1,17 @@ +import { Link } from 'react-router-dom'; +import { Video } from 'lucide-react'; + +const PublicHeader = () => { + return ( +
+
+ +
+
+ ); +}; + +export default PublicHeader; diff --git a/frontend/src/components/layout/PublicLayout.jsx b/frontend/src/components/layout/PublicLayout.jsx new file mode 100644 index 0000000..d313255 --- /dev/null +++ b/frontend/src/components/layout/PublicLayout.jsx @@ -0,0 +1,16 @@ +import PublicHeader from './PublicHeader'; +import PublicFooter from './PublicFooter'; + +const PublicLayout = ({ children }) => { + return ( +
+ +
+ {children} +
+ +
+ ); +}; + +export default PublicLayout; diff --git a/frontend/src/pages/AboutUsPage.jsx b/frontend/src/pages/AboutUsPage.jsx index de1e9ae..6d19b28 100644 --- a/frontend/src/pages/AboutUsPage.jsx +++ b/frontend/src/pages/AboutUsPage.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; -import Layout from '../components/layout/Layout'; +import PublicLayout from '../components/layout/PublicLayout'; export default function AboutUsPage() { const [content, setContent] = useState(''); @@ -29,32 +29,32 @@ export default function AboutUsPage() { if (loading) { return ( - -
+ +

Loading...

- +
); } if (error) { return ( - -
+ +

{error}

- +
); } return ( - -
+ +
- +
); } diff --git a/frontend/src/pages/ContactPage.jsx b/frontend/src/pages/ContactPage.jsx index abf0760..b9559f8 100644 --- a/frontend/src/pages/ContactPage.jsx +++ b/frontend/src/pages/ContactPage.jsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { Send, Mail, User, MessageSquare } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { publicAPI } from '../services/api'; -import Layout from '../components/layout/Layout'; +import PublicLayout from '../components/layout/PublicLayout'; export default function ContactPage() { const { user } = useAuth(); @@ -125,7 +125,7 @@ export default function ContactPage() { if (success) { return ( - +
@@ -138,12 +138,12 @@ export default function ContactPage() {

Redirecting to homepage...

- + ); } return ( - +
{/* Header */} @@ -312,6 +312,6 @@ export default function ContactPage() {
-
+ ); } diff --git a/frontend/src/pages/ForgotPasswordPage.jsx b/frontend/src/pages/ForgotPasswordPage.jsx index 94a88d2..87c94f4 100644 --- a/frontend/src/pages/ForgotPasswordPage.jsx +++ b/frontend/src/pages/ForgotPasswordPage.jsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Link } from 'react-router-dom'; import { authAPI } from '../services/api'; import { Video, Mail, ArrowLeft, CheckCircle, Loader2 } from 'lucide-react'; +import PublicLayout from '../components/layout/PublicLayout'; const ForgotPasswordPage = () => { const [email, setEmail] = useState(''); @@ -26,8 +27,9 @@ const ForgotPasswordPage = () => { if (success) { return ( -
-
+ +
+
@@ -50,13 +52,15 @@ const ForgotPasswordPage = () => {
-
+
+
); } return ( -
-
+ +
+
-
+
+ ); }; diff --git a/frontend/src/pages/HowItWorksPage.jsx b/frontend/src/pages/HowItWorksPage.jsx index e9888ba..853a067 100644 --- a/frontend/src/pages/HowItWorksPage.jsx +++ b/frontend/src/pages/HowItWorksPage.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; -import Layout from '../components/layout/Layout'; +import PublicLayout from '../components/layout/PublicLayout'; export default function HowItWorksPage() { const [content, setContent] = useState(''); @@ -29,32 +29,32 @@ export default function HowItWorksPage() { if (loading) { return ( - -
+ +

Loading...

- +
); } if (error) { return ( - -
+ +

{error}

- +
); } return ( - -
+ +
- +
); } diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index 44cf333..78fdd72 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -5,6 +5,7 @@ import { Video, Mail, Lock } from 'lucide-react'; import FormInput from '../components/common/FormInput'; import LoadingButton from '../components/common/LoadingButton'; import Alert from '../components/common/Alert'; +import PublicLayout from '../components/layout/PublicLayout'; const LoginPage = () => { const [email, setEmail] = useState(''); @@ -30,13 +31,14 @@ const LoginPage = () => { }; return ( -
-
-
-
+ +
+
+
+
@@ -94,7 +96,8 @@ const LoginPage = () => {

-
+
+ ); }; diff --git a/frontend/src/pages/NotFoundPage.jsx b/frontend/src/pages/NotFoundPage.jsx index d62ade3..3d44960 100644 --- a/frontend/src/pages/NotFoundPage.jsx +++ b/frontend/src/pages/NotFoundPage.jsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { Home, ArrowLeft } from 'lucide-react'; import { publicAPI } from '../services/api'; +import PublicLayout from '../components/layout/PublicLayout'; export default function NotFoundPage() { const location = useLocation(); @@ -21,18 +22,8 @@ export default function NotFoundPage() { }, [location.pathname, location.search]); return ( -
- {/* Simple header */} -
-
- - - spotlight.cam - -
-
- -
+ +
{/* 404 Icon */}
@@ -80,6 +71,6 @@ export default function NotFoundPage() {
-
+ ); } diff --git a/frontend/src/pages/RegisterPage.jsx b/frontend/src/pages/RegisterPage.jsx index 9b51ce0..bda9f4d 100644 --- a/frontend/src/pages/RegisterPage.jsx +++ b/frontend/src/pages/RegisterPage.jsx @@ -7,6 +7,7 @@ import PasswordStrengthIndicator from '../components/common/PasswordStrengthIndi import FormInput from '../components/common/FormInput'; import LoadingButton from '../components/common/LoadingButton'; import Alert from '../components/common/Alert'; +import PublicLayout from '../components/layout/PublicLayout'; const RegisterPage = () => { // Step management @@ -218,8 +219,9 @@ const RegisterPage = () => { // Step 1: WSDC ID Check if (step === 1) { return ( -
-
+ +
+
-
+
+ ); } // Step 2: Registration Form return ( -
-
+ +
+
-
+
+ ); }; diff --git a/frontend/src/pages/ResetPasswordPage.jsx b/frontend/src/pages/ResetPasswordPage.jsx index 7882c12..df9c175 100644 --- a/frontend/src/pages/ResetPasswordPage.jsx +++ b/frontend/src/pages/ResetPasswordPage.jsx @@ -3,6 +3,7 @@ import { useNavigate, useSearchParams, Link } from 'react-router-dom'; import { authAPI } from '../services/api'; import { Video, Lock, CheckCircle, XCircle, Loader2 } from 'lucide-react'; import PasswordStrengthIndicator from '../components/common/PasswordStrengthIndicator'; +import PublicLayout from '../components/layout/PublicLayout'; const ResetPasswordPage = () => { const [searchParams] = useSearchParams(); @@ -50,146 +51,152 @@ const ResetPasswordPage = () => { // Success state if (success) { return ( -
-
-
-
- + +
+
+
+
+ +
+

+ Password Reset Successfully! 🎉 +

+

+ Your password has been updated. You can now log in with your new password. +

+
-

- Password Reset Successfully! 🎉 -

-

- Your password has been updated. You can now log in with your new password. -

-
-
+ ); } // Invalid token state if (!token) { return ( -
-
-
-
- + +
+
+
+
+ +
+

+ Invalid Reset Link +

+

+ This password reset link is invalid or has expired. Please request a new one. +

+ + Request New Link +
-

- Invalid Reset Link -

-

- This password reset link is invalid or has expired. Please request a new one. -

- - Request New Link -
-
+ ); } // Reset password form return ( -
-
-
-
- - {error && ( -
- -

{error}

+ +
+
+
+
- )} -
- {/* New Password */} -
- -
-
- -
- setNewPassword(e.target.value)} - className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" - placeholder="••••••••" - required - disabled={loading} - /> + {error && ( +
+ +

{error}

- -
+ )} - {/* Confirm Password */} -
- -
-
- + + {/* New Password */} +
+ +
+
+ +
+ setNewPassword(e.target.value)} + className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" + placeholder="••••••••" + required + disabled={loading} + />
- setConfirmPassword(e.target.value)} - className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" - placeholder="••••••••" - required - disabled={loading} - /> +
- {confirmPassword && newPassword !== confirmPassword && ( -

Passwords do not match

- )} + + {/* Confirm Password */} +
+ +
+
+ +
+ setConfirmPassword(e.target.value)} + className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" + placeholder="••••••••" + required + disabled={loading} + /> +
+ {confirmPassword && newPassword !== confirmPassword && ( +

Passwords do not match

+ )} +
+ + + + +
+ + Back to Login +
- - - - -
- - Back to Login -
-
+ ); }; diff --git a/frontend/src/pages/VerifyEmailPage.jsx b/frontend/src/pages/VerifyEmailPage.jsx index c33b215..567247b 100644 --- a/frontend/src/pages/VerifyEmailPage.jsx +++ b/frontend/src/pages/VerifyEmailPage.jsx @@ -3,6 +3,7 @@ import { useNavigate, useSearchParams, Link } from 'react-router-dom'; import { authAPI } from '../services/api'; import { useAuth } from '../contexts/AuthContext'; import { Video, Mail, CheckCircle, XCircle, Loader2, ArrowRight } from 'lucide-react'; +import PublicLayout from '../components/layout/PublicLayout'; const VerifyEmailPage = () => { const [searchParams] = useSearchParams(); @@ -101,147 +102,153 @@ const VerifyEmailPage = () => { // Success state if (success) { return ( -
-
-
-
- + +
+
+
+
+ +
+

+ Email Verified! 🎉 +

+

+ Your email has been successfully verified. You can now access all features of spotlight.cam! +

+
-

- Email Verified! 🎉 -

-

- Your email has been successfully verified. You can now access all features of spotlight.cam! -

-
-
+ ); } // Loading state (for token verification) if (loading && verificationMode === 'token') { return ( -
-
-
- -

- Verifying your email... -

-

- Please wait while we verify your email address. -

+ +
+
+
+ +

+ Verifying your email... +

+

+ Please wait while we verify your email address. +

+
-
+ ); } // Code verification form return ( -
-
-
-
- - {error && ( -
- -

{error}

-
- )} - -
- {/* Email */} -
- -
-
- -
- setEmail(e.target.value)} - className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" - placeholder="your@email.com" - required - disabled={loading} - /> -
-
- - {/* Verification Code */} -
- - setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} - className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 text-center text-2xl font-mono tracking-widest" - placeholder="000000" - maxLength="6" - required - disabled={loading} - /> -

- Enter the 6-digit code from your email + +

+
+
+
- - + {error && ( +
+ +

{error}

+
+ )} + +
+ {/* Email */} +
+ +
+
+ +
+ setEmail(e.target.value)} + className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" + placeholder="your@email.com" + required + disabled={loading} + /> +
+
+ + {/* Verification Code */} +
+ + setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 text-center text-2xl font-mono tracking-widest" + placeholder="000000" + maxLength="6" + required + disabled={loading} + /> +

+ Enter the 6-digit code from your email +

+
-
-

- Didn't receive the code?{' '} -

-
+
-
- - Skip for now → - +
+

+ Didn't receive the code?{' '} + +

+
+ +
+ + Skip for now → + +
-
+ ); };