Compare commits
6 Commits
dev
...
081c9ea521
| Author | SHA1 | Date | |
|---|---|---|---|
| 081c9ea521 | |||
| f814f58e2d | |||
| 3324365ea7 | |||
| ae729dc1d2 | |||
| 4677d8c1ee | |||
| b20b46aa0f |
@@ -0,0 +1,38 @@
|
|||||||
|
name: Deploy SecondTech
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: self-hosted
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Pull latest code
|
||||||
|
run: |
|
||||||
|
cd /var/www/secondtech
|
||||||
|
git fetch origin main
|
||||||
|
git reset --hard origin/main
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
cd /var/www/secondtech
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd /var/www/secondtech
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
- name: Restart backend
|
||||||
|
run: |
|
||||||
|
cd /var/www/secondtech
|
||||||
|
pm2 restart secondtech-api || pm2 start "pnpm run start" --name secondtech-api
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
- name: Reload nginx
|
||||||
|
run: |
|
||||||
|
nginx -t
|
||||||
|
systemctl reload nginx
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="container-page py-10">
|
|
||||||
<RouterLink to="/marketplace" class="mb-6 inline-flex text-sm font-bold text-brand-700">
|
|
||||||
<- Kembali ke Marketplace
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<div v-if="loading" class="card p-10 text-center">
|
|
||||||
<h1 class="text-2xl font-extrabold">Memuat produk...</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="product" class="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(380px,520px)]">
|
|
||||||
<div class="grid gap-4 sm:grid-cols-[88px_1fr]">
|
|
||||||
<div class="order-2 flex gap-3 overflow-x-auto sm:order-1 sm:grid sm:content-start sm:max-h-[78vh] sm:overflow-y-auto">
|
|
||||||
<button
|
|
||||||
v-for="(image, index) in productImages"
|
|
||||||
:key="`${image}-${index}`"
|
|
||||||
type="button"
|
|
||||||
class="h-20 w-20 shrink-0 overflow-hidden rounded-xl border-2 bg-white"
|
|
||||||
:class="selectedImage === image ? 'border-brand-600' : 'border-slate-200'"
|
|
||||||
@click="selectedImage = image"
|
|
||||||
>
|
|
||||||
<img :src="getProductImageUrl(image)" :alt="`${product.title} ${index + 1}`" class="h-full w-full object-cover" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card order-1 overflow-hidden sm:order-2">
|
|
||||||
<div class="flex min-h-[260px] items-center justify-center bg-black">
|
|
||||||
<img
|
|
||||||
:src="getProductImageUrl(selectedImage)"
|
|
||||||
:alt="product.title"
|
|
||||||
class="h-auto max-h-[78vh] w-full object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span class="rounded-full bg-brand-50 px-3 py-1 text-sm font-bold text-brand-700">
|
|
||||||
{{ product.category }}
|
|
||||||
</span>
|
|
||||||
<h1 class="mt-4 text-3xl font-extrabold text-slate-950">{{ product.title }}</h1>
|
|
||||||
<p class="mt-3 text-3xl font-extrabold text-brand-700">{{ formatRupiah(product.price) }}</p>
|
|
||||||
|
|
||||||
<div class="mt-6 grid gap-3 sm:grid-cols-2">
|
|
||||||
<div class="card p-4">
|
|
||||||
<p class="text-xs font-bold uppercase text-slate-500">Kondisi</p>
|
|
||||||
<p class="mt-1 font-bold">{{ product.condition }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="card p-4">
|
|
||||||
<p class="text-xs font-bold uppercase text-slate-500">Lokasi</p>
|
|
||||||
<p class="mt-1 font-bold">{{ product.location }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="card p-4">
|
|
||||||
<p class="text-xs font-bold uppercase text-slate-500">Penjual</p>
|
|
||||||
<p class="mt-1 font-bold">{{ product.seller }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="card p-4">
|
|
||||||
<p class="text-xs font-bold uppercase text-slate-500">WhatsApp</p>
|
|
||||||
<p class="mt-1 font-bold">{{ product.whatsapp }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<h2 class="text-lg font-extrabold">Deskripsi</h2>
|
|
||||||
<p class="mt-2 leading-7 text-slate-600">{{ product.description }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8 flex flex-wrap gap-3">
|
|
||||||
<a :href="waLink" target="_blank" class="btn-primary">Hubungi via WhatsApp</a>
|
|
||||||
<button class="btn-secondary" @click="toggleSave">
|
|
||||||
{{ saved ? "Hapus dari Tersimpan" : "Simpan Barang" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="card p-10 text-center">
|
|
||||||
<h1 class="text-2xl font-extrabold">Produk tidak ditemukan</h1>
|
|
||||||
<RouterLink to="/marketplace" class="btn-primary mt-5">Lihat Marketplace</RouterLink>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, onMounted, ref, watch } from "vue"
|
|
||||||
import { useRoute } from "vue-router"
|
|
||||||
import { products as defaultProducts } from "../data/products"
|
|
||||||
import { fetchProduct, formatRupiah, getProductImageUrl, getSavedProducts, setSavedProducts } from "../utils"
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const id = Number(route.params.id)
|
|
||||||
const product = ref(null)
|
|
||||||
const loading = ref(true)
|
|
||||||
const selectedImage = ref("")
|
|
||||||
|
|
||||||
const productImages = computed(() => {
|
|
||||||
if (!product.value) return []
|
|
||||||
const images = product.value.images?.length ? product.value.images : [product.value.image]
|
|
||||||
return [...new Set(images.filter(Boolean))]
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
productImages,
|
|
||||||
(images) => {
|
|
||||||
if (!images.includes(selectedImage.value)) {
|
|
||||||
selectedImage.value = images[0] || ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const savedIds = ref(getSavedProducts())
|
|
||||||
const saved = computed(() => savedIds.value.includes(id))
|
|
||||||
|
|
||||||
const waLink = computed(() => {
|
|
||||||
if (!product.value) return "#"
|
|
||||||
const text = encodeURIComponent(`Halo, saya tertarik dengan ${product.value.title} di SecondTech Market.`)
|
|
||||||
return `https://wa.me/${product.value.whatsapp}?text=${text}`
|
|
||||||
})
|
|
||||||
|
|
||||||
function toggleSave() {
|
|
||||||
if (saved.value) {
|
|
||||||
savedIds.value = savedIds.value.filter((item) => item !== id)
|
|
||||||
} else {
|
|
||||||
savedIds.value.push(id)
|
|
||||||
}
|
|
||||||
setSavedProducts(savedIds.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadProduct() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = await fetchProduct(id)
|
|
||||||
product.value = data.product
|
|
||||||
} catch {
|
|
||||||
const fallbackProduct = defaultProducts.find((item) => Number(item.id) === id) || null
|
|
||||||
product.value = fallbackProduct ? { ...fallbackProduct, images: [fallbackProduct.image] } : null
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadProduct)
|
|
||||||
</script>
|
|
||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
"dev:api": "node server/server.js",
|
"dev:api": "node server/server.js",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview --host 0.0.0.0"
|
"preview": "vite preview --host 0.0.0.0",
|
||||||
|
"start": "node server/server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitejs/plugin-vue": "latest",
|
"@vitejs/plugin-vue": "latest",
|
||||||
|
|||||||
Reference in New Issue
Block a user