-

+
);
};
-const Index = PlaceholderIndex;
-
export default Index;
diff --git a/src/pages/ModDetails.tsx b/src/pages/ModDetails.tsx
new file mode 100644
index 0000000..d4bbecb
--- /dev/null
+++ b/src/pages/ModDetails.tsx
@@ -0,0 +1,172 @@
+import { useParams } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Download, Heart, Calendar, ExternalLink } from "lucide-react";
+import { getProject, getProjectVersions } from "@/lib/api";
+
+const formatNumber = (num: number) => {
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
+ if (num >= 1000) return (num / 1000).toFixed(1) + "K";
+ return num.toString();
+};
+
+const formatDate = (dateStr: string) => {
+ return new Date(dateStr).toLocaleDateString("ar-SA", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ });
+};
+
+const ModDetails = () => {
+ const { id } = useParams<{ id: string }>();
+
+ const { data: project, isLoading: loadingProject } = useQuery({
+ queryKey: ["project", id],
+ queryFn: () => getProject(id!),
+ enabled: !!id,
+ });
+
+ const { data: versions, isLoading: loadingVersions } = useQuery({
+ queryKey: ["versions", id],
+ queryFn: () => getProjectVersions(id!),
+ enabled: !!id,
+ });
+
+ return (
+
+
+
+
+ {loadingProject ? (
+
+ ) : project ? (
+ <>
+ {/* Header */}
+
+
+ {project.icon_url ? (
+

+ ) : (
+
🧊
+ )}
+
+
+
{project.title}
+
{project.description}
+
+
+
+ {formatNumber(project.downloads)} تحميل
+
+
+
+ {formatNumber(project.followers)} متابع
+
+
+
+ {formatDate(project.published)}
+
+
+
+ {project.categories?.map((cat: string) => (
+ {cat}
+ ))}
+
+
+
+
+ {/* Description */}
+ {project.body && (
+
+
+ الوصف
+
+
+
+
+
+ )}
+
+ {/* Versions */}
+
+
+ الإصدارات
+
+
+ {loadingVersions ? (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ ) : versions && versions.length > 0 ? (
+
+ {versions.slice(0, 10).map((version: any) => (
+
+
+
+ {version.name}
+
+ {version.version_type}
+
+
+
+ {version.game_versions?.slice(0, 5).map((gv: string) => (
+ {gv}
+ ))}
+ {version.loaders?.map((l: string) => (
+ {l}
+ ))}
+
+
+ {version.files?.[0] && (
+
+ )}
+
+ ))}
+
+ ) : (
+ لا توجد إصدارات متاحة
+ )}
+
+
+ >
+ ) : (
+
+ الإضافة غير موجودة
+
+ )}
+
+
+
+
+ );
+};
+
+export default ModDetails;
diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx
new file mode 100644
index 0000000..da5b9bc
--- /dev/null
+++ b/src/pages/SearchPage.tsx
@@ -0,0 +1,84 @@
+import { useState } 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 { Search } from "lucide-react";
+import { searchMods } from "@/lib/api";
+
+const SearchPage = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const initialQuery = searchParams.get("q") || "";
+ const [query, setQuery] = useState(initialQuery);
+
+ const { data, isLoading } = useQuery({
+ queryKey: ["search", initialQuery],
+ queryFn: () => searchMods(initialQuery),
+ enabled: !!initialQuery,
+ });
+
+ const handleSearch = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (query.trim()) {
+ setSearchParams({ q: query.trim() });
+ }
+ };
+
+ 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 (
+
+
+
+
+
البحث عن إضافات
+
+
+
+ {initialQuery ? (
+ <>
+
+ نتائج البحث عن: "{initialQuery}"
+ {data && ` (${data.total_hits || 0} نتيجة)`}
+
+
+ >
+ ) : (
+
+ ابدأ بالبحث عن إضافتك المفضلة
+
+ )}
+
+
+
+
+ );
+};
+
+export default SearchPage;
diff --git a/supabase/functions/fetch-mods/index.ts b/supabase/functions/fetch-mods/index.ts
new file mode 100644
index 0000000..b9e12d0
--- /dev/null
+++ b/supabase/functions/fetch-mods/index.ts
@@ -0,0 +1,88 @@
+import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
+
+const corsHeaders = {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
+};
+
+const MODRINTH_BASE = "https://api.modrinth.com/v2";
+
+serve(async (req) => {
+ if (req.method === "OPTIONS") {
+ return new Response(null, { headers: corsHeaders });
+ }
+
+ try {
+ const apiKey = Deno.env.get("MODRINTH_API_KEY");
+ const url = new URL(req.url);
+ const action = url.searchParams.get("action");
+
+ const headers: Record
= {
+ "User-Agent": "FXCraft/1.0",
+ };
+ if (apiKey) {
+ headers["Authorization"] = apiKey;
+ }
+
+ let modrinthUrl = "";
+
+ switch (action) {
+ case "user_projects": {
+ const username = url.searchParams.get("username") || "fxfelixzero";
+ modrinthUrl = `${MODRINTH_BASE}/user/${username}/projects`;
+ break;
+ }
+ case "search": {
+ const query = url.searchParams.get("query") || "";
+ const facets = url.searchParams.get("facets") || "";
+ const offset = url.searchParams.get("offset") || "0";
+ const limit = url.searchParams.get("limit") || "20";
+ modrinthUrl = `${MODRINTH_BASE}/search?query=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}`;
+ if (facets) {
+ modrinthUrl += `&facets=${encodeURIComponent(facets)}`;
+ }
+ break;
+ }
+ case "project": {
+ const id = url.searchParams.get("id");
+ if (!id) {
+ return new Response(JSON.stringify({ error: "Missing project id" }), {
+ status: 400,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ }
+ modrinthUrl = `${MODRINTH_BASE}/project/${id}`;
+ break;
+ }
+ case "versions": {
+ const id = url.searchParams.get("id");
+ if (!id) {
+ return new Response(JSON.stringify({ error: "Missing project id" }), {
+ status: 400,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ }
+ modrinthUrl = `${MODRINTH_BASE}/project/${id}/version`;
+ break;
+ }
+ default:
+ return new Response(JSON.stringify({ error: "Invalid action" }), {
+ status: 400,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ }
+
+ const response = await fetch(modrinthUrl, { headers });
+ const data = await response.json();
+
+ return new Response(JSON.stringify(data), {
+ status: response.status,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ } catch (error) {
+ return new Response(JSON.stringify({ error: error.message }), {
+ status: 500,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ }
+});
diff --git a/tailwind.config.ts b/tailwind.config.ts
index a1edb69..518a1d6 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -13,6 +13,9 @@ export default {
},
},
extend: {
+ fontFamily: {
+ cairo: ["Cairo", "sans-serif"],
+ },
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
@@ -65,25 +68,22 @@ export default {
},
keyframes: {
"accordion-down": {
- from: {
- height: "0",
- },
- to: {
- height: "var(--radix-accordion-content-height)",
- },
+ from: { height: "0" },
+ to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
- from: {
- height: "var(--radix-accordion-content-height)",
- },
- to: {
- height: "0",
- },
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: "0" },
+ },
+ "fade-in": {
+ from: { opacity: "0", transform: "translateY(10px)" },
+ to: { opacity: "1", transform: "translateY(0)" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
+ "fade-in": "fade-in 0.5s ease-out forwards",
},
},
},