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 { 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 { myPermissionsOptions } from '@/api/rbac'
import { Avatar } from '@/components/shared/avatar-upload'
@@ -17,6 +19,49 @@ export const Route = createFileRoute('/_authenticated')({
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 }) {
return (
<Link
@@ -64,8 +109,9 @@ function AuthenticatedLayout() {
<div className="min-h-screen bg-background text-foreground">
<div className="flex">
<nav className="w-56 border-r border-border bg-sidebar min-h-screen flex flex-col">
<div className="p-4">
<h2 className="text-lg font-semibold text-sidebar-foreground">Forte</h2>
<div className="p-4 space-y-1">
<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>
{/* Sidebar links use `as any` on search because TanStack Router