lotteryWinnersSection update

This commit is contained in:
Kakabay 2025-01-02 17:47:39 +05:00
parent 6c03d191b6
commit fb6090d9fd
9 changed files with 202 additions and 191 deletions

View File

@ -1,24 +1,8 @@
"use client";
'use client';
import LotteryAuthForm from "@/components/lottery/auth/LotteryAuthForm";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useLotteryAuth } from "@/store/useLotteryAuth";
import LotteryAuthForm from '@/components/lottery/auth/LotteryAuthForm';
const LotteryAuthPage = () => {
const router = useRouter();
const { isAuthenticated, logout } = useLotteryAuth();
useEffect(() => {
if (isAuthenticated) {
router.push("/lottery");
}
}, [isAuthenticated, router]);
useEffect(() => {
logout();
}, [logout]);
return (
<div className="container">
<div className="flex justify-center items-center min-h-[50vh] py-[200px]">

View File

@ -9,6 +9,7 @@ import LotteryWinnersSection from '@/components/lottery/LotteryWinnersSection';
import LotteryRulesSection from '@/components/lottery/rules/LotteryRulesSection';
import LotteryCountDown from '@/components/lottery/countDown/LotteryCountDown';
import LotteryCountDownAllert from '@/components/lottery/countDown/countDownAllert/LotteryCountDownAllert';
import { LotteryWinnerDataSimplified } from '@/typings/lottery/lottery.types';
const LotteryPage = () => {
const { lotteryData } = useLotteryAuth();
@ -42,15 +43,7 @@ const LotteryPage = () => {
<LotteryRulesSection />
{lotteryData && (status === 'ended' || status === 'started') && (
<div className="flex flex-col gap-[0px]">
<LotteryCountDownAllert
lotteryStatus={status}
setLotteryStatus={setStatus}
endDate={lotteryData.data.end_time}
startDate={lotteryData.data.start_time}
/>
<LotteryWinnersSection lotteryStatus={status} />
</div>
)}
</div>
</ProtectedRoute>

View File

@ -0,0 +1,49 @@
import { motion } from 'framer-motion';
interface AnimatedTextProps {
text: string;
className?: string;
wordClassName?: string;
initialY?: number;
duration?: number;
wordDelay?: number;
}
const AnimatedText = ({
text,
className = '',
wordClassName = '',
initialY = -100,
duration = 0.5,
wordDelay = 0.2,
}: AnimatedTextProps) => {
const words = text.split(' ');
return (
<div className="overflow-hidden">
<motion.p
className={className}
initial={{ y: initialY, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: initialY, opacity: 0 }}>
{words.map((word, i) => (
<motion.span
key={i}
initial={{ y: initialY, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: initialY, opacity: 0 }}
transition={{
duration,
delay: i * wordDelay,
ease: 'easeOut',
}}
className={`inline-block mx-2 ${wordClassName}`}>
{word}
</motion.span>
))}
</motion.p>
</div>
);
};
export default AnimatedText;

View File

@ -16,25 +16,6 @@ const LotteryHeader = ({ title, description, image, smsCode }: LotteryHeaderProp
{title}
</h1>
<p className="text-center text-textLarge leading-textLarge">{description}</p>
<div className="flex items-center gap-[8px] px-4 py-3 bg-lightInfoAllertContainer">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.92893 4.92893C6.8043 3.05357 9.34784 2 12 2C14.6522 2 17.1957 3.05357 19.0711 4.92893C20.9464 6.8043 22 9.34784 22 12C22 13.3132 21.7413 14.6136 21.2388 15.8268C20.7362 17.0401 19.9997 18.1425 19.0711 19.0711C18.1425 19.9997 17.0401 20.7362 15.8268 21.2388C14.6136 21.7413 13.3132 22 12 22C10.6868 22 9.38642 21.7413 8.17317 21.2388C6.95991 20.7362 5.85752 19.9997 4.92893 19.0711C4.00035 18.1425 3.26375 17.0401 2.7612 15.8268C2.25866 14.6136 2 13.3132 2 12C2 9.34784 3.05357 6.8043 4.92893 4.92893ZM12 4C9.87827 4 7.84344 4.84285 6.34315 6.34315C4.84285 7.84344 4 9.87827 4 12C4 13.0506 4.20693 14.0909 4.60896 15.0615C5.011 16.0321 5.60028 16.914 6.34315 17.6569C7.08601 18.3997 7.96793 18.989 8.93853 19.391C9.90914 19.7931 10.9494 20 12 20C13.0506 20 14.0909 19.7931 15.0615 19.391C16.0321 18.989 16.914 18.3997 17.6569 17.6569C18.3997 16.914 18.989 16.0321 19.391 15.0615C19.7931 14.0909 20 13.0506 20 12C20 9.87827 19.1571 7.84344 17.6569 6.34315C16.1566 4.84285 14.1217 4 12 4ZM11 9C11 8.44772 11.4477 8 12 8H12.01C12.5623 8 13.01 8.44772 13.01 9C13.01 9.55228 12.5623 10 12.01 10H12C11.4477 10 11 9.55228 11 9ZM10 12C10 11.4477 10.4477 11 11 11H12C12.5523 11 13 11.4477 13 12V15C13.5523 15 14 15.4477 14 16C14 16.5523 13.5523 17 13 17H12C11.4477 17 11 16.5523 11 16V13C10.4477 13 10 12.5523 10 12Z"
fill="#1E3A5F"
/>
</svg>
<span className="font-base-medium text-lightOnInfoAllertContainer">
SMS-kod: {smsCode}
</span>
</div>
</div>
{image && (
<div className="md:mb-8 sm:mb-[40px] mb-[16px]">

View File

@ -1,42 +1,40 @@
"use client";
'use client';
import { useState, useEffect, useRef } from "react";
import { useLotteryAuth } from "@/store/useLotteryAuth";
import { LotteryWinnerDataSimplified } from "@/typings/lottery/lottery.types";
import LotteryWinnersList from "./winners/LotteryWinnersList";
import LotterySlotCounter from "./slotCounter/LotterySlotCounter";
import ReactConfetti from "react-confetti";
import { useWindowSize } from "react-use";
import LotteryCountDownAllert from "./countDown/countDownAllert/LotteryCountDownAllert";
import { useState, useEffect, useRef } from 'react';
import { useLotteryAuth } from '@/store/useLotteryAuth';
import { LotteryWinnerDataSimplified } from '@/typings/lottery/lottery.types';
import LotteryWinnersList from './winners/LotteryWinnersList';
import LotterySlotCounter from './slotCounter/LotterySlotCounter';
import ReactConfetti from 'react-confetti';
import { useWindowSize } from 'react-use';
import LotteryCountDownAllert from './countDown/countDownAllert/LotteryCountDownAllert';
import { motion } from 'framer-motion';
import AnimatedText from '@/components/common/AnimatedText';
const WEBSOCKET_URL = "wss://sms.turkmentv.gov.tm/ws/lottery?dst=0506";
const WEBSOCKET_URL = 'wss://sms.turkmentv.gov.tm/ws/lottery?dst=0506';
const PING_INTERVAL = 25000;
const SLOT_COUNTER_DURATION = 20000;
const LotteryWinnersSection = ({
lotteryStatus,
}: {
lotteryStatus: string;
}) => {
const LotteryWinnersSection = ({ lotteryStatus }: { lotteryStatus: string }) => {
// UI States
const [winners, setWinners] = useState<LotteryWinnerDataSimplified[]>([]);
const [currentNumber, setCurrentNumber] = useState<string>("00-00-00-00-00");
const [currentNumber, setCurrentNumber] = useState<string>('00-00-00-00-00');
const [isConfettiActive, setIsConfettiActive] = useState(false);
const [wsStatus, setWsStatus] = useState<
"connecting" | "connected" | "error"
>("connecting");
const [wsStatus, setWsStatus] = useState<'connecting' | 'connected' | 'error'>('connecting');
const { width, height } = useWindowSize();
const { lotteryData } = useLotteryAuth();
const [isSlotCounterAnimating, setIsSlotCounterAnimating] = useState(false);
const [pendingWinner, setPendingWinner] =
useState<LotteryWinnerDataSimplified | null>(null);
const [pendingWinner, setPendingWinner] = useState<LotteryWinnerDataSimplified | null>(null);
// Refs
const wsRef = useRef<WebSocket | null>(null);
const pingIntervalRef = useRef<NodeJS.Timeout>();
const mountedRef = useRef(false);
// Add new state for display text
const [displayText, setDisplayText] = useState<string>('...');
// Initialize winners from lottery data
useEffect(() => {
if (lotteryData?.data.winners) {
@ -46,9 +44,7 @@ const LotteryWinnersSection = ({
ticket: winner.ticket,
}));
setWinners(simplifiedWinners);
setCurrentNumber(
lotteryData.data.winners.at(-1)?.ticket || "00-00-00-00-00"
);
setCurrentNumber(lotteryData.data.winners.at(-1)?.ticket || '00-00-00-00-00');
}
}, [lotteryData]);
@ -61,21 +57,21 @@ const LotteryWinnersSection = ({
const socket = new WebSocket(WEBSOCKET_URL);
wsRef.current = socket;
socket.addEventListener("open", () => {
socket.addEventListener('open', () => {
if (!mountedRef.current) return;
console.log("WebSocket Connected");
setWsStatus("connected");
console.log('WebSocket Connected');
setWsStatus('connected');
pingIntervalRef.current = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "ping" }));
socket.send(JSON.stringify({ type: 'ping' }));
}
}, PING_INTERVAL);
});
socket.addEventListener("message", async (event) => {
socket.addEventListener('message', async (event) => {
if (!mountedRef.current) return;
console.log("Message received:", event.data);
console.log('Message received:', event.data);
try {
const newWinner = JSON.parse(event.data);
@ -85,67 +81,58 @@ const LotteryWinnersSection = ({
ticket: newWinner.ticket,
};
// Set initial animation text
setDisplayText(`${winnerData.winner_no}-nji ýeňiji saýlanýar`);
// Start the sequence
setIsSlotCounterAnimating(true);
setPendingWinner(winnerData);
setCurrentNumber(winnerData.ticket);
// Wait for slot counter animation
await new Promise((resolve) =>
setTimeout(resolve, SLOT_COUNTER_DURATION)
);
await new Promise((resolve) => setTimeout(resolve, SLOT_COUNTER_DURATION));
// Update text to show winner's phone
setDisplayText(winnerData.client);
setIsConfettiActive(true);
setWinners((prev) => [...prev, winnerData]);
// Hide confetti after 5 seconds
// setTimeout(() => {
// if (mountedRef.current) {
// setIsConfettiActive(false);
// setIsSlotCounterAnimating(false);
// setPendingWinner(null);
// }
// }, 5000);
// Show confetti and add winner simultaneously
if (mountedRef.current) {
setIsConfettiActive(true);
setWinners((prev) => [...prev, winnerData]);
// Hide confetti after 5 seconds
// Reset everything after 5 seconds
setTimeout(() => {
if (mountedRef.current) {
setIsConfettiActive(false);
setIsSlotCounterAnimating(false);
setPendingWinner(null);
setDisplayText('...'); // Reset text
}
}, 5000);
}
} catch (error) {
console.error("Error processing message:", error);
console.error('Error processing message:', error);
setIsSlotCounterAnimating(false);
setPendingWinner(null);
setDisplayText('...'); // Reset text on error
}
});
socket.addEventListener("error", (error) => {
socket.addEventListener('error', (error) => {
if (!mountedRef.current) return;
console.error("WebSocket Error:", error);
setWsStatus("error");
console.error('WebSocket Error:', error);
setWsStatus('error');
});
socket.addEventListener("close", () => {
socket.addEventListener('close', () => {
if (!mountedRef.current) return;
console.log("WebSocket Closed");
setWsStatus("error");
console.log('WebSocket Closed');
setWsStatus('error');
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
}
});
} catch (error) {
console.error("Error creating WebSocket:", error);
setWsStatus("error");
console.error('Error creating WebSocket:', error);
setWsStatus('error');
}
};
@ -165,7 +152,7 @@ const LotteryWinnersSection = ({
return (
<section>
{wsStatus === "error" && (
{wsStatus === 'error' && (
<div className="text-red-500 text-center mb-2">
Connection error. Please refresh the page.
</div>
@ -181,26 +168,29 @@ const LotteryWinnersSection = ({
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',
]}
/>
</div>
)}
<div className="container">
<div className="flex flex-col items-center">
<div className="translate-y-1/2 z-10">
<LotterySlotCounter
numberString={currentNumber}
isAnimating={isSlotCounterAnimating}
<div
className="flex flex-col items-center rounded-[32px] pt-[40px]"
style={{ background: 'linear-gradient(180deg, #F0ECF4 0%, #E1E0FF 43.5%)' }}>
<AnimatedText
text={displayText}
className="text-center text-[100px] leading-[108px] text-[#E65E19]"
/>
<div className="translate-y-1/2 z-10">
<LotterySlotCounter numberString={currentNumber} isAnimating={isSlotCounterAnimating} />
</div>
<div className="flex gap-6 bg-lightPrimaryContainer rounded-[12px] flex-1 w-full items-center justify-center md:pt-[122px] sm:pt-[90px] pt-[40px] sm:pb-[62px] pb-[32px] px-4">
<div className="flex gap-6 rounded-[12px] flex-1 w-full items-center justify-center md:pt-[122px] sm:pt-[90px] pt-[40px] sm:pb-[62px] pb-[32px] px-4">
<LotteryWinnersList winners={winners} />
</div>
</div>

View File

@ -1,24 +1,41 @@
'use client';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useLotteryAuth } from '@/store/useLotteryAuth';
import { Queries } from '@/api/queries';
interface ProtectedRouteProps {
children: React.ReactNode;
}
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const { isAuthenticated } = useLotteryAuth();
const { isAuthenticated, phone, code, setAuth } = useLotteryAuth();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!isAuthenticated) {
const checkAuth = async () => {
// First, check if we have credentials in localStorage
if (phone && code) {
try {
// Try to authenticate with stored credentials
const response = await Queries.authenticateLottery(phone, code);
setAuth(response, phone, code);
setIsLoading(false);
return; // Exit early if authentication successful
} catch (err) {
console.error('Authentication failed:', err);
// Only redirect if API request fails
router.replace('/lottery/auth');
}
}, [isAuthenticated, router]);
} else {
// Only redirect if no credentials found
router.replace('/lottery/auth');
}
};
if (!isAuthenticated) {
checkAuth();
}, []);
// Show nothing while checking auth
if (isLoading) {
return null;
}

View File

@ -1,27 +1,24 @@
"use client";
import Image from "next/image";
import React, { useEffect, useState } from "react";
import SlotCounter from "react-slot-counter";
import { useMediaQuery } from "usehooks-ts";
'use client';
import Image from 'next/image';
import React, { useEffect, useState } from 'react';
import SlotCounter from 'react-slot-counter';
import { useMediaQuery } from 'usehooks-ts';
interface LotterySlotCounterProps {
numberString: string;
isAnimating: boolean;
}
const LotterySlotCounter = ({
numberString,
isAnimating,
}: LotterySlotCounterProps) => {
const LotterySlotCounter = ({ numberString, isAnimating }: LotterySlotCounterProps) => {
const [formattedNumber, setFormattedNumber] = useState(numberString);
useEffect(() => {
const formatted = numberString.replace(/-/g, ",");
const formatted = numberString.replace(/-/g, ',');
setFormattedNumber(formatted);
}, [numberString]);
const tablet = useMediaQuery("(max-width: 769px)");
const mobile = useMediaQuery("(max-width: 426px)");
const tablet = useMediaQuery('(max-width: 769px)');
const mobile = useMediaQuery('(max-width: 426px)');
return (
<div className="relative w-fit">
@ -65,31 +62,29 @@ const LotterySlotCounter = ({
className="flex items-center h-fit md:max-w-[1132px] sm:max-w-[640px] max-w-[324px] w-full justify-center text-white md: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 */}
<div
className="absolute top-[50%] -translate-y-1/2 left-0 w-full h-full"
style={{
background:
"linear-gradient(180deg, rgba(87, 89, 146, 0) 0%, #7274AB 50%, rgba(87, 89, 146, 0) 100%)",
}}
></div>
'linear-gradient(180deg, rgba(87, 89, 146, 0) 0%, #7274AB 50%, rgba(87, 89, 146, 0) 100%)',
}}></div>
<div className="z-10">
<SlotCounter
value={formattedNumber}
// startValue={'00,00,00,00,00'}
startValue={formattedNumber}
charClassName="rolling-number"
separatorClassName="slot-seperator"
duration={2}
speed={2}
startFromLastDigit
delay={2}
animateUnchanged={true}
// autoAnimationStart={false}
animateUnchanged={false}
autoAnimationStart={false}
/>
</div>
</div>

View File

@ -1,16 +1,9 @@
import {
LotteryWinnerData,
LotteryWinnerDataSimplified,
} from "@/typings/lottery/lottery.types";
import LotteryWinner from "./LotteryWinner";
import { motion, AnimatePresence } from "framer-motion";
import { v4 } from "uuid";
import { LotteryWinnerData, LotteryWinnerDataSimplified } from '@/typings/lottery/lottery.types';
import LotteryWinner from './LotteryWinner';
import { motion, AnimatePresence } from 'framer-motion';
import { v4 } from 'uuid';
const LotteryWinnersList = ({
winners,
}: {
winners: LotteryWinnerDataSimplified[];
}) => {
const LotteryWinnersList = ({ winners }: { winners: LotteryWinnerDataSimplified[] }) => {
return (
<div className="flex flex-col gap-4 w-full max-w-[1028px]">
<div className="flex flex-col gap-2 w-full pb-4 border-b border-[#CECCFF]">
@ -21,9 +14,8 @@ const LotteryWinnersList = ({
</div>
<motion.div
layout
className="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-x-2 gap-y-4 w-full h-[244px] overflow-y-auto lottery-scrollbar"
>
<AnimatePresence mode="popLayout">
className="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-x-2 gap-y-4 w-full ">
<AnimatePresence mode="wait">
{winners.map((item, index) => (
<LotteryWinner
key={v4()}

View File

@ -1,5 +1,6 @@
import { create } from 'zustand';
import { ILotteryResponse } from '@/models/lottery/lottery.model';
import { persist } from 'zustand/middleware';
interface LotteryAuthState {
isAuthenticated: boolean;
@ -10,21 +11,30 @@ interface LotteryAuthState {
logout: () => void;
}
export const useLotteryAuth = create<LotteryAuthState>((set) => ({
export const useLotteryAuth = create<LotteryAuthState>()(
persist(
(set) => ({
isAuthenticated: false,
lotteryData: null,
phone: null,
code: null,
setAuth: (data, phone, code) => set({
setAuth: (data, phone, code) =>
set({
isAuthenticated: true,
lotteryData: data,
phone,
code
code,
}),
logout: () => set({
logout: () =>
set({
isAuthenticated: false,
lotteryData: null,
phone: null,
code: null
code: null,
}),
}));
}),
{
name: 'lottery-auth-storage',
},
),
);