Merge pull request #41 from Yiling-J/fix-fitting-room-and-extractor-save-14637309535569615021
Fix Fitting Room overflow and add Save as Outfit to Extractor
This commit is contained in:
15 files changed
+146
-34
No files matched your search
@@ -38,6 +38,7 @@ COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/bunfig.toml ./bunfig.toml
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
|
||||
COPY --from=builder /app/example-data ./example-data
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p data
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"@prisma/adapter-libsql": "7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
"path": "01KHNYBPY85F6BZAJXZDSKBQY7.webp"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -107,4 +107,4 @@
|
||||
"path": "01KHQ66H2X62ZFQ8E1WHK0DSFE.webp"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -80,4 +80,4 @@
|
||||
"path": "01KHNY8VTPEQMHJ7X0C31F6YA6.webp"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -69,4 +69,4 @@
|
||||
"01KHQ66H318BPBAE4ZFBY7JHBD"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -80,4 +80,4 @@
|
||||
"path": "01KHNY603BYFF3M1RA5AWYHCD2.webp"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -17,6 +17,7 @@
|
||||
"@prisma/adapter-libsql": "7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
|
||||
@@ -34,6 +34,10 @@ export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
|
||||
const [results, setResults] = useState<ExtractedAsset[]>([])
|
||||
const [isRefineComplete, setIsRefineComplete] = useState(false)
|
||||
|
||||
// Save as Outfit state
|
||||
const [saveAsOutfit, setSaveAsOutfit] = useState(false)
|
||||
const [outfitName, setOutfitName] = useState('')
|
||||
|
||||
// Refs for cleanup
|
||||
const analyzeController = useRef<AbortController | null>(null)
|
||||
const refineController = useRef<AbortController | null>(null)
|
||||
@@ -48,6 +52,8 @@ export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
|
||||
setSelectedIndices([])
|
||||
setResults([])
|
||||
setIsRefineComplete(false)
|
||||
setSaveAsOutfit(false)
|
||||
setOutfitName('')
|
||||
analyzeController.current?.abort()
|
||||
refineController.current?.abort()
|
||||
}
|
||||
@@ -194,7 +200,30 @@ export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
|
||||
|
||||
const handleClose = () => onOpenChange(false)
|
||||
|
||||
const handleDone = () => {
|
||||
const handleDone = async () => {
|
||||
if (saveAsOutfit) {
|
||||
if (!outfitName.trim()) {
|
||||
toast.error('Please enter an outfit name')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const equipmentIds = results.map((r) => r.id)
|
||||
const res = await client.outfits.$post({
|
||||
json: {
|
||||
name: outfitName,
|
||||
equipmentIds,
|
||||
},
|
||||
})
|
||||
if (res.ok) {
|
||||
toast.success('Outfit created')
|
||||
} else {
|
||||
toast.error('Failed to create outfit')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Failed to create outfit')
|
||||
}
|
||||
}
|
||||
onSuccess(results)
|
||||
handleClose()
|
||||
}
|
||||
@@ -234,6 +263,10 @@ export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
|
||||
selectedCandidates={candidates.filter((_, i) => selectedIndices.includes(i))}
|
||||
results={results}
|
||||
isComplete={isRefineComplete}
|
||||
saveAsOutfit={saveAsOutfit}
|
||||
setSaveAsOutfit={setSaveAsOutfit}
|
||||
outfitName={outfitName}
|
||||
setOutfitName={setOutfitName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,24 +2,35 @@ import { CheckCircle2, Loader2 } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import { cn } from '../../lib/utils'
|
||||
import { CandidateAsset, ExtractedAsset } from '../../types'
|
||||
import { Checkbox } from '../ui/checkbox'
|
||||
import { Input } from '../ui/input'
|
||||
import { Label } from '../ui/label'
|
||||
import { ScrollArea } from '../ui/scroll-area'
|
||||
|
||||
interface RefineStageProps {
|
||||
selectedCandidates: CandidateAsset[]
|
||||
results: ExtractedAsset[]
|
||||
isComplete: boolean
|
||||
saveAsOutfit: boolean
|
||||
setSaveAsOutfit: (value: boolean) => void
|
||||
outfitName: string
|
||||
setOutfitName: (value: string) => void
|
||||
}
|
||||
|
||||
export const RefineStage: React.FC<RefineStageProps> = ({
|
||||
selectedCandidates,
|
||||
results,
|
||||
isComplete,
|
||||
saveAsOutfit,
|
||||
setSaveAsOutfit,
|
||||
outfitName,
|
||||
setOutfitName,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full max-w-6xl mx-auto px-4 py-6">
|
||||
{/* Grid */}
|
||||
<ScrollArea className="h-full px-2">
|
||||
<div className="grid grid-cols-3 gap-2 p-1 h-full pb-6">
|
||||
<ScrollArea className="flex-1 px-2">
|
||||
<div className="grid grid-cols-3 gap-2 p-1 pb-6">
|
||||
{selectedCandidates.map((candidate, index) => {
|
||||
const isFinished = index < results.length
|
||||
const isProcessing = index === results.length && !isComplete
|
||||
@@ -114,6 +125,34 @@ export const RefineStage: React.FC<RefineStageProps> = ({
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{isComplete && (
|
||||
<div className="flex flex-col gap-4 mt-4 p-4 bg-white rounded-xl border border-slate-200 shadow-sm shrink-0 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="saveOutfit"
|
||||
checked={saveAsOutfit}
|
||||
onCheckedChange={(c) => setSaveAsOutfit(c === true)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="saveOutfit"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Save as Outfit
|
||||
</Label>
|
||||
</div>
|
||||
{saveAsOutfit && (
|
||||
<Input
|
||||
placeholder="Outfit Name"
|
||||
value={outfitName}
|
||||
onChange={(e) => setOutfitName(e.target.value)}
|
||||
className="w-full animate-in fade-in zoom-in duration-300"
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
@keyframes scanline {
|
||||
0% {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/ui/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
@@ -1,14 +1,14 @@
|
||||
import { InferResponseType } from 'hono/client'
|
||||
import { Plus, Trash2, Upload } from 'lucide-react'
|
||||
import { Plus, ScanLine, Trash2, Upload } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { EQUIPMENT_CATEGORIES } from '../../lib/categories'
|
||||
import { client } from '../client'
|
||||
import { CreateOutfitDialog } from '../components/CreateOutfitDialog'
|
||||
import { EquipmentDetailsDialog } from '../components/EquipmentDetailsDialog'
|
||||
import { UploadEquipmentDialog } from '../components/UploadEquipmentDialog'
|
||||
import { ExtractorDialog } from '../components/ExtractorDialog'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
import { UploadEquipmentDialog } from '../components/UploadEquipmentDialog'
|
||||
import { VibeCard } from '../components/VibeCard'
|
||||
import { Badge } from '../components/ui/badge'
|
||||
import { Button } from '../components/ui/button'
|
||||
@@ -158,11 +158,15 @@ export default function Equipments() {
|
||||
actions={
|
||||
viewMode === 'equipments' ? (
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => setUploadEquipmentOpen(true)} variant="secondary">
|
||||
<Button
|
||||
onClick={() => setUploadEquipmentOpen(true)}
|
||||
variant="secondary"
|
||||
className="border"
|
||||
>
|
||||
<Upload className="mr-2 h-4 w-4" /> Upload
|
||||
</Button>
|
||||
<Button onClick={() => setExtractorOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" /> Extract Equipment
|
||||
<ScanLine className="mr-2 h-4 w-4" /> Extractor
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -111,6 +111,7 @@ export default function Expressions() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<style>{`@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,11 +11,12 @@ import { PageHeader } from '../components/PageHeader'
|
||||
import { Button } from '../components/ui/button'
|
||||
import { Card } from '../components/ui/card'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '../components/ui/dropdown-menu'
|
||||
import { ScrollArea } from '../components/ui/scroll-area'
|
||||
|
||||
// Types - Define locally to avoid complex type extraction if client inference is tricky
|
||||
// But we should try to use inference
|
||||
@@ -82,7 +83,7 @@ export default function FittingRoom() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 pt-2 h-full flex flex-col bg-stone-50/30 relative">
|
||||
<div className="px-8 pb-8 pt-2 h-[calc(100vh-6rem)] flex flex-col bg-stone-50/30 relative">
|
||||
<PageHeader
|
||||
title="Fitting Room"
|
||||
subtitle="System // FITTING_ROOM"
|
||||
@@ -121,7 +122,7 @@ export default function FittingRoom() {
|
||||
<img
|
||||
src={character.image?.path ? `/files/${character.image.path}` : ''}
|
||||
alt={character.name}
|
||||
className="w-full h-full object-cover object-top drop-shadow-2xl hover:scale-105 transition-transform duration-500"
|
||||
className="w-full h-full object-contain drop-shadow-2xl hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 right-4 bg-white/80 backdrop-blur-md p-4 rounded-xl border border-white/50 shadow-sm">
|
||||
<h3 className="font-bold text-stone-800 text-lg">{character.name}</h3>
|
||||
@@ -142,21 +143,21 @@ export default function FittingRoom() {
|
||||
<span className="text-xs font-mono text-stone-400">{generations.length} LOOKS</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar">
|
||||
{generations.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-stone-400 gap-4">
|
||||
<div className="w-16 h-16 rounded-full bg-stone-100 flex items-center justify-center">
|
||||
<Plus className="w-8 h-8 text-stone-300" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="font-medium text-stone-600">No looks generated yet.</p>
|
||||
<p className="text-xs mt-1">Create your first look to get started.</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setCreateLookOpen(true)}>
|
||||
Create Look
|
||||
</Button>
|
||||
{generations.length === 0 ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-stone-400 gap-4">
|
||||
<div className="w-16 h-16 rounded-full bg-stone-100 flex items-center justify-center">
|
||||
<Plus className="w-8 h-8 text-stone-300" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<p className="font-medium text-stone-600">No looks generated yet.</p>
|
||||
<p className="text-xs mt-1">Create your first look to get started.</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setCreateLookOpen(true)}>
|
||||
Create Look
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{generations.map((gen) => (
|
||||
<div
|
||||
@@ -207,8 +208,8 @@ export default function FittingRoom() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ export default function Poses() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<style>{`@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user