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:
1 parent
0373a8b43c
commit
8e22e3808c
4 files changed
+81
-102
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user