Files
places/src/screens/places-list-screen.tsx
2026-05-20 15:40:17 +07:00

151 lines
4.3 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
import { FILTERS } from "@/lib/ui-config";
import type { AppState, Dispatch } from "@/lib/app-state";
import { Header, IconBtn, OfflineBanner, EmptyState } from "@/components/ui-primitives";
import { PlaceCard } from "@/components/place-card";
import { Icons } from "@/components/icons";
export function PlacesListScreen({
state,
dispatch,
}: {
state: AppState;
dispatch: Dispatch;
}) {
const { filter, search, places, offline } = state;
const [searchOpen, setSearchOpen] = useState(false);
const filtered = useMemo(() => {
let out = places;
if (filter !== "all") {
if (filter === "visited") out = out.filter((p) => p.visited);
else if (filter === "unvisited") out = out.filter((p) => !p.visited);
else out = out.filter((p) => p.category === filter);
}
if (search.trim()) {
const s = search.toLowerCase();
out = out.filter(
(p) =>
p.name.toLowerCase().includes(s) ||
p.address.toLowerCase().includes(s) ||
p.tags.some((t) => t.toLowerCase().includes(s)),
);
}
return out;
}, [filter, search, places]);
return (
<div className="app-surface">
<Header
big
title="Địa điểm"
subtitle={`${places.length} chỗ đã lưu · ${places.filter((p) => p.visited).length} đã đến`}
right={
<IconBtn
icon="Search"
label="Tìm"
onClick={() => setSearchOpen((v) => !v)}
variant="muted"
/>
}
/>
{offline && <OfflineBanner />}
{searchOpen && (
<div style={{ padding: "0 16px 12px" }}>
<div className="input" style={{ height: 44 }}>
<Icons.Search
size={18}
stroke={1.75}
style={{ color: "var(--muted-foreground)" }}
/>
<input
autoFocus
placeholder="Tên quán, địa chỉ, tag..."
value={search}
onChange={(e) =>
dispatch({ type: "SET_SEARCH", value: e.target.value })
}
/>
{search && (
<button
type="button"
onClick={() => dispatch({ type: "SET_SEARCH", value: "" })}
style={{
background: "transparent",
border: 0,
color: "var(--muted-foreground)",
}}
>
<Icons.X size={16} stroke={2} />
</button>
)}
</div>
</div>
)}
<div
className="no-scrollbar"
style={{
display: "flex",
gap: 8,
padding: "0 16px 12px",
overflowX: "auto",
overflowY: "hidden",
scrollSnapType: "x proximity",
}}
>
{FILTERS.map((f) => (
<button
key={f.id}
className="pill"
data-active={filter === f.id}
onClick={() => dispatch({ type: "SET_FILTER", value: f.id })}
>
{f.label}
</button>
))}
</div>
<div className="app-scroll">
{filtered.length === 0 ? (
<EmptyState
icon={search ? "Search" : "MapPin"}
title={search ? "Không tìm thấy gì" : "Chưa có địa điểm nào"}
body={
search
? `Không có địa điểm nào khớp với "${search}".`
: "Bấm nút + để lưu địa điểm đầu tiên — quán cà phê quen, chỗ ăn ngon, hay nơi muốn đến."
}
/>
) : (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 10,
padding: "4px 16px 24px",
}}
>
{filtered.map((p, i) => (
<div
key={p.id}
className="page-enter"
style={{ animationDelay: `${i * 18}ms` }}
>
<PlaceCard
place={p}
onTap={() =>
dispatch({ type: "NAV", screen: "place", placeId: p.id })
}
/>
</div>
))}
</div>
)}
</div>
</div>
);
}