Fix Fitting Room overflow and add Save as Outfit to Extractor
- Fixed Fitting Room page overflow by constraining height and using ScrollArea for the Generated Looks section. - Added "Save as Outfit" checkbox and input to the Refine stage of the Extractor dialog. - Installed @radix-ui/react-checkbox and added Shadcn Checkbox component. - Updated Dockerfile to include example-data directory in the final image.
This commit is contained in:
1 parent
9eaf136550
commit
7f12e5f909
7 files changed
+112
-6
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",
|
||||
|
||||
@@ -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 items-center 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="max-w-xs 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 }
|
||||
@@ -10,6 +10,7 @@ import { LookDetailsDialog } from '../components/LookDetailsDialog'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
import { Button } from '../components/ui/button'
|
||||
import { Card } from '../components/ui/card'
|
||||
import { ScrollArea } from '../components/ui/scroll-area'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -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"
|
||||
@@ -142,7 +143,7 @@ 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">
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
{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">
|
||||
@@ -208,7 +209,7 @@ export default function FittingRoom() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user