TorbatYar/frontend/app/search/page.tsx
2026-07-28 20:39:10 +03:30

154 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { FormEvent, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { Suspense } from "react";
import { Button, Container } from "@/components/ui";
import { loadCommercialBundles, loadCommercialProducts } from "@/modules/commercial/adapters";
import type { CommercialBundle, CommercialProduct } from "@/modules/commercial/types";
import { resolveProductLaunchUrl } from "@/modules/commercial/types";
import { CommercialEmptyState, CommercialSkeleton } from "@/modules/commercial/components";
const HELP_HITS = [
{ id: "help-discover", label: "کشف کسب‌وکار", href: "/discover", kind: "help" },
{ id: "help-billing", label: "Billing و Trial", href: "/billing", kind: "help" },
{ id: "help-domains", label: "دامنه و SSL", href: "/domains", kind: "help" },
{ id: "help-settings", label: "تنظیمات حساب", href: "/dashboard/settings", kind: "settings" },
{ id: "help-center", label: "مرکز راهنما", href: "/help", kind: "help" },
];
function SearchInner() {
const params = useSearchParams();
const initial = params.get("q") || "";
const [q, setQ] = useState(initial);
const [query, setQuery] = useState(initial);
const [products, setProducts] = useState<CommercialProduct[]>([]);
const [bundles, setBundles] = useState<CommercialBundle[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
void Promise.all([loadCommercialProducts(), loadCommercialBundles()]).then(([p, b]) => {
if (p.availability === "ready") setProducts(p.data);
if (b.availability === "ready") setBundles(b.data);
setLoading(false);
});
}, []);
const needle = query.trim().toLowerCase();
const results = useMemo(() => {
if (!needle) return [];
const hits: Array<{ id: string; label: string; href: string; kind: string }> = [];
for (const p of products) {
if (
p.display_name.toLowerCase().includes(needle) ||
p.product_code.toLowerCase().includes(needle) ||
(p.category || "").toLowerCase().includes(needle)
) {
hits.push({
id: p.product_code,
label: p.display_name,
href: resolveProductLaunchUrl(p) || `/setup/${encodeURIComponent(p.product_code)}`,
kind: "product",
});
}
}
for (const b of bundles) {
if (
b.display_name.toLowerCase().includes(needle) ||
b.bundle_code.toLowerCase().includes(needle)
) {
hits.push({
id: b.bundle_code,
label: b.display_name,
href: "/apps",
kind: "bundle",
});
}
}
for (const h of HELP_HITS) {
if (h.label.toLowerCase().includes(needle) || h.kind.includes(needle)) {
hits.push(h);
}
}
return hits;
}, [needle, products, bundles]);
const onSubmit = (e: FormEvent) => {
e.preventDefault();
setQuery(q);
};
return (
<Container className="py-12">
<h1 className="text-3xl font-extrabold text-secondary dark:text-gray-100">جستجوی سراسری</h1>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
اپها، محصولات، باندلها، تنظیمات و راهنما.
</p>
<form onSubmit={onSubmit} className="mt-6 flex flex-wrap gap-2">
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="جستجو…"
aria-label="عبارت جستجو"
className="min-w-[220px] flex-1 rounded-xl border border-gray-200 px-4 py-2.5 text-sm dark:border-gray-700 dark:bg-gray-900"
/>
<Button type="submit">جستجو</Button>
</form>
{loading ? <div className="mt-8"><CommercialSkeleton rows={3} /></div> : null}
{!loading && !needle ? (
<div className="mt-8">
<CommercialEmptyState
title="عبارتی وارد کنید"
description="نام محصول، باندل یا موضوع راهنما را جستجو کنید."
actionHref="/apps"
actionLabel="مرور Apps"
secondaryHref="/help"
secondaryLabel="مرکز راهنما"
/>
</div>
) : null}
{!loading && needle && !results.length ? (
<div className="mt-8">
<CommercialEmptyState
title="نتیجه‌ای نیست"
description="چیزی با این عبارت در رجیستری یا راهنما پیدا نشد."
actionHref="/discover"
actionLabel="کشف کسب‌وکار"
secondaryHref="/help"
secondaryLabel="راهنما"
/>
</div>
) : null}
{results.length ? (
<ul className="mt-8 space-y-2">
{results.map((r) => (
<li key={`${r.kind}-${r.id}`}>
<Link
href={r.href}
className="flex items-center justify-between rounded-xl border border-gray-100 bg-white px-4 py-3 text-sm hover:border-primary dark:border-gray-800 dark:bg-gray-900"
>
<span className="font-medium text-secondary dark:text-gray-100">{r.label}</span>
<span className="text-[10px] uppercase text-gray-400">{r.kind}</span>
</Link>
</li>
))}
</ul>
) : null}
</Container>
);
}
export default function SearchPage() {
return (
<Suspense fallback={<Container className="py-16 text-center text-sm text-gray-500"></Container>}>
<SearchInner />
</Suspense>
);
}