From 7a13a2c0a9ce3a7774ef818f92d7b7975731070d Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sun, 6 Jul 2025 16:59:32 +0700 Subject: [PATCH] feat: try to finding a way idea UI and add some behaviour --- app/components/layoutpengelola/sidebar.tsx | 8 +- app/routes/authpengelola._index.tsx | 0 ...gelola.completingcompanyprofile._index.tsx | 0 .../authpengelola.createanewpin._index.tsx | 0 ...uthpengelola.requestotpforlogin._index.tsx | 0 ...pengelola.requestotpforregister._index.tsx | 0 app/routes/authpengelola.tsx | 0 ...authpengelola.verifyexistingpin._index.tsx | 0 .../authpengelola.verifyotptologin._index.tsx | 0 ...thpengelola.verifyotptoregister._index.tsx | 0 ...aitingapprovalfromadministrator._index.tsx | 0 .../pengelola.dashboard.explorewaste.tsx | 933 ++++++++++++++++++ 12 files changed, 940 insertions(+), 1 deletion(-) create mode 100644 app/routes/authpengelola._index.tsx create mode 100644 app/routes/authpengelola.completingcompanyprofile._index.tsx create mode 100644 app/routes/authpengelola.createanewpin._index.tsx create mode 100644 app/routes/authpengelola.requestotpforlogin._index.tsx create mode 100644 app/routes/authpengelola.requestotpforregister._index.tsx create mode 100644 app/routes/authpengelola.tsx create mode 100644 app/routes/authpengelola.verifyexistingpin._index.tsx create mode 100644 app/routes/authpengelola.verifyotptologin._index.tsx create mode 100644 app/routes/authpengelola.verifyotptoregister._index.tsx create mode 100644 app/routes/authpengelola.waitingapprovalfromadministrator._index.tsx create mode 100644 app/routes/pengelola.dashboard.explorewaste.tsx diff --git a/app/components/layoutpengelola/sidebar.tsx b/app/components/layoutpengelola/sidebar.tsx index 7c4a0d9..0a3172c 100644 --- a/app/components/layoutpengelola/sidebar.tsx +++ b/app/components/layoutpengelola/sidebar.tsx @@ -44,7 +44,8 @@ import { AlertCircle, CheckCircle, Route, - Phone + Phone, + Search } from "lucide-react"; import { cn } from "~/lib/utils"; @@ -104,6 +105,11 @@ const operationalMenuItems: MenuItem[] = [ } ] }, + { + title: "Explore", + icon: , + href: "/pengelola/dashboard/explorewaste" + }, { title: "Manajemen Pengepul", icon: , diff --git a/app/routes/authpengelola._index.tsx b/app/routes/authpengelola._index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/authpengelola.completingcompanyprofile._index.tsx b/app/routes/authpengelola.completingcompanyprofile._index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/authpengelola.createanewpin._index.tsx b/app/routes/authpengelola.createanewpin._index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/authpengelola.requestotpforlogin._index.tsx b/app/routes/authpengelola.requestotpforlogin._index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/authpengelola.requestotpforregister._index.tsx b/app/routes/authpengelola.requestotpforregister._index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/authpengelola.tsx b/app/routes/authpengelola.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/authpengelola.verifyexistingpin._index.tsx b/app/routes/authpengelola.verifyexistingpin._index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/authpengelola.verifyotptologin._index.tsx b/app/routes/authpengelola.verifyotptologin._index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/authpengelola.verifyotptoregister._index.tsx b/app/routes/authpengelola.verifyotptoregister._index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/authpengelola.waitingapprovalfromadministrator._index.tsx b/app/routes/authpengelola.waitingapprovalfromadministrator._index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/pengelola.dashboard.explorewaste.tsx b/app/routes/pengelola.dashboard.explorewaste.tsx new file mode 100644 index 0000000..eff0f95 --- /dev/null +++ b/app/routes/pengelola.dashboard.explorewaste.tsx @@ -0,0 +1,933 @@ +import { json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Badge } from "~/components/ui/badge"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "~/components/ui/select"; +import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; +import { Separator } from "~/components/ui/separator"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger +} from "~/components/ui/dialog"; +import { + Search, + MapPin, + Phone, + MessageSquare, + Star, + Truck, + Package, + Clock, + Filter, + Grid, + List, + Eye, + Users, + Calendar, + Weight, + Recycle, + Leaf, + Zap, + ChevronRight, + ImageOff +} from "lucide-react"; + +// Component untuk Image dengan fallback +const WasteImage = ({ + src, + alt, + className = "", + fallbackType = "waste" +}: { + src: string; + alt: string; + className?: string; + fallbackType?: "waste" | "detail"; +}) => { + const [imageError, setImageError] = useState(false); + const [imageLoading, setImageLoading] = useState(true); + + const handleImageError = () => { + setImageError(true); + setImageLoading(false); + }; + + const handleImageLoad = () => { + setImageLoading(false); + }; + + // Fallback component + const ImageFallback = () => ( +
+
+ +

No Image

+
+
+ ); + + // Loading skeleton + const ImageSkeleton = () => ( +
+ +
+ ); + + if (imageError) { + return ; + } + + return ( +
+ {imageLoading && } + {alt} +
+ ); +}; + +// Interfaces +interface WasteItem { + id: string; + title: string; + category: + | "organic" + | "plastic" + | "paper" + | "metal" + | "glass" + | "electronic" + | "mixed"; + description: string; + quantity: number; // in kg + unit: "kg" | "ton" | "pieces"; + pricePerUnit: number; // per kg + condition: "fresh" | "sorted" | "processed" | "mixed"; + availableUntil: string; + images: string[]; + supplier: { + id: string; + name: string; + type: "individual" | "company" | "cooperative"; + location: string; + district: string; + rating: number; + reviewCount: number; + avatar?: string; + phone: string; + verified: boolean; + responseTime: string; // "< 1 jam", "2-4 jam", etc + }; + pickupAvailable: boolean; + minimumOrder: number; // kg + tags: string[]; + postedAt: string; + viewCount: number; +} + +interface ExploreData { + items: WasteItem[]; + categories: Array<{ + id: string; + name: string; + count: number; + icon: string; + }>; + locations: string[]; + stats: { + totalSuppliers: number; + totalVolume: number; // total kg available + averagePrice: number; + activeListings: number; + }; +} + +export const loader = async (): Promise => { + // Mock data - dalam implementasi nyata, ambil dari database + const exploreData: ExploreData = { + stats: { + totalSuppliers: 45, + totalVolume: 15420, // kg + averagePrice: 850, // per kg + activeListings: 128 + }, + categories: [ + { id: "plastic", name: "Plastik", count: 32, icon: "♻️" }, + { id: "paper", name: "Kertas", count: 28, icon: "📄" }, + { id: "organic", name: "Organik", count: 24, icon: "🌱" }, + { id: "metal", name: "Logam", count: 18, icon: "🔩" }, + { id: "glass", name: "Kaca", count: 12, icon: "🍶" }, + { id: "electronic", name: "Elektronik", count: 8, icon: "📱" }, + { id: "mixed", name: "Campuran", count: 6, icon: "📦" } + ], + locations: [ + "Jakarta Utara", + "Jakarta Selatan", + "Jakarta Timur", + "Jakarta Barat", + "Jakarta Pusat", + "Depok", + "Tangerang", + "Bekasi" + ], + items: [ + { + id: "item-001", + title: "Plastik Botol PET Bersih", + category: "plastic", + description: + "Botol plastik PET bekas air mineral yang sudah dicuci bersih dan dipisahkan berdasarkan warna. Kondisi sangat baik untuk daur ulang.", + quantity: 250, + unit: "kg", + pricePerUnit: 2800, + condition: "sorted", + availableUntil: "2025-07-15", + images: ["https://picsum.photos/300/200?random=1"], + supplier: { + id: "sup-001", + name: "Koperasi Sukamaju", + type: "cooperative", + location: "Kelurahan Merdeka", + district: "Jakarta Utara", + rating: 4.8, + reviewCount: 23, + phone: "081234567890", + verified: true, + responseTime: "< 1 jam" + }, + pickupAvailable: true, + minimumOrder: 50, + tags: ["Bersih", "Tersortir", "Siap Proses"], + postedAt: "2025-07-05", + viewCount: 45 + }, + { + id: "item-002", + title: "Kertas Kardus Bekas Kemasan", + category: "paper", + description: + "Kardus bekas kemasan elektronik dan makanan. Kondisi kering dan tidak basah. Sudah diratakan dan dibundle rapi.", + quantity: 180, + unit: "kg", + pricePerUnit: 1200, + condition: "sorted", + availableUntil: "2025-07-12", + images: ["https://picsum.photos/300/200?random=2"], + supplier: { + id: "sup-002", + name: "Ahmad Wijaya", + type: "individual", + location: "Komplek Permata", + district: "Jakarta Selatan", + rating: 4.5, + reviewCount: 12, + phone: "081234567891", + verified: true, + responseTime: "2-4 jam" + }, + pickupAvailable: true, + minimumOrder: 30, + tags: ["Kering", "Bundle", "Kemasan"], + postedAt: "2025-07-04", + viewCount: 28 + }, + { + id: "item-003", + title: "Sampah Organik Pasar Segar", + category: "organic", + description: + "Limbah organik dari pasar tradisional meliputi sisa sayuran, buah-buahan, dan daun. Cocok untuk kompos atau biogas.", + quantity: 500, + unit: "kg", + pricePerUnit: 400, + condition: "fresh", + availableUntil: "2025-07-07", + images: ["https://picsum.photos/300/200?random=3"], + supplier: { + id: "sup-003", + name: "Pasar Tradisional Sentral", + type: "company", + location: "Pasar Sentral", + district: "Jakarta Tengah", + rating: 4.9, + reviewCount: 67, + phone: "081234567892", + verified: true, + responseTime: "< 30 menit" + }, + pickupAvailable: true, + minimumOrder: 100, + tags: ["Segar", "Organik", "Kompos"], + postedAt: "2025-07-06", + viewCount: 89 + }, + { + id: "item-004", + title: "Kaleng Aluminium Bekas Minuman", + category: "metal", + description: + "Kaleng aluminium bekas minuman ringan dan bir. Sudah dibersihkan dan dipress untuk efisiensi transport.", + quantity: 85, + unit: "kg", + pricePerUnit: 8500, + condition: "processed", + availableUntil: "2025-07-20", + images: ["https://picsum.photos/300/200?random=4"], + supplier: { + id: "sup-004", + name: "Sari Recycling", + type: "company", + location: "Industrial Park", + district: "Jakarta Timur", + rating: 4.7, + reviewCount: 34, + phone: "081234567893", + verified: true, + responseTime: "1-2 jam" + }, + pickupAvailable: false, + minimumOrder: 20, + tags: ["Aluminium", "Dipress", "Bersih"], + postedAt: "2025-07-03", + viewCount: 52 + }, + { + id: "item-005", + title: "Botol Kaca Bekas Wine & Bir", + category: "glass", + description: + "Botol kaca bekas wine, bir, dan minuman lainnya. Berbagai ukuran dan warna. Kondisi utuh tanpa keretakan.", + quantity: 120, + unit: "kg", + pricePerUnit: 650, + condition: "sorted", + availableUntil: "2025-07-18", + images: ["https://picsum.photos/300/200?random=5"], + supplier: { + id: "sup-005", + name: "Dedi Kurniawan", + type: "individual", + location: "Cluster Villa", + district: "Jakarta Barat", + rating: 4.3, + reviewCount: 8, + phone: "081234567894", + verified: false, + responseTime: "4-6 jam" + }, + pickupAvailable: true, + minimumOrder: 25, + tags: ["Utuh", "Beragam", "Bersih"], + postedAt: "2025-07-02", + viewCount: 19 + }, + { + id: "item-006", + title: "Komponen Elektronik Bekas", + category: "electronic", + description: + "Komponen elektronik bekas dari perangkat komputer, TV, dan handphone. Mengandung logam mulia yang bisa diekstrak.", + quantity: 45, + unit: "kg", + pricePerUnit: 12000, + condition: "mixed", + availableUntil: "2025-07-25", + images: ["https://picsum.photos/300/200?random=6"], + supplier: { + id: "sup-006", + name: "TechWaste Solutions", + type: "company", + location: "Industrial Zone", + district: "Jakarta Timur", + rating: 4.6, + reviewCount: 15, + phone: "081234567895", + verified: true, + responseTime: "< 2 jam" + }, + pickupAvailable: false, + minimumOrder: 10, + tags: ["E-waste", "Logam Mulia", "Komponen"], + postedAt: "2025-07-01", + viewCount: 73 + } + ] + }; + + return json(exploreData); +}; + +export default function ExploreWaste() { + const data = useLoaderData(); + + const [searchQuery, setSearchQuery] = useState(""); + const [selectedCategory, setSelectedCategory] = useState("all"); + const [selectedLocation, setSelectedLocation] = useState("all"); + const [priceRange, setPriceRange] = useState("all"); + const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); + const [selectedItem, setSelectedItem] = useState(null); + + // Filter items + const filteredItems = data.items.filter((item) => { + const matchesSearch = + item.title.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description.toLowerCase().includes(searchQuery.toLowerCase()) || + item.tags.some((tag) => + tag.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const matchesCategory = + selectedCategory === "all" || item.category === selectedCategory; + const matchesLocation = + selectedLocation === "all" || item.supplier.district === selectedLocation; + + let matchesPrice = true; + if (priceRange === "low") matchesPrice = item.pricePerUnit < 1000; + else if (priceRange === "medium") + matchesPrice = item.pricePerUnit >= 1000 && item.pricePerUnit < 5000; + else if (priceRange === "high") matchesPrice = item.pricePerUnit >= 5000; + + return matchesSearch && matchesCategory && matchesLocation && matchesPrice; + }); + + const getCategoryIcon = (category: string) => { + switch (category) { + case "plastic": + return ; + case "organic": + return ; + case "electronic": + return ; + default: + return ; + } + }; + + const getConditionBadge = (condition: string) => { + const variants = { + fresh: "default", + sorted: "secondary", + processed: "outline", + mixed: "destructive" + } as const; + + const labels = { + fresh: "Segar", + sorted: "Tersortir", + processed: "Diproses", + mixed: "Campuran" + }; + + return ( + + {labels[condition as keyof typeof labels] || condition} + + ); + }; + + const getSupplierTypeBadge = (type: string) => { + const labels = { + individual: "Individu", + company: "Perusahaan", + cooperative: "Koperasi" + }; + + return ( + + {labels[type as keyof typeof labels] || type} + + ); + }; + + return ( +
+ {/* Header */} +
+
+

+ Explore Waste Marketplace +

+

+ Temukan dan hubungi supplier sampah untuk kebutuhan operasional Anda +

+
+
+ + +
+
+ + {/* Stats Cards */} +
+ + +
+ +
+

+ Total Supplier +

+

+ {data.stats.totalSuppliers} +

+
+
+
+
+ + + +
+ +
+

+ Volume Tersedia +

+

+ {data.stats.totalVolume.toLocaleString()} kg +

+
+
+
+
+ + + +
+ +
+

+ Listing Aktif +

+

+ {data.stats.activeListings} +

+
+
+
+
+ + + +
+ 💰 +
+

+ Harga Rata-rata +

+

+ Rp {data.stats.averagePrice}/kg +

+
+
+
+
+
+ + {/* Filters */} + + +
+ {/* Search */} +
+ +
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+
+ + {/* Category Filter */} +
+ + +
+ + {/* Location Filter */} +
+ + +
+ + {/* Price Filter */} +
+ + +
+
+ +
+ + {filteredItems.length} item ditemukan + + {(searchQuery || + selectedCategory !== "all" || + selectedLocation !== "all" || + priceRange !== "all") && ( + + )} +
+
+
+ + {/* Items Grid/List */} +
+ {filteredItems.map((item) => ( + +
+ +
+ {item.supplier.verified && ( + Verified + )} + {item.pickupAvailable && ( + Pickup Available + )} +
+
+ {getCategoryIcon(item.category)} +
+
+ + +
+ {/* Title & Condition */} +
+

+ {item.title} +

+ {getConditionBadge(item.condition)} +
+ + {/* Price & Quantity */} +
+
+

+ Rp {item.pricePerUnit.toLocaleString()} +

+

+ per {item.unit} +

+
+
+

+ {item.quantity.toLocaleString()} {item.unit} +

+

tersedia

+
+
+ + {/* Supplier Info */} +
+ + + + {item.supplier.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+
+

+ {item.supplier.name} +

+ {getSupplierTypeBadge(item.supplier.type)} +
+
+ + {item.supplier.district} + +
+ + {item.supplier.rating} + ({item.supplier.reviewCount}) +
+
+
+
+ + {/* Tags */} +
+ {item.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} + {item.tags.length > 3 && ( + + +{item.tags.length - 3} + + )} +
+ + {/* Meta Info */} +
+
+ + + Available until{" "} + {new Date(item.availableUntil).toLocaleDateString( + "id-ID" + )} + +
+
+ + {item.viewCount} views +
+
+ + + + {/* Actions */} +
+ + + + + + + {item.title} + + Detail lengkap stok sampah dari {item.supplier.name} + + +
+ +
+
+

+ Informasi Produk +

+
+

+ Kategori: {item.category} +

+

+ Kondisi: {item.condition} +

+

+ Quantity: {item.quantity}{" "} + {item.unit} +

+

+ Minimum Order:{" "} + {item.minimumOrder} {item.unit} +

+

+ Harga: Rp{" "} + {item.pricePerUnit.toLocaleString()} per{" "} + {item.unit} +

+
+
+
+

+ Informasi Supplier +

+
+

+ Nama: {item.supplier.name} +

+

+ Lokasi:{" "} + {item.supplier.location},{" "} + {item.supplier.district} +

+

+ Rating: {item.supplier.rating} + /5 ({item.supplier.reviewCount} reviews) +

+

+ Response Time:{" "} + {item.supplier.responseTime} +

+

+ Pickup:{" "} + {item.pickupAvailable + ? "Available" + : "Self-pickup only"} +

+
+
+
+
+

Deskripsi

+

+ {item.description} +

+
+
+ + +
+
+
+
+ + +
+
+
+
+ ))} +
+ + {/* Empty State */} + {filteredItems.length === 0 && ( + + + +

+ Tidak ada item ditemukan +

+

+ Coba ubah filter pencarian atau kata kunci untuk menemukan stok + sampah yang sesuai. +

+ +
+
+ )} +
+ ); +}