refactor: update MediaDropzone and ProductModal components for improved layout and responsiveness; adjust styles for placeholders, overlays, and modal elements to enhance user experience across different screen sizes

This commit is contained in:
Yoga Pangestu 2026-07-05 14:51:06 +07:00
parent 289fc4dc2c
commit 2a49018c5e
5 changed files with 78 additions and 42 deletions

View File

@ -505,23 +505,37 @@ watch(
/* === Placeholder inside slot === */ /* === Placeholder inside slot === */
.dz-placeholder { .dz-placeholder {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center; align-items: center;
gap: 0.75rem; justify-content: center;
padding: 0.75rem 1rem; text-align: center;
gap: 0.5rem;
padding: 1rem;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
cursor: pointer; cursor: pointer;
overflow: hidden; 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) === */ /* === Overlay (shown when previews exist) === */
.dz-overlay { .dz-overlay {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center; align-items: center;
gap: 0.75rem; justify-content: center;
padding: 0.75rem 1rem; text-align: center;
gap: 0.5rem;
padding: 1rem;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
border: 2px dashed var(--border); border: 2px dashed var(--border);
@ -533,6 +547,16 @@ watch(
box-sizing: border-box; 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 { .dz-overlay:hover {
border-color: var(--primary); border-color: var(--primary);
background: color-mix(in oklch, var(--muted) 80%, transparent); background: color-mix(in oklch, var(--muted) 80%, transparent);
@ -549,43 +573,53 @@ watch(
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
color: var(--foreground); color: var(--foreground);
white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
} }
@media (min-width: 640px) {
.dz-placeholder-primary {
white-space: nowrap;
}
}
.dz-placeholder-secondary { .dz-placeholder-secondary {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--muted-foreground); color: var(--muted-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0; min-width: 0;
flex: 1; flex: 1;
} }
@media (min-width: 640px) {
.dz-placeholder-secondary {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
/* ---- Preview grid — multiple mode (default) ---- */ /* ---- Preview grid — multiple mode (default) ---- */
.preview-grid { .preview-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5rem; gap: 0.5rem;
margin-top: 0.75rem; margin-top: 0.75rem;
} }
@media (min-width: 400px) { @media (min-width: 400px) {
.preview-grid { .preview-grid {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
} }
@media (min-width: 560px) { @media (min-width: 560px) {
.preview-grid { .preview-grid {
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, minmax(0, 1fr));
} }
} }
@media (min-width: 720px) { @media (min-width: 720px) {
.preview-grid { .preview-grid {
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(6, minmax(0, 1fr));
} }
} }

View File

@ -2,7 +2,8 @@
import { X } from '@lucide/vue'; import { X } from '@lucide/vue';
import { ref, computed, watch } from 'vue'; import { ref, computed, watch } from 'vue';
import { formatRupiah } from '@/lib/rupiah'; 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<{ const props = defineProps<{
product: Product | null; product: Product | null;
@ -95,21 +96,27 @@ const handleAddToCart = () => {
<template> <template>
<div v-if="product" <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')"> @click.self="emit('close')">
<div <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 --> <!-- 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 <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" /> <img :src="mainImage" :alt="product.name" class="w-full h-full object-cover" />
</div> </div>
<!-- Image Thumbnails --> <!-- Image Thumbnails -->
<div v-if="allImages.length > 1" class="flex gap-2 overflow-x-auto scrollbar-hide pb-1"> <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="[ <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 activeImageIndex === idx
? 'border-amber-500 shadow-md shadow-amber-200/40' ? 'border-amber-500 shadow-md shadow-amber-200/40'
: 'border-slate-200/60 opacity-60 hover:opacity-100' : 'border-slate-200/60 opacity-60 hover:opacity-100'
@ -121,26 +128,20 @@ const handleAddToCart = () => {
</div> </div>
<!-- Info Side --> <!-- 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="space-y-4">
<div class="flex justify-between items-start"> <div>
<div> <span class="text-[10px] font-bold uppercase tracking-widest text-amber-600">
<span class="text-[10px] font-bold uppercase tracking-widest text-amber-600"> {{product.categories.map(c => c.name).join(', ')}}
{{product.categories.map(c => c.name).join(', ')}} </span>
</span> <h3 class="text-xl md:text-2xl font-serif mt-0.5 text-slate-800">{{ product.name }}</h3>
<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> </div>
<!-- Description --> <!-- 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> <h4 class="text-[10px] uppercase font-bold tracking-widest text-slate-400">Deskripsi</h4>
<p <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 {{ product.description || `Pakaian batik modern dan nyaman hasil karya perajin
terbaik kami.Menggunakan serat kain pilihan yang awet dan menyejukkan kulit.` }} terbaik kami.Menggunakan serat kain pilihan yang awet dan menyejukkan kulit.` }}
</p> </p>
@ -168,13 +169,13 @@ const handleAddToCart = () => {
<div v-if="activeVariant && activeVariant.prices.length > 0" class="space-y-2"> <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 <h4 class="text-[10px] uppercase font-bold tracking-widest text-slate-400">Saluran Harga
(Grosir/Eceran)</h4> (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" <button v-for="price in activeVariant.prices" :key="price.id"
@click="activePriceType = price.type" :class="[ @click="activePriceType = price.type" :class="[
'px-3 py-2 text-left rounded-xl border transition-all cursor-pointer', 'px-3 py-2 text-left rounded-xl border transition-all cursor-pointer',
activePriceType === price.type activePriceType === price.type
? 'border-amber-500 bg-amber-500/5/5' ? 'border-amber-500 bg-amber-500/5'
: 'border-slate-200/60 hover' : 'border-slate-200/60 hover:bg-slate-50/50'
]"> ]">
<span class="block text-[8px] uppercase tracking-wider font-bold text-slate-400"> <span class="block text-[8px] uppercase tracking-wider font-bold text-slate-400">
{{ price.type_label }} {{ price.type_label }}
@ -193,7 +194,7 @@ const handleAddToCart = () => {
<div> <div>
<span class="text-[10px] uppercase font-bold tracking-widest text-slate-400">Harga <span class="text-[10px] uppercase font-bold tracking-widest text-slate-400">Harga
Terpilih</span> 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' }} {{ selectedPrice ? `Rp ${formatRupiah(selectedPrice.price)}` : 'Pilih ukuran' }}
</div> </div>
</div> </div>
@ -208,7 +209,7 @@ const handleAddToCart = () => {
<button v-if="activeVariant && selectedPrice" @click="handleAddToCart" <button v-if="activeVariant && selectedPrice" @click="handleAddToCart"
:disabled="activeVariant.stock <= 0" :class="[ :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 activeVariant.stock > 0
? 'bg-amber-600 text-white hover:bg-amber-500 shadow-amber-200/50' ? 'bg-amber-600 text-white hover:bg-amber-500 shadow-amber-200/50'
: 'bg-slate-100 text-slate-400 cursor-not-allowed shadow-none' : 'bg-slate-100 text-slate-400 cursor-not-allowed shadow-none'

View File

@ -22,7 +22,8 @@ import ProductModal from '@/components/modal/ProductModal.vue';
import DraggableCarousel from '@/components/ui/carousel/DraggableCarousel.vue'; import DraggableCarousel from '@/components/ui/carousel/DraggableCarousel.vue';
import { formatRupiah } from '@/lib/rupiah'; import { formatRupiah } from '@/lib/rupiah';
import admin from '@/routes/admin'; 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 { interface HomepageData {
hero_image_url: string | null; hero_image_url: string | null;
about_image_url: string | null; about_image_url: string | null;

View File

@ -128,7 +128,7 @@ async function submit() {
<template> <template>
<Dialog v-model:open="open"> <Dialog v-model:open="open">
<DialogContent class="sm:max-w-lg"> <DialogContent class="sm:max-w-lg min-w-0">
<DialogHeader> <DialogHeader>
<DialogTitle>Tambah Varian Baru</DialogTitle> <DialogTitle>Tambah Varian Baru</DialogTitle>
</DialogHeader> </DialogHeader>

View File

@ -19,7 +19,7 @@ const open = defineModel<boolean>('open', { required: true });
<template> <template>
<Dialog v-model:open="open"> <Dialog v-model:open="open">
<DialogContent class="sm:max-w-lg"> <DialogContent class="sm:max-w-lg min-w-0">
<DialogHeader> <DialogHeader>
<DialogTitle>Detail Keranjang</DialogTitle> <DialogTitle>Detail Keranjang</DialogTitle>
</DialogHeader> </DialogHeader>