Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f118ca8489 | |||
| f7dbc170c8 |
@@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
email VARCHAR(160) NOT NULL UNIQUE,
|
email VARCHAR(160) NOT NULL UNIQUE,
|
||||||
whatsapp VARCHAR(30) NOT NULL,
|
whatsapp VARCHAR(30) NOT NULL,
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
role ENUM('user', 'superadmin') NOT NULL DEFAULT 'user',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+3
-45
@@ -58,8 +58,7 @@ function publicUser(user) {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
whatsapp: user.whatsapp,
|
whatsapp: user.whatsapp
|
||||||
role: user.role || "user"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ app.post("/api/auth/register", async (req, res, next) => {
|
|||||||
[name, email.toLowerCase(), whatsapp, passwordHash]
|
[name, email.toLowerCase(), whatsapp, passwordHash]
|
||||||
)
|
)
|
||||||
|
|
||||||
const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" }
|
const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp }
|
||||||
res.status(201).json({ token: signUser(user), user: publicUser(user) })
|
res.status(201).json({ token: signUser(user), user: publicUser(user) })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "ER_DUP_ENTRY") {
|
if (error.code === "ER_DUP_ENTRY") {
|
||||||
@@ -153,7 +152,7 @@ app.post("/api/auth/login", async (req, res, next) => {
|
|||||||
|
|
||||||
app.get("/api/auth/me", auth, async (req, res, next) => {
|
app.get("/api/auth/me", auth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id])
|
const [rows] = await pool.query("SELECT id, name, email, whatsapp FROM users WHERE id = ?", [req.user.id])
|
||||||
if (!rows[0]) {
|
if (!rows[0]) {
|
||||||
res.status(404).json({ message: "User tidak ditemukan." })
|
res.status(404).json({ message: "User tidak ditemukan." })
|
||||||
return
|
return
|
||||||
@@ -262,28 +261,6 @@ app.delete("/api/products/:id", auth, async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.delete("/api/admin/products/:id", auth, superadminOnly, async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const product = await findProductById(req.params.id)
|
|
||||||
|
|
||||||
if (!product) {
|
|
||||||
res.status(404).json({ message: "Produk tidak ditemukan." })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await pool.query("DELETE FROM products WHERE id = ?", [req.params.id])
|
|
||||||
|
|
||||||
if (product.image.startsWith("/uploads/")) {
|
|
||||||
fs.rm(path.join(uploadsDir, path.basename(product.image)), { force: true }, () => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ message: "Produk berhasil dihapus oleh superadmin." })
|
|
||||||
} catch (error) {
|
|
||||||
next(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
app.use((error, req, res, next) => {
|
app.use((error, req, res, next) => {
|
||||||
if (error instanceof multer.MulterError) {
|
if (error instanceof multer.MulterError) {
|
||||||
res.status(400).json({ message: "Upload gagal. Ukuran foto maksimal 3MB." })
|
res.status(400).json({ message: "Upload gagal. Ukuran foto maksimal 3MB." })
|
||||||
@@ -302,22 +279,3 @@ app.use((error, req, res, next) => {
|
|||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`SecondTech API berjalan di http://localhost:${port}`)
|
console.log(`SecondTech API berjalan di http://localhost:${port}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
async function superadminOnly(req, res, next) {
|
|
||||||
try {
|
|
||||||
const [rows] = await pool.query(
|
|
||||||
"SELECT role FROM users WHERE id = ?",
|
|
||||||
[req.user.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!rows[0] || rows[0].role !== "superadmin") {
|
|
||||||
res.status(403).json({ message: "Hanya superadmin yang boleh mengakses fitur ini." })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
|
||||||
} catch (error) {
|
|
||||||
next(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 826 KiB |
@@ -1,25 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside class="card p-4">
|
<aside class="card p-4">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<p class="text-xs font-bold uppercase tracking-wider text-slate-500">
|
<p class="text-xs font-bold uppercase tracking-wider text-slate-500">Menu User</p>
|
||||||
Menu User
|
|
||||||
</p>
|
|
||||||
<h2 class="mt-1 text-lg font-extrabold">Dashboard</h2>
|
<h2 class="mt-1 text-lg font-extrabold">Dashboard</h2>
|
||||||
|
|
||||||
<div v-if="currentUser" class="mt-4 rounded-xl bg-slate-50 p-3">
|
|
||||||
<p class="text-sm font-bold text-slate-800">
|
|
||||||
{{ currentUser.name }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-xs text-slate-500">
|
|
||||||
{{ currentUser.email }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
v-if="isSuperadmin"
|
|
||||||
class="mt-2 inline-flex rounded-full bg-red-50 px-3 py-1 text-xs font-bold text-red-700"
|
|
||||||
>
|
|
||||||
Superadmin
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="grid gap-2">
|
<nav class="grid gap-2">
|
||||||
@@ -27,31 +10,18 @@
|
|||||||
<LayoutDashboard size="18" />
|
<LayoutDashboard size="18" />
|
||||||
Barang Saya
|
Barang Saya
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<RouterLink class="side-link" to="/jual">
|
<RouterLink class="side-link" to="/jual">
|
||||||
<PlusCircle size="18" />
|
<PlusCircle size="18" />
|
||||||
Jual Barang
|
Jual Barang
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<RouterLink class="side-link" to="/marketplace">
|
<RouterLink class="side-link" to="/marketplace">
|
||||||
<Store size="18" />
|
<Store size="18" />
|
||||||
Marketplace
|
Marketplace
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<RouterLink class="side-link" to="/tersimpan">
|
<RouterLink class="side-link" to="/tersimpan">
|
||||||
<Heart size="18" />
|
<Heart size="18" />
|
||||||
Barang Tersimpan
|
Barang Tersimpan
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<RouterLink
|
|
||||||
v-if="isSuperadmin"
|
|
||||||
class="side-link admin-link"
|
|
||||||
to="/admin/products"
|
|
||||||
>
|
|
||||||
<ShieldCheck size="18" />
|
|
||||||
Admin Marketplace
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<button class="side-link text-left" type="button" @click="logout">
|
<button class="side-link text-left" type="button" @click="logout">
|
||||||
<LogOut size="18" />
|
<LogOut size="18" />
|
||||||
Logout
|
Logout
|
||||||
@@ -61,66 +31,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from "vue"
|
import { useRouter } from "vue-router"
|
||||||
import { useRoute, useRouter } from "vue-router"
|
import { Heart, LayoutDashboard, LogOut, PlusCircle, Store } from "lucide-vue-next"
|
||||||
import {
|
import { clearAuth } from "../utils"
|
||||||
Heart,
|
|
||||||
LayoutDashboard,
|
|
||||||
LogOut,
|
|
||||||
PlusCircle,
|
|
||||||
ShieldCheck,
|
|
||||||
Store
|
|
||||||
} from "lucide-vue-next"
|
|
||||||
import { clearAuth, fetchCurrentUser, getCurrentUser, getToken, setAuth } from "../utils"
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
|
||||||
const currentUser = ref(getCurrentUser())
|
|
||||||
|
|
||||||
const isSuperadmin = computed(() => {
|
|
||||||
return currentUser.value?.role === "superadmin"
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.fullPath,
|
|
||||||
() => {
|
|
||||||
currentUser.value = getCurrentUser()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function refreshCurrentUser() {
|
|
||||||
if (!getToken()) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await fetchCurrentUser()
|
|
||||||
setAuth({
|
|
||||||
token: getToken(),
|
|
||||||
user: data.user
|
|
||||||
})
|
|
||||||
currentUser.value = getCurrentUser()
|
|
||||||
} catch {
|
|
||||||
clearAuth()
|
|
||||||
currentUser.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
clearAuth()
|
clearAuth()
|
||||||
router.push("/login")
|
router.push("/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(refreshCurrentUser)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.side-link {
|
.side-link {
|
||||||
@apply flex w-full items-center gap-3 rounded-xl px-3 py-3 text-sm font-semibold text-slate-700 transition hover:bg-slate-100;
|
@apply flex w-full items-center gap-3 rounded-xl px-3 py-3 text-sm font-semibold text-slate-700 transition hover:bg-slate-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-link {
|
|
||||||
@apply bg-red-50 text-red-700 hover:bg-red-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.router-link-active {
|
.router-link-active {
|
||||||
@apply bg-brand-50 text-brand-700;
|
@apply bg-brand-50 text-brand-700;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-88
@@ -18,41 +18,16 @@
|
|||||||
<RouterLink class="nav-link" to="/jual">Jual Barang</RouterLink>
|
<RouterLink class="nav-link" to="/jual">Jual Barang</RouterLink>
|
||||||
<RouterLink class="nav-link" to="/dashboard">Dashboard</RouterLink>
|
<RouterLink class="nav-link" to="/dashboard">Dashboard</RouterLink>
|
||||||
<RouterLink class="nav-link" to="/tersimpan">Tersimpan</RouterLink>
|
<RouterLink class="nav-link" to="/tersimpan">Tersimpan</RouterLink>
|
||||||
|
|
||||||
<RouterLink
|
|
||||||
v-if="isSuperadmin"
|
|
||||||
class="nav-link"
|
|
||||||
to="/admin/products"
|
|
||||||
>
|
|
||||||
Admin Marketplace
|
|
||||||
</RouterLink>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="hidden items-center gap-3 md:flex">
|
<div class="hidden items-center gap-3 md:flex">
|
||||||
<template v-if="currentUser">
|
<template v-if="currentUser">
|
||||||
<span class="text-sm font-bold text-slate-700">
|
<span class="text-sm font-bold text-slate-700">{{ currentUser.name }}</span>
|
||||||
{{ currentUser.name }}
|
<button class="btn-secondary !px-4 !py-2" @click="logout">Logout</button>
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="isSuperadmin"
|
|
||||||
class="rounded-full bg-red-50 px-3 py-1 text-xs font-bold text-red-700"
|
|
||||||
>
|
|
||||||
Superadmin
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button class="btn-secondary !px-4 !py-2" @click="logout">
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<RouterLink to="/login" class="btn-secondary !px-4 !py-2">
|
<RouterLink to="/login" class="btn-secondary !px-4 !py-2">Login</RouterLink>
|
||||||
Login
|
<RouterLink to="/register" class="btn-primary !px-4 !py-2">Register</RouterLink>
|
||||||
</RouterLink>
|
|
||||||
<RouterLink to="/register" class="btn-primary !px-4 !py-2">
|
|
||||||
Register
|
|
||||||
</RouterLink>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -63,42 +38,17 @@
|
|||||||
|
|
||||||
<div v-if="open" class="border-t border-slate-200 bg-white md:hidden">
|
<div v-if="open" class="border-t border-slate-200 bg-white md:hidden">
|
||||||
<div class="container-page grid gap-2 py-4">
|
<div class="container-page grid gap-2 py-4">
|
||||||
<RouterLink class="mobile-link" to="/" @click="open = false">Home</RouterLink>
|
<RouterLink class="mobile-link" to="/" @click="open=false">Home</RouterLink>
|
||||||
<RouterLink class="mobile-link" to="/marketplace" @click="open = false">Marketplace</RouterLink>
|
<RouterLink class="mobile-link" to="/marketplace" @click="open=false">Marketplace</RouterLink>
|
||||||
<RouterLink class="mobile-link" to="/jual" @click="open = false">Jual Barang</RouterLink>
|
<RouterLink class="mobile-link" to="/jual" @click="open=false">Jual Barang</RouterLink>
|
||||||
<RouterLink class="mobile-link" to="/dashboard" @click="open = false">Dashboard</RouterLink>
|
<RouterLink class="mobile-link" to="/dashboard" @click="open=false">Dashboard</RouterLink>
|
||||||
<RouterLink class="mobile-link" to="/tersimpan" @click="open = false">Tersimpan</RouterLink>
|
<RouterLink class="mobile-link" to="/tersimpan" @click="open=false">Tersimpan</RouterLink>
|
||||||
|
|
||||||
<RouterLink
|
|
||||||
v-if="isSuperadmin"
|
|
||||||
class="mobile-link"
|
|
||||||
to="/admin/products"
|
|
||||||
@click="open = false"
|
|
||||||
>
|
|
||||||
Admin Marketplace
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<div v-if="currentUser" class="mt-2 grid gap-2">
|
<div v-if="currentUser" class="mt-2 grid gap-2">
|
||||||
<div class="rounded-xl bg-slate-50 px-3 py-2">
|
<button class="btn-secondary !py-2 text-center" @click="logout">Logout</button>
|
||||||
<p class="text-sm font-bold text-slate-800">{{ currentUser.name }}</p>
|
|
||||||
<p class="text-xs text-slate-500">{{ currentUser.email }}</p>
|
|
||||||
<p v-if="isSuperadmin" class="mt-1 text-xs font-bold text-red-700">
|
|
||||||
Superadmin
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn-secondary !py-2 text-center" @click="logout">
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="mt-2 grid grid-cols-2 gap-2">
|
<div v-else class="mt-2 grid grid-cols-2 gap-2">
|
||||||
<RouterLink to="/login" class="btn-secondary !py-2 text-center" @click="open = false">
|
<RouterLink to="/login" class="btn-secondary !py-2 text-center" @click="open=false">Login</RouterLink>
|
||||||
Login
|
<RouterLink to="/register" class="btn-primary !py-2 text-center" @click="open=false">Register</RouterLink>
|
||||||
</RouterLink>
|
|
||||||
<RouterLink to="/register" class="btn-primary !py-2 text-center" @click="open = false">
|
|
||||||
Register
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,20 +56,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useRoute, useRouter } from "vue-router"
|
import { useRoute, useRouter } from "vue-router"
|
||||||
import { Cpu, Menu } from "lucide-vue-next"
|
import { Cpu, Menu } from "lucide-vue-next"
|
||||||
import { clearAuth, fetchCurrentUser, getCurrentUser, getToken, setAuth } from "../utils"
|
import { clearAuth, getCurrentUser } from "../utils"
|
||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const currentUser = ref(getCurrentUser())
|
const currentUser = ref(getCurrentUser())
|
||||||
|
|
||||||
const isSuperadmin = computed(() => {
|
|
||||||
return currentUser.value?.role === "superadmin"
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.fullPath,
|
() => route.fullPath,
|
||||||
() => {
|
() => {
|
||||||
@@ -127,41 +73,21 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async function refreshCurrentUser() {
|
|
||||||
if (!getToken()) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await fetchCurrentUser()
|
|
||||||
setAuth({
|
|
||||||
token: getToken(),
|
|
||||||
user: data.user
|
|
||||||
})
|
|
||||||
currentUser.value = getCurrentUser()
|
|
||||||
} catch {
|
|
||||||
clearAuth()
|
|
||||||
currentUser.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
clearAuth()
|
clearAuth()
|
||||||
currentUser.value = null
|
currentUser.value = null
|
||||||
open.value = false
|
open.value = false
|
||||||
router.push("/login")
|
router.push("/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(refreshCurrentUser)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.nav-link {
|
.nav-link {
|
||||||
@apply text-sm font-semibold text-slate-600 transition hover:text-brand-700;
|
@apply text-sm font-semibold text-slate-600 transition hover:text-brand-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-link-active {
|
.router-link-active {
|
||||||
@apply text-brand-700;
|
@apply text-brand-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-link {
|
.mobile-link {
|
||||||
@apply rounded-xl px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-100;
|
@apply rounded-xl px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-100;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-36
@@ -98,40 +98,13 @@ export const products = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const categories = [
|
export const categories = [
|
||||||
{
|
"Semua",
|
||||||
name: "Semua",
|
"Laptop",
|
||||||
icon: "Boxes"
|
"PC",
|
||||||
},
|
"Monitor",
|
||||||
{
|
"Keyboard",
|
||||||
name: "Laptop",
|
"Router",
|
||||||
icon: "Laptop"
|
"Switch",
|
||||||
},
|
"Server",
|
||||||
{
|
"Access Point"
|
||||||
name: "PC",
|
|
||||||
icon: "PcCase"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Monitor",
|
|
||||||
icon: "Monitor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Keyboard",
|
|
||||||
icon: "Keyboard"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Router",
|
|
||||||
icon: "Router"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Switch",
|
|
||||||
icon: "Network"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Server",
|
|
||||||
icon: "Server"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Access Point",
|
|
||||||
icon: "Wifi"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-14
@@ -8,8 +8,7 @@ import DashboardView from "../views/DashboardView.vue"
|
|||||||
import SavedView from "../views/SavedView.vue"
|
import SavedView from "../views/SavedView.vue"
|
||||||
import LoginView from "../views/LoginView.vue"
|
import LoginView from "../views/LoginView.vue"
|
||||||
import RegisterView from "../views/RegisterView.vue"
|
import RegisterView from "../views/RegisterView.vue"
|
||||||
import AdminProductsView from "../views/AdminProductsView.vue"
|
import { getToken } from "../utils"
|
||||||
import { getCurrentUser, getToken } from "../utils"
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: "/", name: "home", component: HomeView },
|
{ path: "/", name: "home", component: HomeView },
|
||||||
@@ -18,12 +17,6 @@ const routes = [
|
|||||||
{ path: "/jual", name: "sell-product", component: SellProductView, meta: { requiresAuth: true } },
|
{ path: "/jual", name: "sell-product", component: SellProductView, meta: { requiresAuth: true } },
|
||||||
{ path: "/dashboard", name: "dashboard", component: DashboardView, meta: { requiresAuth: true } },
|
{ path: "/dashboard", name: "dashboard", component: DashboardView, meta: { requiresAuth: true } },
|
||||||
{ path: "/tersimpan", name: "saved", component: SavedView, meta: { requiresAuth: true } },
|
{ path: "/tersimpan", name: "saved", component: SavedView, meta: { requiresAuth: true } },
|
||||||
{
|
|
||||||
path: "/admin/products",
|
|
||||||
name: "admin-products",
|
|
||||||
component: AdminProductsView,
|
|
||||||
meta: { requiresAuth: true, requiresSuperadmin: true }
|
|
||||||
},
|
|
||||||
{ path: "/login", name: "login", component: LoginView },
|
{ path: "/login", name: "login", component: LoginView },
|
||||||
{ path: "/register", name: "register", component: RegisterView }
|
{ path: "/register", name: "register", component: RegisterView }
|
||||||
]
|
]
|
||||||
@@ -37,12 +30,6 @@ router.beforeEach((to) => {
|
|||||||
if (to.meta.requiresAuth && !getToken()) {
|
if (to.meta.requiresAuth && !getToken()) {
|
||||||
return "/login"
|
return "/login"
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = getCurrentUser()
|
|
||||||
|
|
||||||
if (to.meta.requiresSuperadmin && user?.role !== "superadmin") {
|
|
||||||
return "/dashboard"
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
+1
-19
@@ -31,14 +31,7 @@ export function getCurrentUser() {
|
|||||||
|
|
||||||
export function setAuth({ token, user }) {
|
export function setAuth({ token, user }) {
|
||||||
localStorage.setItem("secondtech_token", token)
|
localStorage.setItem("secondtech_token", token)
|
||||||
|
setLocal("secondtech_user", user)
|
||||||
setLocal("secondtech_user", {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
whatsapp: user.whatsapp,
|
|
||||||
role: user.role || "user"
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearAuth() {
|
export function clearAuth() {
|
||||||
@@ -63,7 +56,6 @@ async function requestJson(path, options = {}) {
|
|||||||
...options,
|
...options,
|
||||||
headers
|
headers
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json().catch(() => ({}))
|
const data = await response.json().catch(() => ({}))
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -87,10 +79,6 @@ export function loginUser(payload) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchCurrentUser() {
|
|
||||||
return requestJson("/api/auth/me")
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchProducts() {
|
export function fetchProducts() {
|
||||||
return requestJson("/api/products")
|
return requestJson("/api/products")
|
||||||
}
|
}
|
||||||
@@ -123,12 +111,6 @@ export function deleteProductById(id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteProductAsAdmin(id) {
|
|
||||||
return requestJson(`/api/admin/products/${id}`, {
|
|
||||||
method: "DELETE"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSavedProducts() {
|
export function getSavedProducts() {
|
||||||
return getLocal("secondtech_saved", [])
|
return getLocal("secondtech_saved", [])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="container-page py-10">
|
|
||||||
<div class="mb-8">
|
|
||||||
<h1 class="text-3xl font-extrabold">Admin Marketplace</h1>
|
|
||||||
<p class="mt-2 text-slate-600">Kelola semua produk yang tampil di marketplace.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p v-if="errorMessage" class="mb-5 rounded-xl bg-red-50 px-4 py-3 text-sm font-semibold text-red-700">
|
|
||||||
{{ errorMessage }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div v-if="loading" class="card p-10 text-center">
|
|
||||||
<h2 class="text-xl font-extrabold">Memuat produk...</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="products.length" class="grid gap-4">
|
|
||||||
<div
|
|
||||||
v-for="product in products"
|
|
||||||
:key="product.id"
|
|
||||||
class="card grid gap-4 p-4 md:grid-cols-[140px_1fr_auto] md:items-center"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="getProductImageUrl(product.image)"
|
|
||||||
:alt="product.title"
|
|
||||||
class="aspect-[4/3] w-full rounded-xl object-cover md:w-36"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="rounded-full bg-brand-50 px-3 py-1 text-xs font-bold text-brand-700">
|
|
||||||
{{ product.category }}
|
|
||||||
</span>
|
|
||||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-bold text-emerald-700">
|
|
||||||
{{ product.status }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="mt-3 font-extrabold">{{ product.title }}</h3>
|
|
||||||
<p class="mt-1 text-sm text-slate-500">
|
|
||||||
{{ product.seller }} • {{ product.location }} • {{ product.condition }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-2 font-extrabold text-brand-700">
|
|
||||||
{{ formatRupiah(product.price) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 md:flex-col">
|
|
||||||
<RouterLink :to="`/produk/${product.id}`" class="btn-secondary !px-3 !py-2">
|
|
||||||
Detail
|
|
||||||
</RouterLink>
|
|
||||||
<button
|
|
||||||
class="rounded-xl bg-red-50 px-3 py-2 text-sm font-bold text-red-700 hover:bg-red-100"
|
|
||||||
@click="deleteAsAdmin(product.id)"
|
|
||||||
>
|
|
||||||
Hapus
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="card p-10 text-center">
|
|
||||||
<h2 class="text-xl font-extrabold">Belum ada produk</h2>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, ref } from "vue"
|
|
||||||
import { API_BASE_URL, fetchProducts, formatRupiah, getProductImageUrl, getToken } from "../utils"
|
|
||||||
|
|
||||||
const products = ref([])
|
|
||||||
const loading = ref(true)
|
|
||||||
const errorMessage = ref("")
|
|
||||||
|
|
||||||
async function loadProducts() {
|
|
||||||
loading.value = true
|
|
||||||
errorMessage.value = ""
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await fetchProducts()
|
|
||||||
products.value = data.products
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage.value = error.message
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteAsAdmin(id) {
|
|
||||||
if (!confirm("Yakin ingin menghapus produk ini dari marketplace?")) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/api/admin/products/${id}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${getToken()}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json().catch(() => ({}))
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.message || "Gagal menghapus produk.")
|
|
||||||
}
|
|
||||||
|
|
||||||
products.value = products.value.filter((product) => product.id !== id)
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage.value = error.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadProducts)
|
|
||||||
</script>
|
|
||||||
+7
-36
@@ -38,17 +38,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
||||||
<div
|
<div v-for="cat in categoryList" :key="cat" class="card p-5 text-center transition hover:-translate-y-1 hover:shadow-md">
|
||||||
v-for="cat in categoryList"
|
|
||||||
:key="cat.name"
|
|
||||||
class="card p-5 text-center transition hover:-translate-y-1 hover:shadow-md"
|
|
||||||
>
|
|
||||||
<div class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-brand-50 text-brand-700">
|
<div class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-brand-50 text-brand-700">
|
||||||
<component :is="iconMap[cat.icon]" :size="22" />
|
<Cpu size="22" />
|
||||||
|
</div>
|
||||||
|
<p class="font-bold">{{ cat }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="font-bold">{{ cat.name }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -76,38 +71,14 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
|
import { Cpu } from "lucide-vue-next"
|
||||||
import ProductCard from "../components/ProductCard.vue"
|
import ProductCard from "../components/ProductCard.vue"
|
||||||
import { getSavedProducts, setSavedProducts } from "../utils"
|
|
||||||
import {
|
|
||||||
Boxes,
|
|
||||||
Laptop,
|
|
||||||
PcCase,
|
|
||||||
Monitor,
|
|
||||||
Keyboard,
|
|
||||||
Router,
|
|
||||||
Network,
|
|
||||||
Server,
|
|
||||||
Wifi
|
|
||||||
} from "lucide-vue-next"
|
|
||||||
|
|
||||||
import { products, categories } from "../data/products"
|
import { products, categories } from "../data/products"
|
||||||
|
import { getSavedProducts, setSavedProducts } from "../utils"
|
||||||
|
|
||||||
const saved = ref(getSavedProducts())
|
const saved = ref(getSavedProducts())
|
||||||
const categoryList = categories
|
const categoryList = categories.filter((c) => c !== "Semua").slice(0, 6)
|
||||||
.filter((category) => category.name !== "Semua")
|
|
||||||
.slice(0, 6)
|
|
||||||
const latestProducts = computed(() => products.slice(0, 4))
|
const latestProducts = computed(() => products.slice(0, 4))
|
||||||
const iconMap = {
|
|
||||||
Boxes,
|
|
||||||
Laptop,
|
|
||||||
PcCase,
|
|
||||||
Monitor,
|
|
||||||
Keyboard,
|
|
||||||
Router,
|
|
||||||
Network,
|
|
||||||
Server,
|
|
||||||
Wifi
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSave(id) {
|
function toggleSave(id) {
|
||||||
if (saved.value.includes(id)) {
|
if (saved.value.includes(id)) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="container-page grid min-h-[70vh] place-items-center py-10">
|
<section class="container-page grid min-h-[70vh] place-items-center py-10">
|
||||||
<div class="card w-full max-w-md p-6">
|
<div class="card w-full max-w-md p-6">
|
||||||
<h1 class="text-2xl font-extrabold">Login</h1>
|
<h1 class="text-2xl font-extrabold">Selamat Datang Kembali!</h1>
|
||||||
<p class="mt-2 text-sm text-slate-600">Masuk untuk mengelola barang jualanmu.</p>
|
<p class="mt-2 text-sm text-slate-600">Masuk dan lanjutkan jualanmu.</p>
|
||||||
|
|
||||||
<form class="mt-6 grid gap-4" @submit.prevent="login">
|
<form class="mt-6 grid gap-4" @submit.prevent="login">
|
||||||
<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">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<section class="container-page py-10">
|
<section class="container-page py-10">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl font-extrabold">Jual Barang</h1>
|
<h1 class="text-3xl font-extrabold">Jual Barang</h1>
|
||||||
<p class="mt-2 text-slate-600">Posting barang ke marketplace dengan foto asli dari perangkatmu.</p>
|
<p class="mt-2 text-slate-600">Posting barang anda ke marketplace dengan foto produk asli.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-8 lg:grid-cols-[1fr_360px]">
|
<div class="grid gap-8 lg:grid-cols-[1fr_360px]">
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label">Deskripsi</label>
|
<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="Deskripsikan kondisi barang anda..." required></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn-primary w-full" :disabled="loading">
|
<button class="btn-primary w-full" :disabled="loading">
|
||||||
|
|||||||
Reference in New Issue
Block a user