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:
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user