Add store logo and app icon uploads to settings page

AvatarUpload component now supports custom category and placeholder
icon props. Settings page shows two upload circles: Store Logo (for
PDFs/invoices, uses ImageIcon placeholder) and App Icon (for sidebar/
login, uses Building placeholder). Added 'company' to allowed file
entity types.
This commit is contained in:
Ryan Moon
2026-03-29 16:14:08 -05:00
parent 653fff6ce2
commit 8d75586f8b
3 changed files with 26 additions and 9 deletions

View File

@@ -23,9 +23,11 @@ function entityFilesOptions(entityType: string, entityId: string) {
} }
interface AvatarUploadProps { interface AvatarUploadProps {
entityType: 'user' | 'member' entityType: 'user' | 'member' | 'company'
entityId: string entityId: string
size?: 'sm' | 'md' | 'lg' size?: 'sm' | 'md' | 'lg'
category?: string
placeholderIcon?: React.ComponentType<{ className?: string }>
} }
const sizeClasses = { const sizeClasses = {
@@ -40,16 +42,17 @@ const iconSizes = {
lg: 'h-12 w-12', lg: 'h-12 w-12',
} }
export function AvatarUpload({ entityType, entityId, size = 'lg' }: AvatarUploadProps) { export function AvatarUpload({ entityType, entityId, size = 'lg', category = 'profile', placeholderIcon: PlaceholderIcon }: AvatarUploadProps) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const token = useAuthStore((s) => s.token) const token = useAuthStore((s) => s.token)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const IconComponent = PlaceholderIcon ?? User
const { data: filesData } = useQuery(entityFilesOptions(entityType, entityId)) const { data: filesData } = useQuery(entityFilesOptions(entityType, entityId))
// Find profile image from files // Find image by category
const profileFile = filesData?.data?.find((f) => f.path.includes('/profile-')) const profileFile = filesData?.data?.find((f) => f.path.includes(`/${category}-`))
const imageUrl = profileFile ? `/v1/files/serve/${profileFile.path}` : null const imageUrl = profileFile ? `/v1/files/serve/${profileFile.path}` : null
async function handleUpload(file: File) { async function handleUpload(file: File) {
@@ -59,7 +62,7 @@ export function AvatarUpload({ entityType, entityId, size = 'lg' }: AvatarUpload
formData.append('file', file) formData.append('file', file)
formData.append('entityType', entityType) formData.append('entityType', entityType)
formData.append('entityId', entityId) formData.append('entityId', entityId)
formData.append('category', 'profile') formData.append('category', category)
// Delete existing profile image first // Delete existing profile image first
if (profileFile) { if (profileFile) {
@@ -105,7 +108,7 @@ export function AvatarUpload({ entityType, entityId, size = 'lg' }: AvatarUpload
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />
) : ( ) : (
<User className={`${iconSizes[size]} text-muted-foreground`} /> <IconComponent className={`${iconSizes[size]} text-muted-foreground`} />
)} )}
</div> </div>
<Button <Button

View File

@@ -13,7 +13,7 @@ import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { AvatarUpload } from '@/components/shared/avatar-upload' import { AvatarUpload } from '@/components/shared/avatar-upload'
import { Save, Plus, Trash2, MapPin, Building } from 'lucide-react' import { Save, Plus, Trash2, MapPin, Building, ImageIcon } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
interface StoreSettings { interface StoreSettings {
@@ -132,7 +132,21 @@ function SettingsPage() {
</div> </div>
)} )}
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-6">
{/* Logo upload */}
<div className="flex items-start gap-8">
<div className="text-center space-y-2">
<Label className="text-xs text-muted-foreground">Store Logo</Label>
<AvatarUpload entityType="company" entityId={store.id} size="lg" category="logo" placeholderIcon={ImageIcon} />
<p className="text-[10px] text-muted-foreground">Used on PDFs, invoices, receipts</p>
</div>
<div className="text-center space-y-2">
<Label className="text-xs text-muted-foreground">App Icon</Label>
<AvatarUpload entityType="company" entityId={store.id} size="md" category="icon" placeholderIcon={Building} />
<p className="text-[10px] text-muted-foreground">Sidebar & login screen</p>
</div>
</div>
{editing ? ( {editing ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">

View File

@@ -41,7 +41,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
} }
// Validate entityType is a known type // Validate entityType is a known type
const allowedEntityTypes = ['user', 'member', 'member_identifier', 'product', 'rental_agreement', 'repair_ticket', 'repair_note'] const allowedEntityTypes = ['user', 'member', 'member_identifier', 'product', 'rental_agreement', 'repair_ticket', 'repair_note', 'company']
if (!allowedEntityTypes.includes(entityType)) { if (!allowedEntityTypes.includes(entityType)) {
throw new ValidationError(`Invalid entityType: ${entityType}`) throw new ValidationError(`Invalid entityType: ${entityType}`)
} }