general category trip
This commit is contained in:
@@ -7,17 +7,11 @@ import "react-datepicker/dist/react-datepicker.css";
|
||||
import { createTripAction } from "@/features/trip/actions";
|
||||
import { ImageUrlInput } from "@/features/trip/components/image-url-input";
|
||||
import { formatLocalCalendarYmd } from "@/lib/trip-dates";
|
||||
|
||||
const SAMPLE_MOUNTAINS = [
|
||||
{ name: "Gunung Papandayan", location: "Garut, Jawa Barat" },
|
||||
{ name: "Gunung Ciremai", location: "Kuningan, Jawa Barat" },
|
||||
{ name: "Gunung Pangrango", location: "Bogor/Cianjur, Jawa Barat" },
|
||||
{ name: "Gunung Gede", location: "Bogor/Cianjur, Jawa Barat" },
|
||||
{ name: "Gunung Tangkuban Parahu", location: "Bandung, Jawa Barat" },
|
||||
{ name: "Gunung Bukit Tunggul", location: "Bandung, Jawa Barat" },
|
||||
{ name: "Gunung Malabar", location: "Bandung, Jawa Barat" },
|
||||
{ name: "Gunung Guntur", location: "Garut, Jawa Barat" },
|
||||
];
|
||||
import {
|
||||
ACTIVITY_CATEGORIES,
|
||||
categoryMeta,
|
||||
} from "@/lib/activity-category";
|
||||
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
||||
|
||||
function formatRupiahInput(value: string): string {
|
||||
const num = value.replace(/\D/g, "");
|
||||
@@ -37,10 +31,12 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [category, setCategory] = useState<ActivityCategory>("HIKING");
|
||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [priceDisplay, setPriceDisplay] = useState("");
|
||||
|
||||
const meta = categoryMeta(category);
|
||||
const priceNumber = Number(parseRupiahInput(priceDisplay) || "0");
|
||||
const isPaidTrip = priceNumber > 0;
|
||||
const blockedByVerification = isPaidTrip && !isVerifiedOrganizer;
|
||||
@@ -78,23 +74,6 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleMountainSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const selected = SAMPLE_MOUNTAINS.find((m) => m.name === e.target.value);
|
||||
if (selected) {
|
||||
const form = e.target.form;
|
||||
if (form) {
|
||||
const mountainInput = form.elements.namedItem(
|
||||
"mountain"
|
||||
) as HTMLInputElement;
|
||||
const locationInput = form.elements.namedItem(
|
||||
"location"
|
||||
) as HTMLInputElement;
|
||||
mountainInput.value = selected.name;
|
||||
locationInput.value = selected.location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDateChange(dates: [Date | null, Date | null]) {
|
||||
const [start, end] = dates;
|
||||
setStartDate(start);
|
||||
@@ -115,25 +94,34 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* Mountain Quick Picker */}
|
||||
{/* Category Chips */}
|
||||
<div className="rounded-xl bg-primary-50 p-4">
|
||||
<label className="mb-2 flex items-center gap-1.5 text-sm font-bold text-primary-800">
|
||||
<span>🏔️</span> Pilih Gunung Jawa Barat
|
||||
<label className="mb-2 block text-sm font-bold text-primary-800">
|
||||
Kategori Aktivitas
|
||||
</label>
|
||||
<select
|
||||
onChange={handleMountainSelect}
|
||||
className="w-full rounded-lg border border-primary-200 bg-white px-4 py-2.5 text-sm text-neutral-800"
|
||||
defaultValue=""
|
||||
>
|
||||
<option value="" disabled>
|
||||
Pilih gunung...
|
||||
</option>
|
||||
{SAMPLE_MOUNTAINS.map((m) => (
|
||||
<option key={m.name} value={m.name}>
|
||||
{m.name} — {m.location}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{ACTIVITY_CATEGORIES.map((c) => {
|
||||
const m = categoryMeta(c);
|
||||
const active = c === category;
|
||||
return (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => setCategory(c)}
|
||||
aria-pressed={active}
|
||||
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? "border-primary-600 bg-primary-600 text-white"
|
||||
: "border-primary-200 bg-white text-primary-800 hover:bg-primary-100"
|
||||
}`}
|
||||
>
|
||||
<span>{m.icon}</span>
|
||||
<span>{m.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<input type="hidden" name="category" value={category} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -152,16 +140,16 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label htmlFor="mountain" className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
||||
Nama Gunung
|
||||
<label htmlFor="destination" className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
||||
{meta.destinationLabel}
|
||||
</label>
|
||||
<input
|
||||
id="mountain"
|
||||
name="mountain"
|
||||
id="destination"
|
||||
name="destination"
|
||||
type="text"
|
||||
required
|
||||
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:bg-white"
|
||||
placeholder="Gunung Papandayan"
|
||||
placeholder={meta.destinationPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -2,11 +2,14 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { formatRupiah } from "@/lib/utils";
|
||||
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
||||
import { categoryMeta } from "@/lib/activity-category";
|
||||
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
||||
|
||||
interface TripCardProps {
|
||||
id: string;
|
||||
title: string;
|
||||
mountain: string;
|
||||
category: ActivityCategory;
|
||||
destination: string;
|
||||
location: string;
|
||||
date: Date | string;
|
||||
endDate?: Date | string | null;
|
||||
@@ -23,7 +26,8 @@ interface TripCardProps {
|
||||
export function TripCard({
|
||||
id,
|
||||
title,
|
||||
mountain,
|
||||
category,
|
||||
destination,
|
||||
location,
|
||||
date,
|
||||
endDate,
|
||||
@@ -38,6 +42,7 @@ export function TripCard({
|
||||
}: TripCardProps) {
|
||||
const spotsLeft = maxParticipants - participantCount;
|
||||
const isSmallGroup = maxParticipants <= 10;
|
||||
const meta = categoryMeta(category);
|
||||
|
||||
return (
|
||||
<Link href={`/trips/${id}`} className="group block">
|
||||
@@ -55,9 +60,16 @@ export function TripCard({
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center bg-linear-to-br from-primary-800 to-secondary-900">
|
||||
<span className="text-4xl">🏔️</span>
|
||||
<span className="text-4xl">{meta.icon}</span>
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className="absolute left-3 top-3 inline-flex items-center gap-1 rounded-full bg-white/90 px-2 py-0.5 text-[11px] font-semibold text-neutral-700 shadow-sm backdrop-blur-sm"
|
||||
title={`Kategori: ${meta.label}`}
|
||||
>
|
||||
<span>{meta.icon}</span>
|
||||
<span>{meta.label}</span>
|
||||
</span>
|
||||
<span
|
||||
className={`absolute right-3 top-3 rounded-full px-2.5 py-0.5 text-xs font-bold backdrop-blur-sm ${
|
||||
status === "OPEN"
|
||||
@@ -76,7 +88,7 @@ export function TripCard({
|
||||
<h3 className="font-bold text-neutral-800 group-hover:text-primary-700">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mt-0.5 text-sm text-neutral-500">{mountain}</p>
|
||||
<p className="mt-0.5 text-sm text-neutral-500">{destination}</p>
|
||||
|
||||
<div className="mt-3 space-y-1 text-sm text-neutral-600">
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
||||
@@ -5,11 +5,21 @@ import { useState } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { formatLocalCalendarYmd } from "@/lib/trip-dates";
|
||||
import {
|
||||
ACTIVITY_CATEGORIES,
|
||||
categoryMeta,
|
||||
isActivityCategory,
|
||||
} from "@/lib/activity-category";
|
||||
import type { ActivityCategory } from "@/app/generated/prisma/enums";
|
||||
|
||||
export function TripFilter() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const initialCategory = searchParams.get("category");
|
||||
const [category, setCategory] = useState<ActivityCategory | null>(
|
||||
isActivityCategory(initialCategory) ? initialCategory : null
|
||||
);
|
||||
const [query, setQuery] = useState(searchParams.get("q") ?? "");
|
||||
const [startDate, setStartDate] = useState<Date | null>(
|
||||
searchParams.get("from") ? new Date(searchParams.get("from")!) : null
|
||||
@@ -18,45 +28,101 @@ export function TripFilter() {
|
||||
searchParams.get("to") ? new Date(searchParams.get("to")!) : null
|
||||
);
|
||||
|
||||
function buildParams(overrides?: { category?: ActivityCategory | null }) {
|
||||
const params = new URLSearchParams();
|
||||
const nextCategory =
|
||||
overrides && "category" in overrides ? overrides.category : category;
|
||||
if (nextCategory) params.set("category", nextCategory);
|
||||
if (query.trim()) params.set("q", query.trim());
|
||||
if (startDate) params.set("from", formatLocalCalendarYmd(startDate));
|
||||
if (endDate) params.set("to", formatLocalCalendarYmd(endDate));
|
||||
return params;
|
||||
}
|
||||
|
||||
function pushFilters(params: URLSearchParams) {
|
||||
const qs = params.toString();
|
||||
router.push(`/trips${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
function handleSelectCategory(next: ActivityCategory | null) {
|
||||
setCategory(next);
|
||||
pushFilters(buildParams({ category: next }));
|
||||
}
|
||||
|
||||
function handleDateChange(dates: [Date | null, Date | null]) {
|
||||
const [start, end] = dates;
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
|
||||
// When both dates are cleared (via X button), auto-submit to reset results
|
||||
if (!start && !end) {
|
||||
const params = new URLSearchParams();
|
||||
if (category) params.set("category", category);
|
||||
if (query.trim()) params.set("q", query.trim());
|
||||
const qs = params.toString();
|
||||
router.push(`/trips${qs ? `?${qs}` : ""}`);
|
||||
pushFilters(params);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const params = new URLSearchParams();
|
||||
if (query.trim()) params.set("q", query.trim());
|
||||
if (startDate) params.set("from", formatLocalCalendarYmd(startDate));
|
||||
if (endDate) params.set("to", formatLocalCalendarYmd(endDate));
|
||||
|
||||
const qs = params.toString();
|
||||
router.push(`/trips${qs ? `?${qs}` : ""}`);
|
||||
pushFilters(buildParams());
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
setCategory(null);
|
||||
setQuery("");
|
||||
setStartDate(null);
|
||||
setEndDate(null);
|
||||
router.push("/trips");
|
||||
}
|
||||
|
||||
const hasFilters = query || startDate || endDate;
|
||||
const hasFilters = category || query || startDate || endDate;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSearch}
|
||||
className="rounded-2xl border border-neutral-200 bg-white p-4 shadow-sm sm:p-5"
|
||||
>
|
||||
{/* Category chips */}
|
||||
<div className="mb-4">
|
||||
<label className="mb-1.5 block text-xs font-medium text-neutral-500">
|
||||
Kategori
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelectCategory(null)}
|
||||
aria-pressed={category === null}
|
||||
className={`inline-flex items-center gap-1 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
category === null
|
||||
? "border-primary-600 bg-primary-600 text-white"
|
||||
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||
}`}
|
||||
>
|
||||
Semua
|
||||
</button>
|
||||
{ACTIVITY_CATEGORIES.map((c) => {
|
||||
const m = categoryMeta(c);
|
||||
const active = c === category;
|
||||
return (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => handleSelectCategory(c)}
|
||||
aria-pressed={active}
|
||||
className={`inline-flex items-center gap-1 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? "border-primary-600 bg-primary-600 text-white"
|
||||
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
|
||||
}`}
|
||||
>
|
||||
<span>{m.icon}</span>
|
||||
<span>{m.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-3">
|
||||
{/* Search input */}
|
||||
<div className="flex-1">
|
||||
|
||||
Reference in New Issue
Block a user