init
This commit is contained in:
150
src/screens/places-list-screen.tsx
Normal file
150
src/screens/places-list-screen.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { FILTERS } from "@/lib/mock-data";
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user