fitur fitur darkmode dan foto foto
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-slate-50 text-slate-900 font-sans;
|
||||
}
|
||||
|
||||
.container-page {
|
||||
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-xl bg-brand-600 px-5 py-3 text-sm font-semibold text-white transition hover:bg-brand-700;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-800 transition hover:bg-slate-100;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply rounded-2xl border border-slate-200 bg-white shadow-sm;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none transition focus:border-brand-500 focus:ring-4 focus:ring-brand-100;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply text-sm font-semibold text-slate-700;
|
||||
}
|
||||
|
||||
.btn-WA {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-xl bg-green-500 px-5 py-3 text-sm font-semibold text-white transition hover:bg-green-600;
|
||||
}
|
||||
|
||||
/* SecondTech dark mode */
|
||||
html.dark body {
|
||||
background: #121212;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
html.dark header {
|
||||
background: rgba(18, 18, 18, 0.94);
|
||||
border-color: rgba(37, 99, 235, 0.34);
|
||||
}
|
||||
|
||||
html.dark main,
|
||||
html.dark section {
|
||||
background: #121212;
|
||||
}
|
||||
|
||||
html.dark .card {
|
||||
background: #181818;
|
||||
border-color: rgba(37, 99, 235, 0.30);
|
||||
box-shadow: 0 14px 34px rgba(37, 99, 235, 0.20);
|
||||
}
|
||||
|
||||
html.dark .input {
|
||||
background: #181818;
|
||||
border-color: rgba(37, 99, 235, 0.34);
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
html.dark .input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
html.dark .btn-secondary {
|
||||
background: #181818;
|
||||
border-color: rgba(37, 99, 235, 0.38);
|
||||
color: #f8fafc;
|
||||
box-shadow: 0 10px 24px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
html.dark .btn-primary {
|
||||
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.24);
|
||||
}
|
||||
|
||||
html.dark .bg-white,
|
||||
html.dark .bg-slate-50,
|
||||
html.dark .bg-slate-100 {
|
||||
background-color: #181818;
|
||||
}
|
||||
|
||||
html.dark .text-slate-950,
|
||||
html.dark .text-slate-900,
|
||||
html.dark .text-slate-800,
|
||||
html.dark .text-slate-700 {
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
html.dark .text-slate-600,
|
||||
html.dark .text-slate-500 {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
html.dark .border-slate-200 {
|
||||
border-color: rgba(37, 99, 235, 0.30);
|
||||
}
|
||||
|
||||
/* SecondTech stronger dark mode repair */
|
||||
html.dark,
|
||||
html.dark body {
|
||||
background: #121212 !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
|
||||
html.dark header,
|
||||
html.dark footer,
|
||||
html.dark main,
|
||||
html.dark section {
|
||||
background-color: #121212 !important;
|
||||
}
|
||||
|
||||
html.dark header {
|
||||
border-color: rgba(37, 99, 235, 0.35) !important;
|
||||
}
|
||||
|
||||
html.dark .card {
|
||||
background: #181818 !important;
|
||||
border-color: rgba(37, 99, 235, 0.34) !important;
|
||||
box-shadow: 0 14px 34px rgba(37, 99, 235, 0.22) !important;
|
||||
}
|
||||
|
||||
html.dark .input,
|
||||
html.dark input,
|
||||
html.dark select,
|
||||
html.dark textarea {
|
||||
background-color: #181818 !important;
|
||||
border-color: rgba(37, 99, 235, 0.38) !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
|
||||
html.dark .input::placeholder,
|
||||
html.dark input::placeholder,
|
||||
html.dark textarea::placeholder {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
|
||||
html.dark .btn-secondary {
|
||||
background: #181818 !important;
|
||||
border-color: rgba(37, 99, 235, 0.42) !important;
|
||||
color: #f8fafc !important;
|
||||
box-shadow: 0 10px 24px rgba(37, 99, 235, 0.14) !important;
|
||||
}
|
||||
|
||||
html.dark .btn-primary {
|
||||
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.26) !important;
|
||||
}
|
||||
|
||||
html.dark .bg-white,
|
||||
html.dark .bg-slate-50,
|
||||
html.dark .bg-slate-100,
|
||||
html.dark .bg-brand-50 {
|
||||
background-color: #181818 !important;
|
||||
}
|
||||
|
||||
html.dark .text-slate-950,
|
||||
html.dark .text-slate-900,
|
||||
html.dark .text-slate-800,
|
||||
html.dark .text-slate-700,
|
||||
html.dark .text-brand-900,
|
||||
html.dark .text-brand-800,
|
||||
html.dark .text-brand-700 {
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
|
||||
html.dark .text-slate-600,
|
||||
html.dark .text-slate-500,
|
||||
html.dark .text-slate-400 {
|
||||
color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
html.dark .nav-link,
|
||||
html.dark .mobile-link,
|
||||
html.dark a:not(.btn-primary) {
|
||||
color: #dbeafe !important;
|
||||
}
|
||||
|
||||
html.dark .nav-link:hover,
|
||||
html.dark .mobile-link:hover,
|
||||
html.dark a:not(.btn-primary):hover {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
html.dark .border-slate-200,
|
||||
html.dark .border-brand-600 {
|
||||
border-color: rgba(37, 99, 235, 0.42) !important;
|
||||
}
|
||||
|
||||
html.dark .shadow-sm {
|
||||
box-shadow: 0 14px 34px rgba(37, 99, 235, 0.18) !important;
|
||||
}
|
||||
|
||||
/* SecondTech final dark mode repair */
|
||||
html.dark,
|
||||
html.dark body {
|
||||
background: #121212 !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
|
||||
html.dark header,
|
||||
html.dark footer,
|
||||
html.dark main,
|
||||
html.dark section {
|
||||
background-color: #121212 !important;
|
||||
}
|
||||
|
||||
html.dark header {
|
||||
border-color: rgba(37, 99, 235, 0.35) !important;
|
||||
}
|
||||
|
||||
html.dark .card {
|
||||
background: #181818 !important;
|
||||
border-color: rgba(37, 99, 235, 0.34) !important;
|
||||
box-shadow: 0 14px 34px rgba(37, 99, 235, 0.22) !important;
|
||||
}
|
||||
|
||||
html.dark .input,
|
||||
html.dark input,
|
||||
html.dark select,
|
||||
html.dark textarea {
|
||||
background-color: #181818 !important;
|
||||
border-color: rgba(37, 99, 235, 0.38) !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
|
||||
html.dark .input::placeholder,
|
||||
html.dark input::placeholder,
|
||||
html.dark textarea::placeholder {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
|
||||
html.dark .btn-secondary {
|
||||
background: #181818 !important;
|
||||
border-color: rgba(37, 99, 235, 0.42) !important;
|
||||
color: #f8fafc !important;
|
||||
box-shadow: 0 10px 24px rgba(37, 99, 235, 0.14) !important;
|
||||
}
|
||||
|
||||
html.dark .btn-primary {
|
||||
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.26) !important;
|
||||
}
|
||||
|
||||
html.dark .bg-white,
|
||||
html.dark .bg-slate-50,
|
||||
html.dark .bg-slate-100,
|
||||
html.dark .bg-brand-50,
|
||||
html.dark .bg-emerald-50,
|
||||
html.dark .bg-red-50 {
|
||||
background-color: #181818 !important;
|
||||
}
|
||||
|
||||
html.dark .text-zinc-950,
|
||||
html.dark .text-zinc-900,
|
||||
html.dark .text-zinc-800,
|
||||
html.dark .text-slate-950,
|
||||
html.dark .text-slate-900,
|
||||
html.dark .text-slate-800,
|
||||
html.dark .text-slate-700,
|
||||
html.dark .text-brand-900,
|
||||
html.dark .text-brand-800,
|
||||
html.dark .text-brand-700,
|
||||
html.dark .text-emerald-700,
|
||||
html.dark .text-red-700 {
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
|
||||
html.dark .text-slate-600,
|
||||
html.dark .text-slate-500,
|
||||
html.dark .text-slate-400,
|
||||
html.dark .text-zinc-600,
|
||||
html.dark .text-zinc-500,
|
||||
html.dark .text-zinc-400 {
|
||||
color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
html.dark .nav-link,
|
||||
html.dark .mobile-link,
|
||||
html.dark a:not(.btn-primary) {
|
||||
color: #dbeafe !important;
|
||||
}
|
||||
|
||||
html.dark .nav-link:hover,
|
||||
html.dark .mobile-link:hover,
|
||||
html.dark a:not(.btn-primary):hover {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
html.dark .border-slate-200,
|
||||
html.dark .border-brand-600 {
|
||||
border-color: rgba(37, 99, 235, 0.42) !important;
|
||||
}
|
||||
|
||||
html.dark .shadow-sm {
|
||||
box-shadow: 0 14px 34px rgba(37, 99, 235, 0.18) !important;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<aside class="card p-4">
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-bold uppercase tracking-wider text-slate-500">
|
||||
Menu User
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<nav class="grid gap-2">
|
||||
<RouterLink class="side-link" to="/dashboard">
|
||||
<LayoutDashboard size="18" />
|
||||
Barang Saya
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink class="side-link" to="/jual">
|
||||
<PlusCircle size="18" />
|
||||
Jual Barang
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink class="side-link" to="/marketplace">
|
||||
<Store size="18" />
|
||||
Marketplace
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink class="side-link" to="/tersimpan">
|
||||
<Heart size="18" />
|
||||
Barang Tersimpan
|
||||
</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">
|
||||
<LogOut size="18" />
|
||||
Logout
|
||||
</button>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import {
|
||||
Heart,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
PlusCircle,
|
||||
ShieldCheck,
|
||||
Store
|
||||
} from "lucide-vue-next"
|
||||
import { clearAuth, fetchCurrentUser, getCurrentUser, getToken, setAuth } from "../utils"
|
||||
|
||||
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() {
|
||||
clearAuth()
|
||||
router.push("/login")
|
||||
}
|
||||
|
||||
onMounted(refreshCurrentUser)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
}
|
||||
|
||||
.admin-link {
|
||||
@apply bg-red-50 text-red-700 hover:bg-red-100;
|
||||
}
|
||||
|
||||
.router-link-active {
|
||||
@apply bg-brand-50 text-brand-700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="card group overflow-hidden">
|
||||
<div class="relative aspect-[4/3] overflow-hidden bg-slate-100">
|
||||
<img :src="getProductImageUrl(product.image)" :alt="product.title" class="h-full w-full object-cover transition duration-500 group-hover:scale-105" />
|
||||
<span class="absolute left-3 top-3 rounded-full bg-white/90 px-3 py-1 text-xs font-bold text-slate-700">
|
||||
{{ product.category }}
|
||||
</span>
|
||||
<button
|
||||
class="absolute right-3 top-3 rounded-full bg-white/90 p-2 text-slate-700 transition hover:bg-brand-600 hover:text-white"
|
||||
@click.prevent="$emit('toggle-save', product.id)"
|
||||
>
|
||||
<Heart :fill="saved ? 'currentColor' : 'none'" size="18" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<div class="mb-2 flex items-center justify-between gap-3">
|
||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-bold text-emerald-700">
|
||||
{{ product.condition }}
|
||||
</span>
|
||||
<span class="text-xs font-semibold text-slate-500">{{ product.location }}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="line-clamp-2 min-h-[48px] text-base font-bold text-slate-900">
|
||||
{{ product.title }}
|
||||
</h3>
|
||||
|
||||
<p class="mt-2 text-lg font-extrabold text-brand-700">
|
||||
{{ formatRupiah(product.price) }}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<RouterLink :to="`/produk/${product.id}`" class="btn-secondary flex-1 !px-3 !py-2 text-center">
|
||||
Detail
|
||||
</RouterLink>
|
||||
<a :href="waLink" target="_blank" class="btn-primary flex-1 !px-3 !py-2 text-center">
|
||||
Massage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue"
|
||||
import { Heart } from "lucide-vue-next"
|
||||
import { formatRupiah, getProductImageUrl } from "../utils"
|
||||
|
||||
const props = defineProps({
|
||||
product: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
saved: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(["toggle-save"])
|
||||
|
||||
const waLink = computed(() => {
|
||||
const text = encodeURIComponent(`Halo, saya tertarik dengan ${props.product.title} di SecondTech Market.`)
|
||||
return `https://wa.me/${props.product.whatsapp}?text=${text}`
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<section class="bg-gradient-to-br from-brand-50 via-white to-slate-100">
|
||||
<div class="container-page grid min-h-[560px] items-center gap-10 py-16 lg:grid-cols-2">
|
||||
<div>
|
||||
<ShinyText
|
||||
text="SecondTech"
|
||||
className="text-1xl font-extrabold tracking-tight sm:text-1xl lg:text-2xl"
|
||||
:color="'#1c4ed9'"
|
||||
:shineColor="'#e0e7ff'"
|
||||
:spread="45"
|
||||
:speed="1"
|
||||
:delay="2"
|
||||
:direction="'left'"
|
||||
:pauseOnHover="true"
|
||||
:yoyo="true"
|
||||
/>
|
||||
<div>
|
||||
<TextType
|
||||
as="h2"
|
||||
className="mt-6 text-3xl font-extrabold tracking-tight text-zinc-950 sm:text-6xl lg:text-5xl"
|
||||
:text="['Menjual Barang Bekas, yang kamu butuhkan, di satu tempat.']"
|
||||
:typingSpeed="75"
|
||||
:pauseDuration="1500"
|
||||
:showCursor="false"
|
||||
:textColors="['#0f1729']"
|
||||
cursorCharacter="|"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-6 max-w-xl text-lg leading-8 text-slate-600">Temukan laptop bekas, PC, monitor, keyboard, router, switch, access point, dan server bekas untuk berbagai kebutuhan.</p>
|
||||
<div class="mt-8 flex flex-wrap gap-3">
|
||||
<RouterLink to="/jual" class="btn-primary">Jual Barang Sekarang</RouterLink>
|
||||
<RouterLink to="/marketplace" class="btn-secondary">Lihat Marketplace</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-4 rounded-[2rem] bg-brand-100 blur-2xl"></div>
|
||||
<img class="relative aspect-[4/3] w-full rounded-[2rem] object-cover shadow-2xl" src="https://images.unsplash.com/photo-1516321318423-f06f85e504b3?q=80&w=1200&auto=format&fit=crop" alt="Tech marketplace" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container-page py-14">
|
||||
<div class="mb-8 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-extrabold">Kategori Populer</h2>
|
||||
<p class="mt-2 text-slate-600">Fokus ke perangkat teknologi dan jaringan.</p>
|
||||
</div>
|
||||
<RouterLink to="/marketplace" class="text-sm font-bold text-brand-700">Lihat semua</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
||||
<div 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">
|
||||
<component :is="iconMap[cat.icon]" :size="22" />
|
||||
</div>
|
||||
|
||||
<p class="font-bold">{{ cat.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-white py-14">
|
||||
<div class="container-page">
|
||||
<div class="mb-8 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-extrabold">Produk Terbaru</h2>
|
||||
<p class="mt-2 text-slate-600">Contoh barang bekas teknologi yang tersedia.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<ProductCard v-for="product in latestProducts" :key="product.id" :product="product" :saved="saved.includes(product.id)" @toggle-save="toggleSave" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from '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, categoryOptions } from '../data/products';
|
||||
import ShinyText from '../vuebits/ShinyText/ShinyText.vue';
|
||||
import TextType from '../vuebits/TextType/TextType.vue';
|
||||
|
||||
const saved = ref(getSavedProducts());
|
||||
const categoryList = categoryOptions
|
||||
.filter((category) => category.name !== 'Semua')
|
||||
.slice(0, 6);
|
||||
const latestProducts = computed(() => products.slice(0, 4));
|
||||
const iconMap = {
|
||||
Boxes,
|
||||
Laptop,
|
||||
PcCase,
|
||||
Monitor,
|
||||
Keyboard,
|
||||
Router,
|
||||
Network,
|
||||
Server,
|
||||
Wifi,
|
||||
};
|
||||
|
||||
function toggleSave(id) {
|
||||
if (saved.value.includes(id)) {
|
||||
saved.value = saved.value.filter((item) => item !== id);
|
||||
} else {
|
||||
saved.value.push(id);
|
||||
}
|
||||
setSavedProducts(saved.value);
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user