2 Commits

Author SHA1 Message Date
Riku 0168395347 menambahkan icon loading berputar 2026-05-26 08:39:04 +07:00
Riku 08821eaff0 style : menambahkan icon loading yang berputar 2026-05-26 08:30:44 +07:00
13 changed files with 51 additions and 434 deletions
-1
View File
@@ -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
View File
@@ -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

+4 -78
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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", [])
} }
-113
View File
@@ -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
View File
@@ -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)) {
+4
View File
@@ -17,6 +17,10 @@
<input v-model="password" type="password" class="input mt-2" placeholder="••••••••" required /> <input v-model="password" type="password" class="input mt-2" placeholder="••••••••" required />
</div> </div>
<button class="btn-primary w-full" :disabled="loading"> <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..." : "Masuk" }} {{ loading ? "Memproses..." : "Masuk" }}
</button> </button>
</form> </form>
+4
View File
@@ -25,6 +25,10 @@
<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> </div>
<button class="btn-primary w-full" :disabled="loading"> <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" }} {{ loading ? "Memproses..." : "Daftar" }}
</button> </button>
</form> </form>
+2 -2
View File
@@ -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 anda ke marketplace dengan foto produk asli.</p> <p class="mt-2 text-slate-600">Posting barang ke marketplace dengan foto asli dari perangkatmu.</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="Deskripsikan kondisi barang anda..." required></textarea> <textarea v-model="form.description" class="input mt-2 min-h-32" placeholder="Jelaskan kondisi barang..." required></textarea>
</div> </div>
<button class="btn-primary w-full" :disabled="loading"> <button class="btn-primary w-full" :disabled="loading">