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 = () => (
+
+ );
+
+ // Loading skeleton
+ const ImageSkeleton = () => (
+
+ );
+
+ if (imageError) {
+ return ;
+ }
+
+ return (
+
+ {imageLoading &&
}
+

+
+ );
+};
+
+// 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 */}
+
+
+
+
+
+
+
+
+ ))}
+
+
+ {/* Empty State */}
+ {filteredItems.length === 0 && (
+
+
+
+
+ Tidak ada item ditemukan
+
+
+ Coba ubah filter pencarian atau kata kunci untuk menemukan stok
+ sampah yang sesuai.
+
+
+
+
+ )}
+
+ );
+}