From d74c5a27a96373ab311fbd0c012f0d151bf5942d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:30:13 +0000 Subject: [PATCH] Added Supabase integration setup Co-authored-by: felix-fx-top <253056634+felix-fx-top@users.noreply.github.com> --- index.html | 24 ++-- src/App.css | 43 +------ src/App.tsx | 9 +- src/components/Footer.tsx | 17 +++ src/components/HeroSection.tsx | 41 ++++++ src/components/ModCard.tsx | 72 +++++++++++ src/components/ModGrid.tsx | 67 ++++++++++ src/components/Navbar.tsx | 94 ++++++++++++++ src/index.css | 105 ++++----------- src/lib/api.ts | 42 ++++++ src/pages/Index.tsx | 34 +++-- src/pages/ModDetails.tsx | 172 +++++++++++++++++++++++++ src/pages/SearchPage.tsx | 84 ++++++++++++ supabase/functions/fetch-mods/index.ts | 88 +++++++++++++ tailwind.config.ts | 24 ++-- 15 files changed, 758 insertions(+), 158 deletions(-) create mode 100644 src/components/Footer.tsx create mode 100644 src/components/HeroSection.tsx create mode 100644 src/components/ModCard.tsx create mode 100644 src/components/ModGrid.tsx create mode 100644 src/components/Navbar.tsx create mode 100644 src/lib/api.ts create mode 100644 src/pages/ModDetails.tsx create mode 100644 src/pages/SearchPage.tsx create mode 100644 supabase/functions/fetch-mods/index.ts diff --git a/index.html b/index.html index 38a5fa7..0f65376 100644 --- a/index.html +++ b/index.html @@ -1,24 +1,18 @@ - + - - Lovable App - - - - - - + FXCraft - إضافات ماين كرافت + + + + - - - - - + + + -
diff --git a/src/App.css b/src/App.css index b9d355d..cd6b3cc 100644 --- a/src/App.css +++ b/src/App.css @@ -1,42 +1 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} +/* FXCraft global styles */ diff --git a/src/App.tsx b/src/App.tsx index ed7ca23..004650b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,8 +3,10 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; -import Index from "./pages/Index.tsx"; -import NotFound from "./pages/NotFound.tsx"; +import Index from "./pages/Index"; +import ModDetails from "./pages/ModDetails"; +import SearchPage from "./pages/SearchPage"; +import NotFound from "./pages/NotFound"; const queryClient = new QueryClient(); @@ -16,7 +18,8 @@ const App = () => ( } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + } /> } /> diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..947e33f --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,17 @@ +const Footer = () => { + return ( + + ); +}; + +export default Footer; diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx new file mode 100644 index 0000000..f1588b5 --- /dev/null +++ b/src/components/HeroSection.tsx @@ -0,0 +1,41 @@ +import { Link } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Pickaxe, Download } from "lucide-react"; + +const HeroSection = () => { + return ( +
+ {/* Glow effect */} +
+ +
+
+
+ + أفضل إضافات ماين كرافت +
+ +

+ اكتشف عالماً جديداً من{" "} + إضافات ماين كرافت +

+ +

+ تصفّح وحمّل أفضل المودات وحزم الموارد والإضافات لتحسين تجربة لعبك +

+ +
+ +
+
+
+
+ ); +}; + +export default HeroSection; diff --git a/src/components/ModCard.tsx b/src/components/ModCard.tsx new file mode 100644 index 0000000..6977f00 --- /dev/null +++ b/src/components/ModCard.tsx @@ -0,0 +1,72 @@ +import { Link } from "react-router-dom"; +import { Download, Heart } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; + +interface ModCardProps { + id: string; + slug: string; + title: string; + description: string; + iconUrl?: string; + downloads: number; + followers: number; + categories: string[]; + projectType: string; +} + +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 typeLabels: Record = { + mod: "مود", + resourcepack: "حزمة موارد", + datapack: "حزمة بيانات", + shader: "شيدر", + modpack: "حزمة مودات", + plugin: "إضافة سيرفر", +}; + +const ModCard = ({ id, slug, title, description, iconUrl, downloads, followers, categories, projectType }: ModCardProps) => { + return ( + + + +
+ {iconUrl ? ( + {title} + ) : ( +
🧊
+ )} +
+
+
+

+ {title} +

+ + {typeLabels[projectType] || projectType} + +
+

{description}

+
+ + + {formatNumber(downloads)} + + + + {formatNumber(followers)} + +
+
+
+
+ + ); +}; + +export default ModCard; diff --git a/src/components/ModGrid.tsx b/src/components/ModGrid.tsx new file mode 100644 index 0000000..5bdaf67 --- /dev/null +++ b/src/components/ModGrid.tsx @@ -0,0 +1,67 @@ +import ModCard from "./ModCard"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface Mod { + id: string; + slug: string; + title: string; + description: string; + icon_url?: string; + downloads: number; + followers: number; + categories: string[]; + project_type: string; +} + +interface ModGridProps { + mods: Mod[]; + isLoading?: boolean; + title?: string; +} + +const ModGrid = ({ mods, isLoading, title }: ModGridProps) => { + return ( +
+ {title && ( +

{title}

+ )} + {isLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ +
+ + + +
+
+ ))} +
+ ) : mods.length === 0 ? ( +
+ لا توجد إضافات حالياً +
+ ) : ( +
+ {mods.map((mod) => ( + + ))} +
+ )} +
+ ); +}; + +export default ModGrid; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..ec3744f --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,94 @@ +import { Link, useNavigate } from "react-router-dom"; +import { Search, Menu, X } from "lucide-react"; +import { useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +const Navbar = () => { + const [searchQuery, setSearchQuery] = useState(""); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const navigate = useNavigate(); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + if (searchQuery.trim()) { + navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`); + setSearchQuery(""); + } + }; + + return ( + + ); +}; + +export default Navbar; diff --git a/src/index.css b/src/index.css index 4844bbd..13504cf 100644 --- a/src/index.css +++ b/src/index.css @@ -2,95 +2,46 @@ @tailwind components; @tailwind utilities; -/* Definition of the design system. All colors, gradients, fonts, etc should be defined here. -All colors MUST be HSL. -*/ - @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + --background: 240 30% 11%; + --foreground: 210 40% 96%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card: 240 25% 14%; + --card-foreground: 210 40% 96%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover: 240 25% 14%; + --popover-foreground: 210 40% 96%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; + --primary: 122 39% 49%; + --primary-foreground: 240 30% 6%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: 240 20% 20%; + --secondary-foreground: 210 40% 96%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: 240 15% 18%; + --muted-foreground: 215 15% 55%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: 122 39% 49%; + --accent-foreground: 240 30% 6%; - --destructive: 0 84.2% 60.2%; + --destructive: 0 84% 60%; --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: 240 15% 22%; + --input: 240 15% 22%; + --ring: 122 39% 49%; - --radius: 0.5rem; + --radius: 0.75rem; - --sidebar-background: 0 0% 98%; - - --sidebar-foreground: 240 5.3% 26.1%; - - --sidebar-primary: 240 5.9% 10%; - - --sidebar-primary-foreground: 0 0% 98%; - - --sidebar-accent: 240 4.8% 95.9%; - - --sidebar-accent-foreground: 240 5.9% 10%; - - --sidebar-border: 220 13% 91%; - - --sidebar-ring: 217.2 91.2% 59.8%; - } - - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + --sidebar-background: 240 25% 10%; + --sidebar-foreground: 210 40% 96%; + --sidebar-primary: 122 39% 49%; + --sidebar-primary-foreground: 240 30% 6%; + --sidebar-accent: 240 15% 18%; + --sidebar-accent-foreground: 210 40% 96%; + --sidebar-border: 240 15% 22%; + --sidebar-ring: 122 39% 49%; } } @@ -100,6 +51,6 @@ All colors MUST be HSL. } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground font-cairo; } } diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..5b65a0d --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,42 @@ +import { supabase } from "@/integrations/supabase/client"; + +const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL; + +async function callFetchMods(params: Record) { + const searchParams = new URLSearchParams(params); + const { data, error } = await supabase.functions.invoke("fetch-mods", { + method: "GET", + headers: { "Content-Type": "application/json" }, + body: null, + }); + + // Use direct fetch for GET with query 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}`, + }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + return response.json(); +} + +export async function getUserProjects(username = "fxfelixzero") { + return callFetchMods({ action: "user_projects", username }); +} + +export async function searchMods(query: string, offset = 0, limit = 20) { + return callFetchMods({ action: "search", query, offset: String(offset), limit: String(limit) }); +} + +export async function getProject(id: string) { + return callFetchMods({ action: "project", id }); +} + +export async function getProjectVersions(id: string) { + return callFetchMods({ action: "versions", id }); +} diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 8a25453..f2d6d0b 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,16 +1,32 @@ -// Update this page (the content is just a fallback if you fail to update the page) +import { useQuery } from "@tanstack/react-query"; +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"; + +const Index = () => { + const { data: mods, isLoading } = useQuery({ + queryKey: ["user-projects"], + queryFn: () => getUserProjects(), + }); -// IMPORTANT: Fully REPLACE this with your own code -const PlaceholderIndex = () => { - // PLACEHOLDER: Replace this entire return statement with the user's app. - // The inline background color is intentionally not part of the design system. return ( -
- Your app will live here! +
+ +
+ +
+ +
+
+
); }; -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.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 ( +
+ +
+
+

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

+ +
+
+ setQuery(e.target.value)} + placeholder="ابحث عن مود، حزمة موارد، خريطة..." + className="bg-secondary pr-10" + /> + +
+ +
+ + {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", }, }, },