154 lines
5.7 KiB
TypeScript
154 lines
5.7 KiB
TypeScript
"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>
|
||
);
|
||
}
|