diff --git a/.gsd/milestones/M019/slices/S02/S02-PLAN.md b/.gsd/milestones/M019/slices/S02/S02-PLAN.md index fc356e0..a363e05 100644 --- a/.gsd/milestones/M019/slices/S02/S02-PLAN.md +++ b/.gsd/milestones/M019/slices/S02/S02-PLAN.md @@ -148,7 +148,7 @@ - Estimate: 1.5h - Files: frontend/src/context/AuthContext.tsx, frontend/src/api/public-client.ts, frontend/src/pages/Login.tsx, frontend/src/pages/Login.module.css, frontend/src/pages/Register.tsx, frontend/src/pages/Register.module.css, frontend/src/App.tsx - Verify: cd frontend && npm run build && test -f src/context/AuthContext.tsx && test -f src/pages/Login.tsx && test -f src/pages/Register.tsx -- [ ] **T04: Build dashboard shell, profile settings, ProtectedRoute, and nav auth state** — Create the creator dashboard shell page with sidebar navigation, profile settings page, ProtectedRoute wrapper component, and update the main nav to show authenticated user state. +- [x] **T04: Added ProtectedRoute component, CreatorDashboard with sidebar nav, CreatorSettings with profile/password forms, protected routes in App.tsx, and auth-aware header nav** — Create the creator dashboard shell page with sidebar navigation, profile settings page, ProtectedRoute wrapper component, and update the main nav to show authenticated user state. ## Steps diff --git a/.gsd/milestones/M019/slices/S02/tasks/T03-VERIFY.json b/.gsd/milestones/M019/slices/S02/tasks/T03-VERIFY.json new file mode 100644 index 0000000..35ce271 --- /dev/null +++ b/.gsd/milestones/M019/slices/S02/tasks/T03-VERIFY.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "taskId": "T03", + "unitId": "M019/S02/T03", + "timestamp": 1775253488528, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd frontend", + "exitCode": 0, + "durationMs": 4, + "verdict": "pass" + }, + { + "command": "npm run build", + "exitCode": 254, + "durationMs": 90, + "verdict": "fail" + }, + { + "command": "test -f src/context/AuthContext.tsx", + "exitCode": 1, + "durationMs": 5, + "verdict": "fail" + }, + { + "command": "test -f src/pages/Login.tsx", + "exitCode": 1, + "durationMs": 5, + "verdict": "fail" + }, + { + "command": "test -f src/pages/Register.tsx", + "exitCode": 1, + "durationMs": 5, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M019/slices/S02/tasks/T04-SUMMARY.md b/.gsd/milestones/M019/slices/S02/tasks/T04-SUMMARY.md new file mode 100644 index 0000000..63b93c6 --- /dev/null +++ b/.gsd/milestones/M019/slices/S02/tasks/T04-SUMMARY.md @@ -0,0 +1,97 @@ +--- +id: T04 +parent: S02 +milestone: M019 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/components/ProtectedRoute.tsx", "frontend/src/pages/CreatorDashboard.tsx", "frontend/src/pages/CreatorDashboard.module.css", "frontend/src/pages/CreatorSettings.tsx", "frontend/src/pages/CreatorSettings.module.css", "frontend/src/App.tsx", "frontend/src/App.css", "frontend/src/pages/Login.tsx"] +key_decisions: ["Extracted AppShell from App to allow useAuth() inside AuthProvider", "Login page honors returnTo query param from ProtectedRoute redirect", "SidebarNav exported from CreatorDashboard for reuse in CreatorSettings"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "All task and slice verification checks pass: npm run build exits 0 with zero TypeScript errors, all required files exist, ProtectedRoute and useAuth grep checks pass in App.tsx, backend model/auth/schema imports succeed." +completed_at: 2026-04-03T22:02:01.042Z +blocker_discovered: false +--- + +# T04: Added ProtectedRoute component, CreatorDashboard with sidebar nav, CreatorSettings with profile/password forms, protected routes in App.tsx, and auth-aware header nav + +> Added ProtectedRoute component, CreatorDashboard with sidebar nav, CreatorSettings with profile/password forms, protected routes in App.tsx, and auth-aware header nav + +## What Happened +--- +id: T04 +parent: S02 +milestone: M019 +key_files: + - frontend/src/components/ProtectedRoute.tsx + - frontend/src/pages/CreatorDashboard.tsx + - frontend/src/pages/CreatorDashboard.module.css + - frontend/src/pages/CreatorSettings.tsx + - frontend/src/pages/CreatorSettings.module.css + - frontend/src/App.tsx + - frontend/src/App.css + - frontend/src/pages/Login.tsx +key_decisions: + - Extracted AppShell from App to allow useAuth() inside AuthProvider + - Login page honors returnTo query param from ProtectedRoute redirect + - SidebarNav exported from CreatorDashboard for reuse in CreatorSettings +duration: "" +verification_result: passed +completed_at: 2026-04-03T22:02:01.043Z +blocker_discovered: false +--- + +# T04: Added ProtectedRoute component, CreatorDashboard with sidebar nav, CreatorSettings with profile/password forms, protected routes in App.tsx, and auth-aware header nav + +**Added ProtectedRoute component, CreatorDashboard with sidebar nav, CreatorSettings with profile/password forms, protected routes in App.tsx, and auth-aware header nav** + +## What Happened + +Created ProtectedRoute that checks isAuthenticated and redirects to /login?returnTo=... when unauthenticated. Built CreatorDashboard with two-column layout (sidebar + content) using NavLink for active state, with placeholder cards for future analytics. Built CreatorSettings reusing the same SidebarNav, with profile (display_name editable, email read-only) and password change sections calling authUpdateProfile(). Restructured App.tsx by extracting AppShell so useAuth() works inside AuthProvider, added AuthNav component showing user name + dashboard link + logout when authenticated or login link when not. Updated Login.tsx to honor returnTo param. All CSS uses modules with existing dark theme tokens. + +## Verification + +All task and slice verification checks pass: npm run build exits 0 with zero TypeScript errors, all required files exist, ProtectedRoute and useAuth grep checks pass in App.tsx, backend model/auth/schema imports succeed. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 3300ms | +| 2 | `test -f src/components/ProtectedRoute.tsx` | 0 | ✅ pass | 50ms | +| 3 | `test -f src/pages/CreatorDashboard.tsx && test -f src/pages/CreatorSettings.tsx` | 0 | ✅ pass | 50ms | +| 4 | `grep -q 'ProtectedRoute' src/App.tsx` | 0 | ✅ pass | 50ms | +| 5 | `grep -q 'useAuth' src/App.tsx` | 0 | ✅ pass | 50ms | +| 6 | `cd backend && python -c "from models import User, InviteCode, UserRole; print('OK')"` | 0 | ✅ pass | 500ms | +| 7 | `cd backend && python -c "from auth import hash_password, verify_password, create_access_token, decode_access_token; print('OK')"` | 0 | ✅ pass | 500ms | +| 8 | `cd backend && python -c "from schemas import RegisterRequest, LoginRequest, TokenResponse, UserResponse, UpdateProfileRequest; print('OK')"` | 0 | ✅ pass | 500ms | + + +## Deviations + +Extracted AppShell from App to allow useAuth() inside AuthProvider. AuthNav added as separate component rather than inline. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/components/ProtectedRoute.tsx` +- `frontend/src/pages/CreatorDashboard.tsx` +- `frontend/src/pages/CreatorDashboard.module.css` +- `frontend/src/pages/CreatorSettings.tsx` +- `frontend/src/pages/CreatorSettings.module.css` +- `frontend/src/App.tsx` +- `frontend/src/App.css` +- `frontend/src/pages/Login.tsx` + + +## Deviations +Extracted AppShell from App to allow useAuth() inside AuthProvider. AuthNav added as separate component rather than inline. + +## Known Issues +None. diff --git a/frontend/src/App.css b/frontend/src/App.css index eda2f1d..f571ae5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -778,6 +778,48 @@ a.app-footer__repo:hover { color: var(--color-text-on-header-hover); } +/* ── Auth nav ─────────────────────────────────────────────────────────────── */ + +.auth-nav__user { + color: var(--color-accent); + text-decoration: none; + font-size: 0.875rem; + font-weight: 500; + transition: color 0.15s; +} + +.auth-nav__user:hover { + color: var(--color-accent-hover); +} + +.auth-nav__logout { + font-family: inherit; + font-size: 0.8125rem; + color: var(--color-text-secondary); + background: none; + border: 1px solid var(--color-border); + border-radius: 6px; + padding: 0.25rem 0.625rem; + cursor: pointer; + transition: color 0.15s, border-color 0.15s; +} + +.auth-nav__logout:hover { + color: var(--color-text-primary); + border-color: var(--color-text-secondary); +} + +.auth-nav__login { + color: var(--color-text-on-header); + text-decoration: none; + font-size: 0.875rem; + transition: color 0.15s; +} + +.auth-nav__login:hover { + color: var(--color-accent); +} + /* ── Admin dropdown ───────────────────────────────────────────────────────── */ .admin-dropdown { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c6615c4..cc79c1b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,12 +13,38 @@ import AdminTechniquePages from "./pages/AdminTechniquePages"; import About from "./pages/About"; import Login from "./pages/Login"; import Register from "./pages/Register"; +import CreatorDashboard from "./pages/CreatorDashboard"; +import CreatorSettings from "./pages/CreatorSettings"; import AdminDropdown from "./components/AdminDropdown"; import AppFooter from "./components/AppFooter"; import SearchAutocomplete from "./components/SearchAutocomplete"; -import { AuthProvider } from "./context/AuthContext"; +import ProtectedRoute from "./components/ProtectedRoute"; +import { AuthProvider, useAuth } from "./context/AuthContext"; -export default function App() { +function AuthNav() { + const { isAuthenticated, user, logout } = useAuth(); + const navigate = useNavigate(); + + if (isAuthenticated) { + return ( + <> + + {user?.display_name ?? "Dashboard"} + + + + ); + } + + return Login; +} + +function AppShell() { const location = useLocation(); const navigate = useNavigate(); const showNavSearch = location.pathname !== "/"; @@ -61,7 +87,6 @@ export default function App() { }, [menuOpen, handleOutsideClick]); return ( -
Skip to content
@@ -108,6 +133,7 @@ export default function App() { Home Topics Creators + {/* Mobile-only: search bar inside menu when not shown in header */} {menuOpen && showNavSearch && ( @@ -148,6 +174,10 @@ export default function App() { } /> } /> + {/* Creator routes (protected) */} + } /> + } /> + {/* Fallback */} } /> @@ -155,6 +185,13 @@ export default function App() {
+ ); +} + +export default function App() { + return ( + + ); } diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..101a041 --- /dev/null +++ b/frontend/src/components/ProtectedRoute.tsx @@ -0,0 +1,24 @@ +import { Navigate, useLocation } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import type { ReactNode } from "react"; + +/** + * Wraps routes that require authentication. + * Redirects to /login with a returnTo param if not authenticated. + * Shows nothing while the auth state is still loading (token rehydration). + */ +export default function ProtectedRoute({ children }: { children: ReactNode }) { + const { isAuthenticated, loading } = useAuth(); + const location = useLocation(); + + if (loading) { + return null; // Wait for token rehydration before redirecting + } + + if (!isAuthenticated) { + const returnTo = location.pathname + location.search; + return ; + } + + return <>{children}; +} diff --git a/frontend/src/pages/CreatorDashboard.module.css b/frontend/src/pages/CreatorDashboard.module.css new file mode 100644 index 0000000..f0f4afe --- /dev/null +++ b/frontend/src/pages/CreatorDashboard.module.css @@ -0,0 +1,132 @@ +.layout { + display: flex; + gap: 0; + min-height: 60vh; +} + +/* ── Sidebar ───────────────────────────────────────────────────────────────── */ + +.sidebar { + width: 220px; + flex-shrink: 0; + background: var(--color-bg-surface); + border-right: 1px solid var(--color-border); + border-radius: 12px 0 0 12px; + padding: 1.5rem 0; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.sidebarLink { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.625rem 1.25rem; + color: var(--color-text-secondary); + text-decoration: none; + font-size: 0.875rem; + font-weight: 500; + transition: color 0.15s, background 0.15s; + border: none; + background: none; + cursor: pointer; + width: 100%; + text-align: left; +} + +.sidebarLink:hover { + color: var(--color-text-primary); + background: var(--color-bg-surface-hover); +} + +.sidebarLinkActive { + color: var(--color-accent) !important; + background: var(--color-accent-subtle); +} + +.sidebarLinkDisabled { + opacity: 0.4; + cursor: not-allowed; +} + +.sidebarLinkDisabled:hover { + background: none; + color: var(--color-text-secondary); +} + +.sidebarIcon { + width: 18px; + height: 18px; + flex-shrink: 0; +} + +/* ── Content area ──────────────────────────────────────────────────────────── */ + +.content { + flex: 1; + min-width: 0; + padding: 2rem; +} + +.welcome { + margin: 0 0 1.5rem; + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text-primary); +} + +.cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 1rem; +} + +.card { + background: var(--color-bg-surface); + border: 1px solid var(--color-border); + border-radius: 10px; + padding: 1.25rem; +} + +.cardTitle { + font-size: 0.875rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0 0 0.5rem; +} + +.cardBody { + font-size: 0.8125rem; + color: var(--color-text-muted); + line-height: 1.5; +} + +/* ── Mobile ────────────────────────────────────────────────────────────────── */ + +@media (max-width: 768px) { + .layout { + flex-direction: column; + } + + .sidebar { + width: 100%; + flex-direction: row; + overflow-x: auto; + border-right: none; + border-bottom: 1px solid var(--color-border); + border-radius: 12px 12px 0 0; + padding: 0.75rem 0.5rem; + gap: 0; + } + + .sidebarLink { + padding: 0.5rem 1rem; + white-space: nowrap; + font-size: 0.8125rem; + } + + .content { + padding: 1.25rem; + } +} diff --git a/frontend/src/pages/CreatorDashboard.tsx b/frontend/src/pages/CreatorDashboard.tsx new file mode 100644 index 0000000..c580d82 --- /dev/null +++ b/frontend/src/pages/CreatorDashboard.tsx @@ -0,0 +1,79 @@ +import { NavLink } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; +import styles from "./CreatorDashboard.module.css"; + +function SidebarNav() { + const linkClass = ({ isActive }: { isActive: boolean }) => + `${styles.sidebarLink}${isActive ? ` ${styles.sidebarLinkActive}` : ""}`; + + return ( + + ); +} + +export { SidebarNav }; + +export default function CreatorDashboard() { + useDocumentTitle("Creator Dashboard"); + const { user } = useAuth(); + + return ( +
+ +
+

+ Welcome back{user?.display_name ? `, ${user.display_name}` : ""} +

+
+
+

Content Stats

+

+ Content analytics coming in M020. You'll see views, engagement, and technique performance here. +

+
+
+

Recent Activity

+

+ Activity feed coming soon. Track updates to your technique pages. +

+
+
+

Quick Actions

+

+ Content management tools will appear here once the creator content module is live. +

+
+
+
+
+ ); +} diff --git a/frontend/src/pages/CreatorSettings.module.css b/frontend/src/pages/CreatorSettings.module.css new file mode 100644 index 0000000..7d3606f --- /dev/null +++ b/frontend/src/pages/CreatorSettings.module.css @@ -0,0 +1,115 @@ +.heading { + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0 0 1.5rem; +} + +/* ── Sections ──────────────────────────────────────────────────────────────── */ + +.section { + background: var(--color-bg-surface); + border: 1px solid var(--color-border); + border-radius: 10px; + padding: 1.5rem; + margin-bottom: 1.25rem; +} + +.sectionTitle { + font-size: 0.9375rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0 0 1rem; +} + +/* ── Form elements ─────────────────────────────────────────────────────────── */ + +.fieldGroup { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.label { + display: flex; + flex-direction: column; + gap: 0.375rem; + font-size: 0.875rem; + color: var(--color-text-secondary); + font-weight: 500; +} + +.input { + padding: 0.625rem 0.75rem; + background: var(--color-bg-input); + border: 1px solid var(--color-border); + border-radius: 8px; + color: var(--color-text-primary); + font-size: 0.9375rem; + transition: border-color 0.15s; +} + +.input:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 2px var(--color-accent-focus); +} + +.inputReadonly { + composes: input; + opacity: 0.6; + cursor: not-allowed; +} + +/* ── Button ────────────────────────────────────────────────────────────────── */ + +.button { + margin-top: 0.75rem; + padding: 0.625rem 1.25rem; + background: var(--color-accent); + color: #0f0f14; + border: none; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + align-self: flex-start; +} + +.button:hover:not(:disabled) { + background: var(--color-accent-hover); +} + +.button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* ── Feedback ──────────────────────────────────────────────────────────────── */ + +.error { + background: rgba(239, 68, 68, 0.1); + color: #f87171; + border: 1px solid rgba(239, 68, 68, 0.2); + border-radius: 8px; + padding: 0.75rem; + font-size: 0.875rem; + margin-bottom: 1rem; +} + +.success { + background: rgba(34, 197, 94, 0.1); + color: #4ade80; + border: 1px solid rgba(34, 197, 94, 0.2); + border-radius: 8px; + padding: 0.75rem; + font-size: 0.875rem; + margin-bottom: 1rem; +} + +.divider { + border: none; + border-top: 1px solid var(--color-border); + margin: 1.25rem 0; +} diff --git a/frontend/src/pages/CreatorSettings.tsx b/frontend/src/pages/CreatorSettings.tsx new file mode 100644 index 0000000..437f629 --- /dev/null +++ b/frontend/src/pages/CreatorSettings.tsx @@ -0,0 +1,174 @@ +import { useState, type FormEvent } from "react"; +import { useAuth, ApiError } from "../context/AuthContext"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; +import { authUpdateProfile } from "../api/public-client"; +import { SidebarNav } from "./CreatorDashboard"; +import dashStyles from "./CreatorDashboard.module.css"; +import styles from "./CreatorSettings.module.css"; + +export default function CreatorSettings() { + useDocumentTitle("Settings"); + const { user, token } = useAuth(); + + // Profile fields + const [displayName, setDisplayName] = useState(user?.display_name ?? ""); + const [profileMsg, setProfileMsg] = useState<{ type: "success" | "error"; text: string } | null>(null); + const [profileSubmitting, setProfileSubmitting] = useState(false); + + // Password fields + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [passwordMsg, setPasswordMsg] = useState<{ type: "success" | "error"; text: string } | null>(null); + const [passwordSubmitting, setPasswordSubmitting] = useState(false); + + const handleProfileSubmit = async (e: FormEvent) => { + e.preventDefault(); + setProfileMsg(null); + + if (!displayName.trim()) { + setProfileMsg({ type: "error", text: "Display name cannot be empty." }); + return; + } + + if (!token) return; + setProfileSubmitting(true); + try { + await authUpdateProfile(token, { display_name: displayName.trim() }); + setProfileMsg({ type: "success", text: "Profile updated." }); + } catch (err) { + setProfileMsg({ + type: "error", + text: err instanceof ApiError ? err.detail : "Failed to update profile.", + }); + } finally { + setProfileSubmitting(false); + } + }; + + const handlePasswordSubmit = async (e: FormEvent) => { + e.preventDefault(); + setPasswordMsg(null); + + if (!currentPassword || !newPassword) { + setPasswordMsg({ type: "error", text: "Current and new password are required." }); + return; + } + if (newPassword.length < 8) { + setPasswordMsg({ type: "error", text: "New password must be at least 8 characters." }); + return; + } + if (newPassword !== confirmPassword) { + setPasswordMsg({ type: "error", text: "New passwords don't match." }); + return; + } + + if (!token) return; + setPasswordSubmitting(true); + try { + await authUpdateProfile(token, { + current_password: currentPassword, + new_password: newPassword, + }); + setPasswordMsg({ type: "success", text: "Password changed successfully." }); + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + } catch (err) { + setPasswordMsg({ + type: "error", + text: err instanceof ApiError ? err.detail : "Failed to change password.", + }); + } finally { + setPasswordSubmitting(false); + } + }; + + return ( +
+ +
+

Settings

+ + {/* Profile section */} +
+

Profile

+ {profileMsg && ( +
+ {profileMsg.text} +
+ )} +
+ + + +
+
+ + {/* Password section */} +
+

Change Password

+ {passwordMsg && ( +
+ {passwordMsg.text} +
+ )} +
+ + + + +
+
+
+
+ ); +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 74cb7cf..c36c9c7 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -18,6 +18,7 @@ export default function Login() { // Check for success message from registration redirect const params = new URLSearchParams(location.search); const registered = params.get("registered") === "1"; + const returnTo = params.get("returnTo"); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); @@ -31,7 +32,7 @@ export default function Login() { setSubmitting(true); try { await login(email.trim(), password); - navigate("/creator/dashboard", { replace: true }); + navigate(returnTo || "/creator/dashboard", { replace: true }); } catch (err) { if (err instanceof ApiError) { setError(err.detail); diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 4de82e0..68a2c2c 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file