diff --git a/api/index.ts b/api/index.ts new file mode 100644 index 0000000..6cf0d34 --- /dev/null +++ b/api/index.ts @@ -0,0 +1,34 @@ +"use server"; +import baseUrl from "@/baseUrl"; +import routes from "@/routes"; +import { cookies } from "next/headers"; + +export async function authenticateLottery( + phone: string, + code: string, +) { + try { + const res = await fetch(`${baseUrl.QUIZ_SRC}${routes.lotteryActive}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + phone: phone, + key: code, + }), + }); + + if (!res.ok) { + console.log("Authentication failed"); + return undefined; + } + + const result = await res.json(); + + return result; + } catch (err) { + console.log(err); + return undefined; + } +} diff --git a/api/queries.ts b/api/queries.ts index 5d84da5..1f09248 100644 --- a/api/queries.ts +++ b/api/queries.ts @@ -22,7 +22,7 @@ import { VideoModel } from "@/models/video.model"; import { VideosModel } from "@/models/videos.model"; import { IVote } from "@/models/vote.model"; import routes from "@/routes"; -import { CloudFog } from "lucide-react"; +import { cookies } from "next/headers"; export class Queries { public static async getNews( @@ -259,28 +259,26 @@ export class Queries { ).then((res) => res.json().then((res) => res as MessagesByTvAdmin)); } - // Lottery ================================================================================ - - public static async authenticateLottery( - phone: string, - code: string - ): Promise { - return await fetch(`${baseUrl.QUIZ_SRC}${routes.lotteryActive}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - phone: phone, - key: code, - }), - }).then((res) => { - if (!res.ok) { - throw new Error("Authentication failed"); - } - return res.json(); - }); - } - // ============================================================================================ } + +export const getTossData = async ({ + type, + id, +}: { + type: string; + id: string; +}) => { + try { + const res = await fetch(`${baseUrl.QUIZ_SRC}${routes.tossId(type, id)}`); + + if (!res.ok) { + return undefined; + } + + const result = await res.json(); + return result; + } catch (err) { + console.log(err); + } +}; diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 703afde..8d46d7b 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -1,8 +1,6 @@ -import Buble from "@/components/Buble"; import Footer from "@/components/Footer"; import MobileMenu from "@/components/MobileMenu"; import Nav from "@/components/Nav"; -import GlobalContext from "@/context/GlobalContext"; import MainProvider from "@/providers/MainProvider"; interface IProps { @@ -13,11 +11,9 @@ const RootLayout = ({ children }: IProps) => { return (
- {/* */} -
-

Turkmen TV

