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
This commit is contained in:
Radosław Gierwiało
2025-12-05 21:59:56 +01:00
parent c47d182b98
commit 3ae9fd149b
13 changed files with 412 additions and 307 deletions

View File

@@ -1,26 +1,9 @@
# About Us
Hi, Im 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, Ill probably be somewhere near the dance floor, probably pressing “record” on someones 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 shouldnt 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 😄

View File

@@ -0,0 +1,71 @@
import { Link } from 'react-router-dom';
const PublicFooter = () => {
return (
<footer className="bg-white border-t border-gray-200 mt-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Product */}
<div>
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
Product
</h3>
<ul className="space-y-2">
<li>
<Link to="/about-us" className="text-gray-600 hover:text-primary-600 transition-colors">
About Us
</Link>
</li>
<li>
<Link to="/how-it-works" className="text-gray-600 hover:text-primary-600 transition-colors">
How It Works
</Link>
</li>
</ul>
</div>
{/* Account */}
<div>
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
Account
</h3>
<ul className="space-y-2">
<li>
<Link to="/login" className="text-gray-600 hover:text-primary-600 transition-colors">
Sign In
</Link>
</li>
<li>
<Link to="/register" className="text-gray-600 hover:text-primary-600 transition-colors">
Register
</Link>
</li>
</ul>
</div>
{/* Support */}
<div>
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
Support
</h3>
<ul className="space-y-2">
<li>
<Link to="/contact" className="text-gray-600 hover:text-primary-600 transition-colors">
Contact Us
</Link>
</li>
</ul>
</div>
</div>
<div className="mt-8 pt-8 border-t border-gray-200">
<p className="text-center text-gray-500 text-sm">
&copy; {new Date().getFullYear()} spotlight.cam. All rights reserved.
</p>
</div>
</div>
</footer>
);
};
export default PublicFooter;

View File

@@ -0,0 +1,17 @@
import { Link } from 'react-router-dom';
import { Video } from 'lucide-react';
const PublicHeader = () => {
return (
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<Link to="/" className="flex items-center space-x-2 hover:opacity-80 transition-opacity">
<Video className="w-8 h-8 text-primary-600" />
<span className="text-xl font-bold text-gray-900">spotlight.cam</span>
</Link>
</div>
</header>
);
};
export default PublicHeader;

View File

@@ -0,0 +1,16 @@
import PublicHeader from './PublicHeader';
import PublicFooter from './PublicFooter';
const PublicLayout = ({ children }) => {
return (
<div className="min-h-screen flex flex-col">
<PublicHeader />
<main className="flex-grow">
{children}
</main>
<PublicFooter />
</div>
);
};
export default PublicLayout;

View File

@@ -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 (
<Layout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<PublicLayout>
<div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center">
<div className="w-12 h-12 border-4 border-primary-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
</Layout>
</PublicLayout>
);
}
if (error) {
return (
<Layout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<PublicLayout>
<div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center">
<p className="text-red-600">{error}</p>
</div>
</div>
</Layout>
</PublicLayout>
);
}
return (
<Layout>
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<PublicLayout>
<div className="bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<article className="bg-white rounded-lg shadow-sm p-8 md:p-12 prose prose-lg max-w-none">
<ReactMarkdown
@@ -86,6 +86,6 @@ export default function AboutUsPage() {
</article>
</div>
</div>
</Layout>
</PublicLayout>
);
}

View File

@@ -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 (
<Layout>
<PublicLayout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="max-w-md w-full bg-white rounded-lg shadow-sm p-8 text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
@@ -138,12 +138,12 @@ export default function ContactPage() {
<p className="text-sm text-gray-500">Redirecting to homepage...</p>
</div>
</div>
</Layout>
</PublicLayout>
);
}
return (
<Layout>
<PublicLayout>
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl mx-auto">
{/* Header */}
@@ -312,6 +312,6 @@ export default function ContactPage() {
</div>
</div>
</div>
</Layout>
</PublicLayout>
);
}

View File

@@ -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,7 +27,8 @@ const ForgotPasswordPage = () => {
if (success) {
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
@@ -51,11 +53,13 @@ const ForgotPasswordPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center mb-6">
<Video className="w-16 h-16 text-primary-600 mb-4" />
@@ -128,6 +132,7 @@ const ForgotPasswordPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
};

View File

@@ -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 (
<Layout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<PublicLayout>
<div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center">
<div className="w-12 h-12 border-4 border-primary-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
</Layout>
</PublicLayout>
);
}
if (error) {
return (
<Layout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<PublicLayout>
<div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center">
<p className="text-red-600">{error}</p>
</div>
</div>
</Layout>
</PublicLayout>
);
}
return (
<Layout>
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<PublicLayout>
<div className="bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<article className="bg-white rounded-lg shadow-sm p-8 md:p-12 prose prose-lg max-w-none">
<ReactMarkdown
@@ -86,6 +86,6 @@ export default function HowItWorksPage() {
</article>
</div>
</div>
</Layout>
</PublicLayout>
);
}

View File

@@ -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,7 +31,8 @@ const LoginPage = () => {
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center mb-8">
<Video className="w-16 h-16 text-primary-600 mb-4" />
@@ -95,6 +97,7 @@ const LoginPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
};

View File

@@ -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 (
<div className="min-h-screen bg-gray-50">
{/* Simple header */}
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<Link to="/" className="flex items-center gap-2 text-primary-600 hover:text-primary-700">
<Home size={24} />
<span className="text-xl font-bold">spotlight.cam</span>
</Link>
</div>
</header>
<div className="min-h-screen flex items-center justify-center px-4 -mt-16">
<PublicLayout>
<div className="bg-gray-50 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full text-center">
{/* 404 Icon */}
<div className="mb-8">
@@ -80,6 +71,6 @@ export default function NotFoundPage() {
</div>
</div>
</div>
</div>
</PublicLayout>
);
}

View File

@@ -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,7 +219,8 @@ const RegisterPage = () => {
// Step 1: WSDC ID Check
if (step === 1) {
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center mb-8">
<Video className="w-16 h-16 text-primary-600 mb-4" />
@@ -355,12 +357,14 @@ const RegisterPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
}
// Step 2: Registration Form
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-8">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center mb-6">
<Video className="w-12 h-12 text-primary-600 mb-3" />
@@ -479,6 +483,7 @@ const RegisterPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
};

View File

@@ -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,7 +51,8 @@ const ResetPasswordPage = () => {
// Success state
if (success) {
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
@@ -71,13 +73,15 @@ const ResetPasswordPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
}
// Invalid token state
if (!token) {
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center text-center">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
@@ -98,12 +102,14 @@ const ResetPasswordPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
}
// Reset password form
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center mb-6">
<Video className="w-16 h-16 text-primary-600 mb-4" />
@@ -190,6 +196,7 @@ const ResetPasswordPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
};

View File

@@ -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,7 +102,8 @@ const VerifyEmailPage = () => {
// Success state
if (success) {
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
@@ -123,13 +125,15 @@ const VerifyEmailPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
}
// Loading state (for token verification)
if (loading && verificationMode === 'token') {
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center text-center">
<Loader2 className="w-16 h-16 text-primary-600 mb-4 animate-spin" />
@@ -142,12 +146,14 @@ const VerifyEmailPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
}
// Code verification form
return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
<PublicLayout>
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="flex flex-col items-center mb-6">
<Video className="w-16 h-16 text-primary-600 mb-4" />
@@ -242,6 +248,7 @@ const VerifyEmailPage = () => {
</div>
</div>
</div>
</PublicLayout>
);
};