This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { PublicRestaurant } from "@/app/types/Types";
|
||||||
|
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
|
||||||
|
import { ArrowLeft2, Location } from "iconsax-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const FALLBACK_LOGO = "/assets/images/avatar.svg";
|
||||||
|
|
||||||
|
function isValidLogoUrl(url: string | null | undefined): url is string {
|
||||||
|
if (!url?.trim()) return false;
|
||||||
|
const lower = url.toLowerCase();
|
||||||
|
if (lower.endsWith(".pdf")) return false;
|
||||||
|
return lower.startsWith("http") || lower.startsWith("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitials(name: string): string {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!trimmed) return "?";
|
||||||
|
return trimmed.charAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
restaurant: PublicRestaurant;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RestaurantCard({ restaurant }: Props) {
|
||||||
|
const validLogo = isValidLogoUrl(restaurant.logo);
|
||||||
|
const [logoSrc, setLogoSrc] = useState<string>(validLogo && restaurant.logo ? restaurant.logo : FALLBACK_LOGO);
|
||||||
|
const showInitials = !validLogo || logoSrc === FALLBACK_LOGO;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/${restaurant.slug}`}
|
||||||
|
className={glassSurfaceCard(
|
||||||
|
"group flex items-center gap-4 p-4 transition-all duration-200 ease-out",
|
||||||
|
"hover:shadow-md active:scale-[0.98] outline-none [-webkit-tap-highlight-color:transparent]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative shrink-0">
|
||||||
|
{showInitials ? (
|
||||||
|
<div className="flex size-16 items-center justify-center rounded-2xl bg-(--danak-orange-light) text-xl font-medium text-(--danak-orange)">{getInitials(restaurant.name)}</div>
|
||||||
|
) : (
|
||||||
|
<Image src={logoSrc} alt={restaurant.name} width={64} height={64} className="size-16 rounded-2xl bg-[#F2F2F9] object-cover ring-1 ring-black/5" onError={() => setLogoSrc(FALLBACK_LOGO)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<h2 className="truncate text-sm font-medium text-foreground">{restaurant.name}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{restaurant.address && (
|
||||||
|
<div className="mt-1.5 flex items-start gap-1.5 text-[#8C90A3]">
|
||||||
|
<Location stroke="currentColor" size={14} className="mt-0.5 shrink-0 text-(--danak-orange)" variant="Bold" />
|
||||||
|
<p className="line-clamp-2 text-xs leading-5">{restaurant.address?.trim()}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{restaurant.establishedYear != null && (
|
||||||
|
<p className="mt-1.5 text-[11px] text-[#8C90A3]">
|
||||||
|
سال تاسیس <span className="font-medium text-(--danak-orange)">{restaurant.establishedYear}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ArrowLeft2 size={18} className="shrink-0 text-[#C4C7D4] transition-transform duration-200 group-hover:-translate-x-0.5 group-hover:text-(--danak-orange)" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
|
||||||
|
|
||||||
|
function SkeletonCard() {
|
||||||
|
return (
|
||||||
|
<div className={glassSurfaceCard("flex animate-pulse items-center gap-4 p-4")}>
|
||||||
|
<div className="size-16 shrink-0 rounded-2xl bg-(--danak-orange-muted)" />
|
||||||
|
<div className="flex-1 space-y-2.5">
|
||||||
|
<div className="h-4 w-2/3 rounded-lg bg-black/5" />
|
||||||
|
<div className="h-3 w-full rounded-lg bg-black/5" />
|
||||||
|
<div className="h-3 w-4/5 rounded-lg bg-black/5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RestaurantsSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 px-4 pb-8">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<SkeletonCard key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.danak-home {
|
||||||
|
--danak-orange: #ff5e2b;
|
||||||
|
--danak-orange-dark: #e84e1c;
|
||||||
|
--danak-orange-light: rgb(255 94 43 / 12%);
|
||||||
|
--danak-orange-muted: rgb(255 94 43 / 6%);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/Service";
|
||||||
|
|
||||||
|
export const useGetAllRestaurants = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["restaurants"],
|
||||||
|
queryFn: api.getAllRestaurants,
|
||||||
|
});
|
||||||
|
};
|
||||||
+70
-9
@@ -1,14 +1,75 @@
|
|||||||
import Link from 'next/link'
|
"use client";
|
||||||
|
|
||||||
|
import RestaurantCard from "@/app/components/RestaurantCard";
|
||||||
|
import RestaurantsSkeleton from "@/app/components/RestaurantsSkeleton";
|
||||||
|
import { useGetAllRestaurants } from "@/app/hooks/useAppData";
|
||||||
|
import { PublicRestaurant } from "@/app/types/Types";
|
||||||
|
import SearchBox from "@/components/input/SearchBox";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import "./home.css";
|
||||||
|
|
||||||
|
function filterRestaurants(restaurants: PublicRestaurant[], query: string): PublicRestaurant[] {
|
||||||
|
const normalized = query.trim().toLowerCase();
|
||||||
|
if (!normalized) return restaurants;
|
||||||
|
|
||||||
|
return restaurants.filter((r) => r.name.toLowerCase().includes(normalized) || r.slug.toLowerCase().includes(normalized) || r.address?.toLowerCase().includes(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { data, isLoading, error } = useGetAllRestaurants();
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const restaurants = data?.data ?? [];
|
||||||
|
const filtered = useMemo(() => filterRestaurants(restaurants, search), [restaurants, search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="danak-home flex h-full flex-col overflow-hidden bg-[#ff5e2b]/20">
|
||||||
<Link href={'/auth'}>Auth</Link>
|
<header className="shrink-0 px-4 pt-6 pb-3">
|
||||||
<br />
|
<div className="flex flex-col items-center text-center">
|
||||||
<Link href={'/zhivan'}>Menu</Link>
|
<Image src="/icons/logo512.png" alt="BY DANAK" width={52} height={52} className="size-[52px] rounded-[14px] object-cover" priority />
|
||||||
<br />
|
<h1 className="mt-3 text-base font-semibold text-foreground">دیمنو پلاس</h1>
|
||||||
<Link href={'/'}>Index</Link>
|
<p className="mt-0.5 text-[11px] tracking-wide text-(--danak-orange)">BY DANAK</p>
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
<div className="mt-5">
|
||||||
|
<SearchBox value={search} onChange={(e) => setSearch(e.target.value)} placeholder="جستجوی رستوران یا کافه..." />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="min-h-0 flex-1 overflow-y-auto noscrollbar">
|
||||||
|
{!isLoading && !error && (
|
||||||
|
<p className="px-4 pb-3 text-xs text-[#8C90A3]">
|
||||||
|
{filtered.length.toLocaleString("fa-IR")} مورد
|
||||||
|
{search.trim() ? " یافت شد" : ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && <RestaurantsSkeleton />}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mx-4 rounded-2xl border border-red-200 bg-red-50 px-4 py-6 text-center">
|
||||||
|
<p className="text-sm font-medium text-red-700">خطا در دریافت لیست رستورانها</p>
|
||||||
|
<p className="mt-1 text-xs text-red-500">لطفاً اتصال اینترنت خود را بررسی کنید و دوباره تلاش کنید.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !error && filtered.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center px-4 py-16 text-center">
|
||||||
|
<Image src="/icons/logo512.png" alt="" width={48} height={48} className="mb-4 size-12 rounded-xl object-cover opacity-40" />
|
||||||
|
<p className="text-sm font-medium text-foreground">{search.trim() ? "نتیجهای یافت نشد" : "رستورانی یافت نشد"}</p>
|
||||||
|
<p className="mt-1 text-xs text-[#8C90A3]">{search.trim() ? "عبارت جستجو را تغییر دهید" : "در حال حاضر رستورانی در لیست وجود ندارد"}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !error && filtered.length > 0 && (
|
||||||
|
<div className="space-y-3 px-4 pb-8">
|
||||||
|
{filtered.map((restaurant) => (
|
||||||
|
<RestaurantCard key={restaurant.slug} restaurant={restaurant} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { RestaurantsListResponse } from "@/app/types/Types";
|
||||||
|
import { api } from "@/config/axios";
|
||||||
|
|
||||||
|
export const getAllRestaurants = async (): Promise<RestaurantsListResponse> => {
|
||||||
|
const { data } = await api.get("/public/restaurants");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { BaseResponse } from "@/app/[name]/(Main)/types/Types";
|
||||||
|
|
||||||
|
export interface PublicRestaurant {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
address: string | null;
|
||||||
|
logo: string | null;
|
||||||
|
establishedYear: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RestaurantsListResponse = BaseResponse<PublicRestaurant[]>;
|
||||||
Reference in New Issue
Block a user