Add browse screen
This commit is contained in:
161
app/src/app/(tabs)/browse.tsx
Normal file
161
app/src/app/(tabs)/browse.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import type { JSX } from "react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Pressable, ScrollView, View } from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { Chip, SearchField, Switch, Typography, useThemeColor } from "heroui-native";
|
||||||
|
import type { Currency, Product } from "@/data/mock";
|
||||||
|
import { PRODUCTS } from "@/data/mock";
|
||||||
|
import { ListingCard } from "@/components/ListingCard";
|
||||||
|
|
||||||
|
type CurrencyFilter = "all" | Currency;
|
||||||
|
type SortMode = "newest" | "priceLow" | "priceHigh";
|
||||||
|
type ViewMode = "list" | "grid";
|
||||||
|
|
||||||
|
export default function BrowseTab(): JSX.Element {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [currencyFilter, setCurrencyFilter] = useState<CurrencyFilter>("all");
|
||||||
|
const [sortMode, setSortMode] = useState<SortMode>("newest");
|
||||||
|
const [inStockOnly, setInStockOnly] = useState(true);
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>("list");
|
||||||
|
const mutedColor = useThemeColor("muted");
|
||||||
|
const accentForeground = useThemeColor("accent-foreground");
|
||||||
|
|
||||||
|
const products = useMemo<Product[]>(() => {
|
||||||
|
let result = [...PRODUCTS];
|
||||||
|
if (inStockOnly) result = result.filter((product) => product.qty > 0);
|
||||||
|
if (currencyFilter !== "all") result = result.filter((product) => product.cur === currencyFilter);
|
||||||
|
if (search.trim()) {
|
||||||
|
const query = search.trim().toLowerCase();
|
||||||
|
result = result.filter(
|
||||||
|
(product) =>
|
||||||
|
product.name.toLowerCase().includes(query) || product.desc.toLowerCase().includes(query),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sortMode === "priceLow") result.sort((a, b) => a.priceNum - b.priceNum);
|
||||||
|
else if (sortMode === "priceHigh") result.sort((a, b) => b.priceNum - a.priceNum);
|
||||||
|
return result;
|
||||||
|
}, [search, currencyFilter, sortMode, inStockOnly]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1 bg-background">
|
||||||
|
<View className="gap-3 border-b border-border px-4 py-3">
|
||||||
|
<SearchField value={search} onChange={setSearch}>
|
||||||
|
<SearchField.Group>
|
||||||
|
<SearchField.SearchIcon />
|
||||||
|
<SearchField.Input placeholder="Search listings…" />
|
||||||
|
<SearchField.ClearButton />
|
||||||
|
</SearchField.Group>
|
||||||
|
</SearchField>
|
||||||
|
|
||||||
|
<View className="flex-row flex-wrap items-center gap-2">
|
||||||
|
<Typography type="body-xs" color="muted">
|
||||||
|
Currency
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant={currencyFilter === "all" ? "primary" : "soft"}
|
||||||
|
color={currencyFilter === "all" ? "accent" : "default"}
|
||||||
|
onPress={() => setCurrencyFilter("all")}
|
||||||
|
>
|
||||||
|
<Chip.Label>All</Chip.Label>
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant={currencyFilter === "SOL" ? "primary" : "soft"}
|
||||||
|
color={currencyFilter === "SOL" ? "accent" : "default"}
|
||||||
|
onPress={() => setCurrencyFilter("SOL")}
|
||||||
|
>
|
||||||
|
<Chip.Label>SOL</Chip.Label>
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant={currencyFilter === "USDC" ? "primary" : "soft"}
|
||||||
|
color={currencyFilter === "USDC" ? "accent" : "default"}
|
||||||
|
onPress={() => setCurrencyFilter("USDC")}
|
||||||
|
>
|
||||||
|
<Chip.Label>USDC</Chip.Label>
|
||||||
|
</Chip>
|
||||||
|
|
||||||
|
<Typography type="body-xs" color="muted" className="ml-2">
|
||||||
|
Sort
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant={sortMode === "newest" ? "primary" : "soft"}
|
||||||
|
color={sortMode === "newest" ? "accent" : "default"}
|
||||||
|
onPress={() => setSortMode("newest")}
|
||||||
|
>
|
||||||
|
<Chip.Label>Newest</Chip.Label>
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant={sortMode === "priceLow" ? "primary" : "soft"}
|
||||||
|
color={sortMode === "priceLow" ? "accent" : "default"}
|
||||||
|
onPress={() => setSortMode("priceLow")}
|
||||||
|
>
|
||||||
|
<Chip.Label>Price ↑</Chip.Label>
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant={sortMode === "priceHigh" ? "primary" : "soft"}
|
||||||
|
color={sortMode === "priceHigh" ? "accent" : "default"}
|
||||||
|
onPress={() => setSortMode("priceHigh")}
|
||||||
|
>
|
||||||
|
<Chip.Label>Price ↓</Chip.Label>
|
||||||
|
</Chip>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex-row items-center justify-between">
|
||||||
|
<View className="flex-row items-center gap-2">
|
||||||
|
<Switch isSelected={inStockOnly} onSelectedChange={setInStockOnly} />
|
||||||
|
<Typography type="body-xs" color="muted">
|
||||||
|
In stock only
|
||||||
|
</Typography>
|
||||||
|
</View>
|
||||||
|
<View className="flex-row gap-1 rounded-lg border border-border bg-surface p-1">
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setViewMode("list")}
|
||||||
|
className={`h-7 w-8 items-center justify-center rounded-md ${
|
||||||
|
viewMode === "list" ? "bg-accent" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="list"
|
||||||
|
size={14}
|
||||||
|
color={viewMode === "list" ? accentForeground : mutedColor}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setViewMode("grid")}
|
||||||
|
className={`h-7 w-8 items-center justify-center rounded-md ${
|
||||||
|
viewMode === "grid" ? "bg-accent" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="grid"
|
||||||
|
size={14}
|
||||||
|
color={viewMode === "grid" ? accentForeground : mutedColor}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView className="flex-1">
|
||||||
|
{viewMode === "list" ? (
|
||||||
|
<View className="gap-2.5 p-3.5">
|
||||||
|
{products.map((product) => (
|
||||||
|
<ListingCard key={product.id} product={product} variant="list" />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className="flex-row flex-wrap p-2">
|
||||||
|
{products.map((product) => (
|
||||||
|
<ListingCard key={product.id} product={product} variant="grid" />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import type { JSX } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export default function ExploreTab(): JSX.Element {
|
|
||||||
return <View className="flex-1 bg-background" />;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user