+
diff --git a/app/(main)/lottery/auth/page.tsx b/app/(main)/lottery/auth/page.tsx index 54cd361..0eab97f 100644 --- a/app/(main)/lottery/auth/page.tsx +++ b/app/(main)/lottery/auth/page.tsx @@ -1,15 +1,13 @@ -'use client'; - -import LotteryAuthForm from '@/components/lottery/auth/LotteryAuthForm'; +import LotteryAuthForm from "@/components/lottery/auth/LotteryAuthForm"; const LotteryAuthPage = () => { return (
-
+
); }; -export default LotteryAuthPage; +export default LotteryAuthPage; \ No newline at end of file diff --git a/app/(main)/lottery/page.tsx b/app/(main)/lottery/page.tsx index 1c813c8..1459f5e 100644 --- a/app/(main)/lottery/page.tsx +++ b/app/(main)/lottery/page.tsx @@ -1,114 +1,16 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useLotteryAuth } from "@/store/useLotteryAuth"; -import ProtectedRoute from "@/components/lottery/auth/ProtectedRoute"; -import LotteryHeader from "@/components/lottery/LotteryHeader"; - -import LotteryWinnersSection from "@/components/lottery/LotteryWinnersSection"; -import LotteryRulesSection from "@/components/lottery/rules/LotteryRulesSection"; -import LotteryCountDown from "@/components/lottery/countDown/LotteryCountDown"; -import { Queries } from "@/api/queries"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; +import { authenticateLottery } from "@/api"; import Loader from "@/components/Loader"; +import LotteryMain from "@/components/lottery/LotteryMain"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import React, { Suspense } from "react"; -const LotteryPage = () => { - const { lotteryData, setAuth } = useLotteryAuth(); - const [status, setStatus] = useState<"not-started" | "started" | "ended">( - "not-started" - ); - const [isLoading, setIsLoading] = useState(true); - const router = useRouter(); - - useEffect(() => { - const checkAuth = async () => { - // ✅ Check credentials from localStorage - const phone = localStorage.getItem("lotteryPhone"); - const code = localStorage.getItem("lotteryCode"); - - if (phone && code) { - try { - // ✅ Authenticate using stored credentials - const response = await Queries.authenticateLottery(phone, code); - - if (response.errorMessage) { - // If authentication fails, redirect to the auth page - console.log("redirecting form lottery/"); - - router.replace("/lottery/auth"); - } else { - // ✅ Set the authenticated state - setAuth(response, phone, code); - setIsLoading(false); - } - } catch (err) { - console.error("Authentication failed:", err); - router.replace("/lottery/auth"); - } - } else { - // Redirect to the auth page if no credentials are found - router.replace("/lottery/auth"); - } - }; - - checkAuth(); - }, [router, setAuth]); - - if (isLoading && !lotteryData?.data) { -
- -
; - } - +const Page = async () => { return ( - - {lotteryData?.data && ( -
- {lotteryData && ( -
- - - {status === "not-started" ? ( -
- -
- ) : null} -
- )} - - - -
- {lotteryData && (status === "ended" || status === "started") && ( - - )} -
-
- - Çykmak - -
-
-
-
- )} -
+ }> + + ); }; -export default LotteryPage; +export default Page; diff --git a/app/(main)/lotteryOld/auth/page.tsx b/app/(main)/lotteryOld/auth/page.tsx new file mode 100644 index 0000000..46eacee --- /dev/null +++ b/app/(main)/lotteryOld/auth/page.tsx @@ -0,0 +1,13 @@ +import LotteryAuthForm from "@/components/lottery/auth/LotteryAuthForm"; + +const LotteryAuthPage = () => { + return ( +
+
+ {/* */} +
+
+ ); +}; + +export default LotteryAuthPage; \ No newline at end of file diff --git a/app/(main)/lotteryOld/page.tsx b/app/(main)/lotteryOld/page.tsx new file mode 100644 index 0000000..363afdf --- /dev/null +++ b/app/(main)/lotteryOld/page.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useLotteryAuth } from "@/store/useLotteryAuth"; +import ProtectedRoute from "@/components/lottery/auth/ProtectedRoute"; +import LotteryHeader from "@/components/lottery/LotteryHeader"; + +import LotteryWinnersSection from "@/components/lottery/LotteryWinnersSection"; +import LotteryRulesSection from "@/components/lottery/rules/LotteryRulesSection"; +import LotteryCountDown from "@/components/lottery/countDown/LotteryCountDown"; +import { Queries } from "@/api/queries"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import Loader from "@/components/Loader"; +import { authenticateLottery } from "@/api"; +import { ILotteryResponse } from "@/models/lottery/lottery.model"; + +const LotteryPage = () => { + const [lotteryData, setData] = useState(); + const [status, setStatus] = useState<"not-started" | "started" | "ended">( + "not-started" + ); + const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); + + useEffect(() => { + const checkAuth = async () => { + const res = await authenticateLottery(); + setData(res); + }; + + checkAuth(); + }, []); + + if (isLoading && !lotteryData?.data) { +
+ +
; + } + + return ( + + {lotteryData?.data && ( +
+ {lotteryData && ( +
+ + + {status === "not-started" ? ( +
+ +
+ ) : null} +
+ )} + + + +
+ {lotteryData && (status === "ended" || status === "started") && ( + + )} +
+
+ + Çykmak + +
+
+
+
+ )} +
+ ); +}; + +export default LotteryPage; diff --git a/app/(main)/quiz/active/page.tsx b/app/(main)/quiz/active/page.tsx index 4f6eb66..bde3e56 100644 --- a/app/(main)/quiz/active/page.tsx +++ b/app/(main)/quiz/active/page.tsx @@ -1,7 +1,5 @@ 'use client'; import { Queries } from '@/api/queries'; -import Loader from '@/components/Loader'; -import QuizQuestion from '@/components/quiz/QuizQuestion'; import QuizQuestionList from '@/components/quiz/QuizQuestionList'; import QuizSearch from '@/components/quiz/QuizSearch'; import QuizTable from '@/components/quiz/QuizTable'; diff --git a/app/(main)/toss/page.tsx b/app/(main)/toss/page.tsx new file mode 100644 index 0000000..ab924a6 --- /dev/null +++ b/app/(main)/toss/page.tsx @@ -0,0 +1,30 @@ +import Loader from "@/components/Loader"; +import TossPage from "@/components/toss"; +import { notFound } from "next/navigation"; +import React, { Suspense } from "react"; + +type SearchParams = Promise<{ [key: string]: string }>; + +const Page = async (props: { searchParams: SearchParams }) => { + const searchParams = await props.searchParams; + let type: "bije" | "cekilis"; + + if (searchParams?.cekilis) { + type = "cekilis"; + } else if (searchParams?.bije) { + type = "bije"; + } else { + notFound(); + } + + return ( + }> + + + ); +}; + +export default Page; diff --git a/app/globals.css b/app/globals.css index 2893054..eb75839 100644 --- a/app/globals.css +++ b/app/globals.css @@ -5,24 +5,24 @@ @layer base { :root { --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + --foreground: 0 0% 3.9%; --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card-foreground: 0 0% 3.9%; --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; --destructive: 0 84.2% 60.2%; - --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%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; --radius: 0.5rem; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; @@ -32,25 +32,25 @@ } .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%; + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 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%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; @@ -59,21 +59,6 @@ } } -/* @layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} */ - -/* <<<<<<< HEAD * { - scroll-behavior: smooth; -} - -=======>>>>>>>2c1eddb85d62e4709470605eb8df7458c2d6c37e body, */ - * { scroll-behavior: smooth; } @@ -357,10 +342,6 @@ big { box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); /* Add depth */ } -.span.flash { - animation: flash 1s infinite; -} - @keyframes dots-flash { 0%, 100% { diff --git a/components.json b/components.json index 15f2b02..7a12446 100644 --- a/components.json +++ b/components.json @@ -1,17 +1,21 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", + "style": "new-york", "rsc": true, "tsx": true, "tailwind": { - "config": "tailwind.config.ts", + "config": "tailwind.config.js", "css": "app/globals.css", - "baseColor": "slate", + "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", - "utils": "@/lib/utils" - } + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" } \ No newline at end of file diff --git a/components/Nav.tsx b/components/Nav.tsx index 4f2767a..5aae4b3 100644 --- a/components/Nav.tsx +++ b/components/Nav.tsx @@ -49,14 +49,6 @@ const Nav = () => { />
    - {/*
  • - - Habarlar - -
  • */}
  • { + const [recycle, setRecycle] = useState(true); const { width, height } = useWindowSize(); const colors = [ - 'linear-gradient(45deg, #5D5D72, #8589DE)', - 'linear-gradient(45deg, #E1E0FF, #575992)', - '#8589DE', - '#575992', - '#E1E0FF', - '#FF3131', + "linear-gradient(45deg, #5D5D72, #8589DE)", + "linear-gradient(45deg, #E1E0FF, #575992)", + "#8589DE", + "#575992", + "#E1E0FF", + "#FF3131", ]; - const mobile = useMediaQuery('(max-width: 426px)'); + const mobile = useMediaQuery("(max-width: 426px)"); + + useEffect(() => { + setTimeout(() => setRecycle(false), 30000); + }, []); return (
    diff --git a/components/lottery/LotteryMain.tsx b/components/lottery/LotteryMain.tsx new file mode 100644 index 0000000..6998a72 --- /dev/null +++ b/components/lottery/LotteryMain.tsx @@ -0,0 +1,74 @@ +import LotteryHeader from "@/components/lottery/LotteryHeader"; +import LotteryRulesSection from "@/components/lottery/rules/LotteryRulesSection"; +import LotteryCountDown from "@/components/lottery/countDown/LotteryCountDown"; +import Link from "next/link"; +import { authenticateLottery } from "@/api"; +import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; +import { getLotteryStatus } from "@/lib/actions"; +import LotteryWinners from "./LotteryWinners"; + +async function getData() { + const cookieStore = await cookies(); + const phone = cookieStore.get("phoneNumber"); + const key = cookieStore.get("key"); + if (phone?.value && key?.value) { + const res = await authenticateLottery(phone.value, key.value); + return res; + } else { + redirect("/lottery/auth"); + } +} + +const LotteryMain = async () => { + const lotteryData = await getData(); + + const status = await getLotteryStatus( + lotteryData?.data.start_time, + lotteryData?.data.end_time + ); + + return lotteryData?.errorMessage ? ( +
    +

    {lotteryData.errorMessage}

    +
    + ) : ( +
    +
    + + + {status === "Upcoming" && ( +
    + +
    + )} +
    + +
    + +
    +
    + + Çykmak + +
    +
    +
    +
    + ); +}; + +export default LotteryMain; diff --git a/components/lottery/LotteryWinners.tsx b/components/lottery/LotteryWinners.tsx new file mode 100644 index 0000000..93a9e6d --- /dev/null +++ b/components/lottery/LotteryWinners.tsx @@ -0,0 +1,27 @@ +"use client"; +import { useLotteryStatus } from "@/store/store"; +import React, { useEffect } from "react"; +import LotteryWinnersSection from "./LotteryWinnersSection"; +import { ILotteryResponse } from "@/models/lottery/lottery.model"; + +const LotteryWinners = ({ + data, + lotteryStatus, +}: { + data: ILotteryResponse; + lotteryStatus: "Upcoming" | "Ongoing" | "Finished"; +}) => { + const { status, setStatus } = useLotteryStatus(); + + useEffect(() => { + setStatus(lotteryStatus); + }, []); + + return ( + (status === "Finished" || status === "Ongoing") && ( + + ) + ); +}; + +export default LotteryWinners; diff --git a/components/lottery/LotteryWinnersSection.tsx b/components/lottery/LotteryWinnersSection.tsx index a65d6d8..67dd369 100644 --- a/components/lottery/LotteryWinnersSection.tsx +++ b/components/lottery/LotteryWinnersSection.tsx @@ -11,16 +11,10 @@ import { AnimatePresence, motion } from "framer-motion"; const WEBSOCKET_URL = "wss://sms.turkmentv.gov.tm/ws/lottery?dst="; const SLOT_COUNTER_DURATION = 30000; -const LotteryWinnersSection = ({ - lotteryStatus, -}: { - lotteryStatus: string; -}) => { +const LotteryWinnersSection = ({ data }: { data: any }) => { const [winners, setWinners] = useState([]); const [currentNumber, setCurrentNumber] = useState("00-00-00-00-00"); const [isConfettiActive, setIsConfettiActive] = useState(false); - - const { lotteryData } = useLotteryAuth(); const [winnerSelectingStatus, setWinnerSelectingStatus] = useState< "not-selected" | "is-selecting" | "selected" >("not-selected"); @@ -28,52 +22,19 @@ const LotteryWinnersSection = ({ useState(null); const [topText, setTopText] = useState("Bije az wagtdan başlaýar"); const [bottomText, setBottomText] = useState(""); - const [messageQueue, setMessageQueue] = useState< LotteryWinnerDataSimplified[] >([]); // Queue for incoming WebSocket messages const [isProcessing, setIsProcessing] = useState(false); // Track if a message is being processed - + const [startNumber, setStartNumber] = useState("00,00,00,00,00"); const { wsStatus, subscribeToMessages } = useWebsocketLottery( - `${WEBSOCKET_URL}${lotteryData?.data.sms_number}` + `${WEBSOCKET_URL}${data?.data.sms_number}` ); - // Simulate WebSocket message for testing - const simulateMessage = () => { - const dummyWinner: LotteryWinnerDataSimplified = { - phone: `9936${Math.floor(10000000 + Math.random() * 90000000)}`, // Generate random client number - winner_no: winners.length + 1, // Increment winner number - ticket: `${Math.floor(Math.random() * 99) - .toString() - .padStart(2, "0")}-${Math.floor(Math.random() * 99) - .toString() - .padStart(2, "0")}-${Math.floor(Math.random() * 99) - .toString() - .padStart(2, "0")}-${Math.floor(Math.random() * 99) - .toString() - .padStart(2, "0")}-${Math.floor(Math.random() * 99) - .toString() - .padStart(2, "0")}`, // Generate random ticket - }; - - console.log("📩 Simulated Message:", dummyWinner); // Log the simulated message - setMessageQueue((prevQueue) => [...prevQueue, dummyWinner]); - }; - - // useEffect(() => { - // const interval = setInterval(() => { - // simulateMessage(); - // }, 20000); // Trigger every 10 seconds - - // return () => clearInterval(interval); // Clean up interval on unmount - // }, []); - // Initialize winners from lottery data useEffect(() => { - console.log("🎟️ Lottery Data:", lotteryData); - - if (lotteryData && lotteryData.data.winners.length > 0) { - const simplifiedWinners = lotteryData.data.winners.map((winner) => ({ + if (data?.data?.winners.length > 0) { + const simplifiedWinners = data.data.winners.map((winner: any) => ({ phone: winner.client, winner_no: winner.winner_no, ticket: winner.ticket, @@ -84,20 +45,19 @@ const LotteryWinnersSection = ({ setWinnerSelectingStatus("selected"); setTopText(`${lastWinner.winner_no}-nji ýeňiji`); setBottomText(lastWinner.phone); + setStartNumber(lastWinner.ticket.replace(/-/g, ",")); setIsConfettiActive(true); } - }, [lotteryData]); + }, [data]); // Subscribe to WebSocket messages useEffect(() => { const unsubscribe = subscribeToMessages((event) => { try { const newWinner: LotteryWinnerDataSimplified = JSON.parse(event.data); - console.log("📩 WebSocket Message Received:", newWinner); // Log the parsed message // Add new message to the queue setMessageQueue((prevQueue) => { - console.log("📥 Adding to Queue:", newWinner); return [...prevQueue, newWinner]; }); } catch (error) { @@ -110,8 +70,6 @@ const LotteryWinnersSection = ({ // Process queue when a new message is added useEffect(() => { - console.log("📋 Current Message Queue:", messageQueue); - if (!isProcessing && messageQueue.length > 0) { processQueue(); } @@ -124,10 +82,8 @@ const LotteryWinnersSection = ({ setIsProcessing(true); // Lock processing const message = messageQueue[0]; // Get the first message in the queue - console.log("⚙️ Processing Message:", message); // Debug Log 4: Log the message being processed - try { - await handleMessage(message); // Process the message + await handleMessage(message); } catch (error) { console.error("Error processing message:", error); } @@ -138,7 +94,7 @@ const LotteryWinnersSection = ({ // Handle the logic for processing a single WebSocket message const handleMessage = async (winner: LotteryWinnerDataSimplified) => { - console.log("⬇️ Updating Top and Bottom Text:", winner); // Debug Log 5: Log winner data before setting states + setStartNumber("00,00,00,00,00"); setIsConfettiActive(false); setTopText(`${winner.winner_no}-nji ýeňiji saýlanýar`); setBottomText("..."); @@ -152,7 +108,6 @@ const LotteryWinnersSection = ({ // Finalize winner selection setTopText(`${winner.winner_no}-nji ýeňiji`); setBottomText(winner.phone); - console.log("⬇️ Finalized Bottom Text:", winner.phone); // Debug Log 6: Log the final bottomText update setWinnerSelectingStatus("selected"); setIsConfettiActive(true); @@ -172,16 +127,6 @@ const LotteryWinnersSection = ({ )} - {/* Simmulation Button */} - {/*
    - -
    */} -
    {currentNumber && ( - + )}
    diff --git a/components/lottery/auth/LotteryAuthForm.tsx b/components/lottery/auth/LotteryAuthForm.tsx index 696c3ef..b2c5894 100644 --- a/components/lottery/auth/LotteryAuthForm.tsx +++ b/components/lottery/auth/LotteryAuthForm.tsx @@ -1,140 +1,115 @@ "use client"; -import { Queries } from "@/api/queries"; -import { useState, FormEvent } from "react"; +import { useState } from "react"; import { useRouter } from "next/navigation"; -import { useLotteryAuth } from "@/store/useLotteryAuth"; +import { useForm } from "react-hook-form"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form"; +import { FormControl, FormLabel } from "@mui/material"; +import { authenticateLottery } from "@/api"; + +const lotteryAuthSchema = z.object({ + phoneNumber: z.string().regex(/^993\d{8}$/, { + message: "Dogry telefon belgisini girizin", + }), + key: z.string().regex(/^.+-\d{10}$/, { + message: "Dogry acar sozuni girizin", + }), +}); const LotteryAuthForm = () => { - const [phone, setPhone] = useState(""); - const [code, setCode] = useState(""); + const form = useForm>({ + resolver: zodResolver(lotteryAuthSchema), + }); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); - const setAuth = useLotteryAuth((state) => state.setAuth); - - const validatePhone = (value: string) => { - const phoneRegex = /^993\d{8}$/; - const isValid = phoneRegex.test(value); - - return isValid; - }; - - const validateCode = (value: string) => { - const codeRegex = /^.+-\d{10}$/; // Any characters before "-", exactly 10 digits after - const isValid = codeRegex.test(value); - - return isValid; - }; - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setError(null); - - if (!validatePhone(phone)) { - setError("Telefon belgisi nädogry"); - return; - } - - if (!validateCode(code)) { - setError("Açar nädogry"); - return; - } + async function onSubmit(values: z.infer) { + const { phoneNumber, key } = values; setIsLoading(true); try { - const response = await Queries.authenticateLottery(phone, code); + const response = await authenticateLottery(phoneNumber, key); if (response.errorMessage) { setError(response.errorMessage); } else { - localStorage.setItem("lotteryPhone", phone); - localStorage.setItem("lotteryCode", code); - - setAuth(response, phone, code); + console.log(response); + document.cookie = `phoneNumber=${phoneNumber};path=/`; + document.cookie = `key=${key};path=/`; router.replace("/lottery"); } } catch (err) { - console.error("Authentication error:", err); setError("Telefon belgisi ýa-da açar nädogry"); } finally { setIsLoading(false); } - }; - - const handlePhoneChange = (e: React.ChangeEvent) => { - const value = e.target.value.replace(/\\D/g, ""); - if (value.length <= 11) { - setPhone(value); - } - }; - - const handleCodeChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setCode(value); - // if (value.length <= 12) { - // } - }; + } return ( -
    -

    - Giriş -

    -
    -
    - - -
    -
    - - - -
    - {error && ( -

    - {error} -

    - )} -
    - -
    +

    + Giriş +

    +
    + ( + + + Telefon + + + + + + + )} + /> + ( + + + Açar + + + + + + + )} + /> + {error && ( +

    + {error} +

    + )} +
    + + + ); }; diff --git a/components/lottery/countDown/LotteryCountDown.tsx b/components/lottery/countDown/LotteryCountDown.tsx index 8c26bd1..76ca6b3 100644 --- a/components/lottery/countDown/LotteryCountDown.tsx +++ b/components/lottery/countDown/LotteryCountDown.tsx @@ -1,34 +1,31 @@ "use client"; - import { calculateTimeLeft } from "@/lib/hooks/useCalculateTimeLeft"; +import { useLotteryStatus } from "@/store/store"; import React, { useState, useEffect, Dispatch, SetStateAction } from "react"; interface LotteryCountDownProps { startDate: string; // Event start date in "YYYY-MM-DD HH:mm:ss" format endDate: string; // Event end date in "YYYY-MM-DD HH:mm:ss" format - lotteryStatus: string; - setLotteryStatus: Dispatch< - SetStateAction<"not-started" | "started" | "ended"> - >; + lotteryStatus: "Upcoming" | "Ongoing" | "Finished"; } const LotteryCountDown: React.FC = ({ startDate, endDate, lotteryStatus, - setLotteryStatus, }) => { const [timeLeft, setTimeLeft] = useState({ hours: 0, minutes: 0, seconds: 0, }); - - console.log(lotteryStatus); + const { status, setStatus } = useLotteryStatus(); useEffect(() => { + setStatus(lotteryStatus); + const timer = setInterval(() => { - if (lotteryStatus === "not-started") { + if (lotteryStatus === "Upcoming") { const timeToStart = calculateTimeLeft(startDate); setTimeLeft(timeToStart); @@ -37,9 +34,9 @@ const LotteryCountDown: React.FC = ({ timeToStart.minutes === 0 && timeToStart.seconds === 0 ) { - setLotteryStatus("started"); // Update status to "started" + setStatus("Ongoing"); // Update status to "started" } - } else if (lotteryStatus === "started") { + } else if (lotteryStatus === "Ongoing") { const timeToEnd = calculateTimeLeft(endDate); setTimeLeft(timeToEnd); @@ -48,32 +45,32 @@ const LotteryCountDown: React.FC = ({ timeToEnd.minutes === 0 && timeToEnd.seconds === 0 ) { - setLotteryStatus("ended"); // Update status to "finished" + setStatus("Finished"); // Update status to "finished" } } }, 1000); return () => clearInterval(timer); // Clean up interval on component unmount - }, [startDate, endDate, lotteryStatus, setLotteryStatus]); + }, [startDate, endDate, lotteryStatus]); return (

    - {lotteryStatus === "started" + {status === "Ongoing" ? "Bije dowam edýär" - : lotteryStatus === "ended" + : status === "Finished" ? "Bije tamamlandy" : "Bije"}

    {/* LotteryCountDown */} - {lotteryStatus === "not-started" && ( + {status === "Upcoming" && (

    {timeLeft.hours}

    - hours + sagat

    @@ -88,7 +85,7 @@ const LotteryCountDown: React.FC = ({ {timeLeft.minutes}

    - minutes + minut

    @@ -103,7 +100,7 @@ const LotteryCountDown: React.FC = ({ {timeLeft.seconds}

    - seconds + sekunt

    @@ -111,9 +108,9 @@ const LotteryCountDown: React.FC = ({
    - {lotteryStatus === "not-started" + {status === "Upcoming" ? "- den başlar" - : lotteryStatus === "started" + : status === "Ongoing" ? "girmek üçin aşakda kodyňyzy giriziň" : "netijeleri görmek üçin aşakda kodyňyzy giriziň"} diff --git a/components/lottery/rules/LotteryRulesSection.tsx b/components/lottery/rules/LotteryRulesSection.tsx index 6fa65fd..6072749 100644 --- a/components/lottery/rules/LotteryRulesSection.tsx +++ b/components/lottery/rules/LotteryRulesSection.tsx @@ -1,12 +1,62 @@ -import { useLotteryAuth } from "@/store/useLotteryAuth"; +"use client"; +import { useWebsocketLottery } from "@/hooks/useWebSocketLottery"; +import { useEffect, useState } from "react"; interface IProps { - title: string; - content: string; + show?: boolean; + data?: any; } -const LotteryRulesSection = () => { - const { lotteryData } = useLotteryAuth(); +const WEBSOCKET_URL = "wss://sms.turkmentv.gov.tm/ws/lottery?dst="; + +const LotteryRulesSection = ({ show = true, data }: IProps) => { + const [messageQueue, setMessageQueue] = useState([]); // Queue for incoming WebSocket messages + const [isProcessing, setIsProcessing] = useState(false); // Track if a message is being processed + const { subscribeToMessages } = useWebsocketLottery( + `${WEBSOCKET_URL}${data?.data.sms_number}` + ); + const [totalParticipants, setTotalParticipants] = useState( + data ? data?.data?.bije_count : 0 + ); + + if (!data.errorMessage) { + document.cookie = "phoneNumber=;path=/"; + document.cookie = "key=;path=/"; + } + + // Subscribe to WebSocket messages + useEffect(() => { + const unsubscribe = subscribeToMessages((event) => { + // Add new message to the queue + setMessageQueue((prevQueue) => { + return [...prevQueue, JSON.parse(event.data)]; + }); + }); + + return () => unsubscribe(); + }, [subscribeToMessages]); + + // Process queue when a new message is added + useEffect(() => { + if (!isProcessing && messageQueue.length > 0) { + processQueue(); + } + }, [messageQueue, isProcessing]); + + // Process a single message from the queue + const processQueue = async () => { + if (isProcessing || messageQueue.length === 0) return; + + setIsProcessing(true); // Lock processing + const message = messageQueue[0]; // Get the first message in the queue + + if (!message?.winner_no) { + setTotalParticipants(totalParticipants + 1); + } + + setMessageQueue((prevQueue) => prevQueue.slice(1)); // Remove the processed message from the queue + setIsProcessing(false); // Unlock processing + }; return (
    @@ -16,10 +66,10 @@ const LotteryRulesSection = () => { Düzgünleri: -
    +
      - {lotteryData?.data.rules?.map((item, i) => ( + {data?.data.rules?.map((item: any, i: number) => (
    • {item.title}
    • @@ -27,19 +77,31 @@ const LotteryRulesSection = () => {
    +
    +

    + Umumy gatnaşyjylaryň sany: +

    +

    {totalParticipants}

    +
    +
    + + {show && (

    - Siziň kodlaryňyz: + Siziň bijeli sanynyz:

    -
      - {lotteryData?.user_lottery_numbers.map((item, i) => ( -
    • +
        + {data?.user_lottery_numbers.map((item: any, i: number) => ( +
      • {item}
      • ))}
    -
    + )}
    diff --git a/components/lottery/slotCounter/LotterySlotCounter.tsx b/components/lottery/slotCounter/LotterySlotCounter.tsx index f87186a..247cb98 100644 --- a/components/lottery/slotCounter/LotterySlotCounter.tsx +++ b/components/lottery/slotCounter/LotterySlotCounter.tsx @@ -1,23 +1,26 @@ -'use client'; -import Image from 'next/image'; -import React, { useEffect, useRef, useState } from 'react'; -import SlotCounter, { SlotCounterRef } from 'react-slot-counter'; -import { useMediaQuery } from 'usehooks-ts'; +"use client"; +import Image from "next/image"; +import React, { useEffect, useRef, useState } from "react"; +import SlotCounter, { SlotCounterRef } from "react-slot-counter"; +import { useMediaQuery } from "usehooks-ts"; interface LotterySlotCounterProps { numberString: string; + startNumber: string; } -const LotterySlotCounter = ({ numberString }: LotterySlotCounterProps) => { - const [formattedNumber, setFormattedNumber] = useState(numberString); +const LotterySlotCounter = ({ + numberString, + startNumber, +}: LotterySlotCounterProps) => { + const [formattedNumber, setFormattedNumber] = useState("00,00,00,00,00"); const slotCounterRef = useRef(null); // Ref for manual control const isFirstRender = useRef(true); // Ref to track the initial render - const tablet = useMediaQuery('(max-width: 769px)'); - const mobile = useMediaQuery('(max-width: 426px)'); + const mobile = useMediaQuery("(max-width: 426px)"); useEffect(() => { - const formatted = numberString.replace(/-/g, ','); + const formatted = numberString.replace(/-/g, ","); setFormattedNumber(formatted); // Skip animation on the first render @@ -74,22 +77,24 @@ const LotterySlotCounter = ({ numberString }: LotterySlotCounterProps) => { className="flex items-center h-fit md:max-w-[1132px] sm:max-w-[640px] max-w-[400px] w-full justify-center text-white min-[1025px]:py-4 md:px-6 rounded-full overflow-y-hidden overflow-x-visible relative border-4 border-lightPrimary" style={{ background: - 'linear-gradient(180deg, #454673 0%, #575992 10.5%, #575992 90%, #454673 100%)', - boxShadow: '0px 4px 4px 0px #00000040', - }}> + "linear-gradient(180deg, #454673 0%, #575992 10.5%, #575992 90%, #454673 100%)", + boxShadow: "0px 4px 4px 0px #00000040", + }} + > {/* Highlight */}
    + "linear-gradient(180deg, rgba(87, 89, 146, 0) 0%, #7274AB 50%, rgba(87, 89, 146, 0) 100%)", + }} + >
>; @@ -72,12 +72,12 @@ const SpinWheel = ({ setWinners }: IProps) => { tweenDuration={10000} run={true} colors={[ - 'linear-gradient(45deg, #5D5D72, #8589DE)', - 'linear-gradient(45deg, #E1E0FF, #575992)', - '#8589DE', - '#575992', - '#E1E0FF', - '#BA1A1A', + "linear-gradient(45deg, #5D5D72, #8589DE)", + "linear-gradient(45deg, #E1E0FF, #575992)", + "#8589DE", + "#575992", + "#E1E0FF", + "#BA1A1A", ]} />
@@ -85,7 +85,7 @@ const SpinWheel = ({ setWinners }: IProps) => {
{/* Wheel triangle */} wheel {
+ className="lg:p-3 p-2 bg-[#5D5D72] rounded-full overflow-hidden" + >
wheel { disabled={isSpinning || isCountingDown} className={`mt-6 px-6 py-3 rounded-full text-white font-bold ${ isSpinning || isCountingDown - ? 'bg-gray-400 cursor-not-allowed' - : 'bg-blue-500 hover:bg-blue-700' - }`}> + ? "bg-gray-400 cursor-not-allowed" + : "bg-blue-500 hover:bg-blue-700" + }`} + > {isCountingDown ? `Starting in ${countdown}...` : isSpinning - ? 'Spinning...' - : 'Spin the Wheel'} + ? "Spinning..." + : "Spin the Wheel"}
); diff --git a/components/lottery/winners/LotteryWinnersList.tsx b/components/lottery/winners/LotteryWinnersList.tsx index ce9e7d8..d6c8ed4 100644 --- a/components/lottery/winners/LotteryWinnersList.tsx +++ b/components/lottery/winners/LotteryWinnersList.tsx @@ -1,9 +1,8 @@ import { - LotteryWinnerData, LotteryWinnerDataSimplified, } from "@/typings/lottery/lottery.types"; import LotteryWinner from "./LotteryWinner"; -import { motion, AnimatePresence } from "framer-motion"; +import { motion } from "framer-motion"; import { v4 } from "uuid"; const LotteryWinnersList = ({ diff --git a/components/quiz/QuizWinnerTable.tsx b/components/quiz/QuizWinnerTable.tsx index e84af4b..dc0620d 100644 --- a/components/quiz/QuizWinnerTable.tsx +++ b/components/quiz/QuizWinnerTable.tsx @@ -1,11 +1,10 @@ -'use client'; -import { Queries } from '@/api/queries'; +"use client"; +import { Queries } from "@/api/queries"; -import { v4 } from 'uuid'; -import { useState, useEffect, useContext, Dispatch, SetStateAction } from 'react'; -import { Answer, IQuizQuestionsWinners } from '@/models/quizQuestionsWinners.model'; -import QuizContext from '@/context/QuizContext'; -import { IQuizQuestions } from '@/models/quizQuestions.model'; +import { v4 } from "uuid"; +import { useState, useEffect, useContext } from "react"; +import { IQuizQuestionsWinners } from "@/models/quizQuestionsWinners.model"; +import QuizContext from "@/context/QuizContext"; interface Message { answer: string; @@ -133,11 +132,11 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => { // Function to handle incoming WebSocket message and update winnersData const handleOnMessage = (message: Message) => { if (!winnersData) { - console.error('winnersData is undefined'); + console.error("winnersData is undefined"); return; } - console.log('updating winnersData'); + console.log("updating winnersData"); // Update the winnersData by matching phone with starred_src from the message setWinnersData((prevWinnersData) => { @@ -162,7 +161,10 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => { // Calculate the new correct_answers_time by summing serial_number_for_correct const updatedCorrectAnswersTime = updatedAnswers - .reduce((sum, answer) => sum + answer.serial_number_for_correct, 0) + .reduce( + (sum, answer) => sum + answer.serial_number_for_correct, + 0 + ) .toString(); return { @@ -182,7 +184,7 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => { }; }); - console.log('winnersData is updated'); + console.log("winnersData is updated"); }; return winnersData?.data.length !== 0 ? ( @@ -231,9 +233,10 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => { {winnersData?.data.map((winner, id) => (
+ key={v4()} + >
{id + 1}
@@ -246,46 +249,35 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
{questionsData ? questionsData.map((question) => { - // const matchingAnswer = winner.client.answers.find( - // (answer) => answer.question_id === question.id, - // ); - const matchingAnswer = winner.client.answers.find( - (answer) => answer.question_id === question.id && answer.score > 0, + (answer) => + answer.question_id === question.id && + answer.score > 0 ) || winner.client.answers.find( - (answer) => answer.question_id === question.id, + (answer) => answer.question_id === question.id ); - // const matchingAnswer = () => { - // const duplicateAnswers: Answer[] = []; - - // winner.client.answers.map((answer) => - // answer.question_id === question.id - // ? duplicateAnswers.push(answer) - // : null, - // ); - - // console.log(duplicateAnswers); - // }; - return ( + matchingAnswer?.serial_number_for_correct === + 0 + ? "text-fillRed" + : "text-textLight" + }`} + > {matchingAnswer && matchingAnswer.score !== 0 ? matchingAnswer.serial_number_for_correct : matchingAnswer && matchingAnswer?.score === 0 - ? 'X' - : '0'} + ? "X" + : "0"} ); }) @@ -345,9 +337,10 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => { {winnersData?.data.map((winner, id) => (
+ key={v4()} + >
{id + 1}
@@ -385,26 +378,36 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
{questionsData ? questionsData.map((question) => { - const matchingAnswer = winner.client.answers.find( - (answer) => answer.question_id === question.id, - ); + const matchingAnswer = + winner.client.answers.find( + (answer) => + answer.question_id === question.id && + answer.score > 0 + ) || + winner.client.answers.find( + (answer) => answer.question_id === question.id + ); return ( - {matchingAnswer && matchingAnswer.serial_number_for_correct !== 0 + matchingAnswer.serial_number_for_correct === + 0 + ? "text-fillRed" + : "text-textLight" + }`} + > + {matchingAnswer && matchingAnswer.score !== 0 ? matchingAnswer.serial_number_for_correct : matchingAnswer && - matchingAnswer.serial_number_for_correct === 0 - ? 'X' - : '0'} + matchingAnswer?.score === 0 + ? "X" + : "0"} ); }) @@ -436,7 +439,9 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
- 100 + + 100 +
@@ -445,7 +450,9 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
- 1 + + 1 +
Dogry jogaplara näçinji bolup jogap berdi @@ -453,7 +460,9 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
- X + + X +
Soraga nädogry jogap berdi @@ -462,9 +471,13 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
- 0 + + 0 +
- Soraga jogap ugratmady + + Soraga jogap ugratmady +
diff --git a/components/toss/index.tsx b/components/toss/index.tsx new file mode 100644 index 0000000..1dab014 --- /dev/null +++ b/components/toss/index.tsx @@ -0,0 +1,63 @@ +import LotteryHeader from "@/components/lottery/LotteryHeader"; +import LotteryRulesSection from "@/components/lottery/rules/LotteryRulesSection"; +import LotteryCountDown from "@/components/lottery/countDown/LotteryCountDown"; +import { getTossData } from "@/api/queries"; +import { getLotteryStatus } from "@/lib/actions"; +import LotteryWinners from "../lottery/LotteryWinners"; + +const TossPage = async ({ + type, + id, +}: { + type: "bije" | "cekilis"; + id: string; +}) => { + const tossData = await getTossData({ type, id }); + + const status = await getLotteryStatus( + tossData?.data?.start_time, + tossData?.data?.end_time + ); + + return ( + <> + {tossData?.data ? ( +
+ {tossData && ( +
+ + + {status === "Upcoming" && ( +
+ +
+ )} +
+ )} + + + +
+ +
+
+ ) : ( +
+

{tossData?.errorMessage}

+
+ )} + + ); +}; + +export default TossPage; diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..b6daa65 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +