diff --git a/Dockerfile b/Dockerfile index f147eac..57a72cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/bun.lock b/bun.lock index 0c98846..20eb8ff 100644 --- a/bun.lock +++ b/bun.lock @@ -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", diff --git a/example-data/characters.json b/example-data/characters.json index 50035b2..283f693 100644 --- a/example-data/characters.json +++ b/example-data/characters.json @@ -19,4 +19,4 @@ "path": "01KHNYBPY85F6BZAJXZDSKBQY7.webp" } } -] \ No newline at end of file +] diff --git a/example-data/equipments.json b/example-data/equipments.json index 7dba3b6..712bcf8 100644 --- a/example-data/equipments.json +++ b/example-data/equipments.json @@ -107,4 +107,4 @@ "path": "01KHQ66H2X62ZFQ8E1WHK0DSFE.webp" } } -] \ No newline at end of file +] diff --git a/example-data/expressions.json b/example-data/expressions.json index b8ce734..d4e7f5d 100644 --- a/example-data/expressions.json +++ b/example-data/expressions.json @@ -80,4 +80,4 @@ "path": "01KHNY8VTPEQMHJ7X0C31F6YA6.webp" } } -] \ No newline at end of file +] diff --git a/example-data/looks.json b/example-data/looks.json index b1a945a..248e598 100644 --- a/example-data/looks.json +++ b/example-data/looks.json @@ -69,4 +69,4 @@ "01KHQ66H318BPBAE4ZFBY7JHBD" ] } -] \ No newline at end of file +] diff --git a/example-data/poses.json b/example-data/poses.json index 082467c..0937e9e 100644 --- a/example-data/poses.json +++ b/example-data/poses.json @@ -80,4 +80,4 @@ "path": "01KHNY603BYFF3M1RA5AWYHCD2.webp" } } -] \ No newline at end of file +] diff --git a/package.json b/package.json index 39e3d17..7953427 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ui/components/ExtractorDialog.tsx b/src/ui/components/ExtractorDialog.tsx index 9e63890..2188b4c 100644 --- a/src/ui/components/ExtractorDialog.tsx +++ b/src/ui/components/ExtractorDialog.tsx @@ -34,6 +34,10 @@ export const ExtractorDialog: React.FC = ({ const [results, setResults] = useState([]) const [isRefineComplete, setIsRefineComplete] = useState(false) + // Save as Outfit state + const [saveAsOutfit, setSaveAsOutfit] = useState(false) + const [outfitName, setOutfitName] = useState('') + // Refs for cleanup const analyzeController = useRef(null) const refineController = useRef(null) @@ -48,6 +52,8 @@ export const ExtractorDialog: React.FC = ({ setSelectedIndices([]) setResults([]) setIsRefineComplete(false) + setSaveAsOutfit(false) + setOutfitName('') analyzeController.current?.abort() refineController.current?.abort() } @@ -194,7 +200,30 @@ export const ExtractorDialog: React.FC = ({ 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 = ({ selectedCandidates={candidates.filter((_, i) => selectedIndices.includes(i))} results={results} isComplete={isRefineComplete} + saveAsOutfit={saveAsOutfit} + setSaveAsOutfit={setSaveAsOutfit} + outfitName={outfitName} + setOutfitName={setOutfitName} /> )} diff --git a/src/ui/components/extractor/RefineStage.tsx b/src/ui/components/extractor/RefineStage.tsx index a5d9c76..c3a66c3 100644 --- a/src/ui/components/extractor/RefineStage.tsx +++ b/src/ui/components/extractor/RefineStage.tsx @@ -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 = ({ selectedCandidates, results, isComplete, + saveAsOutfit, + setSaveAsOutfit, + outfitName, + setOutfitName, }) => { return (
{/* Grid */} - -
+ +
{selectedCandidates.map((candidate, index) => { const isFinished = index < results.length const isProcessing = index === results.length && !isComplete @@ -114,6 +125,34 @@ export const RefineStage: React.FC = ({ })}
+ + {isComplete && ( +
+
+ setSaveAsOutfit(c === true)} + /> + +
+ {saveAsOutfit && ( + setOutfitName(e.target.value)} + className="w-full animate-in fade-in zoom-in duration-300" + autoFocus + /> + )} +
+ )} +
) } diff --git a/src/ui/pages/FittingRoom.tsx b/src/ui/pages/FittingRoom.tsx index 85d9669..19fe2e6 100644 --- a/src/ui/pages/FittingRoom.tsx +++ b/src/ui/pages/FittingRoom.tsx @@ -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 ( -
+

{character.name}

@@ -142,21 +143,21 @@ export default function FittingRoom() { {generations.length} LOOKS
-
- {generations.length === 0 ? ( -
-
- -
-
-

No looks generated yet.

-

Create your first look to get started.

-
- + {generations.length === 0 ? ( +
+
+
- ) : ( +
+

No looks generated yet.

+

Create your first look to get started.

+
+ +
+ ) : ( +
{generations.map((gen) => (
))}
- )} -
+
+ )}
diff --git a/src/ui/pages/Poses.tsx b/src/ui/pages/Poses.tsx index 77c2ef0..4497b76 100644 --- a/src/ui/pages/Poses.tsx +++ b/src/ui/pages/Poses.tsx @@ -111,6 +111,7 @@ export default function Poses() { } }} /> +
) }