195 lines
7.0 KiB
TypeScript
195 lines
7.0 KiB
TypeScript
import { useState, useCallback } from "react";
|
||
import { useSearchParams } from "react-router-dom";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import Navbar from "@/components/Navbar";
|
||
import ModGrid from "@/components/ModGrid";
|
||
import Footer from "@/components/Footer";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Switch } from "@/components/ui/switch";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Search, Languages, Loader2 } from "lucide-react";
|
||
import { searchMods, translateQuery } from "@/lib/api";
|
||
import { toast } from "sonner";
|
||
|
||
const SearchPage = () => {
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
const initialQuery = searchParams.get("q") || "";
|
||
const [query, setQuery] = useState(initialQuery);
|
||
const [autoTranslate, setAutoTranslate] = useState(true);
|
||
const [translatedText, setTranslatedText] = useState<string | null>(null);
|
||
const [isTranslating, setIsTranslating] = useState(false);
|
||
const [platform, setPlatform] = useState("all");
|
||
const [lastSubmit, setLastSubmit] = useState(0);
|
||
|
||
const facets = platform === "java"
|
||
? '[["project_type:mod"],["categories:fabric","categories:forge","categories:neoforge","categories:quilt"]]'
|
||
: platform === "bedrock"
|
||
? '[["project_type:mod"],["categories:bedrock"]]'
|
||
: "";
|
||
|
||
const { data, isLoading } = useQuery({
|
||
queryKey: ["search", initialQuery, translatedText, platform],
|
||
queryFn: async () => {
|
||
const searchTerm = translatedText || initialQuery;
|
||
const result = await searchMods(searchTerm, 0, 20, facets);
|
||
|
||
if (autoTranslate && !translatedText && result.total_hits === 0 && initialQuery) {
|
||
setIsTranslating(true);
|
||
try {
|
||
const { translated } = await translateQuery(initialQuery);
|
||
if (translated !== initialQuery) {
|
||
setTranslatedText(translated);
|
||
const translatedResult = await searchMods(translated, 0, 20, facets);
|
||
setIsTranslating(false);
|
||
return translatedResult;
|
||
}
|
||
} catch {
|
||
toast.error("فشل في ترجمة البحث");
|
||
}
|
||
setIsTranslating(false);
|
||
}
|
||
|
||
return result;
|
||
},
|
||
enabled: !!initialQuery,
|
||
});
|
||
|
||
const handleSearch = useCallback((e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
const trimmed = query.trim().slice(0, 200);
|
||
if (!trimmed) return;
|
||
|
||
const now = Date.now();
|
||
if (now - lastSubmit < 1000) {
|
||
toast.error("انتظر قليلاً قبل البحث مرة أخرى");
|
||
return;
|
||
}
|
||
setLastSubmit(now);
|
||
setTranslatedText(null);
|
||
setSearchParams({ q: trimmed });
|
||
}, [query, lastSubmit, setSearchParams]);
|
||
|
||
const handleManualTranslate = async () => {
|
||
if (!initialQuery) return;
|
||
setIsTranslating(true);
|
||
try {
|
||
const { translated } = await translateQuery(initialQuery);
|
||
if (translated !== initialQuery) {
|
||
setTranslatedText(translated);
|
||
toast.success(`تمت الترجمة: "${translated}"`);
|
||
} else {
|
||
toast.info("النص بالفعل باللغة الإنجليزية");
|
||
}
|
||
} catch {
|
||
toast.error("فشل في ترجمة البحث");
|
||
}
|
||
setIsTranslating(false);
|
||
};
|
||
|
||
const mods = data?.hits?.map((hit: any) => ({
|
||
id: hit.project_id,
|
||
slug: hit.slug,
|
||
title: hit.title,
|
||
description: hit.description,
|
||
icon_url: hit.icon_url,
|
||
downloads: hit.downloads,
|
||
followers: hit.follows,
|
||
categories: hit.categories || [],
|
||
project_type: hit.project_type,
|
||
})) || [];
|
||
|
||
return (
|
||
<div className="flex min-h-screen flex-col">
|
||
<Navbar />
|
||
<main className="flex-1">
|
||
<div className="container mx-auto px-3 py-6 sm:px-4 sm:py-8">
|
||
<h1 className="mb-4 text-2xl font-black sm:mb-6 sm:text-3xl">البحث عن إضافات</h1>
|
||
|
||
<form onSubmit={handleSearch} className="mb-3 flex gap-2 sm:mb-4 sm:gap-3">
|
||
<div className="relative flex-1">
|
||
<Input
|
||
value={query}
|
||
onChange={(e) => setQuery(e.target.value)}
|
||
placeholder="ابحث عن مود، حزمة موارد، خريطة..."
|
||
className="bg-secondary pr-10 text-sm sm:text-base"
|
||
maxLength={200}
|
||
/>
|
||
<Search className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||
</div>
|
||
<Button type="submit" className="font-bold text-sm sm:text-base">
|
||
بحث
|
||
</Button>
|
||
</form>
|
||
|
||
{/* Translation controls */}
|
||
<div className="mb-4 flex flex-col gap-3 sm:mb-6 sm:flex-row sm:items-center">
|
||
<div className="flex items-center gap-2">
|
||
<Switch
|
||
id="auto-translate"
|
||
checked={autoTranslate}
|
||
onCheckedChange={setAutoTranslate}
|
||
/>
|
||
<Label htmlFor="auto-translate" className="text-xs text-muted-foreground sm:text-sm">
|
||
ترجمة تلقائية عند عدم وجود نتائج
|
||
</Label>
|
||
</div>
|
||
{initialQuery && (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleManualTranslate}
|
||
disabled={isTranslating}
|
||
className="gap-1 text-xs sm:text-sm"
|
||
>
|
||
{isTranslating ? (
|
||
<Loader2 className="h-3 w-3 animate-spin" />
|
||
) : (
|
||
<Languages className="h-3 w-3" />
|
||
)}
|
||
ترجم للإنجليزية
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{initialQuery ? (
|
||
<>
|
||
<p className="mb-3 text-xs text-muted-foreground sm:mb-4 sm:text-sm">
|
||
نتائج البحث عن: <span className="font-bold text-foreground">"{initialQuery}"</span>
|
||
{translatedText && (
|
||
<span className="mr-2 text-primary">
|
||
→ تمت الترجمة إلى: "{translatedText}"
|
||
</span>
|
||
)}
|
||
{data && ` (${data.total_hits || 0} نتيجة)`}
|
||
</p>
|
||
{isTranslating ? (
|
||
<div className="flex items-center justify-center gap-2 py-20 text-muted-foreground">
|
||
<Loader2 className="h-5 w-5 animate-spin" />
|
||
جاري الترجمة والبحث...
|
||
</div>
|
||
) : (
|
||
<ModGrid
|
||
mods={mods}
|
||
isLoading={isLoading}
|
||
showViewToggle
|
||
showPlatformFilter
|
||
selectedPlatform={platform}
|
||
onPlatformChange={setPlatform}
|
||
/>
|
||
)}
|
||
</>
|
||
) : (
|
||
<div className="py-20 text-center text-muted-foreground">
|
||
ابدأ بالبحث عن إضافتك المفضلة
|
||
</div>
|
||
)}
|
||
</div>
|
||
</main>
|
||
<Footer />
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default SearchPage;
|