Compare commits
3 Commits
57d57f7b91
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a55641250 | |||
| 913af09e3d | |||
| 90b925a0f5 |
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
+171
-96
@@ -2,7 +2,9 @@
|
||||
<section class="container-page py-10">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-extrabold">Edit Barang</h1>
|
||||
<p class="mt-2 text-slate-600">Ubah informasi barang yang sudah kamu posting.</p>
|
||||
<p class="mt-2 text-slate-600">
|
||||
Ubah informasi barang yang sudah kamu posting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingProduct" class="card p-10 text-center">
|
||||
@@ -11,31 +13,51 @@
|
||||
|
||||
<div v-else-if="notFound" class="card p-10 text-center">
|
||||
<h2 class="text-xl font-extrabold">Barang tidak ditemukan</h2>
|
||||
<p class="mt-2 text-slate-600">Barang yang ingin kamu edit tidak ditemukan.</p>
|
||||
<RouterLink to="/dashboard" class="btn-primary mt-5">Kembali ke Dashboard</RouterLink>
|
||||
<p class="mt-2 text-slate-600">
|
||||
Barang yang ingin kamu edit tidak ditemukan.
|
||||
</p>
|
||||
<RouterLink to="/dashboard" class="btn-primary mt-5"
|
||||
>Kembali ke Dashboard</RouterLink
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid gap-8 lg:grid-cols-[1fr_360px]">
|
||||
<form class="card grid gap-5 p-6" @submit.prevent="submitEdit">
|
||||
<p v-if="errorMessage" class="rounded-xl bg-red-50 px-4 py-3 text-sm font-semibold text-red-700">
|
||||
<p
|
||||
v-if="errorMessage"
|
||||
class="rounded-xl bg-red-50 px-4 py-3 text-sm font-semibold text-red-700"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label class="label">Nama Barang</label>
|
||||
<input v-model="form.title" class="input mt-2" placeholder="Contoh: Laptop ThinkPad T480" required />
|
||||
<input
|
||||
v-model="form.title"
|
||||
class="input mt-2"
|
||||
placeholder="Contoh: Laptop ThinkPad T480"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Kategori</label>
|
||||
<select v-model="form.category" class="input mt-2" required>
|
||||
<option v-for="cat in realCategories" :key="cat" :value="cat">{{ cat }}</option>
|
||||
<option v-for="cat in realCategories" :key="cat" :value="cat">
|
||||
{{ cat }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Harga</label>
|
||||
<input v-model.number="form.price" type="number" class="input mt-2" placeholder="2500000" required />
|
||||
<input
|
||||
v-model.number="form.price"
|
||||
type="number"
|
||||
class="input mt-2"
|
||||
placeholder="2500000"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,25 +73,41 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Lokasi</label>
|
||||
<input v-model="form.location" class="input mt-2" placeholder="Yogyakarta" required />
|
||||
<input
|
||||
v-model="form.location"
|
||||
class="input mt-2"
|
||||
placeholder="Yogyakarta"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Nama Penjual</label>
|
||||
<input v-model="form.seller" class="input mt-2" placeholder="Nama kamu" required />
|
||||
<input
|
||||
v-model="form.seller"
|
||||
class="input mt-2"
|
||||
placeholder="Nama kamu"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Nomor WhatsApp</label>
|
||||
<input v-model="form.whatsapp" class="input mt-2" placeholder="6281234567890" required />
|
||||
<label class="label">Nomor WhatsApp +62</label>
|
||||
<input
|
||||
v-model="form.whatsapp"
|
||||
class="input mt-2"
|
||||
placeholder="+6281234567890"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">Foto Barang</label>
|
||||
<p v-if="existingImages.length" class="mb-3 text-sm text-slate-500">
|
||||
{{ existingImages.length }} foto saat ini. Upload foto baru jika ingin mengganti.
|
||||
{{ existingImages.length }} foto saat ini. Upload foto baru jika
|
||||
ingin mengganti.
|
||||
</p>
|
||||
|
||||
<input
|
||||
@@ -81,39 +119,67 @@
|
||||
@change="handleImageChange"
|
||||
/>
|
||||
|
||||
<div v-if="selectedImages.length" class="mt-3 flex items-center justify-between gap-3 rounded-xl bg-brand-50 px-4 py-3 text-sm font-bold text-brand-700">
|
||||
<div
|
||||
v-if="selectedImages.length"
|
||||
class="mt-3 flex items-center justify-between gap-3 rounded-xl bg-brand-50 px-4 py-3 text-sm font-bold text-brand-700"
|
||||
>
|
||||
<span>{{ selectedImages.length }} foto baru dipilih</span>
|
||||
<button type="button" class="text-red-700" @click="clearImages">Hapus semua</button>
|
||||
<button type="button" class="text-red-700" @click="clearImages">
|
||||
Hapus semua
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="previewUrls.length" class="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<div v-for="(preview, index) in previewUrls" :key="preview.url" class="overflow-hidden rounded-xl border border-slate-200">
|
||||
<img :src="preview.url" :alt="`Preview foto barang ${index + 1}`" class="aspect-[4/3] w-full object-cover" />
|
||||
<button type="button" class="w-full bg-red-50 px-3 py-2 text-xs font-bold text-red-700" @click="removeImage(index)">
|
||||
<div
|
||||
v-if="previewUrls.length"
|
||||
class="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3"
|
||||
>
|
||||
<div
|
||||
v-for="(preview, index) in previewUrls"
|
||||
:key="preview.url"
|
||||
class="overflow-hidden rounded-xl border border-slate-200"
|
||||
>
|
||||
<img
|
||||
:src="preview.url"
|
||||
:alt="`Preview foto barang ${index + 1}`"
|
||||
class="aspect-[4/3] w-full object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full bg-red-50 px-3 py-2 text-xs font-bold text-red-700"
|
||||
@click="removeImage(index)"
|
||||
>
|
||||
Hapus foto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
Maksimal 6 foto, 10 MB per foto. Biarkan kosong jika tidak ingin mengganti foto.
|
||||
Maksimal 6 foto, 10 MB per foto. Biarkan kosong jika tidak ingin
|
||||
mengganti foto.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">Deskripsi</label>
|
||||
<textarea v-model="form.description" class="input mt-2 min-h-32" placeholder="Jelaskan kondisi barang..." required></textarea>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
class="input mt-2 min-h-32"
|
||||
placeholder="Jelaskan kondisi barang..."
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary w-full" :disabled="loading">
|
||||
{{ loading ? "Menyimpan..." : "Simpan Perubahan" }}
|
||||
{{ loading ? 'Menyimpan...' : 'Simpan Perubahan' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="card h-fit p-6">
|
||||
<h2 class="text-lg font-extrabold">Tips Edit Produk</h2>
|
||||
<ul class="mt-4 grid gap-3 text-sm leading-6 text-slate-600">
|
||||
<li>- Perbarui judul dan deskripsi jika ada perubahan kondisi barang.</li>
|
||||
<li>
|
||||
- Perbarui judul dan deskripsi jika ada perubahan kondisi barang.
|
||||
</li>
|
||||
<li>- Upload foto baru jika kondisi fisik barang berubah.</li>
|
||||
<li>- Pastikan nomor WhatsApp masih aktif.</li>
|
||||
</ul>
|
||||
@@ -123,126 +189,135 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, computed, ref, onMounted } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { categories } from "../data/products"
|
||||
import { fetchProduct, updateProduct } from "../utils"
|
||||
import { reactive, computed, ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { categories } from '../data/products';
|
||||
import { fetchProduct, updateProduct } from '../utils';
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const realCategories = computed(() => categories.filter((cat) => cat !== "Semua"))
|
||||
const fileInput = ref(null)
|
||||
const MAX_IMAGES = 6
|
||||
const MAX_IMAGE_SIZE_MB = 10
|
||||
const MAX_IMAGE_SIZE = MAX_IMAGE_SIZE_MB * 1024 * 1024
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const realCategories = computed(() =>
|
||||
categories.filter((cat) => cat !== 'Semua')
|
||||
);
|
||||
const fileInput = ref(null);
|
||||
const MAX_IMAGES = 6;
|
||||
const MAX_IMAGE_SIZE_MB = 10;
|
||||
const MAX_IMAGE_SIZE = MAX_IMAGE_SIZE_MB * 1024 * 1024;
|
||||
|
||||
const form = reactive({
|
||||
title: "",
|
||||
category: "Laptop",
|
||||
price: "",
|
||||
condition: "Bekas Normal",
|
||||
location: "",
|
||||
seller: "",
|
||||
whatsapp: "",
|
||||
description: ""
|
||||
})
|
||||
const existingImages = ref([])
|
||||
const selectedImages = ref([])
|
||||
const previewUrls = ref([])
|
||||
const loading = ref(false)
|
||||
const loadingProduct = ref(true)
|
||||
const notFound = ref(false)
|
||||
const errorMessage = ref("")
|
||||
title: '',
|
||||
category: 'Laptop',
|
||||
price: '',
|
||||
condition: 'Bekas Normal',
|
||||
location: '',
|
||||
seller: '',
|
||||
whatsapp: '',
|
||||
description: '',
|
||||
});
|
||||
const existingImages = ref([]);
|
||||
const selectedImages = ref([]);
|
||||
const previewUrls = ref([]);
|
||||
const loading = ref(false);
|
||||
const loadingProduct = ref(true);
|
||||
const notFound = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
function imageKey(file) {
|
||||
return `${file.name}-${file.size}-${file.lastModified}`
|
||||
return `${file.name}-${file.size}-${file.lastModified}`;
|
||||
}
|
||||
|
||||
function rebuildPreviews() {
|
||||
previewUrls.value.forEach((preview) => URL.revokeObjectURL(preview.url))
|
||||
previewUrls.value.forEach((preview) => URL.revokeObjectURL(preview.url));
|
||||
previewUrls.value = selectedImages.value.map((file) => ({
|
||||
key: imageKey(file),
|
||||
url: URL.createObjectURL(file)
|
||||
}))
|
||||
url: URL.createObjectURL(file),
|
||||
}));
|
||||
}
|
||||
|
||||
function handleImageChange(event) {
|
||||
const incomingFiles = Array.from(event.target.files || [])
|
||||
const oversizedFile = incomingFiles.find((file) => file.size > MAX_IMAGE_SIZE)
|
||||
const incomingFiles = Array.from(event.target.files || []);
|
||||
const oversizedFile = incomingFiles.find(
|
||||
(file) => file.size > MAX_IMAGE_SIZE
|
||||
);
|
||||
|
||||
if (oversizedFile) {
|
||||
errorMessage.value = `Foto "${oversizedFile.name}" lebih dari ${MAX_IMAGE_SIZE_MB} MB. Pilih foto yang lebih kecil.`
|
||||
event.target.value = ""
|
||||
return
|
||||
errorMessage.value = `Foto "${oversizedFile.name}" lebih dari ${MAX_IMAGE_SIZE_MB} MB. Pilih foto yang lebih kecil.`;
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const merged = [...selectedImages.value]
|
||||
const merged = [...selectedImages.value];
|
||||
incomingFiles.forEach((file) => {
|
||||
if (!merged.some((item) => imageKey(item) === imageKey(file)) && merged.length < MAX_IMAGES) {
|
||||
merged.push(file)
|
||||
if (
|
||||
!merged.some((item) => imageKey(item) === imageKey(file)) &&
|
||||
merged.length < MAX_IMAGES
|
||||
) {
|
||||
merged.push(file);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
selectedImages.value = merged
|
||||
rebuildPreviews()
|
||||
errorMessage.value = ""
|
||||
event.target.value = ""
|
||||
selectedImages.value = merged;
|
||||
rebuildPreviews();
|
||||
errorMessage.value = '';
|
||||
event.target.value = '';
|
||||
}
|
||||
|
||||
function removeImage(index) {
|
||||
selectedImages.value = selectedImages.value.filter((_, itemIndex) => itemIndex !== index)
|
||||
rebuildPreviews()
|
||||
selectedImages.value = selectedImages.value.filter(
|
||||
(_, itemIndex) => itemIndex !== index
|
||||
);
|
||||
rebuildPreviews();
|
||||
}
|
||||
|
||||
function clearImages() {
|
||||
selectedImages.value = []
|
||||
rebuildPreviews()
|
||||
if (fileInput.value) fileInput.value.value = ""
|
||||
selectedImages.value = [];
|
||||
rebuildPreviews();
|
||||
if (fileInput.value) fileInput.value.value = '';
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
loading.value = true
|
||||
errorMessage.value = ""
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
const payload = new FormData()
|
||||
const payload = new FormData();
|
||||
Object.entries(form).forEach(([key, value]) => {
|
||||
payload.append(key, value)
|
||||
})
|
||||
payload.append(key, value);
|
||||
});
|
||||
selectedImages.value.forEach((image) => {
|
||||
payload.append("images", image)
|
||||
})
|
||||
payload.append('images', image);
|
||||
});
|
||||
|
||||
try {
|
||||
await updateProduct(route.params.id, payload)
|
||||
router.push("/dashboard")
|
||||
await updateProduct(route.params.id, payload);
|
||||
router.push('/dashboard');
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message
|
||||
errorMessage.value = error.message;
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await fetchProduct(route.params.id)
|
||||
const product = data.product
|
||||
const data = await fetchProduct(route.params.id);
|
||||
const product = data.product;
|
||||
|
||||
form.title = product.title
|
||||
form.category = product.category
|
||||
form.price = product.price
|
||||
form.condition = product.condition
|
||||
form.location = product.location
|
||||
form.seller = product.seller
|
||||
form.whatsapp = product.whatsapp
|
||||
form.description = product.description
|
||||
form.title = product.title;
|
||||
form.category = product.category;
|
||||
form.price = product.price;
|
||||
form.condition = product.condition;
|
||||
form.location = product.location;
|
||||
form.seller = product.seller;
|
||||
form.whatsapp = product.whatsapp;
|
||||
form.description = product.description;
|
||||
|
||||
if (product.images && product.images.length) {
|
||||
existingImages.value = product.images
|
||||
existingImages.value = product.images;
|
||||
}
|
||||
} catch {
|
||||
notFound.value = true
|
||||
notFound.value = true;
|
||||
} finally {
|
||||
loadingProduct.value = false
|
||||
loadingProduct.value = false;
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
+77
-31
@@ -2,73 +2,119 @@
|
||||
<section class="container-page grid min-h-[70vh] place-items-center py-10">
|
||||
<div class="card w-full max-w-md p-6">
|
||||
<h1 class="text-2xl font-extrabold">Register</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">Buat akun untuk mulai menjual barang.</p>
|
||||
<p class="mt-2 text-sm text-slate-600">
|
||||
Buat akun untuk mulai menjual barang.
|
||||
</p>
|
||||
|
||||
<form class="mt-6 grid gap-4" @submit.prevent="register">
|
||||
<p v-if="errorMessage" class="rounded-xl bg-red-50 px-4 py-3 text-sm font-semibold text-red-700">
|
||||
<p
|
||||
v-if="errorMessage"
|
||||
class="rounded-xl bg-red-50 px-4 py-3 text-sm font-semibold text-red-700"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<div>
|
||||
<label class="label">Nama Lengkap</label>
|
||||
<input v-model="form.name" class="input mt-2" placeholder="Nama kamu" required />
|
||||
<input
|
||||
v-model="form.name"
|
||||
class="input mt-2"
|
||||
placeholder="Nama kamu"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Email</label>
|
||||
<input v-model="form.email" type="email" class="input mt-2" placeholder="nama@email.com" required />
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
class="input mt-2"
|
||||
placeholder="nama@email.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Nomor WhatsApp</label>
|
||||
<input v-model="form.whatsapp" class="input mt-2" placeholder="6281234567890" required />
|
||||
<label class="label">Nomor WhatsApp +62</label>
|
||||
<input
|
||||
v-model="form.whatsapp"
|
||||
class="input mt-2"
|
||||
placeholder="+6281234567890"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Password</label>
|
||||
<input v-model="form.password" type="password" class="input mt-2" placeholder="••••••••" required />
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="input mt-2"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button class="btn-primary w-full" :disabled="loading">
|
||||
<svg v-if="loading" class="mr-3 h-5 w-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ loading ? "Memproses..." : "Daftar" }}
|
||||
<svg
|
||||
v-if="loading"
|
||||
class="mr-3 h-5 w-5 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ loading ? 'Memproses...' : 'Daftar' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-5 text-center text-sm text-slate-600">
|
||||
Sudah punya akun?
|
||||
<RouterLink to="/login" class="font-bold text-brand-700">Login</RouterLink>
|
||||
<RouterLink to="/login" class="font-bold text-brand-700"
|
||||
>Login</RouterLink
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { registerUser, setAuth } from "../utils"
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { registerUser, setAuth } from '../utils';
|
||||
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
const form = reactive({
|
||||
name: "",
|
||||
email: "",
|
||||
whatsapp: "",
|
||||
password: ""
|
||||
})
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref("")
|
||||
name: '',
|
||||
email: '',
|
||||
whatsapp: '',
|
||||
password: '',
|
||||
});
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
async function register() {
|
||||
loading.value = true
|
||||
errorMessage.value = ""
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
try {
|
||||
const data = await registerUser(form)
|
||||
setAuth(data)
|
||||
router.push("/dashboard")
|
||||
const data = await registerUser(form);
|
||||
setAuth(data);
|
||||
router.push('/dashboard');
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message
|
||||
errorMessage.value = error.message;
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+151
-80
@@ -2,30 +2,49 @@
|
||||
<section class="container-page py-10">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-extrabold">Jual Barang</h1>
|
||||
<p class="mt-2 text-slate-600">Posting barang ke marketplace dengan beberapa foto asli dari perangkatmu.</p>
|
||||
<p class="mt-2 text-slate-600">
|
||||
Posting barang ke marketplace dengan beberapa foto asli dari
|
||||
perangkatmu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-8 lg:grid-cols-[1fr_360px]">
|
||||
<form class="card grid gap-5 p-6" @submit.prevent="submitProduct">
|
||||
<p v-if="errorMessage" class="rounded-xl bg-red-50 px-4 py-3 text-sm font-semibold text-red-700">
|
||||
<p
|
||||
v-if="errorMessage"
|
||||
class="rounded-xl bg-red-50 px-4 py-3 text-sm font-semibold text-red-700"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label class="label">Nama Barang</label>
|
||||
<input v-model="form.title" class="input mt-2" placeholder="Contoh: Laptop ThinkPad T480" required />
|
||||
<input
|
||||
v-model="form.title"
|
||||
class="input mt-2"
|
||||
placeholder="Contoh: Laptop ThinkPad T480"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Kategori</label>
|
||||
<select v-model="form.category" class="input mt-2" required>
|
||||
<option v-for="cat in realCategories" :key="cat" :value="cat">{{ cat }}</option>
|
||||
<option v-for="cat in realCategories" :key="cat" :value="cat">
|
||||
{{ cat }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Harga</label>
|
||||
<input v-model.number="form.price" type="number" class="input mt-2" placeholder="2500000" required />
|
||||
<input
|
||||
v-model.number="form.price"
|
||||
type="number"
|
||||
class="input mt-2"
|
||||
placeholder="2500000"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,18 +60,33 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Lokasi</label>
|
||||
<input v-model="form.location" class="input mt-2" placeholder="Yogyakarta" required />
|
||||
<input
|
||||
v-model="form.location"
|
||||
class="input mt-2"
|
||||
placeholder="Yogyakarta"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Nama Penjual</label>
|
||||
<input v-model="form.seller" class="input mt-2" placeholder="Nama kamu" required />
|
||||
<input
|
||||
v-model="form.seller"
|
||||
class="input mt-2"
|
||||
placeholder="Nama kamu"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Nomor WhatsApp</label>
|
||||
<input v-model="form.whatsapp" class="input mt-2" placeholder="6281234567890" required />
|
||||
<label class="label">Nomor WhatsApp +62</label>
|
||||
<input
|
||||
v-model="form.whatsapp"
|
||||
class="input mt-2"
|
||||
placeholder="+6281234567890"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,39 +101,67 @@
|
||||
@change="handleImageChange"
|
||||
/>
|
||||
|
||||
<div v-if="selectedImages.length" class="mt-3 flex items-center justify-between gap-3 rounded-xl bg-brand-50 px-4 py-3 text-sm font-bold text-brand-700">
|
||||
<div
|
||||
v-if="selectedImages.length"
|
||||
class="mt-3 flex items-center justify-between gap-3 rounded-xl bg-brand-50 px-4 py-3 text-sm font-bold text-brand-700"
|
||||
>
|
||||
<span>{{ selectedImages.length }} foto dipilih</span>
|
||||
<button type="button" class="text-red-700" @click="clearImages">Hapus semua</button>
|
||||
<button type="button" class="text-red-700" @click="clearImages">
|
||||
Hapus semua
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="previewUrls.length" class="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<div v-for="(preview, index) in previewUrls" :key="preview.url" class="overflow-hidden rounded-xl border border-slate-200">
|
||||
<img :src="preview.url" :alt="`Preview foto barang ${index + 1}`" class="aspect-[4/3] w-full object-cover" />
|
||||
<button type="button" class="w-full bg-red-50 px-3 py-2 text-xs font-bold text-red-700" @click="removeImage(index)">
|
||||
<div
|
||||
v-if="previewUrls.length"
|
||||
class="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3"
|
||||
>
|
||||
<div
|
||||
v-for="(preview, index) in previewUrls"
|
||||
:key="preview.url"
|
||||
class="overflow-hidden rounded-xl border border-slate-200"
|
||||
>
|
||||
<img
|
||||
:src="preview.url"
|
||||
:alt="`Preview foto barang ${index + 1}`"
|
||||
class="aspect-[4/3] w-full object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full bg-red-50 px-3 py-2 text-xs font-bold text-red-700"
|
||||
@click="removeImage(index)"
|
||||
>
|
||||
Hapus foto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
Maksimal 6 foto, 10 MB per foto. Kamu boleh pilih beberapa sekaligus atau tambah satu per satu.
|
||||
Maksimal 6 foto, 10 MB per foto. Kamu boleh pilih beberapa sekaligus
|
||||
atau tambah satu per satu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">Deskripsi</label>
|
||||
<textarea v-model="form.description" class="input mt-2 min-h-32" placeholder="Jelaskan kondisi barang..." required></textarea>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
class="input mt-2 min-h-32"
|
||||
placeholder="Jelaskan kondisi barang..."
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary w-full" :disabled="loading">
|
||||
{{ loading ? "Memposting..." : "Posting Barang" }}
|
||||
{{ loading ? 'Memposting...' : 'Posting Barang' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="card h-fit p-6">
|
||||
<h2 class="text-lg font-extrabold">Tips Isi Produk</h2>
|
||||
<ul class="mt-4 grid gap-3 text-sm leading-6 text-slate-600">
|
||||
<li>- Pakai judul yang jelas, misal "MikroTik RB941 Bekas Normal".</li>
|
||||
<li>
|
||||
- Pakai judul yang jelas, misal "MikroTik RB941 Bekas Normal".
|
||||
</li>
|
||||
<li>- Jelaskan minus barang kalau ada.</li>
|
||||
<li>- Pakai nomor WhatsApp aktif.</li>
|
||||
<li>- Upload beberapa foto dari sisi yang berbeda.</li>
|
||||
@@ -110,109 +172,118 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, computed, ref } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { categories } from "../data/products"
|
||||
import { createProduct, getCurrentUser } from "../utils"
|
||||
import { reactive, computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { categories } from '../data/products';
|
||||
import { createProduct, getCurrentUser } from '../utils';
|
||||
|
||||
const router = useRouter()
|
||||
const realCategories = computed(() => categories.filter((cat) => cat !== "Semua"))
|
||||
const fileInput = ref(null)
|
||||
const MAX_IMAGES = 6
|
||||
const MAX_IMAGE_SIZE_MB = 10
|
||||
const MAX_IMAGE_SIZE = MAX_IMAGE_SIZE_MB * 1024 * 1024
|
||||
const router = useRouter();
|
||||
const realCategories = computed(() =>
|
||||
categories.filter((cat) => cat !== 'Semua')
|
||||
);
|
||||
const fileInput = ref(null);
|
||||
const MAX_IMAGES = 6;
|
||||
const MAX_IMAGE_SIZE_MB = 10;
|
||||
const MAX_IMAGE_SIZE = MAX_IMAGE_SIZE_MB * 1024 * 1024;
|
||||
|
||||
const form = reactive({
|
||||
title: "",
|
||||
category: "Laptop",
|
||||
price: "",
|
||||
condition: "Bekas Normal",
|
||||
location: "",
|
||||
seller: "",
|
||||
whatsapp: "",
|
||||
description: ""
|
||||
})
|
||||
const selectedImages = ref([])
|
||||
const previewUrls = ref([])
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref("")
|
||||
title: '',
|
||||
category: 'Laptop',
|
||||
price: '',
|
||||
condition: 'Bekas Normal',
|
||||
location: '',
|
||||
seller: '',
|
||||
whatsapp: '',
|
||||
description: '',
|
||||
});
|
||||
const selectedImages = ref([]);
|
||||
const previewUrls = ref([]);
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const currentUser = getCurrentUser()
|
||||
const currentUser = getCurrentUser();
|
||||
if (currentUser) {
|
||||
form.seller = currentUser.name || ""
|
||||
form.whatsapp = currentUser.whatsapp || ""
|
||||
form.seller = currentUser.name || '';
|
||||
form.whatsapp = currentUser.whatsapp || '';
|
||||
}
|
||||
|
||||
function imageKey(file) {
|
||||
return `${file.name}-${file.size}-${file.lastModified}`
|
||||
return `${file.name}-${file.size}-${file.lastModified}`;
|
||||
}
|
||||
|
||||
function rebuildPreviews() {
|
||||
previewUrls.value.forEach((preview) => URL.revokeObjectURL(preview.url))
|
||||
previewUrls.value.forEach((preview) => URL.revokeObjectURL(preview.url));
|
||||
previewUrls.value = selectedImages.value.map((file) => ({
|
||||
key: imageKey(file),
|
||||
url: URL.createObjectURL(file)
|
||||
}))
|
||||
url: URL.createObjectURL(file),
|
||||
}));
|
||||
}
|
||||
|
||||
function handleImageChange(event) {
|
||||
const incomingFiles = Array.from(event.target.files || [])
|
||||
const oversizedFile = incomingFiles.find((file) => file.size > MAX_IMAGE_SIZE)
|
||||
const incomingFiles = Array.from(event.target.files || []);
|
||||
const oversizedFile = incomingFiles.find(
|
||||
(file) => file.size > MAX_IMAGE_SIZE
|
||||
);
|
||||
|
||||
if (oversizedFile) {
|
||||
errorMessage.value = `Foto "${oversizedFile.name}" lebih dari ${MAX_IMAGE_SIZE_MB} MB. Pilih foto yang lebih kecil.`
|
||||
event.target.value = ""
|
||||
return
|
||||
errorMessage.value = `Foto "${oversizedFile.name}" lebih dari ${MAX_IMAGE_SIZE_MB} MB. Pilih foto yang lebih kecil.`;
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const merged = [...selectedImages.value]
|
||||
const merged = [...selectedImages.value];
|
||||
incomingFiles.forEach((file) => {
|
||||
if (!merged.some((item) => imageKey(item) === imageKey(file)) && merged.length < MAX_IMAGES) {
|
||||
merged.push(file)
|
||||
if (
|
||||
!merged.some((item) => imageKey(item) === imageKey(file)) &&
|
||||
merged.length < MAX_IMAGES
|
||||
) {
|
||||
merged.push(file);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
selectedImages.value = merged
|
||||
rebuildPreviews()
|
||||
errorMessage.value = ""
|
||||
event.target.value = ""
|
||||
selectedImages.value = merged;
|
||||
rebuildPreviews();
|
||||
errorMessage.value = '';
|
||||
event.target.value = '';
|
||||
}
|
||||
|
||||
function removeImage(index) {
|
||||
selectedImages.value = selectedImages.value.filter((_, itemIndex) => itemIndex !== index)
|
||||
rebuildPreviews()
|
||||
selectedImages.value = selectedImages.value.filter(
|
||||
(_, itemIndex) => itemIndex !== index
|
||||
);
|
||||
rebuildPreviews();
|
||||
}
|
||||
|
||||
function clearImages() {
|
||||
selectedImages.value = []
|
||||
rebuildPreviews()
|
||||
if (fileInput.value) fileInput.value.value = ""
|
||||
selectedImages.value = [];
|
||||
rebuildPreviews();
|
||||
if (fileInput.value) fileInput.value.value = '';
|
||||
}
|
||||
|
||||
async function submitProduct() {
|
||||
if (!selectedImages.value.length) {
|
||||
errorMessage.value = "Minimal 1 foto barang wajib diupload."
|
||||
return
|
||||
errorMessage.value = 'Minimal 1 foto barang wajib diupload.';
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ""
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
const payload = new FormData()
|
||||
const payload = new FormData();
|
||||
Object.entries(form).forEach(([key, value]) => {
|
||||
payload.append(key, value)
|
||||
})
|
||||
payload.append(key, value);
|
||||
});
|
||||
selectedImages.value.forEach((image) => {
|
||||
payload.append("images", image)
|
||||
})
|
||||
payload.append('images', image);
|
||||
});
|
||||
|
||||
try {
|
||||
await createProduct(payload)
|
||||
router.push("/dashboard")
|
||||
await createProduct(payload);
|
||||
router.push('/dashboard');
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message
|
||||
errorMessage.value = error.message;
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user