feat(marketplace): connect CreateProductView to enriched product API
This commit is contained in:
parent
13d9e96001
commit
3d7cc141fe
5 changed files with 116 additions and 22 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue