diff --git a/backup-before-dark-multiphoto-20260519-091652/database.sql b/backup-before-dark-multiphoto-20260519-091652/database.sql deleted file mode 100644 index a58e462..0000000 --- a/backup-before-dark-multiphoto-20260519-091652/database.sql +++ /dev/null @@ -1,58 +0,0 @@ -CREATE DATABASE IF NOT EXISTS secondtech_market - CHARACTER SET utf8mb4 - COLLATE utf8mb4_unicode_ci; - -USE secondtech_market; - -CREATE TABLE IF NOT EXISTS users ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(120) NOT NULL, - email VARCHAR(160) NOT NULL UNIQUE, - whatsapp VARCHAR(30) NOT NULL, - password_hash VARCHAR(255) NOT NULL, - role ENUM('user', 'superadmin') NOT NULL DEFAULT 'user', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS products ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - user_id INT UNSIGNED NULL, - title VARCHAR(180) NOT NULL, - category VARCHAR(80) NOT NULL, - price INT UNSIGNED NOT NULL, - `condition` VARCHAR(80) NOT NULL, - location VARCHAR(120) NOT NULL, - seller VARCHAR(120) NOT NULL, - whatsapp VARCHAR(30) NOT NULL, - image VARCHAR(255) NOT NULL, - description TEXT NOT NULL, - status ENUM('Tersedia', 'Terjual') NOT NULL DEFAULT 'Tersedia', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT fk_products_user - FOREIGN KEY (user_id) REFERENCES users(id) - ON DELETE SET NULL -); - -INSERT INTO products - (id, title, category, price, `condition`, location, seller, whatsapp, image, description, status) -VALUES - (1, 'Laptop Lenovo ThinkPad T480', 'Laptop', 3200000, 'Bekas Normal', 'Yogyakarta', 'Raka SIJA', '6281234567890', 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?q=80&w=1200&auto=format&fit=crop', 'Laptop bekas cocok untuk belajar coding, jaringan, virtual machine ringan, dan kebutuhan sekolah. RAM 8GB, SSD 256GB, keyboard normal.', 'Tersedia'), - (2, 'PC Rakitan i5 Gen 8', 'PC', 4100000, 'Bekas Normal', 'Bantul', 'Dimas Tech', '628111222333', 'https://images.unsplash.com/photo-1587202372775-e229f172b9d7?q=80&w=1200&auto=format&fit=crop', 'PC rakitan untuk lab jaringan, desain ringan, dan multitasking. Intel Core i5, RAM 16GB, SSD 512GB.', 'Tersedia'), - (3, 'Monitor LG 24 Inch IPS', 'Monitor', 1150000, 'Bekas Mulus', 'Sleman', 'Adit Hardware', '628555666777', 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?q=80&w=1200&auto=format&fit=crop', 'Monitor IPS 24 inch, warna masih bagus, cocok untuk coding dan editing. Include kabel power dan HDMI.', 'Tersedia'), - (4, 'Keyboard Mechanical Keychron K2', 'Keyboard', 850000, 'Bekas Normal', 'Solo', 'Naufal Keys', '628333444555', 'https://images.unsplash.com/photo-1618384887929-16ec33fab9ef?q=80&w=1200&auto=format&fit=crop', 'Keyboard mechanical wireless, switch brown, cocok untuk coding. Kondisi normal dan keycap lengkap.', 'Tersedia'), - (5, 'MikroTik RB941 hAP Lite', 'Router', 180000, 'Bekas Normal', 'Kulon Progo', 'Lab Network', '628777888999', 'https://images.unsplash.com/photo-1606904825846-647eb07f5be2?q=80&w=1200&auto=format&fit=crop', 'Router MikroTik untuk belajar routing, firewall, DHCP, hotspot, dan konfigurasi dasar jaringan.', 'Tersedia'), - (6, 'Switch TP-Link 8 Port Gigabit', 'Switch', 250000, 'Bekas Normal', 'Magelang', 'Fajar Net', '628999111222', 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1200&auto=format&fit=crop', 'Switch 8 port gigabit, cocok untuk lab kecil, warnet mini, dan praktik jaringan lokal.', 'Tersedia'), - (7, 'Server Dell PowerEdge R620', 'Server', 6500000, 'Bekas Server Room', 'Jakarta', 'Server Bekas ID', '628222333444', 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1200&auto=format&fit=crop', 'Server rackmount bekas data center, cocok untuk belajar virtualization, Proxmox, Docker, dan homelab.', 'Tersedia'), - (8, 'Access Point TP-Link EAP225', 'Access Point', 620000, 'Bekas Mulus', 'Semarang', 'WiFi Store', '6281212121212', 'https://images.unsplash.com/photo-1544197150-b99a580bb7a8?q=80&w=1200&auto=format&fit=crop', 'Access point ceiling untuk hotspot sekolah, kantor, dan lab jaringan. Kondisi normal.', 'Tersedia') -ON DUPLICATE KEY UPDATE - title = VALUES(title), - category = VALUES(category), - price = VALUES(price), - `condition` = VALUES(`condition`), - location = VALUES(location), - seller = VALUES(seller), - whatsapp = VALUES(whatsapp), - image = VALUES(image), - description = VALUES(description), - status = VALUES(status); diff --git a/backup-before-dark-multiphoto-20260519-091652/server/server.js b/backup-before-dark-multiphoto-20260519-091652/server/server.js deleted file mode 100644 index 7bbd7c8..0000000 --- a/backup-before-dark-multiphoto-20260519-091652/server/server.js +++ /dev/null @@ -1,356 +0,0 @@ -import bcrypt from "bcryptjs" -import cors from "cors" -import dotenv from "dotenv" -import express from "express" -import fs from "fs" -import jwt from "jsonwebtoken" -import multer from "multer" -import path from "path" -import { fileURLToPath } from "url" -import { pool } from "./db.js" - -dotenv.config() - -const app = express() -const port = Number(process.env.PORT || 5000) -const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret" -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const uploadsDir = path.join(__dirname, "uploads") - -if (!fs.existsSync(uploadsDir)) { - fs.mkdirSync(uploadsDir, { recursive: true }) -} - -app.use(cors({ - origin: process.env.CLIENT_URL || "http://localhost:5173", - credentials: true -})) -app.use(express.json()) -app.use("/uploads", express.static(uploadsDir)) - -const storage = multer.diskStorage({ - destination: uploadsDir, - filename: (req, file, cb) => { - const ext = path.extname(file.originalname).toLowerCase() - cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`) - } -}) - -const upload = multer({ - storage, - limits: { fileSize: 3 * 1024 * 1024 }, - fileFilter: (req, file, cb) => { - if (!file.mimetype.startsWith("image/")) { - cb(new Error("File harus berupa gambar.")) - return - } - cb(null, true) - } -}) - -function signUser(user) { - return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" }) -} - -function publicUser(user) { - return { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - } -} - -function auth(req, res, next) { - const header = req.headers.authorization || "" - const token = header.startsWith("Bearer ") ? header.slice(7) : "" - - if (!token) { - res.status(401).json({ message: "Silakan login terlebih dahulu." }) - return - } - - try { - req.user = jwt.verify(token, jwtSecret) - next() - } catch { - res.status(401).json({ message: "Sesi login tidak valid." }) - } -} - -async function findProductById(id) { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE id = ?", - [id] - ) - - const product = rows[0] - if (!product) return null - - const [images] = await pool.query( - "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", - [id] - ) - - product.images = images.length ? images.map((item) => item.image) : [product.image] - - return product -} - - -app.get("/api/health", async (req, res, next) => { - try { - await pool.query("SELECT 1") - res.json({ status: "ok" }) - } catch (error) { - next(error) - } -}) - -app.post("/api/auth/register", async (req, res, next) => { - try { - const { name, email, whatsapp, password } = req.body - - if (!name || !email || !whatsapp || !password) { - res.status(400).json({ message: "Semua field wajib diisi." }) - return - } - - if (password.length < 6) { - res.status(400).json({ message: "Password minimal 6 karakter." }) - return - } - - const passwordHash = await bcrypt.hash(password, 10) - const [result] = await pool.query( - "INSERT INTO users (name, email, whatsapp, password_hash) VALUES (?, ?, ?, ?)", - [name, email.toLowerCase(), whatsapp, passwordHash] - ) - - const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" } - res.status(201).json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - if (error.code === "ER_DUP_ENTRY") { - res.status(409).json({ message: "Email sudah terdaftar." }) - return - } - next(error) - } -}) - -app.post("/api/auth/login", async (req, res, next) => { - try { - const { email, password } = req.body - - if (!email || !password) { - res.status(400).json({ message: "Email dan password wajib diisi." }) - return - } - - const [rows] = await pool.query("SELECT * FROM users WHERE email = ?", [email.toLowerCase()]) - const user = rows[0] - - if (!user || !(await bcrypt.compare(password, user.password_hash))) { - res.status(401).json({ message: "Email atau password salah." }) - return - } - - res.json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/auth/me", auth, async (req, res, next) => { - try { - const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id]) - if (!rows[0]) { - res.status(404).json({ message: "User tidak ditemukan." }) - return - } - res.json({ user: rows[0] }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products", async (req, res, next) => { - const products = await Promise.all( - rows.map(async (product) => { - const [images] = await pool.query( - "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", - [product.id] - ) - - return { - ...product, - images: images.length ? images.map((item) => item.image) : [product.image] - } - }) -) - -res.json({ products }) - -}) - -app.get("/api/products/mine", auth, async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE user_id = ? ORDER BY created_at DESC, id DESC", - [req.user.id] - ) - res.json({ products: rows }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/:id", async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product) { - res.status(404).json({ message: "Produk tidak ditemukan." }) - return - } - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.post("/api/products", auth, upload.array("images", 6), async (req, res, next) => { - try { - const { title, category, price, condition, location, seller, whatsapp, description } = req.body - - if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !req.files?.length) { - res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." }) - return - } - - const mainImagePath = `/uploads/${req.files[0].filename}` - - const [result] = await pool.query( - "INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [req.user.id, title, category, Number(price), condition, location, seller, whatsapp, mainImagePath, description] - ) - - const imageRows = req.files.map((file, index) => [ - result.insertId, - `/uploads/${file.filename}`, - index - ]) - - await pool.query( - "INSERT INTO product_images (product_id, image, sort_order) VALUES ?", - [imageRows] - ) - - const product = await findProductById(result.insertId) - res.status(201).json({ product }) - } catch (error) { - next(error) - } -}) - - -app.patch("/api/products/:id/status", auth, async (req, res, next) => { - try { - const status = req.body.status === "Terjual" ? "Terjual" : "Tersedia" - const [result] = await pool.query( - "UPDATE products SET status = ? WHERE id = ? AND user_id = ?", - [status, req.params.id, req.user.id] - ) - - if (!result.affectedRows) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - const product = await findProductById(req.params.id) - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.delete("/api/products/:id", auth, async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product || product.user_id !== req.user.id) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - await pool.query("DELETE FROM products WHERE id = ? AND user_id = ?", [req.params.id, req.user.id]) - - if (product.image.startsWith("/uploads/")) { - fs.rm(path.join(uploadsDir, path.basename(product.image)), { force: true }, () => {}) - } - - res.json({ message: "Produk berhasil dihapus." }) - } catch (error) { - next(error) - } -}) - -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) => { - if (error instanceof multer.MulterError) { - res.status(400).json({ message: "Upload gagal. Ukuran foto maksimal 10MB." }) - return - } - - if (error.message === "File harus berupa gambar.") { - res.status(400).json({ message: error.message }) - return - } - - console.error(error) - res.status(500).json({ message: "Terjadi kesalahan pada server." }) -}) - -app.listen(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) - } -} diff --git a/backup-before-dark-multiphoto-20260519-091652/src/assets/main.css b/backup-before-dark-multiphoto-20260519-091652/src/assets/main.css deleted file mode 100644 index a5213cc..0000000 --- a/backup-before-dark-multiphoto-20260519-091652/src/assets/main.css +++ /dev/null @@ -1,41 +0,0 @@ -@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; -} diff --git a/backup-before-dark-multiphoto-20260519-091652/src/components/Navbar.vue b/backup-before-dark-multiphoto-20260519-091652/src/components/Navbar.vue deleted file mode 100644 index f043072..0000000 --- a/backup-before-dark-multiphoto-20260519-091652/src/components/Navbar.vue +++ /dev/null @@ -1,168 +0,0 @@ - - - - - diff --git a/backup-before-dark-multiphoto-20260519-091652/src/utils.js b/backup-before-dark-multiphoto-20260519-091652/src/utils.js deleted file mode 100644 index 1754fc4..0000000 --- a/backup-before-dark-multiphoto-20260519-091652/src/utils.js +++ /dev/null @@ -1,138 +0,0 @@ -export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:5000" - -export function formatRupiah(value) { - return new Intl.NumberFormat("id-ID", { - style: "currency", - currency: "IDR", - maximumFractionDigits: 0 - }).format(value) -} - -export function getLocal(key, fallback) { - try { - const value = localStorage.getItem(key) - return value ? JSON.parse(value) : fallback - } catch { - return fallback - } -} - -export function setLocal(key, value) { - localStorage.setItem(key, JSON.stringify(value)) -} - -export function getToken() { - return localStorage.getItem("secondtech_token") -} - -export function getCurrentUser() { - return getLocal("secondtech_user", null) -} - -export function setAuth({ token, user }) { - localStorage.setItem("secondtech_token", token) - - setLocal("secondtech_user", { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - }) -} - -export function clearAuth() { - localStorage.removeItem("secondtech_token") - localStorage.removeItem("secondtech_user") -} - -export function getProductImageUrl(image) { - if (!image) return "" - if (image.startsWith("http://") || image.startsWith("https://") || image.startsWith("data:")) return image - return `${API_BASE_URL}${image}` -} - -async function requestJson(path, options = {}) { - const headers = new Headers(options.headers || {}) - const token = getToken() - - if (token) headers.set("Authorization", `Bearer ${token}`) - if (!(options.body instanceof FormData)) headers.set("Content-Type", "application/json") - - const response = await fetch(`${API_BASE_URL}${path}`, { - ...options, - headers - }) - - const data = await response.json().catch(() => ({})) - - if (!response.ok) { - throw new Error(data.message || "Request gagal.") - } - - return data -} - -export function registerUser(payload) { - return requestJson("/api/auth/register", { - method: "POST", - body: JSON.stringify(payload) - }) -} - -export function loginUser(payload) { - return requestJson("/api/auth/login", { - method: "POST", - body: JSON.stringify(payload) - }) -} - -export function fetchCurrentUser() { - return requestJson("/api/auth/me") -} - -export function fetchProducts() { - return requestJson("/api/products") -} - -export function fetchProduct(id) { - return requestJson(`/api/products/${id}`) -} - -export function fetchMyProducts() { - return requestJson("/api/products/mine") -} - -export function createProduct(formData) { - return requestJson("/api/products", { - method: "POST", - body: formData - }) -} - -export function updateProductStatus(id, status) { - return requestJson(`/api/products/${id}/status`, { - method: "PATCH", - body: JSON.stringify({ status }) - }) -} - -export function deleteProductById(id) { - return requestJson(`/api/products/${id}`, { - method: "DELETE" - }) -} - -export function deleteProductAsAdmin(id) { - return requestJson(`/api/admin/products/${id}`, { - method: "DELETE" - }) -} - -export function getSavedProducts() { - return getLocal("secondtech_saved", []) -} - -export function setSavedProducts(ids) { - setLocal("secondtech_saved", ids) -} diff --git a/backup-before-dark-multiphoto-20260519-091652/src/views/ProductDetailView.vue b/backup-before-dark-multiphoto-20260519-091652/src/views/ProductDetailView.vue deleted file mode 100644 index eec3fe5..0000000 --- a/backup-before-dark-multiphoto-20260519-091652/src/views/ProductDetailView.vue +++ /dev/null @@ -1,105 +0,0 @@ - - - diff --git a/backup-before-dark-multiphoto-20260519-091652/src/views/SellProductView.vue b/backup-before-dark-multiphoto-20260519-091652/src/views/SellProductView.vue deleted file mode 100644 index 4628875..0000000 --- a/backup-before-dark-multiphoto-20260519-091652/src/views/SellProductView.vue +++ /dev/null @@ -1,171 +0,0 @@ - - - diff --git a/backup-before-dark-multiphoto-20260519-091652/tailwind.config.js b/backup-before-dark-multiphoto-20260519-091652/tailwind.config.js deleted file mode 100644 index 5203320..0000000 --- a/backup-before-dark-multiphoto-20260519-091652/tailwind.config.js +++ /dev/null @@ -1,25 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./index.html", - "./src/**/*.{vue,js}" - ], - theme: { - extend: { - fontFamily: { - sans: ["Inter", "system-ui", "sans-serif"] - }, - colors: { - brand: { - 50: "#eff6ff", - 100: "#dbeafe", - 500: "#2563eb", - 600: "#1d4ed8", - 700: "#1e40af", - 900: "#172554" - } - } - } - }, - plugins: [] -} diff --git a/backup-before-dark-multiphoto-20260519-092338/database.sql b/backup-before-dark-multiphoto-20260519-092338/database.sql deleted file mode 100644 index f559507..0000000 --- a/backup-before-dark-multiphoto-20260519-092338/database.sql +++ /dev/null @@ -1,69 +0,0 @@ -CREATE DATABASE IF NOT EXISTS secondtech_market - CHARACTER SET utf8mb4 - COLLATE utf8mb4_unicode_ci; - -USE secondtech_market; - -CREATE TABLE IF NOT EXISTS users ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(120) NOT NULL, - email VARCHAR(160) NOT NULL UNIQUE, - whatsapp VARCHAR(30) NOT NULL, - password_hash VARCHAR(255) NOT NULL, - role ENUM('user', 'superadmin') NOT NULL DEFAULT 'user', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS products ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - user_id INT UNSIGNED NULL, - title VARCHAR(180) NOT NULL, - category VARCHAR(80) NOT NULL, - price INT UNSIGNED NOT NULL, - `condition` VARCHAR(80) NOT NULL, - location VARCHAR(120) NOT NULL, - seller VARCHAR(120) NOT NULL, - whatsapp VARCHAR(30) NOT NULL, - image VARCHAR(255) NOT NULL, - description TEXT NOT NULL, - status ENUM('Tersedia', 'Terjual') NOT NULL DEFAULT 'Tersedia', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT fk_products_user - FOREIGN KEY (user_id) REFERENCES users(id) - ON DELETE SET NULL -); - -INSERT INTO products - (id, title, category, price, `condition`, location, seller, whatsapp, image, description, status) -VALUES - (1, 'Laptop Lenovo ThinkPad T480', 'Laptop', 3200000, 'Bekas Normal', 'Yogyakarta', 'Raka SIJA', '6281234567890', 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?q=80&w=1200&auto=format&fit=crop', 'Laptop bekas cocok untuk belajar coding, jaringan, virtual machine ringan, dan kebutuhan sekolah. RAM 8GB, SSD 256GB, keyboard normal.', 'Tersedia'), - (2, 'PC Rakitan i5 Gen 8', 'PC', 4100000, 'Bekas Normal', 'Bantul', 'Dimas Tech', '628111222333', 'https://images.unsplash.com/photo-1587202372775-e229f172b9d7?q=80&w=1200&auto=format&fit=crop', 'PC rakitan untuk lab jaringan, desain ringan, dan multitasking. Intel Core i5, RAM 16GB, SSD 512GB.', 'Tersedia'), - (3, 'Monitor LG 24 Inch IPS', 'Monitor', 1150000, 'Bekas Mulus', 'Sleman', 'Adit Hardware', '628555666777', 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?q=80&w=1200&auto=format&fit=crop', 'Monitor IPS 24 inch, warna masih bagus, cocok untuk coding dan editing. Include kabel power dan HDMI.', 'Tersedia'), - (4, 'Keyboard Mechanical Keychron K2', 'Keyboard', 850000, 'Bekas Normal', 'Solo', 'Naufal Keys', '628333444555', 'https://images.unsplash.com/photo-1618384887929-16ec33fab9ef?q=80&w=1200&auto=format&fit=crop', 'Keyboard mechanical wireless, switch brown, cocok untuk coding. Kondisi normal dan keycap lengkap.', 'Tersedia'), - (5, 'MikroTik RB941 hAP Lite', 'Router', 180000, 'Bekas Normal', 'Kulon Progo', 'Lab Network', '628777888999', 'https://images.unsplash.com/photo-1606904825846-647eb07f5be2?q=80&w=1200&auto=format&fit=crop', 'Router MikroTik untuk belajar routing, firewall, DHCP, hotspot, dan konfigurasi dasar jaringan.', 'Tersedia'), - (6, 'Switch TP-Link 8 Port Gigabit', 'Switch', 250000, 'Bekas Normal', 'Magelang', 'Fajar Net', '628999111222', 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1200&auto=format&fit=crop', 'Switch 8 port gigabit, cocok untuk lab kecil, warnet mini, dan praktik jaringan lokal.', 'Tersedia'), - (7, 'Server Dell PowerEdge R620', 'Server', 6500000, 'Bekas Server Room', 'Jakarta', 'Server Bekas ID', '628222333444', 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1200&auto=format&fit=crop', 'Server rackmount bekas data center, cocok untuk belajar virtualization, Proxmox, Docker, dan homelab.', 'Tersedia'), - (8, 'Access Point TP-Link EAP225', 'Access Point', 620000, 'Bekas Mulus', 'Semarang', 'WiFi Store', '6281212121212', 'https://images.unsplash.com/photo-1544197150-b99a580bb7a8?q=80&w=1200&auto=format&fit=crop', 'Access point ceiling untuk hotspot sekolah, kantor, dan lab jaringan. Kondisi normal.', 'Tersedia') -ON DUPLICATE KEY UPDATE - title = VALUES(title), - category = VALUES(category), - price = VALUES(price), - `condition` = VALUES(`condition`), - location = VALUES(location), - seller = VALUES(seller), - whatsapp = VALUES(whatsapp), - image = VALUES(image), - description = VALUES(description), - status = VALUES(status); - -CREATE TABLE IF NOT EXISTS product_images ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - product_id INT UNSIGNED NOT NULL, - image VARCHAR(255) NOT NULL, - sort_order INT UNSIGNED NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_product_images_product - FOREIGN KEY (product_id) REFERENCES products(id) - ON DELETE CASCADE -); diff --git a/backup-before-dark-multiphoto-20260519-092338/server/server.js b/backup-before-dark-multiphoto-20260519-092338/server/server.js deleted file mode 100644 index eae1282..0000000 --- a/backup-before-dark-multiphoto-20260519-092338/server/server.js +++ /dev/null @@ -1,348 +0,0 @@ -import bcrypt from "bcryptjs" -import cors from "cors" -import dotenv from "dotenv" -import express from "express" -import fs from "fs" -import jwt from "jsonwebtoken" -import multer from "multer" -import path from "path" -import { fileURLToPath } from "url" -import { pool } from "./db.js" - -dotenv.config() - -const app = express() -const port = Number(process.env.PORT || 5000) -const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret" -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const uploadsDir = path.join(__dirname, "uploads") - -if (!fs.existsSync(uploadsDir)) { - fs.mkdirSync(uploadsDir, { recursive: true }) -} - -app.use(cors({ - origin: process.env.CLIENT_URL || "http://localhost:5173", - credentials: true -})) -app.use(express.json()) -app.use("/uploads", express.static(uploadsDir)) - -const storage = multer.diskStorage({ - destination: uploadsDir, - filename: (req, file, cb) => { - const ext = path.extname(file.originalname).toLowerCase() - cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`) - } -}) - -const upload = multer({ - storage, - limits: { fileSize: 3 * 1024 * 1024 }, - fileFilter: (req, file, cb) => { - if (!file.mimetype.startsWith("image/")) { - cb(new Error("File harus berupa gambar.")) - return - } - cb(null, true) - } -}) - -function signUser(user) { - return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" }) -} - -function publicUser(user) { - return { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - } -} - -function auth(req, res, next) { - const header = req.headers.authorization || "" - const token = header.startsWith("Bearer ") ? header.slice(7) : "" - - if (!token) { - res.status(401).json({ message: "Silakan login terlebih dahulu." }) - return - } - - try { - req.user = jwt.verify(token, jwtSecret) - next() - } catch { - res.status(401).json({ message: "Sesi login tidak valid." }) - } -} - -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) - } -} - -async function getProductImages(productId, fallbackImage) { - const [images] = await pool.query( - "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", - [productId] - ) - - return images.length ? images.map((item) => item.image) : [fallbackImage] -} - -async function attachProductImages(products) { - return Promise.all( - products.map(async (product) => ({ - ...product, - images: await getProductImages(product.id, product.image) - })) - ) -} - -async function findProductById(id) { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE id = ?", - [id] - ) - - const product = rows[0] - if (!product) return null - - product.images = await getProductImages(product.id, product.image) - return product -} - -async function deleteUploadedImages(product) { - const images = product.images?.length ? product.images : [product.image] - - images.forEach((image) => { - if (image?.startsWith("/uploads/")) { - fs.rm(path.join(uploadsDir, path.basename(image)), { force: true }, () => {}) - } - }) -} - -app.get("/api/health", async (req, res, next) => { - try { - await pool.query("SELECT 1") - res.json({ status: "ok" }) - } catch (error) { - next(error) - } -}) - -app.post("/api/auth/register", async (req, res, next) => { - try { - const { name, email, whatsapp, password } = req.body - - if (!name || !email || !whatsapp || !password) { - res.status(400).json({ message: "Semua field wajib diisi." }) - return - } - - if (password.length < 6) { - res.status(400).json({ message: "Password minimal 6 karakter." }) - return - } - - const passwordHash = await bcrypt.hash(password, 10) - const [result] = await pool.query( - "INSERT INTO users (name, email, whatsapp, password_hash) VALUES (?, ?, ?, ?)", - [name, email.toLowerCase(), whatsapp, passwordHash] - ) - - const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" } - res.status(201).json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - if (error.code === "ER_DUP_ENTRY") { - res.status(409).json({ message: "Email sudah terdaftar." }) - return - } - next(error) - } -}) - -app.post("/api/auth/login", async (req, res, next) => { - try { - const { email, password } = req.body - - if (!email || !password) { - res.status(400).json({ message: "Email dan password wajib diisi." }) - return - } - - const [rows] = await pool.query("SELECT * FROM users WHERE email = ?", [email.toLowerCase()]) - const user = rows[0] - - if (!user || !(await bcrypt.compare(password, user.password_hash))) { - res.status(401).json({ message: "Email atau password salah." }) - return - } - - res.json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/auth/me", auth, async (req, res, next) => { - try { - const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id]) - if (!rows[0]) { - res.status(404).json({ message: "User tidak ditemukan." }) - return - } - res.json({ user: rows[0] }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products", async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products ORDER BY created_at DESC, id DESC" - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/mine", auth, async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE user_id = ? ORDER BY created_at DESC, id DESC", - [req.user.id] - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/:id", async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product) { - res.status(404).json({ message: "Produk tidak ditemukan." }) - return - } - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.post("/api/products", auth, upload.array("images", 6), async (req, res, next) => { - try { - const { title, category, price, condition, location, seller, whatsapp, description } = req.body - - if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !req.files?.length) { - res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." }) - return - } - - const mainImagePath = `/uploads/${req.files[0].filename}` - const [result] = await pool.query( - "INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [req.user.id, title, category, Number(price), condition, location, seller, whatsapp, mainImagePath, description] - ) - - const imageRows = req.files.map((file, index) => [result.insertId, `/uploads/${file.filename}`, index]) - await pool.query("INSERT INTO product_images (product_id, image, sort_order) VALUES ?", [imageRows]) - - const product = await findProductById(result.insertId) - res.status(201).json({ product }) - } catch (error) { - next(error) - } -}) - -app.patch("/api/products/:id/status", auth, async (req, res, next) => { - try { - const status = req.body.status === "Terjual" ? "Terjual" : "Tersedia" - const [result] = await pool.query( - "UPDATE products SET status = ? WHERE id = ? AND user_id = ?", - [status, req.params.id, req.user.id] - ) - - if (!result.affectedRows) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - const product = await findProductById(req.params.id) - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.delete("/api/products/:id", auth, async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product || product.user_id !== req.user.id) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - await pool.query("DELETE FROM products WHERE id = ? AND user_id = ?", [req.params.id, req.user.id]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus." }) - } catch (error) { - next(error) - } -}) - -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]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus oleh superadmin." }) - } catch (error) { - next(error) - } -}) - -app.use((error, req, res, next) => { - if (error instanceof multer.MulterError) { - res.status(400).json({ message: "Upload gagal. Ukuran setiap foto maksimal 3MB dan maksimal 6 foto." }) - return - } - - if (error.message === "File harus berupa gambar.") { - res.status(400).json({ message: error.message }) - return - } - - console.error(error) - res.status(500).json({ message: "Terjadi kesalahan pada server." }) -}) - -app.listen(port, () => { - console.log(`SecondTech API berjalan di http://localhost:${port}`) -}) diff --git a/backup-before-dark-multiphoto-20260519-092338/src/assets/main.css b/backup-before-dark-multiphoto-20260519-092338/src/assets/main.css deleted file mode 100644 index 75edddb..0000000 --- a/backup-before-dark-multiphoto-20260519-092338/src/assets/main.css +++ /dev/null @@ -1,95 +0,0 @@ -@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.92); - border-color: rgba(37, 99, 235, 0.28); -} - -html.dark .card { - background: #181818; - border-color: rgba(37, 99, 235, 0.28); - box-shadow: 0 12px 32px rgba(37, 99, 235, 0.18); -} - -html.dark .input { - background: #181818; - border-color: rgba(37, 99, 235, 0.28); - color: #f8fafc; -} - -html.dark .input::placeholder { - color: #94a3b8; -} - -html.dark .btn-secondary { - background: #181818; - border-color: rgba(37, 99, 235, 0.32); - color: #f8fafc; -} - -html.dark .bg-white, -html.dark .bg-slate-50 { - 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.28); -} diff --git a/backup-before-dark-multiphoto-20260519-092338/src/components/Navbar.vue b/backup-before-dark-multiphoto-20260519-092338/src/components/Navbar.vue deleted file mode 100644 index b2715a5..0000000 --- a/backup-before-dark-multiphoto-20260519-092338/src/components/Navbar.vue +++ /dev/null @@ -1,150 +0,0 @@ - - - - - diff --git a/backup-before-dark-multiphoto-20260519-092338/src/utils.js b/backup-before-dark-multiphoto-20260519-092338/src/utils.js deleted file mode 100644 index 725ba6a..0000000 --- a/backup-before-dark-multiphoto-20260519-092338/src/utils.js +++ /dev/null @@ -1,137 +0,0 @@ -export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:5000" - -export function formatRupiah(value) { - return new Intl.NumberFormat("id-ID", { - style: "currency", - currency: "IDR", - maximumFractionDigits: 0 - }).format(value) -} - -export function getLocal(key, fallback) { - try { - const value = localStorage.getItem(key) - return value ? JSON.parse(value) : fallback - } catch { - return fallback - } -} - -export function setLocal(key, value) { - localStorage.setItem(key, JSON.stringify(value)) -} - -export function getToken() { - return localStorage.getItem("secondtech_token") -} - -export function getCurrentUser() { - return getLocal("secondtech_user", null) -} - -export function setAuth({ token, user }) { - localStorage.setItem("secondtech_token", token) - - setLocal("secondtech_user", { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - }) -} - -export function clearAuth() { - localStorage.removeItem("secondtech_token") - localStorage.removeItem("secondtech_user") -} - -export function getProductImageUrl(image) { - if (!image) return "" - if (image.startsWith("http://") || image.startsWith("https://") || image.startsWith("data:")) return image - return `${API_BASE_URL}${image}` -} - -async function requestJson(path, options = {}) { - const headers = new Headers(options.headers || {}) - const token = getToken() - - if (token) headers.set("Authorization", `Bearer ${token}`) - if (!(options.body instanceof FormData)) headers.set("Content-Type", "application/json") - - const response = await fetch(`${API_BASE_URL}${path}`, { - ...options, - headers - }) - const data = await response.json().catch(() => ({})) - - if (!response.ok) { - throw new Error(data.message || "Request gagal.") - } - - return data -} - -export function registerUser(payload) { - return requestJson("/api/auth/register", { - method: "POST", - body: JSON.stringify(payload) - }) -} - -export function loginUser(payload) { - return requestJson("/api/auth/login", { - method: "POST", - body: JSON.stringify(payload) - }) -} - -export function fetchCurrentUser() { - return requestJson("/api/auth/me") -} - -export function fetchProducts() { - return requestJson("/api/products") -} - -export function fetchProduct(id) { - return requestJson(`/api/products/${id}`) -} - -export function fetchMyProducts() { - return requestJson("/api/products/mine") -} - -export function createProduct(formData) { - return requestJson("/api/products", { - method: "POST", - body: formData - }) -} - -export function updateProductStatus(id, status) { - return requestJson(`/api/products/${id}/status`, { - method: "PATCH", - body: JSON.stringify({ status }) - }) -} - -export function deleteProductById(id) { - return requestJson(`/api/products/${id}`, { - method: "DELETE" - }) -} - -export function deleteProductAsAdmin(id) { - return requestJson(`/api/admin/products/${id}`, { - method: "DELETE" - }) -} - -export function getSavedProducts() { - return getLocal("secondtech_saved", []) -} - -export function setSavedProducts(ids) { - setLocal("secondtech_saved", ids) -} diff --git a/backup-before-dark-multiphoto-20260519-092338/src/views/ProductDetailView.vue b/backup-before-dark-multiphoto-20260519-092338/src/views/ProductDetailView.vue deleted file mode 100644 index bf34d2c..0000000 --- a/backup-before-dark-multiphoto-20260519-092338/src/views/ProductDetailView.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/backup-before-dark-multiphoto-20260519-092338/src/views/SellProductView.vue b/backup-before-dark-multiphoto-20260519-092338/src/views/SellProductView.vue deleted file mode 100644 index 435c21f..0000000 --- a/backup-before-dark-multiphoto-20260519-092338/src/views/SellProductView.vue +++ /dev/null @@ -1,149 +0,0 @@ - - - diff --git a/backup-before-dark-multiphoto-20260519-092338/tailwind.config.js b/backup-before-dark-multiphoto-20260519-092338/tailwind.config.js deleted file mode 100644 index c5e9579..0000000 --- a/backup-before-dark-multiphoto-20260519-092338/tailwind.config.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: "class", - content: [ - "./index.html", - "./src/**/*.{vue,js}" - ], - theme: { - extend: { - fontFamily: { - sans: ["Inter", "system-ui", "sans-serif"] - }, - colors: { - brand: { - 50: "#eff6ff", - 100: "#dbeafe", - 500: "#2563eb", - 600: "#1d4ed8", - 700: "#1e40af", - 900: "#172554" - } - } - } - }, - plugins: [] -} - diff --git a/backup-before-darkmode-final-20260519-103706/src/assets/main.css b/backup-before-darkmode-final-20260519-103706/src/assets/main.css deleted file mode 100644 index b8743a8..0000000 --- a/backup-before-darkmode-final-20260519-103706/src/assets/main.css +++ /dev/null @@ -1,304 +0,0 @@ -@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; -} diff --git a/backup-before-darkmode-final-20260519-103706/src/components/DashboardSidebar.vue b/backup-before-darkmode-final-20260519-103706/src/components/DashboardSidebar.vue deleted file mode 100644 index 5820201..0000000 --- a/backup-before-darkmode-final-20260519-103706/src/components/DashboardSidebar.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - - - diff --git a/backup-before-darkmode-final-20260519-103706/src/components/ProductCard.vue b/backup-before-darkmode-final-20260519-103706/src/components/ProductCard.vue deleted file mode 100644 index b70be73..0000000 --- a/backup-before-darkmode-final-20260519-103706/src/components/ProductCard.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/backup-before-darkmode-final-20260519-103706/src/views/HomeView.vue b/backup-before-darkmode-final-20260519-103706/src/views/HomeView.vue deleted file mode 100644 index 88e668d..0000000 --- a/backup-before-darkmode-final-20260519-103706/src/views/HomeView.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - diff --git a/backup-before-fix-20260519-092338/server/server.js b/backup-before-fix-20260519-092338/server/server.js deleted file mode 100644 index eae1282..0000000 --- a/backup-before-fix-20260519-092338/server/server.js +++ /dev/null @@ -1,348 +0,0 @@ -import bcrypt from "bcryptjs" -import cors from "cors" -import dotenv from "dotenv" -import express from "express" -import fs from "fs" -import jwt from "jsonwebtoken" -import multer from "multer" -import path from "path" -import { fileURLToPath } from "url" -import { pool } from "./db.js" - -dotenv.config() - -const app = express() -const port = Number(process.env.PORT || 5000) -const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret" -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const uploadsDir = path.join(__dirname, "uploads") - -if (!fs.existsSync(uploadsDir)) { - fs.mkdirSync(uploadsDir, { recursive: true }) -} - -app.use(cors({ - origin: process.env.CLIENT_URL || "http://localhost:5173", - credentials: true -})) -app.use(express.json()) -app.use("/uploads", express.static(uploadsDir)) - -const storage = multer.diskStorage({ - destination: uploadsDir, - filename: (req, file, cb) => { - const ext = path.extname(file.originalname).toLowerCase() - cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`) - } -}) - -const upload = multer({ - storage, - limits: { fileSize: 3 * 1024 * 1024 }, - fileFilter: (req, file, cb) => { - if (!file.mimetype.startsWith("image/")) { - cb(new Error("File harus berupa gambar.")) - return - } - cb(null, true) - } -}) - -function signUser(user) { - return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" }) -} - -function publicUser(user) { - return { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - } -} - -function auth(req, res, next) { - const header = req.headers.authorization || "" - const token = header.startsWith("Bearer ") ? header.slice(7) : "" - - if (!token) { - res.status(401).json({ message: "Silakan login terlebih dahulu." }) - return - } - - try { - req.user = jwt.verify(token, jwtSecret) - next() - } catch { - res.status(401).json({ message: "Sesi login tidak valid." }) - } -} - -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) - } -} - -async function getProductImages(productId, fallbackImage) { - const [images] = await pool.query( - "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", - [productId] - ) - - return images.length ? images.map((item) => item.image) : [fallbackImage] -} - -async function attachProductImages(products) { - return Promise.all( - products.map(async (product) => ({ - ...product, - images: await getProductImages(product.id, product.image) - })) - ) -} - -async function findProductById(id) { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE id = ?", - [id] - ) - - const product = rows[0] - if (!product) return null - - product.images = await getProductImages(product.id, product.image) - return product -} - -async function deleteUploadedImages(product) { - const images = product.images?.length ? product.images : [product.image] - - images.forEach((image) => { - if (image?.startsWith("/uploads/")) { - fs.rm(path.join(uploadsDir, path.basename(image)), { force: true }, () => {}) - } - }) -} - -app.get("/api/health", async (req, res, next) => { - try { - await pool.query("SELECT 1") - res.json({ status: "ok" }) - } catch (error) { - next(error) - } -}) - -app.post("/api/auth/register", async (req, res, next) => { - try { - const { name, email, whatsapp, password } = req.body - - if (!name || !email || !whatsapp || !password) { - res.status(400).json({ message: "Semua field wajib diisi." }) - return - } - - if (password.length < 6) { - res.status(400).json({ message: "Password minimal 6 karakter." }) - return - } - - const passwordHash = await bcrypt.hash(password, 10) - const [result] = await pool.query( - "INSERT INTO users (name, email, whatsapp, password_hash) VALUES (?, ?, ?, ?)", - [name, email.toLowerCase(), whatsapp, passwordHash] - ) - - const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" } - res.status(201).json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - if (error.code === "ER_DUP_ENTRY") { - res.status(409).json({ message: "Email sudah terdaftar." }) - return - } - next(error) - } -}) - -app.post("/api/auth/login", async (req, res, next) => { - try { - const { email, password } = req.body - - if (!email || !password) { - res.status(400).json({ message: "Email dan password wajib diisi." }) - return - } - - const [rows] = await pool.query("SELECT * FROM users WHERE email = ?", [email.toLowerCase()]) - const user = rows[0] - - if (!user || !(await bcrypt.compare(password, user.password_hash))) { - res.status(401).json({ message: "Email atau password salah." }) - return - } - - res.json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/auth/me", auth, async (req, res, next) => { - try { - const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id]) - if (!rows[0]) { - res.status(404).json({ message: "User tidak ditemukan." }) - return - } - res.json({ user: rows[0] }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products", async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products ORDER BY created_at DESC, id DESC" - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/mine", auth, async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE user_id = ? ORDER BY created_at DESC, id DESC", - [req.user.id] - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/:id", async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product) { - res.status(404).json({ message: "Produk tidak ditemukan." }) - return - } - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.post("/api/products", auth, upload.array("images", 6), async (req, res, next) => { - try { - const { title, category, price, condition, location, seller, whatsapp, description } = req.body - - if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !req.files?.length) { - res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." }) - return - } - - const mainImagePath = `/uploads/${req.files[0].filename}` - const [result] = await pool.query( - "INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [req.user.id, title, category, Number(price), condition, location, seller, whatsapp, mainImagePath, description] - ) - - const imageRows = req.files.map((file, index) => [result.insertId, `/uploads/${file.filename}`, index]) - await pool.query("INSERT INTO product_images (product_id, image, sort_order) VALUES ?", [imageRows]) - - const product = await findProductById(result.insertId) - res.status(201).json({ product }) - } catch (error) { - next(error) - } -}) - -app.patch("/api/products/:id/status", auth, async (req, res, next) => { - try { - const status = req.body.status === "Terjual" ? "Terjual" : "Tersedia" - const [result] = await pool.query( - "UPDATE products SET status = ? WHERE id = ? AND user_id = ?", - [status, req.params.id, req.user.id] - ) - - if (!result.affectedRows) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - const product = await findProductById(req.params.id) - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.delete("/api/products/:id", auth, async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product || product.user_id !== req.user.id) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - await pool.query("DELETE FROM products WHERE id = ? AND user_id = ?", [req.params.id, req.user.id]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus." }) - } catch (error) { - next(error) - } -}) - -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]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus oleh superadmin." }) - } catch (error) { - next(error) - } -}) - -app.use((error, req, res, next) => { - if (error instanceof multer.MulterError) { - res.status(400).json({ message: "Upload gagal. Ukuran setiap foto maksimal 3MB dan maksimal 6 foto." }) - return - } - - if (error.message === "File harus berupa gambar.") { - res.status(400).json({ message: error.message }) - return - } - - console.error(error) - res.status(500).json({ message: "Terjadi kesalahan pada server." }) -}) - -app.listen(port, () => { - console.log(`SecondTech API berjalan di http://localhost:${port}`) -}) diff --git a/backup-before-fix-20260519-092338/src/assets/main.css b/backup-before-fix-20260519-092338/src/assets/main.css deleted file mode 100644 index 75edddb..0000000 --- a/backup-before-fix-20260519-092338/src/assets/main.css +++ /dev/null @@ -1,95 +0,0 @@ -@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.92); - border-color: rgba(37, 99, 235, 0.28); -} - -html.dark .card { - background: #181818; - border-color: rgba(37, 99, 235, 0.28); - box-shadow: 0 12px 32px rgba(37, 99, 235, 0.18); -} - -html.dark .input { - background: #181818; - border-color: rgba(37, 99, 235, 0.28); - color: #f8fafc; -} - -html.dark .input::placeholder { - color: #94a3b8; -} - -html.dark .btn-secondary { - background: #181818; - border-color: rgba(37, 99, 235, 0.32); - color: #f8fafc; -} - -html.dark .bg-white, -html.dark .bg-slate-50 { - 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.28); -} diff --git a/backup-before-fix-20260519-092338/src/components/Navbar.vue b/backup-before-fix-20260519-092338/src/components/Navbar.vue deleted file mode 100644 index b2715a5..0000000 --- a/backup-before-fix-20260519-092338/src/components/Navbar.vue +++ /dev/null @@ -1,150 +0,0 @@ - - - - - diff --git a/backup-before-fix-20260519-092338/src/components/ProductCard.vue b/backup-before-fix-20260519-092338/src/components/ProductCard.vue deleted file mode 100644 index b70be73..0000000 --- a/backup-before-fix-20260519-092338/src/components/ProductCard.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/backup-before-fix-20260519-092338/src/data/products.js b/backup-before-fix-20260519-092338/src/data/products.js deleted file mode 100644 index ff2457c..0000000 --- a/backup-before-fix-20260519-092338/src/data/products.js +++ /dev/null @@ -1,137 +0,0 @@ -export const products = [ - { - id: 1, - title: "Laptop Lenovo ThinkPad T480", - category: "Laptop", - price: 3200000, - condition: "Bekas Normal", - location: "Yogyakarta", - seller: "Raka SIJA", - whatsapp: "6281234567890", - image: "https://images.unsplash.com/photo-1496181133206-80ce9b88a853?q=80&w=1200&auto=format&fit=crop", - description: "Laptop bekas cocok untuk belajar coding, jaringan, virtual machine ringan, dan kebutuhan sekolah. RAM 8GB, SSD 256GB, keyboard normal." - }, - { - id: 2, - title: "PC Rakitan i5 Gen 8", - category: "PC", - price: 4100000, - condition: "Bekas Normal", - location: "Bantul", - seller: "Dimas Tech", - whatsapp: "628111222333", - image: "https://images.unsplash.com/photo-1587202372775-e229f172b9d7?q=80&w=1200&auto=format&fit=crop", - description: "PC rakitan untuk lab jaringan, desain ringan, dan multitasking. Intel Core i5, RAM 16GB, SSD 512GB." - }, - { - id: 3, - title: "Monitor LG 24 Inch IPS", - category: "Monitor", - price: 1150000, - condition: "Bekas Mulus", - location: "Sleman", - seller: "Adit Hardware", - whatsapp: "628555666777", - image: "https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?q=80&w=1200&auto=format&fit=crop", - description: "Monitor IPS 24 inch, warna masih bagus, cocok untuk coding dan editing. Include kabel power dan HDMI." - }, - { - id: 4, - title: "Keyboard Mechanical Keychron K2", - category: "Keyboard", - price: 850000, - condition: "Bekas Normal", - location: "Solo", - seller: "Naufal Keys", - whatsapp: "628333444555", - image: "https://images.unsplash.com/photo-1618384887929-16ec33fab9ef?q=80&w=1200&auto=format&fit=crop", - description: "Keyboard mechanical wireless, switch brown, cocok untuk coding. Kondisi normal dan keycap lengkap." - }, - { - id: 5, - title: "MikroTik RB941 hAP Lite", - category: "Router", - price: 180000, - condition: "Bekas Normal", - location: "Kulon Progo", - seller: "Lab Network", - whatsapp: "628777888999", - image: "https://images.unsplash.com/photo-1606904825846-647eb07f5be2?q=80&w=1200&auto=format&fit=crop", - description: "Router MikroTik untuk belajar routing, firewall, DHCP, hotspot, dan konfigurasi dasar jaringan." - }, - { - id: 6, - title: "Switch TP-Link 8 Port Gigabit", - category: "Switch", - price: 250000, - condition: "Bekas Normal", - location: "Magelang", - seller: "Fajar Net", - whatsapp: "628999111222", - image: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1200&auto=format&fit=crop", - description: "Switch 8 port gigabit, cocok untuk lab kecil, warnet mini, dan praktik jaringan lokal." - }, - { - id: 7, - title: "Server Dell PowerEdge R620", - category: "Server", - price: 6500000, - condition: "Bekas Server Room", - location: "Jakarta", - seller: "Server Bekas ID", - whatsapp: "628222333444", - image: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1200&auto=format&fit=crop", - description: "Server rackmount bekas data center, cocok untuk belajar virtualization, Proxmox, Docker, dan homelab." - }, - { - id: 8, - title: "Access Point TP-Link EAP225", - category: "Access Point", - price: 620000, - condition: "Bekas Mulus", - location: "Semarang", - seller: "WiFi Store", - whatsapp: "6281212121212", - image: "https://images.unsplash.com/photo-1544197150-b99a580bb7a8?q=80&w=1200&auto=format&fit=crop", - description: "Access point ceiling untuk hotspot sekolah, kantor, dan lab jaringan. Kondisi normal." - } -] - -export const categories = [ - { - name: "Semua", - icon: "Boxes" - }, - { - name: "Laptop", - icon: "Laptop" - }, - { - 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" - } -] diff --git a/backup-before-fix-20260519-092338/src/views/ProductDetailView.vue b/backup-before-fix-20260519-092338/src/views/ProductDetailView.vue deleted file mode 100644 index bf34d2c..0000000 --- a/backup-before-fix-20260519-092338/src/views/ProductDetailView.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/backup-before-fix-20260519-092338/src/views/SellProductView.vue b/backup-before-fix-20260519-092338/src/views/SellProductView.vue deleted file mode 100644 index 0842d46..0000000 --- a/backup-before-fix-20260519-092338/src/views/SellProductView.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - diff --git a/backup-before-fix-20260519-092338/tailwind.config.js b/backup-before-fix-20260519-092338/tailwind.config.js deleted file mode 100644 index c5e9579..0000000 --- a/backup-before-fix-20260519-092338/tailwind.config.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: "class", - content: [ - "./index.html", - "./src/**/*.{vue,js}" - ], - theme: { - extend: { - fontFamily: { - sans: ["Inter", "system-ui", "sans-serif"] - }, - colors: { - brand: { - 50: "#eff6ff", - 100: "#dbeafe", - 500: "#2563eb", - 600: "#1d4ed8", - 700: "#1e40af", - 900: "#172554" - } - } - } - }, - plugins: [] -} - diff --git a/backup-before-force-multiphoto-dark-20260519-103129/server/server.js b/backup-before-force-multiphoto-dark-20260519-103129/server/server.js deleted file mode 100644 index efec7d1..0000000 --- a/backup-before-force-multiphoto-dark-20260519-103129/server/server.js +++ /dev/null @@ -1,395 +0,0 @@ -import bcrypt from "bcryptjs" -import cors from "cors" -import dotenv from "dotenv" -import express from "express" -import fs from "fs" -import jwt from "jsonwebtoken" -import multer from "multer" -import path from "path" -import { fileURLToPath } from "url" -import { pool } from "./db.js" - -dotenv.config() - -const app = express() -const port = Number(process.env.PORT || 5000) -const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret" -const maxImageSizeMb = Number(process.env.MAX_IMAGE_SIZE_MB || 10) -const maxImageCount = Number(process.env.MAX_IMAGE_COUNT || 6) -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const uploadsDir = path.join(__dirname, "uploads") - -if (!fs.existsSync(uploadsDir)) { - fs.mkdirSync(uploadsDir, { recursive: true }) -} - -app.use(cors({ - origin: process.env.CLIENT_URL || "http://localhost:5173", - credentials: true -})) -app.use(express.json()) -app.use("/uploads", express.static(uploadsDir)) - -const storage = multer.diskStorage({ - destination: uploadsDir, - filename: (req, file, cb) => { - const ext = path.extname(file.originalname).toLowerCase() - cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`) - } -}) - -const upload = multer({ - storage, - limits: { fileSize: maxImageSizeMb * 1024 * 1024 }, - fileFilter: (req, file, cb) => { - if (!file.mimetype.startsWith("image/")) { - cb(new Error("File harus berupa gambar.")) - return - } - cb(null, true) - } -}) - -async function ensureSchema() { - await pool.query(` - CREATE TABLE IF NOT EXISTS product_images ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - product_id INT UNSIGNED NOT NULL, - image VARCHAR(255) NOT NULL, - sort_order INT UNSIGNED NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_product_images_product - FOREIGN KEY (product_id) REFERENCES products(id) - ON DELETE CASCADE - ) - `) -} - -function signUser(user) { - return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" }) -} - -function publicUser(user) { - return { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - } -} - -function auth(req, res, next) { - const header = req.headers.authorization || "" - const token = header.startsWith("Bearer ") ? header.slice(7) : "" - - if (!token) { - res.status(401).json({ message: "Silakan login terlebih dahulu." }) - return - } - - try { - req.user = jwt.verify(token, jwtSecret) - next() - } catch { - res.status(401).json({ message: "Sesi login tidak valid." }) - } -} - -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) - } -} - -function getUploadedImages(req) { - if (Array.isArray(req.files)) return req.files.slice(0, maxImageCount) - return [ - ...(req.files?.images || []), - ...(req.files?.image || []) - ].slice(0, maxImageCount) -} - -async function getProductImages(productId, fallbackImage) { - const [images] = await pool.query( - "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", - [productId] - ) - - const imagePaths = images.map((item) => item.image) - if (imagePaths.length) return imagePaths - return fallbackImage ? [fallbackImage] : [] -} - -async function attachProductImages(products) { - return Promise.all( - products.map(async (product) => ({ - ...product, - images: await getProductImages(product.id, product.image) - })) - ) -} - -async function findProductById(id) { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE id = ?", - [id] - ) - - const product = rows[0] - if (!product) return null - - product.images = await getProductImages(product.id, product.image) - return product -} - -async function deleteUploadedImages(product) { - const images = product.images?.length ? product.images : [product.image] - - images.forEach((image) => { - if (image?.startsWith("/uploads/")) { - fs.rm(path.join(uploadsDir, path.basename(image)), { force: true }, () => {}) - } - }) -} - -app.get("/api/health", async (req, res, next) => { - try { - await pool.query("SELECT 1") - res.json({ status: "ok" }) - } catch (error) { - next(error) - } -}) - -app.post("/api/auth/register", async (req, res, next) => { - try { - const { name, email, whatsapp, password } = req.body - - if (!name || !email || !whatsapp || !password) { - res.status(400).json({ message: "Semua field wajib diisi." }) - return - } - - if (password.length < 6) { - res.status(400).json({ message: "Password minimal 6 karakter." }) - return - } - - const passwordHash = await bcrypt.hash(password, 10) - const [result] = await pool.query( - "INSERT INTO users (name, email, whatsapp, password_hash) VALUES (?, ?, ?, ?)", - [name, email.toLowerCase(), whatsapp, passwordHash] - ) - - const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" } - res.status(201).json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - if (error.code === "ER_DUP_ENTRY") { - res.status(409).json({ message: "Email sudah terdaftar." }) - return - } - next(error) - } -}) - -app.post("/api/auth/login", async (req, res, next) => { - try { - const { email, password } = req.body - - if (!email || !password) { - res.status(400).json({ message: "Email dan password wajib diisi." }) - return - } - - const [rows] = await pool.query("SELECT * FROM users WHERE email = ?", [email.toLowerCase()]) - const user = rows[0] - - if (!user || !(await bcrypt.compare(password, user.password_hash))) { - res.status(401).json({ message: "Email atau password salah." }) - return - } - - res.json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/auth/me", auth, async (req, res, next) => { - try { - const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id]) - if (!rows[0]) { - res.status(404).json({ message: "User tidak ditemukan." }) - return - } - res.json({ user: rows[0] }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products", async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products ORDER BY created_at DESC, id DESC" - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/mine", auth, async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE user_id = ? ORDER BY created_at DESC, id DESC", - [req.user.id] - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/:id", async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product) { - res.status(404).json({ message: "Produk tidak ditemukan." }) - return - } - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.post( - "/api/products", - auth, - upload.fields([ - { name: "images", maxCount: maxImageCount }, - { name: "image", maxCount: maxImageCount } - ]), - async (req, res, next) => { - try { - const { title, category, price, condition, location, seller, whatsapp, description } = req.body - const uploadedImages = getUploadedImages(req) - - if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !uploadedImages.length) { - res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." }) - return - } - - const mainImagePath = `/uploads/${uploadedImages[0].filename}` - const [result] = await pool.query( - "INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [req.user.id, title, category, Number(price), condition, location, seller, whatsapp, mainImagePath, description] - ) - - const imageRows = uploadedImages.map((file, index) => [result.insertId, `/uploads/${file.filename}`, index]) - await pool.query("INSERT INTO product_images (product_id, image, sort_order) VALUES ?", [imageRows]) - - const product = await findProductById(result.insertId) - res.status(201).json({ product }) - } catch (error) { - next(error) - } - } -) - -app.patch("/api/products/:id/status", auth, async (req, res, next) => { - try { - const status = req.body.status === "Terjual" ? "Terjual" : "Tersedia" - const [result] = await pool.query( - "UPDATE products SET status = ? WHERE id = ? AND user_id = ?", - [status, req.params.id, req.user.id] - ) - - if (!result.affectedRows) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - const product = await findProductById(req.params.id) - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.delete("/api/products/:id", auth, async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product || product.user_id !== req.user.id) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - await pool.query("DELETE FROM products WHERE id = ? AND user_id = ?", [req.params.id, req.user.id]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus." }) - } catch (error) { - next(error) - } -}) - -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]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus oleh superadmin." }) - } catch (error) { - next(error) - } -}) - -app.use((error, req, res, next) => { - if (error instanceof multer.MulterError) { - res.status(400).json({ message: `Upload gagal. Ukuran setiap foto maksimal ${maxImageSizeMb} MB dan maksimal ${maxImageCount} foto.` }) - return - } - - if (error.message === "File harus berupa gambar.") { - res.status(400).json({ message: error.message }) - return - } - - console.error(error) - res.status(500).json({ - message: "Terjadi kesalahan pada server.", - detail: process.env.NODE_ENV === "production" ? undefined : error.message - }) -}) - -ensureSchema() - .then(() => { - app.listen(port, () => { - console.log(`SecondTech API berjalan di http://localhost:${port}`) - console.log(`Upload foto: maksimal ${maxImageSizeMb} MB per foto, maksimal ${maxImageCount} foto.`) - }) - }) - .catch((error) => { - console.error("Gagal menyiapkan database:", error) - process.exit(1) - }) diff --git a/backup-before-force-multiphoto-dark-20260519-103129/src/assets/main.css b/backup-before-force-multiphoto-dark-20260519-103129/src/assets/main.css deleted file mode 100644 index 003ca64..0000000 --- a/backup-before-force-multiphoto-dark-20260519-103129/src/assets/main.css +++ /dev/null @@ -1,200 +0,0 @@ -@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; -} diff --git a/backup-before-force-multiphoto-dark-20260519-103129/src/views/ProductDetailView.vue b/backup-before-force-multiphoto-dark-20260519-103129/src/views/ProductDetailView.vue deleted file mode 100644 index f50eaa2..0000000 --- a/backup-before-force-multiphoto-dark-20260519-103129/src/views/ProductDetailView.vue +++ /dev/null @@ -1,138 +0,0 @@ - - - diff --git a/backup-before-force-multiphoto-dark-20260519-103129/src/views/SellProductView.vue b/backup-before-force-multiphoto-dark-20260519-103129/src/views/SellProductView.vue deleted file mode 100644 index c31b072..0000000 --- a/backup-before-force-multiphoto-dark-20260519-103129/src/views/SellProductView.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - diff --git a/backup-before-gallery-darkmode-repair-20260519-100158/server/server.js b/backup-before-gallery-darkmode-repair-20260519-100158/server/server.js deleted file mode 100644 index 72df172..0000000 --- a/backup-before-gallery-darkmode-repair-20260519-100158/server/server.js +++ /dev/null @@ -1,393 +0,0 @@ -import bcrypt from "bcryptjs" -import cors from "cors" -import dotenv from "dotenv" -import express from "express" -import fs from "fs" -import jwt from "jsonwebtoken" -import multer from "multer" -import path from "path" -import { fileURLToPath } from "url" -import { pool } from "./db.js" - -dotenv.config() - -const app = express() -const port = Number(process.env.PORT || 5000) -const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret" -const maxImageSizeMb = Number(process.env.MAX_IMAGE_SIZE_MB || 10) -const maxImageCount = Number(process.env.MAX_IMAGE_COUNT || 6) -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const uploadsDir = path.join(__dirname, "uploads") - -if (!fs.existsSync(uploadsDir)) { - fs.mkdirSync(uploadsDir, { recursive: true }) -} - -app.use(cors({ - origin: process.env.CLIENT_URL || "http://localhost:5173", - credentials: true -})) -app.use(express.json()) -app.use("/uploads", express.static(uploadsDir)) - -const storage = multer.diskStorage({ - destination: uploadsDir, - filename: (req, file, cb) => { - const ext = path.extname(file.originalname).toLowerCase() - cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`) - } -}) - -const upload = multer({ - storage, - limits: { fileSize: maxImageSizeMb * 1024 * 1024 }, - fileFilter: (req, file, cb) => { - if (!file.mimetype.startsWith("image/")) { - cb(new Error("File harus berupa gambar.")) - return - } - cb(null, true) - } -}) - -async function ensureSchema() { - await pool.query(` - CREATE TABLE IF NOT EXISTS product_images ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - product_id INT UNSIGNED NOT NULL, - image VARCHAR(255) NOT NULL, - sort_order INT UNSIGNED NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_product_images_product - FOREIGN KEY (product_id) REFERENCES products(id) - ON DELETE CASCADE - ) - `) -} - -function signUser(user) { - return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" }) -} - -function publicUser(user) { - return { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - } -} - -function auth(req, res, next) { - const header = req.headers.authorization || "" - const token = header.startsWith("Bearer ") ? header.slice(7) : "" - - if (!token) { - res.status(401).json({ message: "Silakan login terlebih dahulu." }) - return - } - - try { - req.user = jwt.verify(token, jwtSecret) - next() - } catch { - res.status(401).json({ message: "Sesi login tidak valid." }) - } -} - -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) - } -} - -function getUploadedImages(req) { - if (Array.isArray(req.files)) return req.files - return [ - ...(req.files?.images || []), - ...(req.files?.image || []) - ].slice(0, maxImageCount) -} - -async function getProductImages(productId, fallbackImage) { - const [images] = await pool.query( - "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", - [productId] - ) - - return images.length ? images.map((item) => item.image) : [fallbackImage] -} - -async function attachProductImages(products) { - return Promise.all( - products.map(async (product) => ({ - ...product, - images: await getProductImages(product.id, product.image) - })) - ) -} - -async function findProductById(id) { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE id = ?", - [id] - ) - - const product = rows[0] - if (!product) return null - - product.images = await getProductImages(product.id, product.image) - return product -} - -async function deleteUploadedImages(product) { - const images = product.images?.length ? product.images : [product.image] - - images.forEach((image) => { - if (image?.startsWith("/uploads/")) { - fs.rm(path.join(uploadsDir, path.basename(image)), { force: true }, () => {}) - } - }) -} - -app.get("/api/health", async (req, res, next) => { - try { - await pool.query("SELECT 1") - res.json({ status: "ok" }) - } catch (error) { - next(error) - } -}) - -app.post("/api/auth/register", async (req, res, next) => { - try { - const { name, email, whatsapp, password } = req.body - - if (!name || !email || !whatsapp || !password) { - res.status(400).json({ message: "Semua field wajib diisi." }) - return - } - - if (password.length < 6) { - res.status(400).json({ message: "Password minimal 6 karakter." }) - return - } - - const passwordHash = await bcrypt.hash(password, 10) - const [result] = await pool.query( - "INSERT INTO users (name, email, whatsapp, password_hash) VALUES (?, ?, ?, ?)", - [name, email.toLowerCase(), whatsapp, passwordHash] - ) - - const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" } - res.status(201).json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - if (error.code === "ER_DUP_ENTRY") { - res.status(409).json({ message: "Email sudah terdaftar." }) - return - } - next(error) - } -}) - -app.post("/api/auth/login", async (req, res, next) => { - try { - const { email, password } = req.body - - if (!email || !password) { - res.status(400).json({ message: "Email dan password wajib diisi." }) - return - } - - const [rows] = await pool.query("SELECT * FROM users WHERE email = ?", [email.toLowerCase()]) - const user = rows[0] - - if (!user || !(await bcrypt.compare(password, user.password_hash))) { - res.status(401).json({ message: "Email atau password salah." }) - return - } - - res.json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/auth/me", auth, async (req, res, next) => { - try { - const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id]) - if (!rows[0]) { - res.status(404).json({ message: "User tidak ditemukan." }) - return - } - res.json({ user: rows[0] }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products", async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products ORDER BY created_at DESC, id DESC" - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/mine", auth, async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE user_id = ? ORDER BY created_at DESC, id DESC", - [req.user.id] - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/:id", async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product) { - res.status(404).json({ message: "Produk tidak ditemukan." }) - return - } - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.post( - "/api/products", - auth, - upload.fields([ - { name: "images", maxCount: maxImageCount }, - { name: "image", maxCount: maxImageCount } - ]), - async (req, res, next) => { - try { - const { title, category, price, condition, location, seller, whatsapp, description } = req.body - const uploadedImages = getUploadedImages(req) - - if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !uploadedImages.length) { - res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." }) - return - } - - const mainImagePath = `/uploads/${uploadedImages[0].filename}` - const [result] = await pool.query( - "INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [req.user.id, title, category, Number(price), condition, location, seller, whatsapp, mainImagePath, description] - ) - - const imageRows = uploadedImages.map((file, index) => [result.insertId, `/uploads/${file.filename}`, index]) - await pool.query("INSERT INTO product_images (product_id, image, sort_order) VALUES ?", [imageRows]) - - const product = await findProductById(result.insertId) - res.status(201).json({ product }) - } catch (error) { - next(error) - } - } -) - -app.patch("/api/products/:id/status", auth, async (req, res, next) => { - try { - const status = req.body.status === "Terjual" ? "Terjual" : "Tersedia" - const [result] = await pool.query( - "UPDATE products SET status = ? WHERE id = ? AND user_id = ?", - [status, req.params.id, req.user.id] - ) - - if (!result.affectedRows) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - const product = await findProductById(req.params.id) - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.delete("/api/products/:id", auth, async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product || product.user_id !== req.user.id) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - await pool.query("DELETE FROM products WHERE id = ? AND user_id = ?", [req.params.id, req.user.id]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus." }) - } catch (error) { - next(error) - } -}) - -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]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus oleh superadmin." }) - } catch (error) { - next(error) - } -}) - -app.use((error, req, res, next) => { - if (error instanceof multer.MulterError) { - res.status(400).json({ message: `Upload gagal. Ukuran setiap foto maksimal ${maxImageSizeMb} MB dan maksimal ${maxImageCount} foto.` }) - return - } - - if (error.message === "File harus berupa gambar.") { - res.status(400).json({ message: error.message }) - return - } - - console.error(error) - res.status(500).json({ - message: "Terjadi kesalahan pada server.", - detail: process.env.NODE_ENV === "production" ? undefined : error.message - }) -}) - -ensureSchema() - .then(() => { - app.listen(port, () => { - console.log(`SecondTech API berjalan di http://localhost:${port}`) - console.log(`Upload foto: maksimal ${maxImageSizeMb} MB per foto, maksimal ${maxImageCount} foto.`) - }) - }) - .catch((error) => { - console.error("Gagal menyiapkan database:", error) - process.exit(1) - }) diff --git a/backup-before-gallery-darkmode-repair-20260519-100158/src/assets/main.css b/backup-before-gallery-darkmode-repair-20260519-100158/src/assets/main.css deleted file mode 100644 index 00deec0..0000000 --- a/backup-before-gallery-darkmode-repair-20260519-100158/src/assets/main.css +++ /dev/null @@ -1,106 +0,0 @@ -@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); -} diff --git a/backup-before-gallery-darkmode-repair-20260519-100158/src/components/ProductCard.vue b/backup-before-gallery-darkmode-repair-20260519-100158/src/components/ProductCard.vue deleted file mode 100644 index b70be73..0000000 --- a/backup-before-gallery-darkmode-repair-20260519-100158/src/components/ProductCard.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/backup-before-gallery-darkmode-repair-20260519-100158/src/utils.js b/backup-before-gallery-darkmode-repair-20260519-100158/src/utils.js deleted file mode 100644 index 725ba6a..0000000 --- a/backup-before-gallery-darkmode-repair-20260519-100158/src/utils.js +++ /dev/null @@ -1,137 +0,0 @@ -export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:5000" - -export function formatRupiah(value) { - return new Intl.NumberFormat("id-ID", { - style: "currency", - currency: "IDR", - maximumFractionDigits: 0 - }).format(value) -} - -export function getLocal(key, fallback) { - try { - const value = localStorage.getItem(key) - return value ? JSON.parse(value) : fallback - } catch { - return fallback - } -} - -export function setLocal(key, value) { - localStorage.setItem(key, JSON.stringify(value)) -} - -export function getToken() { - return localStorage.getItem("secondtech_token") -} - -export function getCurrentUser() { - return getLocal("secondtech_user", null) -} - -export function setAuth({ token, user }) { - localStorage.setItem("secondtech_token", token) - - setLocal("secondtech_user", { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - }) -} - -export function clearAuth() { - localStorage.removeItem("secondtech_token") - localStorage.removeItem("secondtech_user") -} - -export function getProductImageUrl(image) { - if (!image) return "" - if (image.startsWith("http://") || image.startsWith("https://") || image.startsWith("data:")) return image - return `${API_BASE_URL}${image}` -} - -async function requestJson(path, options = {}) { - const headers = new Headers(options.headers || {}) - const token = getToken() - - if (token) headers.set("Authorization", `Bearer ${token}`) - if (!(options.body instanceof FormData)) headers.set("Content-Type", "application/json") - - const response = await fetch(`${API_BASE_URL}${path}`, { - ...options, - headers - }) - const data = await response.json().catch(() => ({})) - - if (!response.ok) { - throw new Error(data.message || "Request gagal.") - } - - return data -} - -export function registerUser(payload) { - return requestJson("/api/auth/register", { - method: "POST", - body: JSON.stringify(payload) - }) -} - -export function loginUser(payload) { - return requestJson("/api/auth/login", { - method: "POST", - body: JSON.stringify(payload) - }) -} - -export function fetchCurrentUser() { - return requestJson("/api/auth/me") -} - -export function fetchProducts() { - return requestJson("/api/products") -} - -export function fetchProduct(id) { - return requestJson(`/api/products/${id}`) -} - -export function fetchMyProducts() { - return requestJson("/api/products/mine") -} - -export function createProduct(formData) { - return requestJson("/api/products", { - method: "POST", - body: formData - }) -} - -export function updateProductStatus(id, status) { - return requestJson(`/api/products/${id}/status`, { - method: "PATCH", - body: JSON.stringify({ status }) - }) -} - -export function deleteProductById(id) { - return requestJson(`/api/products/${id}`, { - method: "DELETE" - }) -} - -export function deleteProductAsAdmin(id) { - return requestJson(`/api/admin/products/${id}`, { - method: "DELETE" - }) -} - -export function getSavedProducts() { - return getLocal("secondtech_saved", []) -} - -export function setSavedProducts(ids) { - setLocal("secondtech_saved", ids) -} diff --git a/backup-before-gallery-darkmode-repair-20260519-100158/src/views/ProductDetailView.vue b/backup-before-gallery-darkmode-repair-20260519-100158/src/views/ProductDetailView.vue deleted file mode 100644 index bf34d2c..0000000 --- a/backup-before-gallery-darkmode-repair-20260519-100158/src/views/ProductDetailView.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/backup-before-gallery-darkmode-repair-20260519-100158/src/views/SellProductView.vue b/backup-before-gallery-darkmode-repair-20260519-100158/src/views/SellProductView.vue deleted file mode 100644 index c31b072..0000000 --- a/backup-before-gallery-darkmode-repair-20260519-100158/src/views/SellProductView.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - diff --git a/backup-before-upload-limit-20260519-093708/SellProductView.vue b/backup-before-upload-limit-20260519-093708/SellProductView.vue deleted file mode 100644 index d927e52..0000000 --- a/backup-before-upload-limit-20260519-093708/SellProductView.vue +++ /dev/null @@ -1,158 +0,0 @@ - - - diff --git a/backup-before-upload-limit-20260519-093708/server.js b/backup-before-upload-limit-20260519-093708/server.js deleted file mode 100644 index eae1282..0000000 --- a/backup-before-upload-limit-20260519-093708/server.js +++ /dev/null @@ -1,348 +0,0 @@ -import bcrypt from "bcryptjs" -import cors from "cors" -import dotenv from "dotenv" -import express from "express" -import fs from "fs" -import jwt from "jsonwebtoken" -import multer from "multer" -import path from "path" -import { fileURLToPath } from "url" -import { pool } from "./db.js" - -dotenv.config() - -const app = express() -const port = Number(process.env.PORT || 5000) -const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret" -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const uploadsDir = path.join(__dirname, "uploads") - -if (!fs.existsSync(uploadsDir)) { - fs.mkdirSync(uploadsDir, { recursive: true }) -} - -app.use(cors({ - origin: process.env.CLIENT_URL || "http://localhost:5173", - credentials: true -})) -app.use(express.json()) -app.use("/uploads", express.static(uploadsDir)) - -const storage = multer.diskStorage({ - destination: uploadsDir, - filename: (req, file, cb) => { - const ext = path.extname(file.originalname).toLowerCase() - cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`) - } -}) - -const upload = multer({ - storage, - limits: { fileSize: 3 * 1024 * 1024 }, - fileFilter: (req, file, cb) => { - if (!file.mimetype.startsWith("image/")) { - cb(new Error("File harus berupa gambar.")) - return - } - cb(null, true) - } -}) - -function signUser(user) { - return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" }) -} - -function publicUser(user) { - return { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - } -} - -function auth(req, res, next) { - const header = req.headers.authorization || "" - const token = header.startsWith("Bearer ") ? header.slice(7) : "" - - if (!token) { - res.status(401).json({ message: "Silakan login terlebih dahulu." }) - return - } - - try { - req.user = jwt.verify(token, jwtSecret) - next() - } catch { - res.status(401).json({ message: "Sesi login tidak valid." }) - } -} - -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) - } -} - -async function getProductImages(productId, fallbackImage) { - const [images] = await pool.query( - "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", - [productId] - ) - - return images.length ? images.map((item) => item.image) : [fallbackImage] -} - -async function attachProductImages(products) { - return Promise.all( - products.map(async (product) => ({ - ...product, - images: await getProductImages(product.id, product.image) - })) - ) -} - -async function findProductById(id) { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE id = ?", - [id] - ) - - const product = rows[0] - if (!product) return null - - product.images = await getProductImages(product.id, product.image) - return product -} - -async function deleteUploadedImages(product) { - const images = product.images?.length ? product.images : [product.image] - - images.forEach((image) => { - if (image?.startsWith("/uploads/")) { - fs.rm(path.join(uploadsDir, path.basename(image)), { force: true }, () => {}) - } - }) -} - -app.get("/api/health", async (req, res, next) => { - try { - await pool.query("SELECT 1") - res.json({ status: "ok" }) - } catch (error) { - next(error) - } -}) - -app.post("/api/auth/register", async (req, res, next) => { - try { - const { name, email, whatsapp, password } = req.body - - if (!name || !email || !whatsapp || !password) { - res.status(400).json({ message: "Semua field wajib diisi." }) - return - } - - if (password.length < 6) { - res.status(400).json({ message: "Password minimal 6 karakter." }) - return - } - - const passwordHash = await bcrypt.hash(password, 10) - const [result] = await pool.query( - "INSERT INTO users (name, email, whatsapp, password_hash) VALUES (?, ?, ?, ?)", - [name, email.toLowerCase(), whatsapp, passwordHash] - ) - - const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" } - res.status(201).json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - if (error.code === "ER_DUP_ENTRY") { - res.status(409).json({ message: "Email sudah terdaftar." }) - return - } - next(error) - } -}) - -app.post("/api/auth/login", async (req, res, next) => { - try { - const { email, password } = req.body - - if (!email || !password) { - res.status(400).json({ message: "Email dan password wajib diisi." }) - return - } - - const [rows] = await pool.query("SELECT * FROM users WHERE email = ?", [email.toLowerCase()]) - const user = rows[0] - - if (!user || !(await bcrypt.compare(password, user.password_hash))) { - res.status(401).json({ message: "Email atau password salah." }) - return - } - - res.json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/auth/me", auth, async (req, res, next) => { - try { - const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id]) - if (!rows[0]) { - res.status(404).json({ message: "User tidak ditemukan." }) - return - } - res.json({ user: rows[0] }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products", async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products ORDER BY created_at DESC, id DESC" - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/mine", auth, async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE user_id = ? ORDER BY created_at DESC, id DESC", - [req.user.id] - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/:id", async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product) { - res.status(404).json({ message: "Produk tidak ditemukan." }) - return - } - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.post("/api/products", auth, upload.array("images", 6), async (req, res, next) => { - try { - const { title, category, price, condition, location, seller, whatsapp, description } = req.body - - if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !req.files?.length) { - res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." }) - return - } - - const mainImagePath = `/uploads/${req.files[0].filename}` - const [result] = await pool.query( - "INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [req.user.id, title, category, Number(price), condition, location, seller, whatsapp, mainImagePath, description] - ) - - const imageRows = req.files.map((file, index) => [result.insertId, `/uploads/${file.filename}`, index]) - await pool.query("INSERT INTO product_images (product_id, image, sort_order) VALUES ?", [imageRows]) - - const product = await findProductById(result.insertId) - res.status(201).json({ product }) - } catch (error) { - next(error) - } -}) - -app.patch("/api/products/:id/status", auth, async (req, res, next) => { - try { - const status = req.body.status === "Terjual" ? "Terjual" : "Tersedia" - const [result] = await pool.query( - "UPDATE products SET status = ? WHERE id = ? AND user_id = ?", - [status, req.params.id, req.user.id] - ) - - if (!result.affectedRows) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - const product = await findProductById(req.params.id) - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.delete("/api/products/:id", auth, async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product || product.user_id !== req.user.id) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - await pool.query("DELETE FROM products WHERE id = ? AND user_id = ?", [req.params.id, req.user.id]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus." }) - } catch (error) { - next(error) - } -}) - -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]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus oleh superadmin." }) - } catch (error) { - next(error) - } -}) - -app.use((error, req, res, next) => { - if (error instanceof multer.MulterError) { - res.status(400).json({ message: "Upload gagal. Ukuran setiap foto maksimal 3MB dan maksimal 6 foto." }) - return - } - - if (error.message === "File harus berupa gambar.") { - res.status(400).json({ message: error.message }) - return - } - - console.error(error) - res.status(500).json({ message: "Terjadi kesalahan pada server." }) -}) - -app.listen(port, () => { - console.log(`SecondTech API berjalan di http://localhost:${port}`) -}) diff --git a/backup-before-upload-repair-20260519-095013/server/server.js b/backup-before-upload-repair-20260519-095013/server/server.js deleted file mode 100644 index 0abda9f..0000000 --- a/backup-before-upload-repair-20260519-095013/server/server.js +++ /dev/null @@ -1,349 +0,0 @@ -import bcrypt from "bcryptjs" -import cors from "cors" -import dotenv from "dotenv" -import express from "express" -import fs from "fs" -import jwt from "jsonwebtoken" -import multer from "multer" -import path from "path" -import { fileURLToPath } from "url" -import { pool } from "./db.js" - -dotenv.config() - -const app = express() -const port = Number(process.env.PORT || 5000) -const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret" -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const uploadsDir = path.join(__dirname, "uploads") - -if (!fs.existsSync(uploadsDir)) { - fs.mkdirSync(uploadsDir, { recursive: true }) -} - -app.use(cors({ - origin: process.env.CLIENT_URL || "http://localhost:5173", - credentials: true -})) -app.use(express.json()) -app.use("/uploads", express.static(uploadsDir)) - -const storage = multer.diskStorage({ - destination: uploadsDir, - filename: (req, file, cb) => { - const ext = path.extname(file.originalname).toLowerCase() - cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`) - } -}) - -const upload = multer({ - storage, - limits: { fileSize: 10 * 1024 * 1024 }, - fileFilter: (req, file, cb) => { - if (!file.mimetype.startsWith("image/")) { - cb(new Error("File harus berupa gambar.")) - return - } - cb(null, true) - } -}) - -function signUser(user) { - return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" }) -} - -function publicUser(user) { - return { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - } -} - -function auth(req, res, next) { - const header = req.headers.authorization || "" - const token = header.startsWith("Bearer ") ? header.slice(7) : "" - - if (!token) { - res.status(401).json({ message: "Silakan login terlebih dahulu." }) - return - } - - try { - req.user = jwt.verify(token, jwtSecret) - next() - } catch { - res.status(401).json({ message: "Sesi login tidak valid." }) - } -} - -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) - } -} - -async function getProductImages(productId, fallbackImage) { - const [images] = await pool.query( - "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", - [productId] - ) - - return images.length ? images.map((item) => item.image) : [fallbackImage] -} - -async function attachProductImages(products) { - return Promise.all( - products.map(async (product) => ({ - ...product, - images: await getProductImages(product.id, product.image) - })) - ) -} - -async function findProductById(id) { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE id = ?", - [id] - ) - - const product = rows[0] - if (!product) return null - - product.images = await getProductImages(product.id, product.image) - return product -} - -async function deleteUploadedImages(product) { - const images = product.images?.length ? product.images : [product.image] - - images.forEach((image) => { - if (image?.startsWith("/uploads/")) { - fs.rm(path.join(uploadsDir, path.basename(image)), { force: true }, () => {}) - } - }) -} - -app.get("/api/health", async (req, res, next) => { - try { - await pool.query("SELECT 1") - res.json({ status: "ok" }) - } catch (error) { - next(error) - } -}) - -app.post("/api/auth/register", async (req, res, next) => { - try { - const { name, email, whatsapp, password } = req.body - - if (!name || !email || !whatsapp || !password) { - res.status(400).json({ message: "Semua field wajib diisi." }) - return - } - - if (password.length < 6) { - res.status(400).json({ message: "Password minimal 6 karakter." }) - return - } - - const passwordHash = await bcrypt.hash(password, 10) - const [result] = await pool.query( - "INSERT INTO users (name, email, whatsapp, password_hash) VALUES (?, ?, ?, ?)", - [name, email.toLowerCase(), whatsapp, passwordHash] - ) - - const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" } - res.status(201).json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - if (error.code === "ER_DUP_ENTRY") { - res.status(409).json({ message: "Email sudah terdaftar." }) - return - } - next(error) - } -}) - -app.post("/api/auth/login", async (req, res, next) => { - try { - const { email, password } = req.body - - if (!email || !password) { - res.status(400).json({ message: "Email dan password wajib diisi." }) - return - } - - const [rows] = await pool.query("SELECT * FROM users WHERE email = ?", [email.toLowerCase()]) - const user = rows[0] - - if (!user || !(await bcrypt.compare(password, user.password_hash))) { - res.status(401).json({ message: "Email atau password salah." }) - return - } - - res.json({ token: signUser(user), user: publicUser(user) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/auth/me", auth, async (req, res, next) => { - try { - const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id]) - if (!rows[0]) { - res.status(404).json({ message: "User tidak ditemukan." }) - return - } - res.json({ user: rows[0] }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products", async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products ORDER BY created_at DESC, id DESC" - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/mine", auth, async (req, res, next) => { - try { - const [rows] = await pool.query( - "SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE user_id = ? ORDER BY created_at DESC, id DESC", - [req.user.id] - ) - res.json({ products: await attachProductImages(rows) }) - } catch (error) { - next(error) - } -}) - -app.get("/api/products/:id", async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product) { - res.status(404).json({ message: "Produk tidak ditemukan." }) - return - } - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.post("/api/products", auth, upload.array("images", 6), async (req, res, next) => { - try { - const { title, category, price, condition, location, seller, whatsapp, description } = req.body - - if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !req.files?.length) { - res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." }) - return - } - - const mainImagePath = `/uploads/${req.files[0].filename}` - const [result] = await pool.query( - "INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [req.user.id, title, category, Number(price), condition, location, seller, whatsapp, mainImagePath, description] - ) - - const imageRows = req.files.map((file, index) => [result.insertId, `/uploads/${file.filename}`, index]) - await pool.query("INSERT INTO product_images (product_id, image, sort_order) VALUES ?", [imageRows]) - - const product = await findProductById(result.insertId) - res.status(201).json({ product }) - } catch (error) { - next(error) - } -}) - -app.patch("/api/products/:id/status", auth, async (req, res, next) => { - try { - const status = req.body.status === "Terjual" ? "Terjual" : "Tersedia" - const [result] = await pool.query( - "UPDATE products SET status = ? WHERE id = ? AND user_id = ?", - [status, req.params.id, req.user.id] - ) - - if (!result.affectedRows) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - const product = await findProductById(req.params.id) - res.json({ product }) - } catch (error) { - next(error) - } -}) - -app.delete("/api/products/:id", auth, async (req, res, next) => { - try { - const product = await findProductById(req.params.id) - if (!product || product.user_id !== req.user.id) { - res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) - return - } - - await pool.query("DELETE FROM products WHERE id = ? AND user_id = ?", [req.params.id, req.user.id]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus." }) - } catch (error) { - next(error) - } -}) - -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]) - await deleteUploadedImages(product) - - res.json({ message: "Produk berhasil dihapus oleh superadmin." }) - } catch (error) { - next(error) - } -}) - -app.use((error, req, res, next) => { - if (error instanceof multer.MulterError) { - res.status(400).json({ message: "Upload gagal. Ukuran setiap foto maksimal 10 MB dan maksimal 6 foto." }) - return - } - - if (error.message === "File harus berupa gambar.") { - res.status(400).json({ message: error.message }) - return - } - - console.error(error) - res.status(500).json({ message: "Terjadi kesalahan pada server." }) -}) - -app.listen(port, () => { - console.log(`SecondTech API berjalan di http://localhost:${port}`) -}) - diff --git a/backup-before-upload-repair-20260519-095013/src/utils.js b/backup-before-upload-repair-20260519-095013/src/utils.js deleted file mode 100644 index 725ba6a..0000000 --- a/backup-before-upload-repair-20260519-095013/src/utils.js +++ /dev/null @@ -1,137 +0,0 @@ -export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:5000" - -export function formatRupiah(value) { - return new Intl.NumberFormat("id-ID", { - style: "currency", - currency: "IDR", - maximumFractionDigits: 0 - }).format(value) -} - -export function getLocal(key, fallback) { - try { - const value = localStorage.getItem(key) - return value ? JSON.parse(value) : fallback - } catch { - return fallback - } -} - -export function setLocal(key, value) { - localStorage.setItem(key, JSON.stringify(value)) -} - -export function getToken() { - return localStorage.getItem("secondtech_token") -} - -export function getCurrentUser() { - return getLocal("secondtech_user", null) -} - -export function setAuth({ token, user }) { - localStorage.setItem("secondtech_token", token) - - setLocal("secondtech_user", { - id: user.id, - name: user.name, - email: user.email, - whatsapp: user.whatsapp, - role: user.role || "user" - }) -} - -export function clearAuth() { - localStorage.removeItem("secondtech_token") - localStorage.removeItem("secondtech_user") -} - -export function getProductImageUrl(image) { - if (!image) return "" - if (image.startsWith("http://") || image.startsWith("https://") || image.startsWith("data:")) return image - return `${API_BASE_URL}${image}` -} - -async function requestJson(path, options = {}) { - const headers = new Headers(options.headers || {}) - const token = getToken() - - if (token) headers.set("Authorization", `Bearer ${token}`) - if (!(options.body instanceof FormData)) headers.set("Content-Type", "application/json") - - const response = await fetch(`${API_BASE_URL}${path}`, { - ...options, - headers - }) - const data = await response.json().catch(() => ({})) - - if (!response.ok) { - throw new Error(data.message || "Request gagal.") - } - - return data -} - -export function registerUser(payload) { - return requestJson("/api/auth/register", { - method: "POST", - body: JSON.stringify(payload) - }) -} - -export function loginUser(payload) { - return requestJson("/api/auth/login", { - method: "POST", - body: JSON.stringify(payload) - }) -} - -export function fetchCurrentUser() { - return requestJson("/api/auth/me") -} - -export function fetchProducts() { - return requestJson("/api/products") -} - -export function fetchProduct(id) { - return requestJson(`/api/products/${id}`) -} - -export function fetchMyProducts() { - return requestJson("/api/products/mine") -} - -export function createProduct(formData) { - return requestJson("/api/products", { - method: "POST", - body: formData - }) -} - -export function updateProductStatus(id, status) { - return requestJson(`/api/products/${id}/status`, { - method: "PATCH", - body: JSON.stringify({ status }) - }) -} - -export function deleteProductById(id) { - return requestJson(`/api/products/${id}`, { - method: "DELETE" - }) -} - -export function deleteProductAsAdmin(id) { - return requestJson(`/api/admin/products/${id}`, { - method: "DELETE" - }) -} - -export function getSavedProducts() { - return getLocal("secondtech_saved", []) -} - -export function setSavedProducts(ids) { - setLocal("secondtech_saved", ids) -} diff --git a/backup-before-upload-repair-20260519-095013/src/views/SellProductView.vue b/backup-before-upload-repair-20260519-095013/src/views/SellProductView.vue deleted file mode 100644 index c31b072..0000000 --- a/backup-before-upload-repair-20260519-095013/src/views/SellProductView.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - diff --git a/public/Hero.jpeg b/public/Hero.jpeg new file mode 100644 index 0000000..2080586 Binary files /dev/null and b/public/Hero.jpeg differ diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue index 6a9df3c..6fdf52d 100644 --- a/src/components/Navbar.vue +++ b/src/components/Navbar.vue @@ -2,9 +2,7 @@
-
- -
+ SecondTech logo SecondTech
@@ -52,7 +50,7 @@
@@ -74,60 +72,60 @@