feat(marketplace): connect CreateProductView to enriched product API

This commit is contained in:
senke 2026-02-22 14:10:26 +01:00
parent 13d9e96001
commit 3d7cc141fe
5 changed files with 116 additions and 22 deletions

View file

@ -25,6 +25,8 @@ export const CreateProductView: React.FC = () => {
updateLicense,
handlePublish,
handleSaveDraft,
previewFile,
setPreviewFile,
} = useCreateProductView();
return (
@ -37,7 +39,10 @@ export const CreateProductView: React.FC = () => {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="space-y-8">
<CreateProductViewCoverCard />
<CreateProductViewFilesCard />
<CreateProductViewFilesCard
previewFile={previewFile}
onPreviewFileChange={setPreviewFile}
/>
</div>
<div className="lg:col-span-2 space-y-8">
<CreateProductViewDetailsCard

View file

@ -64,10 +64,10 @@ export function CreateProductViewDetailsCard({
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option>Sample Pack</option>
<option>Presets</option>
<option>DAW Template</option>
<option>MIDI Pack</option>
<option value="Sample Pack">Sample Pack</option>
<option value="Beat">Beat</option>
<option value="Sample">Sample</option>
<option value="Preset">Preset</option>
</select>
</div>
<Input

View file

@ -1,7 +1,30 @@
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { Card } from '@/components/ui/card';
import { Music } from 'lucide-react';
import { Music, X } from 'lucide-react';
interface CreateProductViewFilesCardProps {
previewFile: File | null;
onPreviewFileChange: (file: File | null) => void;
}
export function CreateProductViewFilesCard({
previewFile,
onPreviewFileChange,
}: CreateProductViewFilesCardProps) {
const onDrop = useCallback(
(accepted: File[]) => {
onPreviewFileChange(accepted[0] ?? null);
},
[onPreviewFileChange]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: { 'audio/*': ['.mp3', '.wav', '.m4a', '.ogg'] },
maxFiles: 1,
disabled: false,
});
export function CreateProductViewFilesCard() {
return (
<Card variant="default">
<h3 className="font-bold text-foreground mb-4 flex items-center gap-2 text-sm uppercase tracking-wider">
@ -22,10 +45,33 @@ export function CreateProductViewFilesCard() {
<label className="text-xs text-muted-foreground mb-1 block">
Audio Preview (MP3)
</label>
<div className="h-12 bg-card border border-border rounded-xl flex items-center justify-center cursor-pointer hover:border-primary transition-colors duration-[var(--sumi-duration-normal)]">
<span className="text-xs text-muted-foreground">
Drop preview audio
</span>
<div
{...getRootProps()}
className={`h-12 bg-card border border-border rounded-xl flex items-center justify-center cursor-pointer transition-colors duration-[var(--sumi-duration-normal)] ${
isDragActive ? 'border-primary bg-primary/5' : 'hover:border-primary'
}`}
>
<input {...getInputProps()} />
{previewFile ? (
<span className="text-xs text-foreground flex items-center gap-2">
{previewFile.name}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onPreviewFileChange(null);
}}
className="p-1 rounded hover:bg-muted"
aria-label="Remove preview"
>
<X className="w-3 h-3" />
</button>
</span>
) : (
<span className="text-xs text-muted-foreground">
Drop preview audio
</span>
)}
</div>
</div>
</div>

View file

@ -1,6 +1,7 @@
import { useState, useCallback } from 'react';
import toast from '@/utils/toast';
import { productService, CreateProductData } from '@/services/productService';
import { marketplaceService } from '@/services/marketplaceService';
import { logger } from '@/utils/logger';
import type { LicenseConfig } from './types';
@ -19,6 +20,7 @@ export function useCreateProductView() {
const [bpm, setBpm] = useState('');
const [key, setKey] = useState('');
const [licenses, setLicenses] = useState<LicenseConfig[]>(INITIAL_LICENSES);
const [previewFile, setPreviewFile] = useState<File | null>(null);
const updateLicense = useCallback(
(type: string, field: keyof LicenseConfig, value: string | boolean) => {
@ -54,10 +56,19 @@ export function useCreateProductView() {
bpm,
key,
};
await productService.create(productData);
const product = await productService.create(productData);
if (previewFile && product?.id) {
try {
await marketplaceService.uploadProductPreview(product.id, previewFile);
} catch (uploadErr) {
logger.error('Preview upload failed', { error: uploadErr });
toast.error('Product created but preview upload failed');
}
}
toast.success('Product published successfully!');
setTitle('');
setDescription('');
setPreviewFile(null);
} catch (e) {
toast.error('Failed to publish product');
logger.error('Failed to publish product', { error: e });
@ -68,8 +79,14 @@ export function useCreateProductView() {
title,
description,
licenses,
category,
tags,
bpm,
key,
previewFile,
]);
const handleSaveDraft = useCallback(() => {
toast('Draft saved');
}, []);
@ -92,5 +109,7 @@ export function useCreateProductView() {
updateLicense,
handlePublish,
handleSaveDraft,
previewFile,
setPreviewFile,
};
}

View file

@ -28,8 +28,9 @@ export interface CreateProductData {
description: string;
price: number;
category: string;
tags: string[];
tags?: string[];
license_type: 'personal' | 'commercial' | 'exclusive';
product_type?: 'track' | 'pack' | 'service';
bpm?: string;
key?: string;
}
@ -49,11 +50,27 @@ export const productService = {
},
create: async (data: CreateProductData) => {
const response = await apiClient.post<{ product: Product }>(
const categoryMap: Record<string, string> = {
'Sample Pack': 'pack',
Beat: 'beat',
Sample: 'sample',
Preset: 'preset',
};
const payload = {
title: data.title,
description: data.description,
price: data.price,
product_type: data.product_type ?? 'pack',
license_type: data.license_type,
category: categoryMap[data.category] ?? data.category?.toLowerCase() ?? 'pack',
bpm: data.bpm ? parseInt(data.bpm, 10) : undefined,
musical_key: data.key,
};
const response = await apiClient.post<Product>(
'/marketplace/products',
data
payload
);
return response.data.product;
return response.data;
},
update: async (id: string, data: Partial<CreateProductData>) => {
@ -69,18 +86,25 @@ export const productService = {
},
uploadFile: async (id: string, file: File, type: 'main' | 'preview' | 'cover') => {
if (type === 'preview') {
const response = await apiClient.post<{ id: string; file_path: string }>(
`/marketplace/products/${id}/preview`,
(() => {
const fd = new FormData();
fd.append('file', file);
return fd;
})(),
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
return { url: (response.data as { file_path?: string })?.file_path ?? '' };
}
const formData = new FormData();
formData.append('file', file);
formData.append('type', type);
const response = await apiClient.post<{ url: string }>(
`/marketplace/products/${id}/upload`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
return response.data;
}