Show store logo in sidebar with Amplified by Forte branding

Sidebar header now loads the store logo from the files API and
displays it scaled to fit. Below the logo: "Amplified by Forte"
in subtle text. Falls back to store name as text if no logo is
uploaded. Logo fetched via authenticated request with blob URL
and proper cleanup.
This commit is contained in:
Ryan Moon
2026-03-29 16:32:18 -05:00
parent f9bf1c9bff
commit 1002117610

View File

@@ -1,6 +1,8 @@
import { createFileRoute, Outlet, Link, redirect, useRouter } from '@tanstack/react-router' import { createFileRoute, Outlet, Link, redirect, useRouter } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react' import { queryOptions } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { api } from '@/lib/api-client'
import { useAuthStore } from '@/stores/auth.store' import { useAuthStore } from '@/stores/auth.store'
import { myPermissionsOptions } from '@/api/rbac' import { myPermissionsOptions } from '@/api/rbac'
import { Avatar } from '@/components/shared/avatar-upload' import { Avatar } from '@/components/shared/avatar-upload'
@@ -17,6 +19,49 @@ export const Route = createFileRoute('/_authenticated')({
component: AuthenticatedLayout, component: AuthenticatedLayout,
}) })
function StoreLogo() {
const token = useAuthStore((s) => s.token)
const [src, setSrc] = useState<string | null>(null)
const { data: storeData } = useQuery(queryOptions({
queryKey: ['store'],
queryFn: () => api.get<{ id: string; name: string }>('/v1/store'),
enabled: !!token,
}))
const { data: filesData } = useQuery(queryOptions({
queryKey: ['files', 'company', storeData?.id ?? ''],
queryFn: () => api.get<{ data: { id: string; path: string }[] }>('/v1/files', { entityType: 'company', entityId: storeData?.id }),
enabled: !!storeData?.id,
}))
const logoFile = filesData?.data?.find((f) => f.path.includes('/logo-'))
useEffect(() => {
if (!logoFile || !token) { setSrc(null); return }
let cancelled = false
let blobUrl: string | null = null
async function load() {
try {
const res = await fetch(`/v1/files/serve/${logoFile!.path}`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok || cancelled) return
const blob = await res.blob()
if (!cancelled) { blobUrl = URL.createObjectURL(blob); setSrc(blobUrl) }
} catch { /* ignore */ }
}
load()
return () => { cancelled = true; if (blobUrl) URL.revokeObjectURL(blobUrl) }
}, [logoFile?.path, token])
if (src) {
return <img src={src} alt={storeData?.name ?? 'Store'} className="max-h-10 max-w-[180px] object-contain" />
}
return <h2 className="text-lg font-semibold text-sidebar-foreground">{storeData?.name ?? 'Forte'}</h2>
}
function NavLink({ to, icon, label }: { to: string; icon: React.ReactNode; label: string }) { function NavLink({ to, icon, label }: { to: string; icon: React.ReactNode; label: string }) {
return ( return (
<Link <Link
@@ -64,8 +109,9 @@ function AuthenticatedLayout() {
<div className="min-h-screen bg-background text-foreground"> <div className="min-h-screen bg-background text-foreground">
<div className="flex"> <div className="flex">
<nav className="w-56 border-r border-border bg-sidebar min-h-screen flex flex-col"> <nav className="w-56 border-r border-border bg-sidebar min-h-screen flex flex-col">
<div className="p-4"> <div className="p-4 space-y-1">
<h2 className="text-lg font-semibold text-sidebar-foreground">Forte</h2> <StoreLogo />
<p className="text-[10px] text-sidebar-foreground/40 tracking-wide">Amplified by <span className="font-semibold text-sidebar-foreground/60">Forte</span></p>
</div> </div>
{/* Sidebar links use `as any` on search because TanStack Router {/* Sidebar links use `as any` on search because TanStack Router