4 Commits

Author SHA1 Message Date
root 3324365ea7 add secondtech deploy workflow
Deploy SecondTech / deploy (push) Successful in 4s
2026-05-25 10:25:25 +07:00
Grey ae729dc1d2 add production start script 2026-05-25 09:59:59 +07:00
Grey 4677d8c1ee Merge branch 'dev' 2026-05-19 13:35:10 +07:00
Grey b20b46aa0f test 2026-05-19 10:59:03 +07:00
3 changed files with 40 additions and 145 deletions
+38
View File
@@ -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
View File
@@ -7,7 +7,8 @@
"dev": "vite --host 0.0.0.0",
"dev:api": "node server/server.js",
"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": {
"@vitejs/plugin-vue": "latest",