voting page
This commit is contained in:
parent
feec5a2078
commit
8f6bec751f
|
|
@ -1,13 +1,14 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { differenceInSeconds, parseISO, addHours } from 'date-fns';
|
||||
import GradientTitle from './GradientTitle';
|
||||
|
||||
interface CountdownProps {
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
setVoteStatus: Dispatch<SetStateAction<string | undefined>>;
|
||||
}
|
||||
|
||||
const Countdown: React.FC<CountdownProps> = ({ startsAt, endsAt }) => {
|
||||
const Countdown: React.FC<CountdownProps> = ({ startsAt, endsAt, setVoteStatus }) => {
|
||||
const [timeLeft, setTimeLeft] = useState<string>('');
|
||||
const [eventStatus, setEventStatus] = useState<string>('Finished');
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ const Countdown: React.FC<CountdownProps> = ({ startsAt, endsAt }) => {
|
|||
difference = differenceInSeconds(endsAtDate, nowUTC5);
|
||||
} else {
|
||||
setEventStatus('Finished');
|
||||
setVoteStatus('closed');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,12 +29,14 @@ const ParticipantCard = ({
|
|||
const substractedProgress = progress > 99 ? progress - 2 : progress;
|
||||
|
||||
return winner && votes !== 0 ? (
|
||||
////////////////////////////////////////////// Winner card
|
||||
<div className="flex flex-col overflow-hidden bg-fillNavyBlue max-w-[940px] w-full">
|
||||
<div className="flex items-center gap-[5px] sm:gap-[20px] p-[5px] pt-[10px] w-full">
|
||||
<h3 className="text-[26px] sm:text-[80px] leading-[100%] font-bold text-fillNavyBlue text-stroke">
|
||||
{number}
|
||||
</h3>
|
||||
{photo && name ? (
|
||||
// If there an image, show image
|
||||
<div className="relative min-w-[60px] h-[60px] sm:min-w-[140px] sm:h-[140px]">
|
||||
<Image
|
||||
fill
|
||||
|
|
@ -46,7 +48,8 @@ const ParticipantCard = ({
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative min-w-[60px] h-[60px] sm:min-w-[140px] sm:h-[140px]">
|
||||
// If there is no image, show placeholder
|
||||
<div className="relative hover:group min-w-[60px] h-[60px] sm:min-w-[140px] sm:h-[140px]">
|
||||
<Image
|
||||
fill
|
||||
src={placeholder}
|
||||
|
|
@ -61,15 +64,14 @@ const ParticipantCard = ({
|
|||
<div className="flex justify-between items-center w-full">
|
||||
<div className="flex justify-between items-center sm:items-start w-full sm:w-fit sm:justify-start sm:flex-col gap-[14px]">
|
||||
{name ? (
|
||||
<h2 className="text-[18px] sm:text-[24px] leading-[100%] font-bold text-white">
|
||||
<h2 className="text-[18px] group:scale-110 transition-all sm:text-[24px] leading-[100%] font-bold text-white">
|
||||
{name}
|
||||
</h2>
|
||||
) : null}
|
||||
<h4 className="text-[12px] sm:text-[16px] text-white leading-[100%] font-bold">
|
||||
{votes ? votes : 0} ses
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{/* If we have voteCode and voting not closed, then show badge with code. Else dont show */}
|
||||
{voteCode && voteStatus !== 'closed' ? (
|
||||
// Desktop version
|
||||
<p className="hidden sm:block py-[10px] px-[8px] bg-[#1E1E7B] text-fillLightGray text-[14px] leading-[125%] max-w-[232px] rounded-[10px]">
|
||||
Ses bermek üçin{' '}
|
||||
<span className="inline-block w-fit px-1 py-[4px] mx-[4px] font-bold text-white text-[16px] border rounded-md">
|
||||
|
|
@ -79,16 +81,30 @@ const ParticipantCard = ({
|
|||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ProgressBar w-full bg-[#3636A3] rounded-[8px] ">
|
||||
<div className="flex flex-col items-end gap-[8px]">
|
||||
<h4 className="text-[12px] sm:text-[48px] text-white leading-[100%] font-bold">
|
||||
{votes ? votes : 0}
|
||||
</h4>
|
||||
<h4 className="text-[12px] sm:text-[24px] text-white leading-[100%] font-bold">
|
||||
ses
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
{/* <div className="ProgressBar w-full bg-[#3636A3] rounded-[8px]">
|
||||
<div
|
||||
style={{
|
||||
width: `${substractedProgress.toString()}%`,
|
||||
}}
|
||||
className={`rounded-[8px] bg-[#6868B8] w-[${substractedProgress.toString()}%] h-[7px] sm:h-[28px]`}></div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* If we have voteCode and voting not closed, then show badge with code. Else dont show */}
|
||||
{voteCode && voteStatus !== 'closed' ? (
|
||||
// Mobile version
|
||||
<p className="block sm:hidden py-[10px] px-[4px] font-medium bg-[#1E1E7B] rounded-md text-fillLightGray text-[10px] leading-[125%] ">
|
||||
Ses bermek üçin{' '}
|
||||
<span className="inline-block w-fit px-1 py-[4px] mx-[4px] font-bold text-white text-[16px] border rounded-md">
|
||||
|
|
@ -99,6 +115,7 @@ const ParticipantCard = ({
|
|||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
////////////////////////////////////////////// Simple card
|
||||
<div className="flex flex-col max-w-[940px] items-center w-full gap-[5px] sm:gap-[20px]">
|
||||
<div className="flex items-center gap-[5px] sm:gap-[20px] max-w-[900px] w-full px-[5px] sm:p-0">
|
||||
<h3 className="w-[24px] text-[16px] sm:text-[20px] leading-[100%] font-bold">{number}</h3>
|
||||
|
|
@ -127,17 +144,15 @@ const ParticipantCard = ({
|
|||
)}
|
||||
<div className="flex flex-col gap-[10px] sm:gap-[12px] w-full">
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div className="flex justify-between sm:justify-start sm:flex-col gap-[6px]">
|
||||
<div className="flex justify-between sm:justify-start sm:flex-col gap-[8px]">
|
||||
{name ? (
|
||||
<h2 className="text-textBlack text-[16px] sm:text-[18px] leading-[100%] font-bold">
|
||||
{name}
|
||||
</h2>
|
||||
) : null}
|
||||
<h4 className="text-[12px] sm:text-[16px] text-dark font-bold leading-[100%] ">
|
||||
{votes ? votes : 0} ses
|
||||
</h4>
|
||||
</div>
|
||||
{/* If we have voteCode and voting not closed, then show badge with code. Else dont show */}
|
||||
{voteCode && voteStatus !== 'closed' ? (
|
||||
// Desktop version
|
||||
<p className="hidden sm:block py-[5px] px-[8px] bg-[#EAEAFF] text-[#9393DA] text-[14px] leading-[125%] rounded-[10px] w-fit">
|
||||
Ses bermek üçin
|
||||
<span className="inline-block w-fit px-1 py-[2px] mx-[4px] font-bold leading-[100%] text-fillNavyBlue text-[16px] border border-fillNavyBlue rounded-md">
|
||||
|
|
@ -147,16 +162,21 @@ const ParticipantCard = ({
|
|||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ProgressBar w-full bg-[#E0E0E0] rounded-[4px] ">
|
||||
<div
|
||||
style={{
|
||||
width: `${substractedProgress.toString()}%`,
|
||||
}}
|
||||
className={`rounded-[4px] bg-[#6868B8] h-[5px] sm:h-[14px]`}></div>
|
||||
<div className="flex flex-col gap-[4px] items-end">
|
||||
<h4 className="text-[12px] sm:text-[32px] text-[#808080] font-bold leading-[100%] ">
|
||||
{votes ? votes : 0}
|
||||
</h4>
|
||||
<h4 className="text-[12px] sm:text-[16px] text-[#808080] font-bold leading-[100%] ">
|
||||
ses
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* If we have voteCode and voting not closed, then show badge with code. Else dont show */}
|
||||
{voteCode && voteStatus !== 'closed' ? (
|
||||
// Mobile version
|
||||
<p className="block sm:hidden text-[#9393DA] text-[10px] leading-[125%] w-full">
|
||||
Ses bermek üçin{' '}
|
||||
<span className="inline-block w-fit px-1 py-[4px] leading-[100%] mx-[4px] font-bold text-fillNavyBlue text-[16px] border border-fillNavyBlue rounded-md">
|
||||
|
|
|
|||
|
|
@ -11,16 +11,31 @@ import PageBage from './PageBage';
|
|||
import Image from 'next/image';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
import Countdown from './Countdown';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface IParams {
|
||||
vote_id?: string;
|
||||
}
|
||||
|
||||
interface ISocketMessage {
|
||||
voting_id: number;
|
||||
voting_item_id: number;
|
||||
client_id: number;
|
||||
message: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const ParticipantsList = ({ vote_id }: IParams) => {
|
||||
const [data, setData] = useState<IAllVotes>();
|
||||
const [participantsData, setParticipantsData] = useState<VotingItem[]>();
|
||||
const [participantsData, setParticipantsData] = useState<VotingItem[]>([]);
|
||||
const [voteStatus, setVoteStatus] = useState<string>();
|
||||
const [winnersCount, setWinnersCount] = useState<number>(0);
|
||||
|
||||
// States realted to web socket
|
||||
const [smsNumber, setSmsNumber] = useState<string | null>(null);
|
||||
const [socket, setSocket] = useState<WebSocket | null>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
const mobile = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
const { setVoteDescription } = useContext(VoteContext).voteDescriptionContext;
|
||||
|
|
@ -29,9 +44,10 @@ const ParticipantsList = ({ vote_id }: IParams) => {
|
|||
if (!vote_id) {
|
||||
Queries.getAllVotes().then((res) => {
|
||||
setData(res);
|
||||
setParticipantsData(res.data.voting_items);
|
||||
setParticipantsData([...res.data.voting_items]);
|
||||
setVoteDescription(res.data.description);
|
||||
setVoteStatus(res.data.status);
|
||||
setSmsNumber(res.data.sms_number);
|
||||
});
|
||||
} else {
|
||||
Queries.getVote(vote_id).then((res) => {
|
||||
|
|
@ -39,40 +55,120 @@ const ParticipantsList = ({ vote_id }: IParams) => {
|
|||
setParticipantsData(res.data.voting_items);
|
||||
setVoteDescription(res.data.description);
|
||||
setVoteStatus(res.data.status);
|
||||
setSmsNumber(res.data.sms_number);
|
||||
});
|
||||
}
|
||||
|
||||
if (voteStatus !== 'closed') {
|
||||
const interval = setInterval(() => {
|
||||
if (!vote_id) {
|
||||
Queries.getAllVotes().then((res) => {
|
||||
setParticipantsData(res.data.voting_items);
|
||||
setVoteStatus(res.data.status);
|
||||
});
|
||||
} else {
|
||||
Queries.getVote(vote_id).then((res) => {
|
||||
setParticipantsData(res.data.voting_items);
|
||||
setVoteStatus(res.data.status);
|
||||
});
|
||||
}
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
|
||||
// if (voteStatus !== 'closed' && vote_id) {
|
||||
// const interval = setInterval(() => {
|
||||
// Queries.getVote(vote_id).then((res) => {
|
||||
// setParticipantsData(res.data.voting_items);
|
||||
// setVoteStatus(res.data.status);
|
||||
// });
|
||||
// }, 10000);
|
||||
// return () => clearInterval(interval);
|
||||
// }
|
||||
|
||||
if (participantsData) {
|
||||
winnersCountHandle(participantsData);
|
||||
}
|
||||
}, [voteStatus]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let socket: WebSocket | null = null;
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
let pingInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const connectWebSocket = () => {
|
||||
try {
|
||||
socket = new WebSocket(`wss://sms.turkmentv.gov.tm/ws/voting?dst=${smsNumber}`);
|
||||
setSocket(socket);
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log('WebSocket is connected');
|
||||
setIsConnected(true);
|
||||
|
||||
pingInterval = setInterval(() => {
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
socket.send(JSON.stringify({ type: 'ping' }));
|
||||
} catch (error) {
|
||||
console.error('Error sending ping:', error);
|
||||
}
|
||||
}
|
||||
}, 25000); // Ping every 25 seconds
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
// console.log('Message received from WebSocket:', event.data);
|
||||
const message = JSON.parse(event.data);
|
||||
handleWebSocketMessage(message);
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
console.log('WebSocket is closed');
|
||||
setIsConnected(false);
|
||||
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
}
|
||||
|
||||
if (!reconnectTimeout) {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
console.log('Attempting to reconnect WebSocket...');
|
||||
connectWebSocket();
|
||||
}, 5000); // Reconnect after 5 seconds
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (smsNumber) {
|
||||
connectWebSocket();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
}
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
}
|
||||
};
|
||||
}, [smsNumber]);
|
||||
|
||||
const handleWebSocketMessage = (message: ISocketMessage) => {
|
||||
setParticipantsData((prevVotingItems) => {
|
||||
if (!prevVotingItems) return [];
|
||||
|
||||
// Update the corresponding voting item
|
||||
const updatedItems = prevVotingItems.map((item, index) =>
|
||||
item.id === message.voting_item_id
|
||||
? { ...item, votes_count: item.votes_count + 10000 }
|
||||
: item,
|
||||
);
|
||||
|
||||
// Sort the updated items array by votes_count in descending order
|
||||
return updatedItems.sort((a, b) => b.votes_count - a.votes_count);
|
||||
});
|
||||
};
|
||||
|
||||
const addVotes = () => {
|
||||
setParticipantsData((prevVotingItems) => {
|
||||
if (!prevVotingItems) return [];
|
||||
|
||||
// Update the corresponding voting item
|
||||
const updatedItems = prevVotingItems.map((item, index) =>
|
||||
index === 5 ? { ...item, votes_count: item.votes_count + 10000 } : item,
|
||||
);
|
||||
|
||||
// Sort the updated items array by votes_count in descending order
|
||||
return updatedItems.sort((a, b) => b.votes_count - a.votes_count);
|
||||
});
|
||||
};
|
||||
|
||||
const winnersCountHandle = (winners: VotingItem[]) => {
|
||||
let count = 0;
|
||||
|
|
@ -126,20 +222,22 @@ const ParticipantsList = ({ vote_id }: IParams) => {
|
|||
{/* {data.data.title ? <GradientTitle title={data.data.title} size="big" /> : null} */}
|
||||
|
||||
{data.data.ends_at && data.data.starts_at ? (
|
||||
<Countdown endsAt={data.data.ends_at} startsAt={data.data.starts_at} />
|
||||
<Countdown
|
||||
endsAt={data.data.ends_at}
|
||||
startsAt={data.data.starts_at}
|
||||
setVoteStatus={setVoteStatus}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="flex w-full flex-col items-center gap-[10px] sm:gap-[20px]">
|
||||
{winnersCount > 1 ? <GradientTitle title="победители" size="small" /> : null}
|
||||
|
||||
{participantsData &&
|
||||
participantsData[0].votes_count > 0 &&
|
||||
participantsData[0].votes_percents === 100 ? (
|
||||
{participantsData && participantsData[0].votes_count > 0 ? (
|
||||
<div className="flex flex-col items-center overflow-hidden bg-fillNavyBlue rounded-[10px] sm:rounded-[30px] max-w-[940px] w-full px-[5px] py-[20px] sm:p-[20px] sm:gap-[20px] gap-[10px]">
|
||||
{participantsData.map((participant, id) =>
|
||||
participant.votes_percents === 100 &&
|
||||
participant.votes_count === participantsData[0].votes_count &&
|
||||
voteStatus ? (
|
||||
participant.votes_count === participantsData[0].votes_count && voteStatus ? (
|
||||
participant.url ? (
|
||||
<Link href={participant.url} target="_blank" className="w-full">
|
||||
<ParticipantCard
|
||||
key={v4()}
|
||||
voteStatus={voteStatus}
|
||||
|
|
@ -153,6 +251,22 @@ const ParticipantsList = ({ vote_id }: IParams) => {
|
|||
smsNumber={data.data.sms_number}
|
||||
winner={true}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<ParticipantCard
|
||||
key={v4()}
|
||||
voteStatus={voteStatus}
|
||||
isFirst={id === 0 ? true : false}
|
||||
name={participant.title}
|
||||
progress={participant.votes_percents}
|
||||
votes={participant.votes_count}
|
||||
voteCode={participant.vote_code}
|
||||
number={id + 1}
|
||||
photo={participant.photo}
|
||||
smsNumber={data.data.sms_number}
|
||||
winner={true}
|
||||
/>
|
||||
)
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -162,7 +276,7 @@ const ParticipantsList = ({ vote_id }: IParams) => {
|
|||
</div>
|
||||
{participantsData
|
||||
? participantsData.map((participant, id) =>
|
||||
participant.votes_percents !== 100 && voteStatus ? (
|
||||
participant.id !== participantsData[0].id && voteStatus ? (
|
||||
<ParticipantCard
|
||||
key={v4()}
|
||||
voteStatus={voteStatus}
|
||||
|
|
|
|||
Loading…
Reference in New Issue