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 */}
{/* 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.
)}
);
}