From 8077101e77ce386a54619b1ae0342492b628a169 Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Wed, 4 Mar 2026 12:57:42 +0100 Subject: [PATCH] feat(design): add header logo and show community name toggle (#154) * feat(design): add header logo upload and show community name toggle - Add headerLogoUrl and showCommunityName fields to CommunitySettings and PublicSettings types - Add uploadHeaderLogo API client function - Update ForumLayout to accept publicSettings prop instead of communityName, with three-tier header logo priority: custom header logo > community logo > default Barazo SVGs - Add header logo upload, remove, and show community name toggle to the admin design page (DesignImagesSection) - Update all 15 pages using ForumLayout to pass publicSettings - Remove inline styles from logo images (use Tailwind w-auto) - Add mock handler for POST /api/admin/design/header-logo - Update tests for ForumLayout and admin design page * fix(design): resolve rebase conflicts with main (pages, maxReplyDepth) --- src/app/accessibility/page.tsx | 7 +- src/app/admin/design/page.test.tsx | 17 +++++ src/app/admin/design/page.tsx | 39 ++++++++++- src/app/c/[slug]/page.tsx | 4 +- src/app/new/page.tsx | 8 +-- src/app/notifications/page.tsx | 8 +-- src/app/p/[slug]/page.tsx | 7 +- src/app/page.tsx | 2 +- src/app/search/page.tsx | 7 +- src/app/settings/page.tsx | 8 ++- src/app/settings/reports/page.tsx | 8 +-- src/app/t/[slug]/[rkey]/edit/page.tsx | 14 ++-- src/app/t/[slug]/[rkey]/page.tsx | 5 +- src/app/u/[handle]/edit/page.tsx | 13 ++-- src/app/u/[handle]/page.tsx | 16 ++--- .../admin/design/design-images-section.tsx | 41 ++++++++++- src/components/layout/forum-layout.test.tsx | 58 +++++++++++++++- src/components/layout/forum-layout.tsx | 68 +++++++++++++------ src/lib/api/client.ts | 16 +++++ src/lib/api/types.ts | 4 ++ src/mocks/data.ts | 4 ++ src/mocks/handlers.ts | 11 +++ 22 files changed, 285 insertions(+), 80 deletions(-) diff --git a/src/app/accessibility/page.tsx b/src/app/accessibility/page.tsx index 0efdc88..76aafc5 100644 --- a/src/app/accessibility/page.tsx +++ b/src/app/accessibility/page.tsx @@ -20,16 +20,15 @@ export const metadata: Metadata = { } export default async function AccessibilityPage() { - let communityName = '' + let publicSettings = null try { - const settings = await getPublicSettings() - communityName = settings.communityName + publicSettings = await getPublicSettings() } catch { // silently degrade } return ( - +
diff --git a/src/app/admin/design/page.test.tsx b/src/app/admin/design/page.test.tsx index 58f117e..a30f9e2 100644 --- a/src/app/admin/design/page.test.tsx +++ b/src/app/admin/design/page.test.tsx @@ -56,6 +56,14 @@ describe('AdminDesignPage', () => { expect(screen.getByRole('heading', { name: /design/i })).toBeInTheDocument() }) + it('renders header logo upload with help text', async () => { + render() + await waitFor(() => { + expect(screen.getByText('Header Logo')).toBeInTheDocument() + }) + expect(screen.getByText(/Wide logo or wordmark for the forum header/)).toBeInTheDocument() + }) + it('renders logo upload section with help text', async () => { render() await waitFor(() => { @@ -72,6 +80,15 @@ describe('AdminDesignPage', () => { expect(screen.getByText(/256×256px.*JPEG, PNG, WebP, GIF/)).toBeInTheDocument() }) + it('renders "Show community name" toggle, defaults to checked', async () => { + render() + await waitFor(() => { + const toggle = screen.getByRole('checkbox', { name: /show community name/i }) + expect(toggle).toBeInTheDocument() + expect(toggle).toBeChecked() + }) + }) + it('renders primary color input', async () => { render() await waitFor(() => { diff --git a/src/app/admin/design/page.tsx b/src/app/admin/design/page.tsx index c708f69..5a8d715 100644 --- a/src/app/admin/design/page.tsx +++ b/src/app/admin/design/page.tsx @@ -1,7 +1,7 @@ /** * Admin design page. * URL: /admin/design - * Logo upload, favicon upload, primary/accent color configuration. + * Header logo upload, community logo upload, favicon upload, primary/accent color configuration. */ 'use client' @@ -16,6 +16,7 @@ import { updateCommunitySettings, uploadCommunityLogo, uploadCommunityFavicon, + uploadHeaderLogo, } from '@/lib/api/client' import type { CommunitySettings } from '@/lib/api/types' import { useAuth } from '@/hooks/use-auth' @@ -44,6 +45,39 @@ export default function AdminDesignPage() { void fetchSettings() }, [fetchSettings]) + const handleHeaderLogoUpload = useCallback( + async (file: File) => { + const result = await uploadHeaderLogo(file, getAccessToken() ?? '') + setSettings((prev) => (prev ? { ...prev, headerLogoUrl: result.url } : prev)) + return result + }, + [getAccessToken] + ) + + const handleHeaderLogoRemove = useCallback(async () => { + try { + const updated = await updateCommunitySettings({ headerLogoUrl: null }, getAccessToken() ?? '') + setSettings(updated) + } catch { + setSaveError('Failed to remove header logo.') + } + }, [getAccessToken]) + + const handleShowCommunityNameChange = useCallback( + async (value: boolean) => { + try { + const updated = await updateCommunitySettings( + { showCommunityName: value }, + getAccessToken() ?? '' + ) + setSettings(updated) + } catch { + setSaveError('Failed to update community name visibility.') + } + }, + [getAccessToken] + ) + const handleLogoUpload = useCallback( async (file: File) => { const result = await uploadCommunityLogo(file, getAccessToken() ?? '') @@ -118,6 +152,9 @@ export default function AdminDesignPage() {
void handleHeaderLogoRemove()} + onShowCommunityNameChange={(v) => void handleShowCommunityNameChange(v)} onLogoUpload={handleLogoUpload} onLogoRemove={() => void handleLogoRemove()} onFaviconUpload={handleFaviconUpload} diff --git a/src/app/c/[slug]/page.tsx b/src/app/c/[slug]/page.tsx index a90f022..11a7060 100644 --- a/src/app/c/[slug]/page.tsx +++ b/src/app/c/[slug]/page.tsx @@ -91,8 +91,6 @@ export default async function CategoryPage({ params, searchParams }: CategoryPag getPublicSettings().catch(() => null), ]) - const communityName = publicSettings?.communityName ?? '' - const totalPages = Math.max(1, Math.ceil(category.topicCount / TOPICS_PER_PAGE)) const breadcrumbItems = [ @@ -102,7 +100,7 @@ export default async function CategoryPage({ params, searchParams }: CategoryPag return ( } > {/* Breadcrumbs (includes JSON-LD BreadcrumbList) */} diff --git a/src/app/new/page.tsx b/src/app/new/page.tsx index fd2774f..30809f5 100644 --- a/src/app/new/page.tsx +++ b/src/app/new/page.tsx @@ -9,7 +9,7 @@ import { useState, useEffect } from 'react' import { useRouter, useSearchParams } from 'next/navigation' -import type { CreateTopicInput } from '@/lib/api/types' +import type { CreateTopicInput, PublicSettings } from '@/lib/api/types' import { createTopic, getPublicSettings } from '@/lib/api/client' import { getTopicUrl } from '@/lib/format' import { ForumLayout } from '@/components/layout/forum-layout' @@ -26,11 +26,11 @@ export default function NewTopicPage() { const { ensureOnboarded } = useOnboardingContext() const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) - const [communityName, setCommunityName] = useState('') + const [publicSettings, setPublicSettings] = useState(null) useEffect(() => { getPublicSettings() - .then((settings) => setCommunityName(settings.communityName)) + .then((settings) => setPublicSettings(settings)) .catch(() => {}) }, []) @@ -51,7 +51,7 @@ export default function NewTopicPage() { } return ( - +
diff --git a/src/app/notifications/page.tsx b/src/app/notifications/page.tsx index 37e2533..660c4a6 100644 --- a/src/app/notifications/page.tsx +++ b/src/app/notifications/page.tsx @@ -15,7 +15,7 @@ import { Breadcrumbs } from '@/components/breadcrumbs' import { ErrorAlert } from '@/components/error-alert' import { getNotifications, markNotificationsRead, getPublicSettings } from '@/lib/api/client' import { cn } from '@/lib/utils' -import type { Notification, NotificationType } from '@/lib/api/types' +import type { Notification, NotificationType, PublicSettings } from '@/lib/api/types' import { useAuth } from '@/hooks/use-auth' import { ProtectedRoute } from '@/components/auth/protected-route' @@ -40,11 +40,11 @@ function NotificationsContent() { const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(null) const [actionError, setActionError] = useState(null) - const [communityName, setCommunityName] = useState('') + const [publicSettings, setPublicSettings] = useState(null) useEffect(() => { getPublicSettings() - .then((settings) => setCommunityName(settings.communityName)) + .then((settings) => setPublicSettings(settings)) .catch(() => {}) }, []) @@ -91,7 +91,7 @@ function NotificationsContent() { const hasUnread = notifications.some((n) => !n.read) return ( - +
diff --git a/src/app/p/[slug]/page.tsx b/src/app/p/[slug]/page.tsx index 546b6ab..585922d 100644 --- a/src/app/p/[slug]/page.tsx +++ b/src/app/p/[slug]/page.tsx @@ -60,10 +60,9 @@ export default async function PublicPage({ params }: PublicPageProps) { throw error } - let communityName = '' + let publicSettings = null try { - const settings = await getPublicSettings() - communityName = settings.communityName + publicSettings = await getPublicSettings() } catch { // silently degrade } @@ -78,7 +77,7 @@ export default async function PublicPage({ params }: PublicPageProps) { } return ( - +