Compare commits
2 Commits
289fc4dc2c
...
0f9865ffb3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9865ffb3 | ||
|
|
2a49018c5e |
@ -505,23 +505,37 @@ watch(
|
||||
/* === Placeholder inside slot === */
|
||||
.dz-placeholder {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.dz-placeholder {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Overlay (shown when previews exist) === */
|
||||
.dz-overlay {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border: 2px dashed var(--border);
|
||||
@ -533,6 +547,16 @@ watch(
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.dz-overlay {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dz-overlay:hover {
|
||||
border-color: var(--primary);
|
||||
background: color-mix(in oklch, var(--muted) 80%, transparent);
|
||||
@ -549,43 +573,53 @@ watch(
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--foreground);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.dz-placeholder-primary {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.dz-placeholder-secondary {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted-foreground);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.dz-placeholder-secondary {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Preview grid — multiple mode (default) ---- */
|
||||
.preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 400px) {
|
||||
.preview-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 560px) {
|
||||
.preview-grid {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.preview-grid {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
import { X } from '@lucide/vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import type { Product, Variant, Price, MediaItem } from '@/types/product';
|
||||
import type { Product, Variant, Price } from '@/types/product';
|
||||
import type { MediaItem } from '@/types/media';
|
||||
|
||||
const props = defineProps<{
|
||||
product: Product | null;
|
||||
@ -95,21 +96,27 @@ const handleAddToCart = () => {
|
||||
|
||||
<template>
|
||||
<div v-if="product"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm transition-opacity duration-300"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4 bg-black/60 backdrop-blur-sm transition-opacity duration-300"
|
||||
@click.self="emit('close')">
|
||||
<div
|
||||
class="w-full max-w-3xl bg-[#FDFDFC] rounded-3xl p-6 md:p-8 shadow-2xl flex flex-col md:flex-row gap-8 max-h-[90vh] overflow-y-auto border border-amber-100/30 scrollbar-thin">
|
||||
class="w-full max-w-[calc(100%-2rem)] md:max-w-3xl min-w-0 bg-[#FDFDFC] rounded-3xl p-5 md:p-8 shadow-2xl flex flex-col md:flex-row gap-5 md:gap-8 max-h-[90vh] overflow-y-auto border border-amber-100/30 scrollbar-thin relative">
|
||||
|
||||
<!-- Floating Close Button -->
|
||||
<button @click="emit('close')"
|
||||
class="absolute top-4 right-4 z-50 p-1.5 rounded-full bg-white/90 backdrop-blur-xs text-slate-500 hover:bg-slate-100 hover:text-slate-800 transition-all shadow-sm border border-slate-100 cursor-pointer">
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
<!-- Image Side -->
|
||||
<div class="w-full md:w-1/2 flex flex-col space-y-3">
|
||||
<div class="w-full md:w-1/2 min-w-0 flex flex-col space-y-3">
|
||||
<div
|
||||
class="aspect-3/4 rounded-2xl overflow-hidden bg-slate-50 border border-slate-200/50 shadow-sm relative">
|
||||
class="h-44 sm:h-64 md:h-auto aspect-auto md:aspect-3/4 rounded-2xl overflow-hidden bg-slate-50 border border-slate-200/50 shadow-sm relative">
|
||||
<img :src="mainImage" :alt="product.name" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
|
||||
<!-- Image Thumbnails -->
|
||||
<div v-if="allImages.length > 1" class="flex gap-2 overflow-x-auto scrollbar-hide pb-1">
|
||||
<button v-for="(img, idx) in allImages" :key="img.id" @click="activeImageIndex = idx" :class="[
|
||||
'shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 transition-all cursor-pointer',
|
||||
'shrink-0 w-12 h-12 md:w-16 md:h-16 rounded-lg overflow-hidden border-2 transition-all cursor-pointer',
|
||||
activeImageIndex === idx
|
||||
? 'border-amber-500 shadow-md shadow-amber-200/40'
|
||||
: 'border-slate-200/60 opacity-60 hover:opacity-100'
|
||||
@ -121,26 +128,20 @@ const handleAddToCart = () => {
|
||||
</div>
|
||||
|
||||
<!-- Info Side -->
|
||||
<div class="w-full md:w-1/2 flex flex-col justify-between space-y-6 text-left">
|
||||
<div class="w-full md:w-1/2 min-w-0 flex flex-col justify-between space-y-5 md:space-y-6 text-left">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest text-amber-600">
|
||||
{{product.categories.map(c => c.name).join(', ')}}
|
||||
</span>
|
||||
<h3 class="text-2xl font-serif mt-0.5 text-slate-800">{{ product.name }}</h3>
|
||||
</div>
|
||||
<button @click="emit('close')"
|
||||
class="p-1.5 rounded-full hover:bg-slate-100 transition-colors cursor-pointer">
|
||||
<X class="w-5 h-5 text-slate-500" />
|
||||
</button>
|
||||
<div>
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest text-amber-600">
|
||||
{{product.categories.map(c => c.name).join(', ')}}
|
||||
</span>
|
||||
<h3 class="text-xl md:text-2xl font-serif mt-0.5 text-slate-800">{{ product.name }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="space-y-1.5">
|
||||
<div class="space-y-1">
|
||||
<h4 class="text-[10px] uppercase font-bold tracking-widest text-slate-400">Deskripsi</h4>
|
||||
<p
|
||||
class="text-xs text-slate-500 font-light leading-relaxed max-h-32 overflow-y-auto scrollbar-thin pr-1">
|
||||
class="text-xs text-slate-500 font-light leading-relaxed max-h-20 md:max-h-32 overflow-y-auto scrollbar-thin pr-1">
|
||||
{{ product.description || `Pakaian batik modern dan nyaman hasil karya perajin
|
||||
terbaik kami.Menggunakan serat kain pilihan yang awet dan menyejukkan kulit.` }}
|
||||
</p>
|
||||
@ -168,13 +169,13 @@ const handleAddToCart = () => {
|
||||
<div v-if="activeVariant && activeVariant.prices.length > 0" class="space-y-2">
|
||||
<h4 class="text-[10px] uppercase font-bold tracking-widest text-slate-400">Saluran Harga
|
||||
(Grosir/Eceran)</h4>
|
||||
<div class="grid grid-cols-2 gap-1.5 max-h-36 overflow-y-auto scrollbar-thin pr-1">
|
||||
<div class="grid grid-cols-2 gap-1.5 max-h-28 md:max-h-36 overflow-y-auto scrollbar-thin pr-1">
|
||||
<button v-for="price in activeVariant.prices" :key="price.id"
|
||||
@click="activePriceType = price.type" :class="[
|
||||
'px-3 py-2 text-left rounded-xl border transition-all cursor-pointer',
|
||||
activePriceType === price.type
|
||||
? 'border-amber-500 bg-amber-500/5/5'
|
||||
: 'border-slate-200/60 hover'
|
||||
? 'border-amber-500 bg-amber-500/5'
|
||||
: 'border-slate-200/60 hover:bg-slate-50/50'
|
||||
]">
|
||||
<span class="block text-[8px] uppercase tracking-wider font-bold text-slate-400">
|
||||
{{ price.type_label }}
|
||||
@ -193,7 +194,7 @@ const handleAddToCart = () => {
|
||||
<div>
|
||||
<span class="text-[10px] uppercase font-bold tracking-widest text-slate-400">Harga
|
||||
Terpilih</span>
|
||||
<div class="text-xl font-bold text-amber-600 mt-0.5">
|
||||
<div class="text-lg md:text-xl font-bold text-amber-600 mt-0.5">
|
||||
{{ selectedPrice ? `Rp ${formatRupiah(selectedPrice.price)}` : 'Pilih ukuran' }}
|
||||
</div>
|
||||
</div>
|
||||
@ -208,7 +209,7 @@ const handleAddToCart = () => {
|
||||
|
||||
<button v-if="activeVariant && selectedPrice" @click="handleAddToCart"
|
||||
:disabled="activeVariant.stock <= 0" :class="[
|
||||
'w-full py-3.5 text-xs font-bold uppercase tracking-widest rounded-xl transition-all cursor-pointer shadow-md',
|
||||
'w-full py-3 md:py-3.5 text-xs font-bold uppercase tracking-widest rounded-xl transition-all cursor-pointer shadow-md',
|
||||
activeVariant.stock > 0
|
||||
? 'bg-amber-600 text-white hover:bg-amber-500 shadow-amber-200/50'
|
||||
: 'bg-slate-100 text-slate-400 cursor-not-allowed shadow-none'
|
||||
|
||||
@ -22,7 +22,8 @@ import ProductModal from '@/components/modal/ProductModal.vue';
|
||||
import DraggableCarousel from '@/components/ui/carousel/DraggableCarousel.vue';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import admin from '@/routes/admin';
|
||||
import type { Product, Variant, Price, Category, MediaItem } from '@/types/product';
|
||||
import type { Product, Variant, Price, Category } from '@/types/product';
|
||||
import type { MediaItem } from '@/types/media';
|
||||
interface HomepageData {
|
||||
hero_image_url: string | null;
|
||||
about_image_url: string | null;
|
||||
|
||||
@ -79,9 +79,7 @@ function toggleVariant(rawMaterial: CuttingRawMaterialCatalogItem, price: Cuttin
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
if (!selectedMaterials.value[index].is_initial) {
|
||||
selectedMaterials.value.splice(index, 1);
|
||||
}
|
||||
selectedMaterials.value.splice(index, 1);
|
||||
} else {
|
||||
selectedMaterials.value.push({
|
||||
raw_material_price_id: price.id,
|
||||
@ -99,7 +97,7 @@ function toggleVariant(rawMaterial: CuttingRawMaterialCatalogItem, price: Cuttin
|
||||
|
||||
function removeSelected(priceId: number) {
|
||||
selectedMaterials.value = selectedMaterials.value.filter(
|
||||
(item) => item.raw_material_price_id !== priceId || item.is_initial,
|
||||
(item) => item.raw_material_price_id !== priceId,
|
||||
);
|
||||
}
|
||||
|
||||
@ -183,26 +181,32 @@ watch(open, (value) => {
|
||||
<p class="text-sm font-medium">Bahan Baku Terpilih ({{ selectedMaterials.length }})</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="item in selectedMaterials" :key="item.raw_material_price_id"
|
||||
class="flex items-center gap-2 rounded-md border p-2"
|
||||
class="flex flex-col gap-2 rounded-md border p-2 sm:flex-row sm:items-center"
|
||||
:class="item.is_initial ? 'border-primary bg-primary/5' : ''">
|
||||
<PosCatalogVariantThumb :items="item.images" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ item.raw_material_name }} - {{ item.variant }}
|
||||
</p>
|
||||
<Badge variant="outline" class="text-xs mt-0.5 select-none font-normal">
|
||||
Stok: {{ item.stock_formatted }}
|
||||
</Badge>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<PosCatalogVariantThumb :items="item.images" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ item.raw_material_name }} - {{ item.variant }}
|
||||
</p>
|
||||
<Badge variant="outline" class="text-xs mt-0.5 select-none font-normal">
|
||||
Stok: {{ item.stock_formatted }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="icon-sm"
|
||||
class="text-destructive shrink-0 sm:hidden" @click="removeSelected(item.raw_material_price_id)">
|
||||
<X class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1" @click.stop>
|
||||
<div class="flex items-center gap-2 sm:gap-1">
|
||||
<Label class="text-xs whitespace-nowrap">Pemakaian:</Label>
|
||||
<DecimalInput v-model="item.material_usage" class="h-7 w-20" />
|
||||
<span class="text-xs text-muted-foreground">{{ item.unit_abbreviation }}</span>
|
||||
<Button type="button" variant="ghost" size="icon-sm"
|
||||
class="text-destructive shrink-0 hidden sm:inline-flex" @click="removeSelected(item.raw_material_price_id)">
|
||||
<X class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button v-if="!item.is_initial" type="button" variant="ghost" size="icon-sm"
|
||||
class="text-destructive" @click="removeSelected(item.raw_material_price_id)">
|
||||
<X class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -255,8 +259,8 @@ watch(open, (value) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border-t pt-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Label class="text-sm whitespace-nowrap">Hasil:</Label>
|
||||
<NumberInput v-model="combinationResult" class="h-8 w-20" placeholder="0" />
|
||||
@ -267,10 +271,10 @@ watch(open, (value) => {
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button type="button" variant="outline" :disabled="loading" @click="open = false">
|
||||
<Button type="button" variant="outline" class="flex-1 sm:flex-none" :disabled="loading" @click="open = false">
|
||||
Batal
|
||||
</Button>
|
||||
<Button type="button" :disabled="loading || selectedMaterials.length < 2" @click="submit">
|
||||
<Button type="button" class="flex-1 sm:flex-none" :disabled="loading || selectedMaterials.length < 2" @click="submit">
|
||||
<Layers class="size-4 mr-1" />
|
||||
{{ loading ? 'Menyimpan...' : 'Simpan Kombinasi' }}
|
||||
</Button>
|
||||
|
||||
@ -128,7 +128,7 @@ async function submit() {
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogContent class="sm:max-w-lg min-w-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tambah Varian Baru</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@ -19,7 +19,7 @@ const open = defineModel<boolean>('open', { required: true });
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogContent class="sm:max-w-lg min-w-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detail Keranjang</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user