diff --git a/web/app/api/auth/nonce/route.ts b/web/app/api/auth/nonce/route.ts index 29caace..1a2cf9a 100644 --- a/web/app/api/auth/nonce/route.ts +++ b/web/app/api/auth/nonce/route.ts @@ -1,9 +1,9 @@ -import { NextRequest, NextResponse } from 'next/server'; -import crypto from 'crypto'; -import { getRedisClient } from "c:/Users/USER/Desktop/Roster-Rumble/web/lib/redis"; +import { NextRequest, NextResponse } from "next/server"; +import crypto from "crypto"; +import { getRedisClient } from "@/lib/redis"; const generateNonce = (): string => { - return crypto.randomBytes(32).toString('hex'); + return crypto.randomBytes(32).toString("hex"); }; const NONCE_TTL = 300; @@ -11,11 +11,11 @@ const NONCE_TTL = 300; export async function GET(request: NextRequest) { try { const url = request.nextUrl || new URL(request.url); - const walletAddress = url.searchParams.get('walletAddress'); + const walletAddress = url.searchParams.get("walletAddress"); if (!walletAddress) { return NextResponse.json( - { error: 'Missing required parameter: walletAddress' }, + { error: "Missing required parameter: walletAddress" }, { status: 400 } ); } @@ -27,23 +27,23 @@ export async function GET(request: NextRequest) { // Store the nonce in Redis with a 5-minute TTL const redis = await getRedisClient(); const nonceKey = `auth:nonce:${walletAddress}`; - + await redis.set(nonceKey, nonce, { EX: NONCE_TTL }); - + // Return the nonce as JSON return NextResponse.json({ nonce }); } catch (redisError) { - console.error('Redis error:', redisError); + console.error("Redis error:", redisError); return NextResponse.json( - { error: 'Failed to store nonce' }, + { error: "Failed to store nonce" }, { status: 500 } ); } } catch (error) { - console.error('Error generating nonce:', error); + console.error("Error generating nonce:", error); return NextResponse.json( - { error: 'Failed to generate nonce' }, + { error: "Failed to generate nonce" }, { status: 500 } ); } -} \ No newline at end of file +} diff --git a/web/app/api/contests/routes.ts b/web/app/api/contests/routes.ts index 03d01d6..acfcdd7 100644 --- a/web/app/api/contests/routes.ts +++ b/web/app/api/contests/routes.ts @@ -1,17 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { NextRequest, NextResponse } from 'next/server'; -import { z } from 'zod'; -import jwt from 'jsonwebtoken'; -import { Pool } from 'pg'; -import amqp from 'amqplib'; -import { getRedisClient } from '@/lib/redis'; +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import jwt from "jsonwebtoken"; +import { Pool } from "pg"; +import amqp from "amqplib"; +import { getRedisClient } from "@/lib/redis"; const DATABASE_URL = process.env.DATABASE_URL!; const JWT_SECRET = process.env.JWT_SECRET!; const RABBITMQ_URL = process.env.RABBITMQ_URL!; if (!DATABASE_URL || !JWT_SECRET || !RABBITMQ_URL) { - throw new Error('Missing required environment variables.'); + throw new Error("Missing required environment variables."); } // PostgreSQL pool @@ -22,7 +22,7 @@ const contestSchema = z.object({ sport: z.string(), entryFee: z.number().nonnegative(), startsAt: z.string().refine((date: string) => !isNaN(Date.parse(date)), { - message: 'Invalid ISO8601 date string', + message: "Invalid ISO8601 date string", }), maxPlayers: z.number().int().positive(), }); @@ -42,12 +42,12 @@ type ContestQuery = z.infer; // Helper: verify JWT and check admin export function verifyAdmin(token: string | undefined) { if (!token) { - throw new Error('No token provided'); + throw new Error("No token provided"); } try { const payload = jwt.verify(token, JWT_SECRET) as any; - if (payload.role !== 'admin') { - throw new Error('Forbidden'); + if (payload.role !== "admin") { + throw new Error("Forbidden"); } return payload; } catch (err) { @@ -61,7 +61,7 @@ export async function getMqChannel() { if (mqChannel) return mqChannel; const conn = await amqp.connect(RABBITMQ_URL); const channel = await conn.createChannel(); - await channel.assertExchange('contest.events', 'fanout', { durable: true }); + await channel.assertExchange("contest.events", "fanout", { durable: true }); mqChannel = channel; return mqChannel; } @@ -74,7 +74,7 @@ export async function GET(req: NextRequest) { // Build query params object const rawQuery: Record = {}; - for (const key of ['sport', 'minFee', 'maxFee', 'page', 'limit'] as const) { + for (const key of ["sport", "minFee", "maxFee", "page", "limit"] as const) { const v = searchParams.get(key); if (v !== null) rawQuery[key] = v; } @@ -86,7 +86,10 @@ export async function GET(req: NextRequest) { } catch (validationError) { if (validationError instanceof z.ZodError) { return NextResponse.json( - { error: 'Invalid query parameters', details: validationError.errors }, + { + error: "Invalid query parameters", + details: validationError.errors, + }, { status: 400 } ); } @@ -115,7 +118,7 @@ export async function GET(req: NextRequest) { return NextResponse.json(parsed, { status: 200 }); } } catch (cacheErr) { - console.warn('Redis cache error, proceeding without cache:', cacheErr); + console.warn("Redis cache error, proceeding without cache:", cacheErr); } // Build SQL query with filters @@ -140,15 +143,19 @@ export async function GET(req: NextRequest) { paramIndex++; } - const whereClause = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''; + const whereClause = conditions.length + ? `WHERE ${conditions.join(" AND ")}` + : ""; // Get total count const countQuery = `SELECT COUNT(*) AS total FROM contests ${whereClause}`; const countResult = await client.query(countQuery, values); if (!countResult.rows || !countResult.rows[0]) { - throw new Error('Failed to retrieve total count'); + throw new Error("Failed to retrieve total count"); } - const total = countResult.rows[0] ? parseInt(countResult.rows[0].total || '0', 10) : 0; + const total = countResult.rows[0] + ? parseInt(countResult.rows[0].total || "0", 10) + : 0; // Get paginated data const offset = (queryParams.page - 1) * queryParams.limit; @@ -165,7 +172,11 @@ export async function GET(req: NextRequest) { ORDER BY starts_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1} `; - const dataResult = await client.query(dataQuery, [...values, queryParams.limit, offset]); + const dataResult = await client.query(dataQuery, [ + ...values, + queryParams.limit, + offset, + ]); const response = { contests: dataResult.rows || [], @@ -180,19 +191,22 @@ export async function GET(req: NextRequest) { const redis = await getRedisClient(); await redis.setEx(cacheKey, 30, JSON.stringify(response)); } catch (cacheErr) { - console.warn('Failed to cache result:', cacheErr); + console.warn("Failed to cache result:", cacheErr); } return NextResponse.json(response, { status: 200 }); } catch (err: any) { - console.error('GET /contests error:', err.message, err.stack); - return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + console.error("GET /contests error:", err.message, err.stack); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); } finally { if (client) { try { await client.release(); } catch (releaseErr) { - console.warn('Failed to release client:', releaseErr); + console.warn("Failed to release client:", releaseErr); } } } @@ -203,8 +217,10 @@ export async function POST(req: NextRequest) { let client; try { // Admin JWT from Authorization header - const authHeader = req.headers.get('authorization') || ''; - const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : undefined; + const authHeader = req.headers.get("authorization") || ""; + const token = authHeader.startsWith("Bearer ") + ? authHeader.slice(7) + : undefined; verifyAdmin(token); const body = await req.json(); @@ -221,40 +237,45 @@ export async function POST(req: NextRequest) { // Emit event const channel = await getMqChannel(); - const eventPayload = Buffer.from(JSON.stringify({ type: 'ContestCreated', data: contest })); - channel.publish('contest.events', '', eventPayload); + const eventPayload = Buffer.from( + JSON.stringify({ type: "ContestCreated", data: contest }) + ); + channel.publish("contest.events", "", eventPayload); // Invalidate cache after creating new contest try { const redis = await getRedisClient(); - const keys = await redis.keys('contests:*'); + const keys = await redis.keys("contests:*"); if (keys.length > 0) { await redis.del(keys); } } catch (cacheError) { - console.warn('Failed to invalidate cache:', cacheError); + console.warn("Failed to invalidate cache:", cacheError); } return NextResponse.json(contest, { status: 201 }); } catch (err: any) { - if (err.message === 'Forbidden') { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + if (err.message === "Forbidden") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } if (err instanceof z.ZodError) { return NextResponse.json({ errors: err.errors }, { status: 400 }); } - if (err.message === 'No token provided') { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + if (err.message === "No token provided") { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - console.error('POST /contests error:', err); - return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + console.error("POST /contests error:", err); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); } finally { if (client) { try { await client.release(); } catch (releaseErr) { - console.warn('Failed to release client:', releaseErr); + console.warn("Failed to release client:", releaseErr); } } } -} \ No newline at end of file +} diff --git a/web/app/components/ToasterProviderWrapper.tsx b/web/app/components/ToasterProviderWrapper.tsx deleted file mode 100644 index 641528f..0000000 --- a/web/app/components/ToasterProviderWrapper.tsx +++ /dev/null @@ -1,8 +0,0 @@ -'use client'; - -import React from 'react'; -import { ToasterProvider } from '../context/ToasterContext'; - -export function ToasterProviderWrapper({ children }: { children: React.ReactNode }) { - return {children}; -} \ No newline at end of file diff --git a/web/app/context/ToasterContext.tsx b/web/app/context/ToasterContext.tsx index 7296358..6a5c0c2 100644 --- a/web/app/context/ToasterContext.tsx +++ b/web/app/context/ToasterContext.tsx @@ -1,15 +1,19 @@ -'use client'; +"use client"; -import React, { createContext, useContext, ReactNode } from 'react'; -import { useToastStore, ToastStatus } from '../../lib/toastStore'; -import { Toaster } from '../components/Toaster'; +import React, { createContext, useContext, ReactNode } from "react"; +import { useToastStore, ToastStatus } from "../../lib/toastStore"; +import { Toaster } from "@/components/Toaster"; interface ToasterContextValue { - showToast: (message: string, status: ToastStatus, options?: { - txHash?: string; - network?: string; - autoDismiss?: number; - }) => string; + showToast: ( + message: string, + status: ToastStatus, + options?: { + txHash?: string; + network?: string; + autoDismiss?: number; + } + ) => string; updateToast: (id: string, message: string, status: ToastStatus) => void; dismissToast: (id: string) => void; pendingTransactions: Array<{ @@ -20,10 +24,19 @@ interface ToasterContextValue { }>; } -const ToasterContext = createContext(undefined); +const ToasterContext = createContext( + undefined +); -export const ToasterProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const { toasts, addToast, updateToast: updateStoreToast, removeToast } = useToastStore(); +export const ToasterProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const { + toasts, + addToast, + updateToast: updateStoreToast, + removeToast, + } = useToastStore(); const showToast = (message: string, status: ToastStatus, options = {}) => { return addToast(message, status, options); @@ -38,16 +51,18 @@ export const ToasterProvider: React.FC<{ children: ReactNode }> = ({ children }) }; const pendingTransactions = toasts - .filter(toast => toast.status === 'pending' && toast.txHash) - .map(toast => ({ + .filter((toast) => toast.status === "pending" && toast.txHash) + .map((toast) => ({ id: toast.id, - message: toast.message, + message: toast.message, txHash: toast.txHash, - network: toast.network + network: toast.network, })); return ( - + {children} @@ -57,7 +72,7 @@ export const ToasterProvider: React.FC<{ children: ReactNode }> = ({ children }) export const useToaster = (): ToasterContextValue => { const context = useContext(ToasterContext); if (context === undefined) { - throw new Error('useToaster must be used within a ToasterProvider'); + throw new Error("useToaster must be used within a ToasterProvider"); } return context; -}; \ No newline at end of file +}; diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 016ad4c..ddf1162 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,8 +1,7 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; import "./globals.css"; -import { ToasterProviderWrapper } from './components/ToasterProviderWrapper'; - +import { ToasterProviderWrapper } from "@/components/ToasterProviderWrapper"; const geistSans = localFont({ src: "./fonts/GeistVF.woff", @@ -16,8 +15,8 @@ const geistMono = localFont({ }); export const metadata: Metadata = { - title: 'Toast System Demo', - description: 'Blockchain transaction toast notification system', + title: "Toast System Demo", + description: "Blockchain transaction toast notification system", }; export default function RootLayout({ @@ -30,9 +29,7 @@ export default function RootLayout({ - - {children} - + {children} ); diff --git a/web/app/page.tsx b/web/app/page.tsx index 433c8aa..0deaf0f 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,101 +1,30 @@ -import Image from "next/image"; +// import { Footer } from "./components/footer"; +import { Footer } from "@/components/footer"; +import { Header } from "@/components/header"; +import { AppPromotionSection } from "@/components/landing-page/app-promotion-section"; +import { ContactSection } from "@/components/landing-page/contact-section"; +import { EasyStepsSection } from "@/components/landing-page/easy-steps-section"; +import { FAQSection } from "@/components/landing-page/faq-section"; +import { HeroCarousel } from "@/components/landing-page/hero-carousel"; +import { StatsSection } from "@/components/landing-page/stats-section"; +import { TestimonialsSection } from "@/components/landing-page/testimonials-section"; +import { WinnersSection } from "@/components/landing-page/winners-section"; -export default function Home() { +export default function HomePage() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
- - + ); } diff --git a/web/app/components/MobileNav.tsx b/web/components/MobileNav.tsx similarity index 100% rename from web/app/components/MobileNav.tsx rename to web/components/MobileNav.tsx diff --git a/web/app/components/Toaster.tsx b/web/components/Toaster.tsx similarity index 65% rename from web/app/components/Toaster.tsx rename to web/components/Toaster.tsx index b958af0..c7cbb68 100644 --- a/web/app/components/Toaster.tsx +++ b/web/components/Toaster.tsx @@ -1,9 +1,14 @@ -'use client'; +"use client"; -import React, { useEffect } from 'react'; -import * as Toast from '@radix-ui/react-toast'; -import { useToastStore, getExplorerUrl } from '../../lib/toastStore'; -import { X as XIcon, Check as CheckIcon, RefreshCw as RefreshIcon, ExternalLink as ExternalLinkIcon } from 'lucide-react'; +import React, { useEffect } from "react"; +import * as Toast from "@radix-ui/react-toast"; +import { useToastStore, getExplorerUrl } from "@/lib/toastStore"; +import { + X as XIcon, + Check as CheckIcon, + RefreshCw as RefreshIcon, + ExternalLink as ExternalLinkIcon, +} from "lucide-react"; const DEFAULT_AUTO_DISMISS = 5000; @@ -13,11 +18,11 @@ export const Toaster = () => { // Set up auto-dismiss useEffect(() => { toasts.forEach((toast) => { - if (toast.status !== 'pending' && (toast.autoDismiss !== 0)) { + if (toast.status !== "pending" && toast.autoDismiss !== 0) { const timeout = setTimeout(() => { removeToast(toast.id); }, toast.autoDismiss || DEFAULT_AUTO_DISMISS); - + return () => clearTimeout(timeout); } }); @@ -25,14 +30,16 @@ export const Toaster = () => { const handleRetry = (toastId: string) => { // In a real app, you'd call your transaction function here - updateToast(toastId, { status: 'pending' }); - + updateToast(toastId, { status: "pending" }); + // This is just for demo purposes setTimeout(() => { const success = Math.random() > 0.5; - updateToast(toastId, { - status: success ? 'success' : 'error', - message: success ? 'Transaction successful!' : 'Transaction failed. Try again?' + updateToast(toastId, { + status: success ? "success" : "error", + message: success + ? "Transaction successful!" + : "Transaction failed. Try again?", }); }, 2000); }; @@ -41,12 +48,14 @@ export const Toaster = () => {
{toasts.map((toast) => ( - { @@ -54,22 +63,26 @@ export const Toaster = () => { }} >
- {toast.status === 'pending' && ( + {toast.status === "pending" && (
)} - {toast.status === 'success' && } - {toast.status === 'error' && } + {toast.status === "success" && ( + + )} + {toast.status === "error" && ( + + )}
- + {toast.message} - +
- {toast.status === 'error' && ( - )} - - {toast.status === 'success' && toast.txHash && ( - { )} - -
))} - +
); -}; \ No newline at end of file +}; diff --git a/web/components/ToasterProviderWrapper.tsx b/web/components/ToasterProviderWrapper.tsx new file mode 100644 index 0000000..ce901af --- /dev/null +++ b/web/components/ToasterProviderWrapper.tsx @@ -0,0 +1,12 @@ +"use client"; + +import React from "react"; +import { ToasterProvider } from "@/app/context/ToasterContext"; + +export function ToasterProviderWrapper({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/web/components/footer.tsx b/web/components/footer.tsx new file mode 100644 index 0000000..fcb2dbd --- /dev/null +++ b/web/components/footer.tsx @@ -0,0 +1,298 @@ +import Link from "next/link"; +import { + Facebook, + Twitter, + Instagram, + Linkedin, + Send, + Shield, + Award, + CreditCard, + Phone, + Mail, + MapPin, + Download, + Star, + Users, + Trophy, + ChevronRight, +} from "lucide-react"; + +/** + * Footer Component + * + * Ultra-modern footer with advanced design and perfect responsiveness + * Features gradient backgrounds, hover animations, and organized sections + * Links arranged in compact columns for better space utilization + */ +export function Footer() { + // All links organized by category + const footerLinks = [ + { + category: "Quick Links", + links: [ + { name: "Home", href: "/" }, + { name: "How to Play", href: "/how-to-play" }, + { name: "Contests", href: "/contests" }, + { name: "Winners", href: "/winners" }, + { name: "Points System", href: "/points" }, + { name: "Tips & Tricks", href: "/tips" }, + { name: "Fantasy Cricket League", href: "/cricket-league" }, + { name: "Help", href: "/help" }, + ], + }, + { + category: "Games", + links: [ + { name: "Fantasy Cricket", href: "/cricket" }, + { name: "Fantasy Football", href: "/football" }, + { name: "Cricket Schedule", href: "/schedule" }, + { name: "TATA IPL 2025", href: "/ipl" }, + { name: "T20 World Cup", href: "/t20" }, + { name: "Live Matches", href: "/live" }, + { name: "Today's Match Prediction", href: "/prediction" }, + { name: "Cricket Records", href: "/records" }, + ], + }, + { + category: "Support", + links: [ + { name: "Help Center", href: "/help" }, + { name: "Contact Us", href: "/contact" }, + { name: "Withdraw Cash", href: "/withdraw" }, + { name: "Responsible Gaming", href: "/responsible" }, + { name: "Terms of Service", href: "/terms" }, + { name: "Privacy Policy", href: "/privacy" }, + { name: "About Us", href: "/about" }, + { name: "Blog", href: "/blog" }, + ], + }, + ]; + + const socialLinks = [ + { + icon: Facebook, + href: "#", + name: "Facebook", + color: "from-blue-600 to-blue-700", + }, + { + icon: Twitter, + href: "#", + name: "Twitter", + color: "from-sky-500 to-sky-600", + }, + { + icon: Instagram, + href: "#", + name: "Instagram", + color: "from-pink-500 to-purple-600", + }, + { + icon: Linkedin, + href: "#", + name: "LinkedIn", + color: "from-blue-700 to-blue-800", + }, + { + icon: Send, + href: "#", + name: "Telegram", + color: "from-blue-500 to-blue-600", + }, + ]; + + const stats = [ + { icon: Users, value: "10M+", label: "Active Players" }, + { icon: Trophy, value: "₹500Cr+", label: "Prizes Won" }, + { icon: Star, value: "4.8", label: "App Rating" }, + ]; + + return ( +
+ {/* Background Pattern */} +
+
+
+ +
+ {/* Top Section with Stats */} +
+
+
+ {stats.map((stat, index) => ( +
+
+ +
+ {stat.value} +
+
{stat.label}
+
+
+ ))} +
+
+
+ + {/* Main Footer Content */} +
+
+ {/* Brand Section */} +
+
+
+
+
+
+
+
+ + ROSTERRUMBLE + + + Fantasy Sports Platform + +
+
+ +

+ India's most trusted fantasy sports platform. Create your + dream team, join contests, and win exciting prizes every day. + Experience the thrill of fantasy cricket and football. +

+ +
+
+ + +91 8010400200 +
+
+ + support@rosterrumble.com +
+
+ + Mumbai, India +
+
+ + {/* Social Links */} +
+

Follow Us

+
+ {socialLinks.map((social) => ( + + + + ))} +
+
+
+ + {/* Links Section - Compact Side-by-Side Layout */} +
+ {footerLinks.map((section) => ( +
+

+ {section.category} +

+
+ {section.links.map((link, index) => ( + + + {link.name} + + ))} +
+
+ ))} +
+ + {/* Download & Security Section */} +
+ {/* Download App */} +
+

+ Download App +

+
+ + +
+
+ + {/* Security Badges */} +
+

+ Security +

+
+
+ +
+ SSL Secure +
+
+
+ +
+ Certified +
+
+
+ +
+ Safe Payments +
+
+
+
+
+
+
+ + {/* Bottom Section */} +
+
+
+
+

+ © {new Date().getFullYear()} Play Games24x7 Pvt. Ltd. All + Rights Reserved. +

+
+ +
+ *18+ only. T&C Apply. + | + Play responsibly + | + Fantasy sports involves financial risk +
+
+
+
+
+
+ ); +} diff --git a/web/components/header.tsx b/web/components/header.tsx new file mode 100644 index 0000000..9df5402 --- /dev/null +++ b/web/components/header.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; + +/** + * Header Component + * + * Professional navigation header with beautiful mobile menu + * Mobile menu shows on screens smaller than 1280px (xl breakpoint) + * Features outside click to close and backdrop blur + */ +export function Header() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + const checkScreenSize = () => { + setIsMobile(window.innerWidth < 1280); // xl breakpoint - shows menu earlier + }; + + checkScreenSize(); + window.addEventListener("resize", checkScreenSize); + + // Handle outside click + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsMenuOpen(false); + } + }; + + if (isMenuOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + window.removeEventListener("resize", checkScreenSize); + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isMenuOpen]); + + const menuItems = [ + { name: "Home", href: "/", isActive: true }, + { name: "How to Play", href: "/how-to-play" }, + { name: "Contests", href: "/contests" }, + { name: "Winners", href: "/winners" }, + { name: "About Us", href: "/about" }, + { name: "Support", href: "/support" }, + ]; + + return ( +
+
+
+ {/* Hamburger Menu - Shows on mobile/tablet (earlier breakpoint) */} + {isMobile && ( + + )} + + {/* Logo - Center on mobile, left on desktop */} + +
+ + ROSTERRUMBLE + +
+ + + {/* Desktop Navigation - Hidden on mobile */} + {!isMobile && ( + + )} + + {/* Auth Section - Right */} +
+
+ Not a Member Yet? + + Register Now + +
+ +
+
+ + {/* Modern Mobile Menu Dropdown */} + {isMenuOpen && isMobile && ( + <> + {/* Backdrop Blur Overlay */} +
setIsMenuOpen(false)} + /> + + {/* Dropdown Menu */} +
+
+ +
+
+ + )} +
+
+ ); +} diff --git a/web/components/landing-page/app-promotion-section.tsx b/web/components/landing-page/app-promotion-section.tsx new file mode 100644 index 0000000..ce62433 --- /dev/null +++ b/web/components/landing-page/app-promotion-section.tsx @@ -0,0 +1,86 @@ +import { Button } from "@/components/ui/button"; +import { Star } from "lucide-react"; +import Image from "next/image"; + +/** + * App Promotion Section + * + * Showcases the mobile app with features and app screenshot + * Clean and advanced layout without childish elements + */ +export function AppPromotionSection() { + const features = [ + "Lightning-fast gameplay experience", + "Real-time match updates and notifications", + "Secure payment gateway integration", + "Expert analysis and player insights", + "24/7 customer support", + ]; + + return ( +
+
+
+ {/* Content */} +
+
+

+ Play Fantasy Cricket on RosterRumble App +

+
+
+ +
+

+ Want to enjoy fantasy games like cricket but just can't + manage the time? Well, RosterRumble.com is the answer you need. + This is the place where your favorite fantasy sports come alive. +

+ +

+ RosterRumble.com brings the best fantasy games at your + fingertips. It is committed to offering the same gameplay + experience with millions of players. Register with us, pick a + game and win cash daily. +

+ +

+ Fantasy cricket and football boost your skill and let you win + real cash rewards. We offer a safe and secured platform to enjoy + online fantasy sports at your leisure. +

+
+ + {/* Features List */} +
+ {features.map((feature, index) => ( +
+ + {feature} +
+ ))} +
+ + +
+ + {/* Mobile App Image */} +
+
+ RosterRumble Mobile App Screenshot +
+
+
+
+
+ ); +} diff --git a/web/components/landing-page/contact-section.tsx b/web/components/landing-page/contact-section.tsx new file mode 100644 index 0000000..6958be9 --- /dev/null +++ b/web/components/landing-page/contact-section.tsx @@ -0,0 +1,69 @@ +import { Phone, Smartphone, ArrowRight } from "lucide-react"; + +/** + * Missed Call Section + * + * Enhanced call-to-action section with phone number and app download + * Advanced design with subtle animations and visual elements + */ +export function ContactSection() { + return ( +
+ {/* Background Pattern */} +
+
+
+ +
+
+ {/* Missed Call Info */} +
+
+
+
+ +
+
+
+ + GIVE A MISSED CALL + +
+ + 8010400200 + +
+
+
+
+ + {/* App Download Button */} +
+
+
+
+ +
+
+
+ To Download Your +
+
+ ROSTER + RUMBLE + APP + +
+
+
+
+
+
+
+ ); +} diff --git a/web/components/landing-page/easy-steps-section.tsx b/web/components/landing-page/easy-steps-section.tsx new file mode 100644 index 0000000..d9cb63b --- /dev/null +++ b/web/components/landing-page/easy-steps-section.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useState } from "react"; +import { Play, Target, Users, Trophy } from "lucide-react"; + +/** + * Easy Steps Section + * + * Red background section with playable video and step guide + */ +export function EasyStepsSection() { + const [isVideoPlaying, setIsVideoPlaying] = useState(false); + + const steps = [ + { + icon: Target, + title: "Select a Match", + description: + "Select an upcoming match of your choice from our extensive list", + }, + { + icon: Users, + title: "Create your own team", + description: + "Use your sports knowledge and check player stats to create a winning team using 100 credits", + }, + { + icon: Trophy, + title: "Join Free & Cash Contests", + description: + "Participate in Cash or Practice Contests and win exciting prizes", + }, + ]; + + const handleVideoPlay = () => { + setIsVideoPlaying(true); + // You can add actual video play logic here + }; + + return ( +
+
+

+ 3 Easy Steps +

+ +
+ {/* Video Section */} +
+
+ {!isVideoPlaying ? ( + // Video Thumbnail/Placeholder +
+
+
+ +
+
+

+ How to play on RosterRumble +

+

+ Click to play video +

+
+
+ ) : ( + // Actual Video Element + + )} +
+ + {/* Video Title Overlay */} +
+

+ How to play on RosterRumble | Tutorial +

+
+
+ + {/* Steps Section */} +
+ {steps.map((step, index) => ( +
+
+ +
+
+

+ {step.title} +

+

+ {step.description} +

+
+
+ ))} +
+
+
+
+ ); +} diff --git a/web/components/landing-page/faq-section.tsx b/web/components/landing-page/faq-section.tsx new file mode 100644 index 0000000..24798a7 --- /dev/null +++ b/web/components/landing-page/faq-section.tsx @@ -0,0 +1,132 @@ +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +/** + * FAQ Section + * + * Comprehensive FAQ with tabbed interface for Cricket/Football + * Modern accordion design with smooth animations + */ +export function FAQSection() { + const cricketFAQs = [ + { + question: "What is Fantasy Cricket?", + answer: + "Fantasy Cricket is a virtual game where you create your own team of real cricket players and earn points based on their actual performance in live matches.", + }, + { + question: "How to Download the Fantasy Cricket App?", + answer: + "You can download the RosterRumble app from Google Play Store for Android devices or App Store for iOS devices.", + }, + { + question: + "Play Daily Fantasy Cricket Tournaments & Win Real Cash on RosterRumble", + answer: + "Join daily contests, create your team, and compete with millions of players to win exciting cash prizes.", + }, + { + question: "Benefits of Playing Fantasy Sports on RosterRumble App", + answer: + "Enjoy secure gameplay, instant withdrawals, expert analysis, daily bonuses, and 24/7 customer support.", + }, + { + question: "Can I Play Practice Fantasy Cricket Games on RosterRumble?", + answer: + "Yes, you can play practice games to improve your skills before joining cash contests.", + }, + ]; + + const additionalFAQs = [ + { + question: "Why Fantasy Games?", + answer: + "Fantasy games combine your sports knowledge with strategy to win real cash prizes while enjoying your favorite sports.", + }, + { + question: "Unique Features of RosterRumble", + answer: + "We offer unique features like expert tips, live match updates, instant withdrawals, and secure gameplay.", + }, + { + question: "Playing Fantasy Cricket & Football is Safe, Secure & Legal", + answer: + "Yes, fantasy sports are completely legal in India and we ensure 100% secure transactions and fair gameplay.", + }, + ]; + + return ( +
+
+
+ + + + CRICKET + + + FOOTBALL + + + + + + {cricketFAQs.map((faq, index) => ( + + + {faq.question} + + + {faq.answer} + + + ))} + + + + +
+

Football FAQs coming soon...

+
+
+
+ + {/* Additional FAQ Section */} +
+ + {additionalFAQs.map((faq, index) => ( + + + {faq.question} + + + {faq.answer} + + + ))} + +
+
+
+
+ ); +} diff --git a/web/components/landing-page/hero-carousel.tsx b/web/components/landing-page/hero-carousel.tsx new file mode 100644 index 0000000..8afcc9a --- /dev/null +++ b/web/components/landing-page/hero-carousel.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useState, useEffect } from "react"; + +/** + * Hero Carousel Component + * + * Full-width image carousel with smooth transitions + * Auto-plays with manual navigation controls + * Responsive design with proper aspect ratios + */ +export function HeroCarousel() { + const [currentSlide, setCurrentSlide] = useState(0); + + const slides = [ + { + id: 1, + image: "/soccer-image.jpeg", + alt: "Play with Champions - Fantasy Cricket", + }, + { + id: 2, + image: "/soccer-image1.jpeg", + alt: "Win Big Prizes - RosterRumble", + }, + { + id: 3, + image: "/soccer-image2.jpeg", + alt: "Fantasy Sports Experience", + }, + ]; + + useEffect(() => { + const interval = setInterval(() => { + setCurrentSlide((prev) => (prev + 1) % slides.length); + }, 5000); + return () => clearInterval(interval); + }, [slides.length]); + + return ( +
+ {slides.map((slide, index) => ( +
+
+
+ ))} + + {/* Navigation Dots */} +
+ {slides.map((_, index) => ( +
+
+ ); +} diff --git a/web/components/landing-page/stats-section.tsx b/web/components/landing-page/stats-section.tsx new file mode 100644 index 0000000..ed86b29 --- /dev/null +++ b/web/components/landing-page/stats-section.tsx @@ -0,0 +1,64 @@ +import { Star, Users, IndianRupee } from "lucide-react" + +/** + * Stats Section + * + * Displays key platform statistics in modern card design + * Features hover animations and responsive grid layout + */ +export function StatsSection() { + const stats = [ + { + icon: Star, + number: "4.3", + title: "4.3 OUT OF 5", + subtitle: "USER RATING", + color: "text-yellow-500", + }, + { + icon: Users, + number: "4+", + title: "CRORE", + subtitle: "TOTAL USERS", + color: "text-blue-500", + }, + { + icon: IndianRupee, + number: "₹500", + title: "CRORE+", + subtitle: "PRIZES WON", + color: "text-green-500", + }, + ] + + return ( +
+
+
+ {stats.map((stat, index) => ( +
+
+ {/* Icon & Number */} +
+
+ + {stat.number} +
+
+ + {/* Text Content */} +
+
{stat.title}
+
{stat.subtitle}
+
+
+
+ ))} +
+
+
+ ) +} diff --git a/web/components/landing-page/testimonials-section.tsx b/web/components/landing-page/testimonials-section.tsx new file mode 100644 index 0000000..470bd9a --- /dev/null +++ b/web/components/landing-page/testimonials-section.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { ChevronLeft, ChevronRight, Quote } from "lucide-react"; +import Image from "next/image"; + +/** + * Testimonials Section + * + * Auto-rotating testimonials with manual navigation + * Showcases user success stories and winnings + */ +export function TestimonialsSection() { + const [currentIndex, setCurrentIndex] = useState(0); + + const testimonials = [ + { + name: "Ramesh Singh", + location: "Ghaziabad, Uttar Pradesh", + winnings: "₹1 Crore", + text: "I won 1 crore on RosterRumble & I am very happy. My experience with playing RosterRumble for last 2 years has been amazing. I recommend it very highly to every sports fantasy player.", + avatar: "/testimonial-img2.jpg", + }, + { + name: "Pradip Apte", + location: "Latur, Maharashtra", + winnings: "₹1 Crore", + text: "I've been an avid cricket follower since my childhood. I got to know about RosterRumble from a YouTube ad. Recently won 1 Crore on RosterRumble! The app is easy, secure, and smooth!", + avatar: "/testimonial-img2.jpg", + }, + { + name: "Anjali Sharma", + location: "Mumbai, Maharashtra", + winnings: "₹50 Lakh", + text: "As a working professional, RosterRumble fits perfectly into my schedule. The interface is user-friendly and I've been consistently winning. Highly recommended for cricket enthusiasts!", + avatar: "/testimonial-img2.jpg", + }, + { + name: "Vikash Kumar", + location: "Patna, Bihar", + winnings: "₹75 Lakh", + text: "Started playing RosterRumble 6 months ago and already won 75 lakhs! The expert analysis feature really helps in team selection. Best fantasy app in India!", + avatar: "/testimonial-img2.jpg", + }, + { + name: "Deepak Mehta", + location: "Jaipur, Rajasthan", + winnings: "₹2 Crore", + text: "RosterRumble changed my life! Won 2 crores in the IPL season. The platform is transparent, secure, and offers great contests. Thank you RosterRumble team!", + avatar: "/testimonial-img2.jpg", + }, + { + name: "Priya Nair", + location: "Kochi, Kerala", + winnings: "₹30 Lakh", + text: "Being a cricket fan, RosterRumble allows me to use my knowledge and earn money. Won 30 lakhs so far and the withdrawal process is super quick!", + avatar: "/testimonial-img2.jpg", + }, + ]; + + // Auto-rotation effect + useEffect(() => { + const interval = setInterval(() => { + setCurrentIndex( + (prev) => (prev + 1) % Math.ceil(testimonials.length / 2) + ); + }, 5000); // Change every 5 seconds + + return () => clearInterval(interval); + }, [testimonials.length]); + + const nextTestimonial = () => { + setCurrentIndex((prev) => (prev + 1) % Math.ceil(testimonials.length / 2)); + }; + + const prevTestimonial = () => { + setCurrentIndex( + (prev) => + (prev - 1 + Math.ceil(testimonials.length / 2)) % + Math.ceil(testimonials.length / 2) + ); + }; + + const getCurrentTestimonials = () => { + const startIndex = currentIndex * 2; + return testimonials.slice(startIndex, startIndex + 2); + }; + + return ( +
+
+
+

+ Players Love RosterRumble +

+
+
+ +
+
+ {getCurrentTestimonials().map((testimonial, index) => ( +
+ + +
+ {testimonial.name} +
+

+ {testimonial.name} +

+

+ {testimonial.location} +

+

+ Winnings {testimonial.winnings} +

+

+ "{testimonial.text}" +

+
+
+
+ ))} +
+ + {/* Navigation Arrows */} + + + + + {/* Dots Indicator */} +
+ {Array.from({ length: Math.ceil(testimonials.length / 2) }).map( + (_, index) => ( +
+
+
+
+ ); +} diff --git a/web/components/landing-page/winners-section.tsx b/web/components/landing-page/winners-section.tsx new file mode 100644 index 0000000..da9e45a --- /dev/null +++ b/web/components/landing-page/winners-section.tsx @@ -0,0 +1,54 @@ +/** + * Winners Section + * + * Clean showcase of crorepati winners with modern card design + */ +export function WinnersSection() { + const winners = [ + { name: "DARSHAN BISHT", amount: "₹1 CRORE", location: "UTTARAKHAND" }, + { name: "PRADIP APTE", amount: "₹1 CRORE", location: "MAHARSHTRA" }, + { name: "UPENDRA KUMAR", amount: "₹5 CRORE", location: "UTTAR PRADESH" }, + { name: "GOUR BHAIRAGYA", amount: "₹1 CRORE", location: "WEST BENGAL" }, + ]; + + return ( +
+
+
+
+ {winners.map((winner, index) => ( +
+
+
+
+ + {winner.name + .split(" ") + .map((n) => n[0]) + .join("")} + +
+
+
+ 👑 +
+
+ +

+ {winner.name} +

+
+ {winner.amount} +
+

{winner.location}

+
+ ))} +
+
+
+
+ ); +} diff --git a/web/components/ui/accordion.tsx b/web/components/ui/accordion.tsx new file mode 100644 index 0000000..2f55a32 --- /dev/null +++ b/web/components/ui/accordion.tsx @@ -0,0 +1,57 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/web/components/ui/button.tsx b/web/components/ui/button.tsx new file mode 100644 index 0000000..65d4fcd --- /dev/null +++ b/web/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/web/components/ui/tabs.tsx b/web/components/ui/tabs.tsx new file mode 100644 index 0000000..0f4caeb --- /dev/null +++ b/web/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/web/package-lock.json b/web/package-lock.json index 18dad24..aee24cf 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,9 @@ "name": "web", "version": "0.1.0", "dependencies": { + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.13", "amqplib": "^0.10.8", "class-variance-authority": "^0.7.1", @@ -19,8 +22,8 @@ "lucide-react": "^0.488.0", "next": "14.2.23", "pg": "^8.15.6", - "react": "^18", - "react-dom": "^18", + "react": "^18.2.0", + "react-dom": "^18.2.0", "redis": "^5.1.1", "starknet": "^6.24.1", "tailwind-merge": "^3.2.0", @@ -31,9 +34,11 @@ "devDependencies": { "@swc/core": "^1.11.29", "@swc/jest": "^0.2.38", + "@types/amqplib": "^0.10.7", "@types/ioredis": "^4.28.10", "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20", + "@types/pg": "^8.15.4", "@types/react": "^18", "@types/react-dom": "^18", "dotenv": "^16.5.0", @@ -1516,6 +1521,139 @@ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", + "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", + "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", @@ -1542,6 +1680,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1572,6 +1728,21 @@ } } }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", @@ -1599,6 +1770,24 @@ } } }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", @@ -1670,7 +1859,7 @@ } } }, - "node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", @@ -1688,6 +1877,157 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.13.tgz", @@ -2215,6 +2555,16 @@ "node": ">= 10" } }, + "node_modules/@types/amqplib": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.7.tgz", + "integrity": "sha512-IVj3avf9AQd2nXCx0PGk/OYq7VmHiyNxWFSb5HhU9ATh+i+gHWvVcljFTcTWQ/dyHJCTrzCixde+r/asL2ErDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2346,6 +2696,18 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/pg": { + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", + "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -8253,7 +8615,7 @@ "integrity": "sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==", "license": "MIT" }, - "node_modules/pg/node_modules/pg-types": { + "node_modules/pg-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", @@ -8269,45 +8631,6 @@ "node": ">=4" } }, - "node_modules/pg/node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pg/node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pg/node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pg/node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pgpass": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", @@ -8535,6 +8858,45 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/web/package.json b/web/package.json index d748a26..3c890e4 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,9 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.13", "amqplib": "^0.10.8", "class-variance-authority": "^0.7.1", @@ -22,8 +25,8 @@ "lucide-react": "^0.488.0", "next": "14.2.23", "pg": "^8.15.6", - "react": "^18", - "react-dom": "^18", + "react": "^18.2.0", + "react-dom": "^18.2.0", "redis": "^5.1.1", "starknet": "^6.24.1", "tailwind-merge": "^3.2.0", @@ -34,9 +37,11 @@ "devDependencies": { "@swc/core": "^1.11.29", "@swc/jest": "^0.2.38", + "@types/amqplib": "^0.10.7", "@types/ioredis": "^4.28.10", "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20", + "@types/pg": "^8.15.4", "@types/react": "^18", "@types/react-dom": "^18", "dotenv": "^16.5.0", diff --git a/web/public/appScreenshot.png b/web/public/appScreenshot.png new file mode 100644 index 0000000..1a0afbb Binary files /dev/null and b/web/public/appScreenshot.png differ diff --git a/web/public/soccer-image.jpeg b/web/public/soccer-image.jpeg new file mode 100644 index 0000000..c33f43e Binary files /dev/null and b/web/public/soccer-image.jpeg differ diff --git a/web/public/soccer-image1.jpeg b/web/public/soccer-image1.jpeg new file mode 100644 index 0000000..610a5de Binary files /dev/null and b/web/public/soccer-image1.jpeg differ diff --git a/web/public/soccer-image2.jpeg b/web/public/soccer-image2.jpeg new file mode 100644 index 0000000..6190d35 Binary files /dev/null and b/web/public/soccer-image2.jpeg differ diff --git a/web/public/testimonial-img2.jpg b/web/public/testimonial-img2.jpg new file mode 100644 index 0000000..d96139e Binary files /dev/null and b/web/public/testimonial-img2.jpg differ diff --git a/web/tailwind.config.ts b/web/tailwind.config.ts index ebc9f38..19b5077 100644 --- a/web/tailwind.config.ts +++ b/web/tailwind.config.ts @@ -55,6 +55,28 @@ const config: Config = { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' } } },