feat: add seller detail page and product management view
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 1m58s

This commit is contained in:
vickytechkey 2026-11-08 17:18:09 +05:30
parent a4e46dc22c
commit 7b4f51e23d
3 changed files with 898 additions and 12 deletions

View file

@ -617,15 +617,28 @@
<td>
<div style="display:flex;align-items:center;gap:12px;">
{#if p.image}
<img src={p.image} alt="" style="width:44px;height:44px;border-radius:4px;object-fit:cover;border:1px solid var(--v2-line);" />
<a href={`/sellers/${seller.id}/products/${p.id}`} target="_blank" rel="noopener noreferrer" title="Open product in new tab">
<img src={p.image} alt="" style="width:44px;height:44px;border-radius:4px;object-fit:cover;border:1px solid var(--v2-line);cursor:pointer;" />
</a>
{:else}
<div style="width:44px;height:44px;border-radius:4px;background:var(--v2-line);display:flex;align-items:center;justify-content:center;color:var(--v2-muted);">
<Package size={20} />
</div>
<a href={`/sellers/${seller.id}/products/${p.id}`} target="_blank" rel="noopener noreferrer" title="Open product in new tab" style="text-decoration:none;">
<div style="width:44px;height:44px;border-radius:4px;background:var(--v2-line);display:flex;align-items:center;justify-content:center;color:var(--v2-muted);cursor:pointer;">
<Package size={20} />
</div>
</a>
{/if}
<div>
<div style="font-weight:600;font-size:13px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
{p.title}
<a
href={`/sellers/${seller.id}/products/${p.id}`}
target="_blank"
rel="noopener noreferrer"
style="color:inherit;text-decoration:none;display:inline-flex;align-items:center;gap:4px;"
title="Open product review in new tab"
>
<span>{p.title}</span>
<ExternalLink size={12} style="opacity:0.6;flex-shrink:0;" />
</a>
{#if p.is_gi_tagged}
<span style="font-size:10px;background:#FDF2F4;color:#671B26;padding:1px 5px;border-radius:4px;font-weight:700;">GI TAG</span>
{/if}
@ -690,15 +703,17 @@
</button>
{/if}
<!-- Edit Product Content -->
<button
type="button"
<!-- Review & Edit in New Tab -->
<a
href={`/sellers/${seller.id}/products/${p.id}`}
target="_blank"
rel="noopener noreferrer"
class="v2-btn"
onclick={() => openEditProduct(p)}
style="font-size:11px;padding:3px 8px;"
style="font-size:11px;padding:3px 8px;display:inline-flex;align-items:center;gap:4px;text-decoration:none;color:inherit;"
title="Open all product details in new tab to review, edit, approve, or reject"
>
Edit Content
</button>
<ExternalLink size={11} /> Review & Edit
</a>
<!-- Visibility Toggle -->
<form method="POST" action="?/toggleProduct" use:enhance style="display:inline-block;">

View file

@ -0,0 +1,103 @@
import { apiRequest } from '#lib/api-helpers.js';
import { error, fail } from '@sveltejs/kit';
/** @type {import('./$types').PageServerLoad} */
export async function load({ cookies, params }) {
const sellerId = params.id;
const productId = params.productId;
try {
const [seller, product] = await Promise.all([
apiRequest(`/sellers/${sellerId}/`, {}, { cookies }),
apiRequest(`/sellers/${sellerId}/products/${productId}/`, {}, { cookies })
]);
return {
seller,
product
};
} catch (err) {
console.error('Failed to load product review detail:', err);
error(404, 'Product not found or inaccessible');
}
}
/** @type {import('./$types').Actions} */
export const actions = {
approve: async ({ cookies, params }) => {
try {
await apiRequest(
`/sellers/${params.id}/products/${params.productId}/approve/`,
{ method: 'POST' },
{ cookies }
);
return { success: true, message: 'Product approved and published to customer storefront' };
} catch (err) {
return fail(400, { error: err.message || 'Failed to approve product' });
}
},
reject: async ({ request, cookies, params }) => {
const data = await request.formData();
const reason = data.get('reason') || 'Product does not comply with catalogue quality standards.';
try {
await apiRequest(
`/sellers/${params.id}/products/${params.productId}/reject/`,
{ method: 'POST', body: { reason: String(reason) } },
{ cookies }
);
return { success: true, message: 'Product rejected' };
} catch (err) {
return fail(400, { error: err.message || 'Failed to reject product' });
}
},
toggle: async ({ request, cookies, params }) => {
const data = await request.formData();
const is_active = data.get('is_active') === 'true';
try {
await apiRequest(
`/sellers/${params.id}/products/${params.productId}/toggle/`,
{ method: 'POST', body: { is_active } },
{ cookies }
);
return { success: true, message: 'Product visibility updated' };
} catch (err) {
return fail(400, { error: err.message || 'Failed to toggle product status' });
}
},
save: async ({ request, cookies, params }) => {
const data = await request.formData();
/** @type {Record<string, unknown>} */
const payload = {};
for (const [key, value] of data.entries()) {
if (key !== 'action') {
if (key === 'is_gi_tagged' || key === 'is_active' || key === 'is_odop' || key === 'is_sustainable') {
payload[key] = value === 'true';
} else if (
key === 'price' ||
key === 'compare_at_price' ||
key === 'stock' ||
key === 'package_weight' ||
key === 'processing_days'
) {
if (value !== '') payload[key] = Number(value);
} else {
payload[key] = value;
}
}
}
try {
await apiRequest(
`/sellers/${params.id}/products/${params.productId}/`,
{ method: 'PATCH', body: payload },
{ cookies }
);
return { success: true, message: 'Product content updated successfully in website catalogue' };
} catch (err) {
return fail(400, { error: err.message || 'Failed to update product content' });
}
}
};

View file

@ -0,0 +1,768 @@
<script>
import { resolve } from '$app/paths';
import { enhance } from '$app/forms';
import PageHeader from '#lib/v2/components/PageHeader.svelte';
import Pill from '#lib/v2/components/Pill.svelte';
import { money, longDate } from '#lib/v2/format.js';
import {
CheckCircle2,
XCircle,
Package,
ExternalLink,
ChevronRight,
Eye,
ShieldCheck,
Award,
Leaf,
MapPin,
Truck,
ArrowLeft,
Clock,
Sparkles,
Layers,
Video,
Tag
} from '@lucide/svelte';
/** @type {{ data: any, form: any }} */
let { data, form } = $props();
let seller = $derived(data.seller || {});
let product = $derived(data.product || {});
// Selected active image preview (defaults to product.image)
let selectedImage = $state('');
$effect(() => {
if (product.image && !selectedImage) {
selectedImage = product.image;
}
});
// Rejection modal state
let showRejectModal = $state(false);
let rejectReason = $state('');
/** @param {string} status */
function statusTone(status) {
switch (status) {
case 'approved':
return 'moss';
case 'pending':
case 'pending_approval':
return 'clay';
case 'rejected':
case 'suspended':
case 'locked':
return 'rust';
default:
return 'slate';
}
}
// Parse additional images array safely
let allImages = $derived(() => {
const list = [];
if (product.image) list.push(product.image);
if (Array.isArray(product.additional_images)) {
for (const img of product.additional_images) {
if (img && !list.includes(img)) list.push(img);
}
}
return list;
});
</script>
<PageHeader title={product.title || 'Product Review'} record>
{#snippet crumb()}
<a href="/sellers">Sellers</a>
<ChevronRight size={12} />
<a href={`/sellers/${seller.id}`}>{seller.store_name || seller.name || `Seller #${seller.id}`}</a>
<ChevronRight size={12} />
<span>Product #{product.id}</span>
{/snippet}
{#snippet sub()}
{[
seller.store_name ? `Store: ${seller.store_name}` : null,
product.category ? `Category: ${product.category}` : null,
product.sku ? `SKU: ${product.sku}` : null,
product.upload_source === 'bulk' ? 'Bulk CSV Import' : 'Individual Upload'
]
.filter(Boolean)
.join(' · ')}
{/snippet}
{#snippet actions()}
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<Pill tone={statusTone(product.status)} dot>
{product.status}
</Pill>
<Pill tone={product.is_active ? 'moss' : 'rust'}>
{product.is_active ? 'Storefront Published' : 'Hidden from Store'}
</Pill>
{#if product.status !== 'approved'}
<form method="POST" action="?/approve" use:enhance style="display:inline-block;">
<button
type="submit"
class="v2-btn v2-btn-primary"
style="background:var(--v2-moss);border-color:var(--v2-moss);color:white;display:inline-flex;align-items:center;gap:6px;"
>
<CheckCircle2 size={14} /> Approve Product
</button>
</form>
{/if}
{#if product.status !== 'rejected'}
<button
type="button"
class="v2-btn"
onclick={() => {
rejectReason = product.rejection_reason || '';
showRejectModal = true;
}}
style="color:var(--v2-rust);display:inline-flex;align-items:center;gap:6px;"
>
<XCircle size={14} /> Reject
</button>
{/if}
<form method="POST" action="?/toggle" use:enhance style="display:inline-block;">
<input type="hidden" name="is_active" value={product.is_active ? 'false' : 'true'} />
<button type="submit" class="v2-btn" style="display:inline-flex;align-items:center;gap:6px;">
<Eye size={14} /> {product.is_active ? 'Hide from Storefront' : 'Publish to Storefront'}
</button>
</form>
<a
href={`/sellers/${seller.id}`}
class="v2-btn"
style="display:inline-flex;align-items:center;gap:6px;text-decoration:none;"
>
<ArrowLeft size={14} /> Back to Seller
</a>
</div>
{/snippet}
</PageHeader>
{#if form?.success}
<div style="padding:12px 16px;background:rgba(46,125,50,0.1);border:1px solid var(--v2-moss);color:var(--v2-moss);border-radius:6px;margin-bottom:16px;font-size:13px;display:flex;align-items:center;gap:8px;">
<CheckCircle2 size={16} />
<span>{form.message || 'Action executed successfully.'}</span>
</div>
{:else if form?.error}
<div style="padding:12px 16px;background:rgba(211,47,47,0.1);border:1px solid var(--v2-rust);color:var(--v2-rust);border-radius:6px;margin-bottom:16px;font-size:13px;display:flex;align-items:center;gap:8px;">
<XCircle size={16} />
<span>{form.error}</span>
</div>
{/if}
{#if product.rejection_reason}
<div style="padding:14px 18px;background:#FFF4F2;border:1px solid #FBD0C8;border-radius:8px;margin-bottom:20px;display:flex;align-items:flex-start;gap:12px;">
<XCircle size={18} style="color:var(--v2-rust);margin-top:2px;flex-shrink:0;" />
<div>
<div style="font-weight:700;color:var(--v2-rust);font-size:13px;margin-bottom:2px;">Product Rejection Notice</div>
<div style="font-size:13px;color:#4A1515;">{product.rejection_reason}</div>
</div>
</div>
{/if}
<div class="product-review-grid">
<!-- LEFT COLUMN: Visual Media & Verification Badges -->
<div class="review-media-col">
<!-- Visual Showcase Card -->
<div class="v2-panel-card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<h3 style="font-size:14px;font-weight:700;margin:0;display:flex;align-items:center;gap:6px;">
<Package size={16} /> Product Media & Visuals
</h3>
<span class="v2-sub" style="font-size:11px;">{allImages().length} Photos</span>
</div>
<!-- Main High-Res Preview -->
<div class="main-image-wrap">
{#if selectedImage || product.image}
<img src={selectedImage || product.image} alt={product.title} class="main-preview-img" />
{:else}
<div class="main-preview-placeholder">
<Package size={48} style="opacity:0.4;" />
<span style="font-size:12px;margin-top:8px;">No image uploaded</span>
</div>
{/if}
</div>
<!-- Thumbnails Carousel/Row -->
{#if allImages().length > 1}
<div class="thumbnails-row">
{#each allImages() as img, idx}
<button
type="button"
class="thumb-btn"
class:active={selectedImage === img || (!selectedImage && idx === 0)}
onclick={() => (selectedImage = img)}
>
<img src={img} alt="" />
</button>
{/each}
</div>
{/if}
<!-- Video / 360 View links if present -->
{#if product.video_url || product.view_360_url}
<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--v2-line);display:flex;gap:8px;flex-wrap:wrap;">
{#if product.video_url}
<a
href={product.video_url}
target="_blank"
rel="noopener noreferrer"
class="v2-btn"
style="font-size:11px;display:inline-flex;align-items:center;gap:6px;"
>
<Video size={13} style="color:var(--v2-moss);" /> Watch 10s Product Video
<ExternalLink size={10} />
</a>
{/if}
{#if product.view_360_url}
<a
href={product.view_360_url}
target="_blank"
rel="noopener noreferrer"
class="v2-btn"
style="font-size:11px;display:inline-flex;align-items:center;gap:6px;"
>
<Sparkles size={13} style="color:#B8860B;" /> View 360° Interactive
<ExternalLink size={10} />
</a>
{/if}
</div>
{/if}
</div>
<!-- Heritage, ODOP, GI Tag & Provenance Badges -->
<div class="v2-panel-card" style="margin-top:16px;">
<h3 style="font-size:14px;font-weight:700;margin:0 0 14px 0;display:flex;align-items:center;gap:6px;">
<Award size={16} style="color:#671B26;" /> Heritage & Statutory Badges
</h3>
<div style="display:flex;flex-direction:column;gap:12px;">
<!-- GI Tag -->
<div class="badge-item" class:badge-active={product.is_gi_tagged}>
<div style="display:flex;align-items:center;gap:8px;">
<ShieldCheck size={18} style="color:#671B26;" />
<div>
<div style="font-weight:600;font-size:12px;">Geographical Indication (GI Tag)</div>
<div class="v2-sub" style="font-size:11px;">
{product.is_gi_tagged
? product.gi_tag_name || 'Registered Geographical Indication Certified'
: 'Not marked as GI Tag'}
</div>
</div>
</div>
<Pill tone={product.is_gi_tagged ? 'rust' : 'slate'}>
{product.is_gi_tagged ? 'Certified GI' : 'Standard'}
</Pill>
</div>
<!-- ODOP -->
<div class="badge-item" class:badge-active={product.is_odop}>
<div style="display:flex;align-items:center;gap:8px;">
<Tag size={18} style="color:#166534;" />
<div>
<div style="font-weight:600;font-size:12px;">One District One Product (ODOP)</div>
<div class="v2-sub" style="font-size:11px;">
{product.is_odop
? product.odop_district || 'ODOP Identified Heritage Craft'
: 'Not marked as ODOP'}
</div>
</div>
</div>
<Pill tone={product.is_odop ? 'moss' : 'slate'}>
{product.is_odop ? 'ODOP Craft' : 'Standard'}
</Pill>
</div>
<!-- Sustainability -->
<div class="badge-item" class:badge-active={product.is_sustainable}>
<div style="display:flex;align-items:center;gap:8px;">
<Leaf size={18} style="color:#1E40AF;" />
<div>
<div style="font-weight:600;font-size:12px;">Sustainable & Eco-Craft</div>
<div class="v2-sub" style="font-size:11px;">
{product.is_sustainable
? product.sustainable_type || 'Eco-compliant materials & traditional dyes'
: 'Not marked as Sustainable'}
</div>
</div>
</div>
<Pill tone={product.is_sustainable ? 'clay' : 'slate'}>
{product.is_sustainable ? 'Eco-Sustainable' : 'Standard'}
</Pill>
</div>
<!-- Coordinates & Provenance -->
{#if product.geographical_origin || product.coordinates}
<div style="padding:10px;background:var(--v2-panel);border:1px solid var(--v2-line);border-radius:6px;font-size:12px;">
<div style="display:flex;align-items:center;gap:6px;font-weight:600;margin-bottom:4px;">
<MapPin size={14} style="color:var(--v2-rust);" /> Geographical Origin
</div>
<div style="color:var(--v2-text);">{product.geographical_origin || 'Origin not specified'}</div>
{#if product.coordinates}
<div class="v2-sub" style="font-size:11px;margin-top:2px;">GPS: {product.coordinates}</div>
{/if}
</div>
{/if}
<!-- Statutory Certifications -->
{#if product.statutory_certifications}
<div style="padding:10px;background:var(--v2-panel);border:1px solid var(--v2-line);border-radius:6px;font-size:12px;">
<div style="display:flex;align-items:center;gap:6px;font-weight:600;margin-bottom:4px;">
<ShieldCheck size={14} style="color:var(--v2-moss);" /> Statutory Certifications
</div>
<div style="color:var(--v2-text);">{product.statutory_certifications}</div>
</div>
{/if}
</div>
</div>
</div>
<!-- RIGHT COLUMN: Detailed Specifications & Content Edit Form -->
<div class="review-content-col">
<div class="v2-panel-card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;border-bottom:1px solid var(--v2-line);padding-bottom:12px;">
<h3 style="font-size:15px;font-weight:700;margin:0;">
Product Specifications & Storefront Content
</h3>
<span class="v2-sub" style="font-size:11px;">Edit details and click Save</span>
</div>
<form method="POST" action="?/save" use:enhance>
<div style="display:flex;flex-direction:column;gap:16px;">
<!-- Product Title -->
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="title">
Product Title
</label>
<input
type="text"
id="title"
name="title"
value={product.title || ''}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
required
/>
</div>
<!-- Category, SKU & Upload Source -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="category">
Category
</label>
<input
type="text"
id="category"
name="category"
value={product.category || ''}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
required
/>
</div>
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="sku">
SKU Identifier
</label>
<input
type="text"
id="sku"
name="sku"
value={product.sku || ''}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
required
/>
</div>
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="upload_source">
Upload Source
</label>
<input
type="text"
id="upload_source"
name="upload_source"
value={product.upload_source || 'individual'}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;background:var(--v2-line);color:var(--v2-muted);"
readonly
/>
</div>
</div>
<!-- Pricing & Inventory -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="price">
Selling Price (₹)
</label>
<input
type="number"
id="price"
name="price"
step="0.01"
value={product.price || 0}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;font-weight:600;"
required
/>
</div>
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="compare_at_price">
MRP / Compare Price (₹)
</label>
<input
type="number"
id="compare_at_price"
name="compare_at_price"
step="0.01"
value={product.compare_at_price || ''}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
/>
</div>
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="stock">
Available Stock (Units)
</label>
<input
type="number"
id="stock"
name="stock"
value={product.stock || 0}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;font-weight:600;"
required
/>
</div>
</div>
<!-- Shipping & Logistics -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="shipping_profile">
Shipping Profile
</label>
<input
type="text"
id="shipping_profile"
name="shipping_profile"
value={product.shipping_profile || 'Standard Fragile'}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
/>
</div>
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="package_weight">
Package Weight (kg)
</label>
<input
type="number"
id="package_weight"
name="package_weight"
step="0.01"
value={product.package_weight || 1.0}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
/>
</div>
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="processing_days">
Dispatch Time (Days)
</label>
<input
type="number"
id="processing_days"
name="processing_days"
value={product.processing_days || 3}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
/>
</div>
</div>
<!-- Craft Story & Full Description -->
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="description">
Craft Story & Detailed Description
</label>
<textarea
id="description"
name="description"
rows="6"
class="v2-input"
style="width:100%;font-size:13px;padding:10px;line-height:1.5;"
>{product.description || ''}</textarea>
</div>
<!-- Geographical Provenance Narrative -->
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="provenance_story">
Geographical Provenance Narrative
</label>
<textarea
id="provenance_story"
name="provenance_story"
rows="3"
class="v2-input"
style="width:100%;font-size:13px;padding:10px;line-height:1.5;"
placeholder="e.g. Origin story of the weaving cluster, indigenous materials and cultural significance..."
>{product.provenance_story || ''}</textarea>
</div>
<!-- Origin & Coordinates -->
<div style="display:grid;grid-template-columns:2fr 1fr;gap:12px;">
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="geographical_origin">
Geographical Origin
</label>
<input
type="text"
id="geographical_origin"
name="geographical_origin"
value={product.geographical_origin || ''}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
placeholder="e.g. Kanchipuram, Tamil Nadu, India"
/>
</div>
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="coordinates">
Cluster GPS Coordinates
</label>
<input
type="text"
id="coordinates"
name="coordinates"
value={product.coordinates || ''}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
placeholder="e.g. 12.8342° N, 79.7036° E"
/>
</div>
</div>
<!-- Statutory Certifications -->
<div>
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="statutory_certifications">
Statutory Certifications & Marks
</label>
<input
type="text"
id="statutory_certifications"
name="statutory_certifications"
value={product.statutory_certifications || ''}
class="v2-input"
style="width:100%;font-size:13px;padding:8px;"
placeholder="e.g. GI Tag #26 Act (1999) Registered, Silk Mark Certified, Handloom Mark"
/>
</div>
<!-- Heritage Checkboxes & Detail Fields -->
<div style="padding:14px;background:var(--v2-panel);border:1px solid var(--v2-line);border-radius:8px;">
<div style="font-weight:600;font-size:13px;margin-bottom:10px;">Heritage Attributes & Moderation Toggles</div>
<div style="display:flex;flex-direction:column;gap:10px;">
<!-- GI Tag -->
<div style="display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;">
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
<input type="checkbox" name="is_gi_tagged" value="true" checked={product.is_gi_tagged} />
<span style="font-weight:600;">Certified GI Tag</span>
</label>
<input
type="text"
name="gi_tag_name"
value={product.gi_tag_name || ''}
class="v2-input"
style="font-size:12px;padding:6px;"
placeholder="GI Tag Name / Register No. (e.g. Kancheepuram Silk Application #26)"
/>
</div>
<!-- ODOP -->
<div style="display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;">
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
<input type="checkbox" name="is_odop" value="true" checked={product.is_odop} />
<span style="font-weight:600;">ODOP Product</span>
</label>
<input
type="text"
name="odop_district"
value={product.odop_district || ''}
class="v2-input"
style="font-size:12px;padding:6px;"
placeholder="ODOP District (e.g. Kanchipuram District, Tamil Nadu)"
/>
</div>
<!-- Sustainable -->
<div style="display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;">
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
<input type="checkbox" name="is_sustainable" value="true" checked={product.is_sustainable} />
<span style="font-weight:600;">Sustainable Craft</span>
</label>
<input
type="text"
name="sustainable_type"
value={product.sustainable_type || ''}
class="v2-input"
style="font-size:12px;padding:6px;"
placeholder="Sustainable Material Type (e.g. Organic Mulberry Silk, Natural Plant Dyes)"
/>
</div>
<!-- Storefront visibility -->
<div style="margin-top:6px;padding-top:8px;border-top:1px solid var(--v2-line);">
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
<input type="checkbox" name="is_active" value="true" checked={product.is_active} />
<span style="font-weight:600;">Visible and purchasable in Website Storefront</span>
</label>
</div>
</div>
</div>
<!-- Action Buttons -->
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:8px;">
<button type="submit" class="v2-btn v2-btn-primary" style="font-size:13px;padding:8px 16px;">
Save Content Changes
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Reject Reason Modal -->
{#if showRejectModal}
<div style="position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;">
<div style="background:var(--v2-panel);border:1px solid var(--v2-line);border-radius:10px;padding:24px;width:100%;max-width:440px;">
<h3 style="font-size:16px;font-weight:700;margin-bottom:8px;">Reject Product Listing</h3>
<p class="v2-sub" style="font-size:13px;margin-bottom:16px;">
State the reason why <strong>{product.title}</strong> is being rejected. This notification will be displayed to the seller.
</p>
<form method="POST" action="?/reject" use:enhance onsubmit={() => (showRejectModal = false)}>
<textarea
name="reason"
bind:value={rejectReason}
placeholder="e.g. Inaccurate GI tag claim, unclear product imagery, or non-compliant packaging weight..."
required
rows="4"
class="v2-input"
style="width:100%;font-size:13px;margin-bottom:16px;padding:8px;"
></textarea>
<div style="display:flex;justify-content:flex-end;gap:8px;">
<button type="button" class="v2-btn" onclick={() => (showRejectModal = false)}>Cancel</button>
<button type="submit" class="v2-btn" style="background:var(--v2-rust);color:white;">Confirm Product Rejection</button>
</div>
</form>
</div>
</div>
{/if}
<style>
.product-review-grid {
display: grid;
grid-template-columns: 380px 1fr;
gap: 20px;
align-items: flex-start;
}
@media (max-width: 960px) {
.product-review-grid {
grid-template-columns: 1fr;
}
}
.v2-panel-card {
background: var(--v2-panel);
border: 1px solid var(--v2-line);
border-radius: 8px;
padding: 16px;
}
.main-image-wrap {
width: 100%;
height: 340px;
border-radius: 6px;
overflow: hidden;
background: var(--v2-bg);
border: 1px solid var(--v2-line);
display: flex;
align-items: center;
justify-content: center;
}
.main-preview-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.main-preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--v2-muted);
}
.thumbnails-row {
display: flex;
gap: 8px;
margin-top: 10px;
overflow-x: auto;
padding-bottom: 4px;
}
.thumb-btn {
width: 58px;
height: 58px;
border-radius: 6px;
border: 2px solid var(--v2-line);
background: var(--v2-bg);
padding: 0;
cursor: pointer;
overflow: hidden;
flex-shrink: 0;
transition: border-color 0.15s ease;
}
.thumb-btn.active {
border-color: var(--v2-moss, #2e7d32);
box-shadow: 0 0 0 2px rgba(46, 125, 50, 0.2);
}
.thumb-btn img {
width: 100%;
height: 100%;
object-fit: cover;
}
.badge-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border: 1px solid var(--v2-line);
border-radius: 6px;
background: var(--v2-panel);
transition: background 0.15s ease;
}
.badge-item.badge-active {
border-color: rgba(46, 125, 50, 0.4);
background: rgba(46, 125, 50, 0.03);
}
</style>