© 2025 Rainbows, Cupcakes and Unicorns. Free fun for all children. No ads, no logins, no worries! 🎓
diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx
index 15955e5..724b000 100644
--- a/frontend/src/components/Navbar/Navbar.tsx
+++ b/frontend/src/components/Navbar/Navbar.tsx
@@ -6,196 +6,199 @@ import { APPS } from '../../config/apps';
import { OptimizedImage } from '../OptimizedImage/OptimizedImage';
export function Navbar() {
- const { isAuthenticated, logout, isAdmin } = useAuth();
- const location = useLocation();
- const [searchParams, setSearchParams] = useSearchParams();
- const { channels } = useChannels();
-
- // Detect current app from registry
- const getCurrentApp = (pathname: string) => {
- return APPS.find(app => pathname === app.link || pathname.startsWith(app.link + '/'));
- };
-
- const currentApp = getCurrentApp(location.pathname);
- const isVideoApp = currentApp?.id === 'videos';
- const [searchInput, setSearchInput] = useState(searchParams.get('search') || '');
-
- // Sync search input with URL params
- useEffect(() => {
- setSearchInput(searchParams.get('search') || '');
- }, [searchParams]);
-
- const handleLogout = async () => {
- await logout();
- };
-
- const handleSearchSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- const newParams = new URLSearchParams(searchParams);
- if (searchInput) {
- newParams.set('search', searchInput);
- } else {
- newParams.delete('search');
- }
- newParams.set('page', '1');
- setSearchParams(newParams);
- };
-
- const handleSortChange = (e: React.ChangeEvent) => {
- const newParams = new URLSearchParams(searchParams);
- newParams.set('sort', e.target.value);
- newParams.set('page', '1');
- setSearchParams(newParams);
- };
-
- const handleChannelChange = (e: React.ChangeEvent) => {
- const newParams = new URLSearchParams(searchParams);
- if (e.target.value) {
- newParams.set('channel', e.target.value);
- } else {
- newParams.delete('channel');
- }
- newParams.set('page', '1');
- setSearchParams(newParams);
- };
-
- const handleClearFilters = () => {
- setSearchInput('');
- setSearchParams(new URLSearchParams());
- };
-
- const hasFilters = searchParams.get('search') || searchParams.get('channel') ||
- (searchParams.get('sort') && searchParams.get('sort') !== 'newest');
-
- return (
- <>
-
-
-
-
-
-
Rainbows, Cupcakes & Unicorns
-
-
-
-
-
- Home
-
-
- {isAdmin && (
-
- Admin
-
- )}
-
- {isAuthenticated ? (
-
- ) : (
-
- Login
-
- )}
-
-
-
-
-
- {isVideoApp && (
-
-
-
-
-
-
-
-
-
-
- {hasFilters && (
-
- )}
-
-
-
-
- )}
- >
- );
+ const { isAuthenticated, logout, isAdmin } = useAuth();
+ const location = useLocation();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const { channels } = useChannels();
+
+ // Detect current app from registry
+ const getCurrentApp = (pathname: string) => {
+ return APPS.find(app => pathname === app.link || pathname.startsWith(app.link + '/'));
+ };
+
+ const currentApp = getCurrentApp(location.pathname);
+ const isVideoApp = currentApp?.id === 'videos';
+ const [searchInput, setSearchInput] = useState(searchParams.get('search') || '');
+
+ // Sync search input with URL params
+ useEffect(() => {
+ setSearchInput(searchParams.get('search') || '');
+ }, [searchParams]);
+
+ const handleLogout = async () => {
+ await logout();
+ };
+
+ const handleSearchSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ const newParams = new URLSearchParams(searchParams);
+ if (searchInput) {
+ newParams.set('search', searchInput);
+ } else {
+ newParams.delete('search');
+ }
+ newParams.set('page', '1');
+ setSearchParams(newParams);
+ };
+
+ const handleSortChange = (e: React.ChangeEvent) => {
+ const newParams = new URLSearchParams(searchParams);
+ newParams.set('sort', e.target.value);
+ newParams.set('page', '1');
+ setSearchParams(newParams);
+ };
+
+ const handleChannelChange = (e: React.ChangeEvent) => {
+ const newParams = new URLSearchParams(searchParams);
+ if (e.target.value) {
+ newParams.set('channel', e.target.value);
+ } else {
+ newParams.delete('channel');
+ }
+ newParams.set('page', '1');
+ setSearchParams(newParams);
+ };
+
+ const handleClearFilters = () => {
+ setSearchInput('');
+ setSearchParams(new URLSearchParams());
+ };
+
+ const hasFilters = searchParams.get('search') || searchParams.get('channel') ||
+ (searchParams.get('sort') && searchParams.get('sort') !== 'newest');
+
+ return (
+ <>
+
+
+
+
+
+
Rainbows, Cupcakes & Unicorns
+
+
+
+
+
+ Home
+
+
+ {!isAuthenticated && (
+
+ Sign In / Register
+
+ )}
+
+ {isAdmin && (
+
+ Admin
+
+ )}
+
+ {isAuthenticated && (
+
+ )}
+
+
+
+
+
+ {isVideoApp && (
+
+
+
+
+
+
+
+
+
+
+ {hasFilters && (
+
+ )}
+
+
+
+
+ )}
+ >
+ );
}
diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx
index f0ab1a1..f6fe703 100644
--- a/frontend/src/pages/LoginPage.tsx
+++ b/frontend/src/pages/LoginPage.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
export function LoginPage() {
@@ -27,7 +27,7 @@ export function LoginPage() {
};
return (
-
+
Admin Login
@@ -75,10 +75,22 @@ export function LoginPage() {
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx
new file mode 100644
index 0000000..6c95af9
--- /dev/null
+++ b/frontend/src/pages/RegisterPage.tsx
@@ -0,0 +1,185 @@
+import { useState } from 'react';
+import { useNavigate, Link } from 'react-router-dom';
+import { useAuth } from '../hooks/useAuth';
+
+export function RegisterPage() {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [dateOfBirth, setDateOfBirth] = useState('');
+ const [error, setError] = useState
(null);
+ const [loading, setLoading] = useState(false);
+
+ const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError(null);
+
+ // Validation
+ if (username.length < 3) {
+ setError('Username must be at least 3 characters long');
+ return;
+ }
+
+ if (password.length < 8) {
+ setError('Password must be at least 8 characters long');
+ return;
+ }
+
+ if (password !== confirmPassword) {
+ setError('Passwords do not match');
+ return;
+ }
+
+ if (!dateOfBirth) {
+ setError('Date of birth is required');
+ return;
+ }
+
+ // Validate age on frontend as well
+ const birthDate = new Date(dateOfBirth);
+ const today = new Date();
+ const age = today.getFullYear() - birthDate.getFullYear();
+ const monthDiff = today.getMonth() - birthDate.getMonth();
+ const dayDiff = today.getDate() - birthDate.getDate();
+
+ let actualAge = age;
+ if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
+ actualAge--;
+ }
+
+ if (actualAge < 18) {
+ setError('You must be at least 18 years old to register');
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ const { authApi } = await import('../services/apiClient');
+ const response: any = await authApi.register(username, password, dateOfBirth);
+
+ // Registration endpoint returns tokens and user data, same as login
+ // Use login function to set user and token in auth context
+ // This ensures consistent state management
+ await login(username, password);
+
+ // Navigate to home page
+ navigate('/');
+ } catch (err: any) {
+ setError(err.error?.message || 'Registration failed. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
Create Account
+
Sign up to get started
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts
index 282619c..c09ba7e 100644
--- a/frontend/src/services/apiClient.ts
+++ b/frontend/src/services/apiClient.ts
@@ -87,6 +87,9 @@ api.interceptors.response.use(
// Auth API
export const authApi = {
+ register: (username: string, password: string, dateOfBirth: string) =>
+ api.post('/auth/register', { username, password, dateOfBirth }),
+
login: (username: string, password: string) =>
api.post('/auth/login', { username, password }),