From a03c38c1ed8895e0a228e13db228448a64b9db37 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:53:20 +0000 Subject: [PATCH] Added search translation edge fn Co-authored-by: felix-fx-top <253056634+felix-fx-top@users.noreply.github.com> --- src/lib/api.ts | 46 ++++++++++++++- src/pages/Index.tsx | 33 +++++++++-- src/pages/SearchPage.tsx | 120 +++++++++++++++++++++++++++++++++++---- 3 files changed, 183 insertions(+), 16 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index 61a5c62..970ee99 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -2,15 +2,29 @@ import { supabase } from "@/integrations/supabase/client"; const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL; +// Anti-spam: debounce tracking +let lastSearchTime = 0; +const SEARCH_COOLDOWN = 500; // ms between searches + async function callFetchMods(params: Record) { + const now = Date.now(); + if (now - lastSearchTime < SEARCH_COOLDOWN) { + await new Promise((r) => setTimeout(r, SEARCH_COOLDOWN - (now - lastSearchTime))); + } + lastSearchTime = Date.now(); + const searchParams = new URLSearchParams(params); const url = `${SUPABASE_URL}/functions/v1/fetch-mods?${searchParams.toString()}`; const response = await fetch(url, { headers: { - "Authorization": `Bearer ${import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY}`, + Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY}`, }, }); + if (response.status === 429) { + throw new Error("تم تجاوز الحد المسموح، حاول مرة أخرى بعد قليل"); + } + if (!response.ok) { throw new Error(`API error: ${response.status}`); } @@ -23,13 +37,41 @@ export async function getUserProjects(username = "fxfelixzero") { } export async function searchMods(query: string, offset = 0, limit = 20) { - return callFetchMods({ action: "search", query, offset: String(offset), limit: String(limit) }); + // Sanitize client-side too + const cleanQuery = query.slice(0, 200).replace(/[<>{}]/g, "").trim(); + if (!cleanQuery) return { hits: [], total_hits: 0 }; + return callFetchMods({ action: "search", query: cleanQuery, offset: String(offset), limit: String(limit) }); } export async function getProject(id: string) { + if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw new Error("Invalid project id"); return callFetchMods({ action: "project", id }); } export async function getProjectVersions(id: string) { + if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw new Error("Invalid project id"); return callFetchMods({ action: "versions", id }); } + +export async function translateQuery(query: string): Promise<{ translated: string; original: string }> { + const cleanQuery = query.slice(0, 200).trim(); + if (!cleanQuery) return { translated: query, original: query }; + + const { data, error } = await supabase.functions.invoke("translate-search", { + body: { query: cleanQuery }, + }); + + if (error) { + console.error("Translation error:", error); + return { translated: query, original: query }; + } + + return data; +} + +// Random search keywords for homepage +const RANDOM_KEYWORDS = ["world", "mod", "skin", "map", "adventure", "survival", "building", "tools", "armor", "biome"]; + +export function getRandomKeyword(): string { + return RANDOM_KEYWORDS[Math.floor(Math.random() * RANDOM_KEYWORDS.length)]; +} diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index f2d6d0b..1fd45da 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,16 +1,36 @@ import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; import Navbar from "@/components/Navbar"; import HeroSection from "@/components/HeroSection"; import ModGrid from "@/components/ModGrid"; import Footer from "@/components/Footer"; -import { getUserProjects } from "@/lib/api"; +import { getUserProjects, searchMods, getRandomKeyword } from "@/lib/api"; const Index = () => { - const { data: mods, isLoading } = useQuery({ + const randomKeyword = useMemo(() => getRandomKeyword(), []); + + const { data: userMods, isLoading: loadingUser } = useQuery({ queryKey: ["user-projects"], queryFn: () => getUserProjects(), }); + const { data: discoverData, isLoading: loadingDiscover } = useQuery({ + queryKey: ["discover", randomKeyword], + queryFn: () => searchMods(randomKeyword, 0, 12), + }); + + const discoverMods = discoverData?.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 (
@@ -19,8 +39,13 @@ const Index = () => {
+
diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index da5b9bc..512adf6 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useCallback } from "react"; import { useSearchParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import Navbar from "@/components/Navbar"; @@ -6,25 +6,82 @@ import ModGrid from "@/components/ModGrid"; import Footer from "@/components/Footer"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { Search } from "lucide-react"; -import { searchMods } from "@/lib/api"; +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(null); + const [isTranslating, setIsTranslating] = useState(false); + + // Spam prevention: track rapid submissions + const [lastSubmit, setLastSubmit] = useState(0); const { data, isLoading } = useQuery({ - queryKey: ["search", initialQuery], - queryFn: () => searchMods(initialQuery), + queryKey: ["search", initialQuery, translatedText], + queryFn: async () => { + const searchTerm = translatedText || initialQuery; + const result = await searchMods(searchTerm); + + // If no results and autoTranslate is on, try translating + 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); + setIsTranslating(false); + return translatedResult; + } + } catch { + toast.error("فشل في ترجمة البحث"); + } + setIsTranslating(false); + } + + return result; + }, enabled: !!initialQuery, }); - const handleSearch = (e: React.FormEvent) => { + const handleSearch = useCallback((e: React.FormEvent) => { e.preventDefault(); - if (query.trim()) { - setSearchParams({ q: query.trim() }); + const trimmed = query.trim().slice(0, 200); + if (!trimmed) return; + + // Anti-spam: minimum 1s between searches + 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) => ({ @@ -46,13 +103,14 @@ const SearchPage = () => {

البحث عن إضافات

-
+
setQuery(e.target.value)} placeholder="ابحث عن مود، حزمة موارد، خريطة..." className="bg-secondary pr-10" + maxLength={200} />
@@ -61,13 +119,55 @@ const SearchPage = () => {
+ {/* Translation controls */} +
+
+ + +
+ {initialQuery && ( + + )} +
+ {initialQuery ? ( <>

نتائج البحث عن: "{initialQuery}" + {translatedText && ( + + → تمت الترجمة إلى: "{translatedText}" + + )} {data && ` (${data.total_hits || 0} نتيجة)`}

- + {isTranslating ? ( +
+ + جاري الترجمة والبحث... +
+ ) : ( + + )} ) : (