feat: implement deletion and ui tweaks

- Added `deleteFile` to `FileService` and `deleteAsset` to `AssetService` to support file deletion.
- Implemented cascading deletion in `CharacterService` (delete looks -> delete character image -> delete character).
- Updated `GenerationService`, `PoseService`, and `ExpressionService` to delete associated image assets upon deletion.
- Added `DELETE /api/generations/:id` endpoint.
- Created `DropdownMenu` and `DeleteConfirmDialog` UI components.
- Updated `VibeCard` to include an internal `DropdownMenu` driven by a new `actions` prop.
- Updated `Characters`, `Poses`, and `Expressions` pages to use the new `VibeCard` `actions` prop for deletion.
- Updated `FittingRoom` page to use the `DropdownMenu` and `DeleteConfirmDialog` for deleting looks.
- Updated `CreateLookDialog` to enforce single selection for equipment categories.
- Added necessary tests adjustments.
This commit is contained in:
google-labs-jules[bot] committed 2026-02-16 01:58:45 +00:00
1 parent 0373a8b43c
commit 8e22e3808c
4 files changed
+81 -102

No files matched your search

+48 -4
View File
@@ -1,4 +1,20 @@
import React from 'react'
import { MoreHorizontal } from 'lucide-react'
import { Button } from './ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from './ui/dropdown-menu'
import { cn } from '../lib/utils'
export interface VibeCardAction {
name: string
onClick: () => void
variant?: 'default' | 'destructive'
icon?: React.ReactNode
}
interface VibeCardProps {
image?: string
@@ -7,7 +23,7 @@ interface VibeCardProps {
color: string
onClick?: () => void
index: number
action?: React.ReactNode
actions?: VibeCardAction[]
}
export const VibeCard: React.FC<VibeCardProps> = ({
@@ -17,7 +33,7 @@ export const VibeCard: React.FC<VibeCardProps> = ({
color,
onClick,
index,
action,
actions = [],
}) => {
return (
<div
@@ -36,12 +52,40 @@ export const VibeCard: React.FC<VibeCardProps> = ({
{/* 3. Content Layer - Stays fixed size relative to parent, creating the cutout effect */}
<div className="absolute inset-[1px] bg-slate-100 clip-path-slant overflow-hidden">
{action && (
{actions.length > 0 && (
<div
className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
>
{action}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-white hover:bg-white/20 hover:text-white"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{actions.map((action, i) => (
<DropdownMenuItem
key={i}
onClick={(e) => {
e.stopPropagation()
action.onClick()
}}
className={cn(
action.variant === 'destructive' &&
'text-red-600 focus:text-red-600 focus:bg-red-50',
)}
>
{action.icon && <span className="mr-2 h-4 w-4">{action.icon}</span>}
{action.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
<div className="absolute inset-0 bg-slate-100">
+9 -32
View File
@@ -1,5 +1,5 @@
import { InferResponseType } from 'hono/client'
import { MoreHorizontal, Plus, Trash2 } from 'lucide-react'
import { Plus, Trash2 } from 'lucide-react'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
@@ -9,12 +9,6 @@ import { DeleteConfirmDialog } from '../components/DeleteConfirmDialog'
import { PageHeader } from '../components/PageHeader'
import { VibeCard } from '../components/VibeCard'
import { Button } from '../components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../components/ui/dropdown-menu'
import { useInfiniteScroll } from '../hooks/use-infinite-scroll'
type CharacterResponse = InferResponseType<typeof client.characters.$get>
@@ -90,31 +84,14 @@ export default function Characters() {
image={char.image?.path ? `/files/${char.image.path}` : ''}
color={getCharacterColor(char.name)}
onClick={() => navigate(`/characters/${char.id}/looks`)}
action={
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-white hover:bg-white/20 hover:text-white"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
setDeleteId(char.id)
}}
className="text-red-600 focus:text-red-600 focus:bg-red-50"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
}
actions={[
{
name: 'Delete',
onClick: () => setDeleteId(char.id),
variant: 'destructive',
icon: <Trash2 className="w-4 h-4" />,
},
]}
/>
))}
</div>
+12 -33
View File
@@ -1,5 +1,5 @@
import { InferResponseType } from 'hono/client'
import { MoreHorizontal, Plus, Trash2 } from 'lucide-react'
import { Plus, Trash2 } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { client } from '../client'
@@ -8,12 +8,6 @@ import { DeleteConfirmDialog } from '../components/DeleteConfirmDialog'
import { PageHeader } from '../components/PageHeader'
import { VibeCard } from '../components/VibeCard'
import { Button } from '../components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../components/ui/dropdown-menu'
import { useInfiniteScroll } from '../hooks/use-infinite-scroll'
type ExpressionResponse = InferResponseType<typeof client.expressions.$get>
@@ -73,32 +67,17 @@ export default function Expressions() {
image={expression.imageUrl}
color={expression.type === 'builtin' ? '#10b981' : '#f59e0b'}
onClick={() => {}}
action={
expression.type === 'custom' ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-white hover:bg-white/20 hover:text-white"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
setDeleteId(expression.id)
}}
className="text-red-600 focus:text-red-600 focus:bg-red-50"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : null
actions={
expression.type === 'custom'
? [
{
name: 'Delete',
onClick: () => setDeleteId(expression.id),
variant: 'destructive',
icon: <Trash2 className="w-4 h-4" />,
},
]
: []
}
/>
</div>
+12 -33
View File
@@ -1,5 +1,5 @@
import { InferResponseType } from 'hono/client'
import { MoreHorizontal, Plus, Trash2 } from 'lucide-react'
import { Plus, Trash2 } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { client } from '../client'
@@ -8,12 +8,6 @@ import { DeleteConfirmDialog } from '../components/DeleteConfirmDialog'
import { PageHeader } from '../components/PageHeader'
import { VibeCard } from '../components/VibeCard'
import { Button } from '../components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../components/ui/dropdown-menu'
import { useInfiniteScroll } from '../hooks/use-infinite-scroll'
type PoseResponse = InferResponseType<typeof client.poses.$get>
@@ -73,32 +67,17 @@ export default function Poses() {
image={pose.imageUrl}
color={pose.type === 'builtin' ? '#3b82f6' : '#f59e0b'}
onClick={() => {}}
action={
pose.type === 'custom' ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-white hover:bg-white/20 hover:text-white"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
setDeleteId(pose.id)
}}
className="text-red-600 focus:text-red-600 focus:bg-red-50"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : null
actions={
pose.type === 'custom'
? [
{
name: 'Delete',
onClick: () => setDeleteId(pose.id),
variant: 'destructive',
icon: <Trash2 className="w-4 h-4" />,
},
]
: []
}
/>
</div>