Compare commits

...

34 Commits

Author SHA1 Message Date
Atash03 effee2c557 added description field to votes 2025-05-07 16:07:52 +05:00
Atash03 0f7e32d067 added description field to votes 2025-05-07 16:05:59 +05:00
Atash03 bb70710c65 fix error 2025-04-25 17:51:05 +05:00
Atash03 875620b64e new feature in quiz page 2025-04-14 16:14:44 +05:00
Atash03 b059d2afd6 ui changes 2025-04-09 17:56:33 +05:00
Ilgeldi dd5590d4c8 ui changes 2025-04-08 14:21:30 +05:00
Ilgeldi 35704d99f4 ui changes 2025-04-07 16:20:17 +05:00
Ilgeldi b6fcb92c8d ui changes 2025-04-04 15:43:52 +05:00
Ilgeldi f37f4bd024 ui changes 2025-04-04 15:28:14 +05:00
Ilgeldi 52b75b5c19 fix conditional in vote page 2025-03-28 22:40:28 +05:00
Ilgeldi 5fe9491f86 route changes 2025-03-28 17:50:41 +05:00
Ilgeldi ce90ebeb50 route changes 2025-03-28 17:11:31 +05:00
Ilgeldi 3cbbdf21c4 route changes 2025-03-28 17:05:40 +05:00
Ilgeldi 35fa202f61 ui changes, fix errors 2025-03-20 16:55:08 +05:00
Ilgeldi 3b06c9baa9 quiz/[id]/results: ui changes 2025-03-20 15:51:51 +05:00
Ilgeldi 94a4df4ff2 fix: quiz ui, and quiz logic 2025-03-14 15:46:10 +05:00
Ilgeldi 21187be32a added search in quiz results 2025-03-13 21:21:41 +05:00
Ilgeldi 06ba1a923f fix: when steps has no length, not showing content 2025-03-10 16:15:29 +05:00
Ilgeldi 30dd824526 quiz[id]/netije route added 2025-03-07 15:18:56 +05:00
Ilgeldi ce4d7348d3 feature: autoPlay when video changes 2025-03-01 15:28:52 +05:00
Ilgeldi fbda9a42f3 playlist route: added next button, autoPlay next video when ended 2025-03-01 15:10:35 +05:00
Ilgeldi abe1590dcb added playlist route 2025-02-27 16:38:46 +05:00
Ilgeldi 37dd838a94 added playlist route 2025-02-27 16:13:29 +05:00
Ilgeldi cff0dd5371 quiz page adjustments, added pagination 2025-02-25 21:29:54 +05:00
Ilgeldi f6fddab1bf treasure section, video controls fix 2025-02-25 16:27:05 +05:00
Ilgeldi a4eae0e528 quiz page adjustments 2025-02-24 16:45:37 +05:00
Ilgeldi 15ac1ca1c7 feature 2025-02-18 15:47:56 +05:00
Ilgeldi 40546c8a3d route changes 2025-02-16 14:20:28 +05:00
Ilgeldi 87771711e2 swiched to client render 2025-02-16 13:44:15 +05:00
Ilgeldi 3a69358445 fix errors 2025-02-16 13:12:03 +05:00
Ilgeldi 6107dea9a5 fix errors 2025-02-16 12:24:13 +05:00
Ilgeldi 714932d3f3 fix ui errors 2025-02-11 17:34:39 +05:00
Ilgeldi f035fd03cc added revalidate tag for toss route 2025-02-10 18:23:33 +05:00
Ilgeldi 55cf8f593f new logic in lottery and toss route 2025-02-10 16:46:43 +05:00
51 changed files with 2794 additions and 905 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
NEXT_PRIVATE_DEBUG_CACHE=1

View File

@ -27,6 +27,7 @@ export async function authenticateLottery(phone: string, code: string) {
const result = await res.json();
console.log("Data fetched successfully " + res.status);
return result;
} catch (err) {
console.log(err);
@ -34,6 +35,6 @@ export async function authenticateLottery(phone: string, code: string) {
}
}
export const revalidateTagName = (tag: string) => {
export const revalidateTagName = async (tag: string) => {
revalidateTag(tag);
};

View File

@ -22,6 +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 { revalidateTag } from "next/cache";
import { cookies } from "next/headers";
export class Queries {
@ -190,6 +191,18 @@ export class Queries {
}).then((res) => res.json().then((res) => res as IQuizQuestions));
}
public static async getQuizByUUID(quiz_id: string): Promise<IQuizQuestions> {
return await fetch(`${baseUrl.QUIZ_SRC}${routes.getQuizUUID(quiz_id)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as IQuizQuestions));
}
public static async getQuizById(quiz_id: string) {
return await fetch(`${baseUrl.QUIZ_SRC}${routes.getQuiz(quiz_id)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res));
}
public static async getQuizHistory(
id: number
): Promise<IQuizQuestionsHistory> {
@ -227,6 +240,12 @@ export class Queries {
}).then((res) => res.json().then((res) => res as IVote));
}
public static async getVoteByUUID(vote_id: string): Promise<IVote> {
return await fetch(`${baseUrl.QUIZ_SRC}${routes.voteUUID(vote_id)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as IVote));
}
// ============================================================================================
// Sms ========================================================================================
@ -270,7 +289,12 @@ export const getTossData = async ({
id: string;
}) => {
try {
const res = await fetch(`${baseUrl.QUIZ_SRC}${routes.tossId(type, id)}`);
const res = await fetch(`${baseUrl.QUIZ_SRC}${routes.tossId(type, id)}`, {
next: {
revalidate: 300,
tags: ["lotteryData"],
},
});
if (!res.ok) {
return undefined;
@ -282,3 +306,134 @@ export const getTossData = async ({
console.log(err);
}
};
export const getQuizNetijeData = async (id: string) => {
try {
const res = await fetch(
`${baseUrl.QUIZ_SRC}${routes.getQuizNetijeWinners(id)}`,
{
next: {
revalidate: 300,
tags: ["netije"],
},
}
);
if (!res.ok) {
return undefined;
}
const result = await res.json();
return result;
} catch (err) {
console.log(err);
}
};
export const getNextQuizNetijeData = async (
id: string,
limit: number,
offset: number
) => {
try {
const res = await fetch(
`${baseUrl.QUIZ_SRC}${routes.getQuizNetijeWinners(
id
)}?limit=${limit}&offset=${offset}`,
{
next: {
revalidate: 300,
tags: ["netije"],
},
}
);
if (!res.ok) {
return undefined;
}
const result = await res.json();
return result;
} catch (err) {
console.log(err);
}
};
export const getQuizWinnersById = async (id: number, step?: number) => {
try {
const res = step
? await fetch(
`${baseUrl.QUIZ_SRC}${routes.getQuizQuestionsWinners(
id
)}?tapgyr=${step}`,
{
next: { revalidate: 3600 },
}
)
: await fetch(
`${baseUrl.QUIZ_SRC}${routes.getQuizQuestionsWinners(id)}`,
{
next: { revalidate: 3600 },
}
);
const result = await res.json();
return result as IQuizQuestionsWinners;
} catch (err) {
console.log(err);
}
};
export const getNextQuizWinnners = async (
id: number,
limit: number,
offset: number,
step?: number
) => {
try {
const res = step
? await fetch(
`${baseUrl.QUIZ_SRC}${routes.getQuizQuestionsWinners(
id
)}?tapgyr=${step}&limit=${limit}&offset=${offset}`,
{
next: { revalidate: 3600 },
}
)
: await fetch(
`${baseUrl.QUIZ_SRC}${routes.getQuizQuestionsWinners(
id
)}?limit=${limit}&offset=${offset}`,
{
next: { revalidate: 3600 },
}
);
const result = await res.json();
console.log(result);
return result as IQuizQuestionsWinners;
} catch (err) {
console.log(err);
}
};
export const getPlaylistById = async (id: string) => {
try {
const res = await fetch(
`${baseUrl.MATERIALS_SRC}${routes.videos(
`?per_page=8&page=1&category_id=${id}`
)}`,
{
next: { revalidate: 3600 },
}
);
const result = await res.json();
return result;
} catch (err) {
console.log(err);
}
};

View File

@ -0,0 +1,29 @@
import { getPlaylistById } from "@/api/queries";
import PlaylistVideos from "@/components/playlist";
import { notFound, redirect } from "next/navigation";
interface IParams {
params: {
id: string;
};
}
const Page = async ({ params }: IParams) => {
const { id } = await params;
const videos = await getPlaylistById(id);
if (videos?.data?.length === 0) {
notFound();
}
return (
<div className="video-item mt-6">
<div className="container">
<PlaylistVideos id={id} data={videos} />
</div>
</div>
);
};
export default Page;

View File

@ -1,19 +1,20 @@
'use client';
"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';
import QuizWinnerTable from '@/components/quiz/QuizWinnerTable';
import GradientTitle from '@/components/vote/GradientTitle';
import { IQuizQuestions, Question } from '@/models/quizQuestions.model';
import QuizProvider from '@/providers/QuizProvider';
import { Validator } from '@/utils/validator';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { useMediaQuery } from 'usehooks-ts';
import { Queries } from "@/api/queries";
import Loader from "@/components/Loader";
import QuizQuestionList from "@/components/quiz/QuizQuestionList";
import QuizSearch from "@/components/quiz/QuizSearch";
import QuizTable from "@/components/quiz/QuizTable";
import QuizWinnerTable from "@/components/quiz/QuizWinnerTable";
import GradientTitle from "@/components/vote/GradientTitle";
import { IQuizQuestions } from "@/models/quizQuestions.model";
import QuizProvider from "@/providers/QuizProvider";
import { useQuizSearchActive, useSteps } from "@/store/store";
import { Validator } from "@/utils/validator";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useMediaQuery } from "usehooks-ts";
interface IParams {
params: {
@ -24,38 +25,90 @@ interface IParams {
const page = ({ params }: IParams) => {
const [quizFinished, setQuizFinished] = useState<boolean>(false);
const [data, setData] = useState<IQuizQuestions>();
// const { data, error, isFetching } = useQuery(
// ['quiz_questions'],
// () => Queries.getQuizQuestions(),
// {
// keepPreviousData: true,
// },
// );
const { active } = useQuizSearchActive();
const { step, setStep } = useSteps();
useEffect(() => {
const local_info = sessionStorage.getItem("TURKMENTV_QUIZ_INFO");
if (!params.quiz_id) {
Queries.getQuizQuestions().then((res) => {
setData(res);
res
? res.data.questions[res.data.questions.length - 1].status === 'closed'
? setQuizFinished(true)
: setQuizFinished(false)
: null;
if (res.data.questions) {
res.data.questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
} else if (res.data.steps && res.data.steps?.length > 0) {
if (local_info) {
if (
JSON.parse(local_info)?.tab &&
JSON.parse(local_info)?.uuid === res.data.uuid
) {
setStep(JSON.parse(local_info)?.tab);
} else {
setStep(res.data.steps[res.data.steps.length - 1].tapgyr);
}
} else {
setStep(res.data.steps[res.data.steps.length - 1].tapgyr);
}
for (let i = 0; i < res.data.steps.length; i++) {
res.data.steps[i].questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
}
}
});
} else {
Queries.getQuiz(params.quiz_id).then((res) => {
setData(res);
res
? res.data.questions[res.data.questions.length - 1]?.status === 'closed'
? setQuizFinished(true)
: setQuizFinished(false)
: null;
if (res.data.questions) {
res.data.questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
} else if (res.data.steps && res.data.steps?.length > 0) {
if (local_info) {
if (
JSON.parse(local_info)?.tab &&
JSON.parse(local_info)?.uuid === res.data.uuid
) {
setStep(JSON.parse(local_info)?.tab);
} else {
setStep(res.data.steps[res.data.steps.length - 1].tapgyr);
}
} else {
setStep(res.data.steps[res.data.steps.length - 1].tapgyr);
}
for (let i = 0; i < res.data.steps.length; i++) {
res.data.steps[i].questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
}
}
});
}
}, []);
const mobile = useMediaQuery('(max-width: 768px)');
useEffect(() => {
if (data) {
sessionStorage.setItem(
"TURKMENTV_QUIZ_INFO",
JSON.stringify({
uuid: data.data.uuid,
tab: step,
})
);
}
}, [data, step]);
const mobile = useMediaQuery("(max-width: 768px)");
if (data) {
if (!data.data) {
@ -70,7 +123,7 @@ const page = ({ params }: IParams) => {
return (
<main className="pt-[60px] pb-[200px]">
{typeof data !== 'string' ? (
{typeof data !== "string" ? (
<div className="container flex flex-col md:gap-[200px] gap-[80px]">
<QuizProvider>
<div className="flex flex-col gap-[100px]">
@ -95,7 +148,7 @@ const page = ({ params }: IParams) => {
? data.data.banner_mobile
: data.data.banner
}
alt={'banner'}
alt={"banner"}
unoptimized
unselectable="off"
fill
@ -104,7 +157,7 @@ const page = ({ params }: IParams) => {
) : (
<Image
src={data?.data.banner}
alt={'banner'}
alt={"banner"}
unoptimized
unselectable="off"
fill
@ -116,14 +169,48 @@ const page = ({ params }: IParams) => {
</div>
{data?.data.rules && data.data.notes ? (
<QuizTable rules={data?.data.rules} notes={data?.data.notes} />
<QuizTable
rules={data?.data.rules}
notes={data?.data.notes}
/>
) : null}
</div>
{data?.data.id && quizFinished ? <QuizSearch quizId={data?.data.id} /> : null}
<div className="flex flex-col md:gap-[160px] gap-[80px]">
{data?.data ? (
{data.data.has_steps !== 0 &&
data.data.steps &&
data.data.steps?.length > 0 && (
<div className="flex flex-col gap-4 items-center w-full">
<h1 className="text-textBlack md:text-[60px] leading-[100%] font-semibold">
Gepleşik
</h1>
<div className="flex w-full md:w-1/2 gap-[10px]">
{data.data.steps.map((item) => (
<button
onClick={() => {
setStep(item.tapgyr);
}}
key={item.tapgyr}
className={`flex-1 py-[5px] rounded-lg transition-all duration-300 ${
step === item.tapgyr
? "bg-lightPrimary text-white"
: "bg-lightPrimaryContainer text-textLight"
}`}
>
{item.tapgyr}
</button>
))}
<Link
href={`/quiz/${params.quiz_id}/results`}
className={`flex-1 py-[5px] rounded-lg transition-all duration-300 bg-lightPrimaryContainer text-center text-textLight`}
>
Netije
</Link>
</div>
</div>
)}
{data?.data && !active ? (
<QuizQuestionList
paramsId={params.quiz_id}
initialQuestionsData={data}
@ -132,11 +219,14 @@ const page = ({ params }: IParams) => {
/>
) : null}
{data?.data.id && (
{data?.data.id && quizFinished && data.data.has_steps === 0 ? (
<QuizSearch quizId={data?.data.id} />
) : null}
{data?.data.id && data.data.has_steps === 0 && (
<QuizWinnerTable
smsNumber={data.data.sms_number}
quizId={data?.data.id}
quizFinished={quizFinished}
questionsData={data.data.questions}
/>
)}
</div>

View File

@ -0,0 +1,102 @@
"use client";
import { Queries } from "@/api/queries";
import QuizHeader from "@/components/quiz/QuizHeader";
import QuizResultsSearch from "@/components/quiz/QuizResultsSearch";
import QuizResultsTabs from "@/components/quiz/QuizResultsTabs";
import QuizTapgyrResults from "@/components/quiz/QuizTapgyrResults";
import QuizTapgyrWinners from "@/components/quiz/QuizTapgyrWinners";
import { Data } from "@/models/quizQuestions.model";
import { useQuizResults } from "@/store/store";
import { notFound } from "next/navigation";
import React, { useEffect, useState } from "react";
interface IParams {
params: {
quiz_id: string;
};
}
const Page = ({ params }: IParams) => {
const [data, setData] = useState<Data>();
const [tab, setTab] = useState<number | string>(0);
const [loading, setLoading] = useState<boolean>(false);
const { resultData, error } = useQuizResults();
useEffect(() => {
const local_info = sessionStorage.getItem("TURKMENTV_QUIZ_RESULTS");
if (!resultData.length && !error) {
setLoading(true);
Queries.getQuizById(params.quiz_id)
.then((res) => {
setData(res.data);
if (res.data.steps?.length) {
if (local_info) {
if (
JSON.parse(local_info)?.tab &&
JSON.parse(local_info)?.uuid === res.data.uuid
) {
setTab(JSON.parse(local_info)?.tab);
} else {
setTab(res.data?.steps[res.data?.steps.length - 1].tapgyr - 1);
}
} else {
setTab(res.data?.steps[res.data?.steps.length - 1].tapgyr - 1);
}
}
setLoading(false);
})
.catch(() => {
setLoading(false);
notFound();
});
}
}, [resultData, error]);
useEffect(() => {
if (data) {
sessionStorage.setItem(
"TURKMENTV_QUIZ_RESULTS",
JSON.stringify({
uuid: data?.uuid,
tab: tab,
})
);
}
}, [data, tab]);
return (
<section className="container py-[40px]">
<div className="flex flex-col w-full py-[40px] gap-[80px]">
<QuizHeader data={data} />
<QuizResultsSearch id={params.quiz_id} />
<QuizResultsTabs
steps={data?.steps ? data?.steps : []}
tab={tab}
setStep={setTab}
loading={loading}
/>
{tab === "results" && (
<QuizTapgyrResults
id={params.quiz_id}
steps={
data?.steps ? data?.steps?.map((item) => String(item.tapgyr)) : []
}
/>
)}
{data?.has_steps &&
data.steps &&
data.steps?.length > 0 &&
tab !== "results" && (
<QuizTapgyrWinners
key={data.steps[Number(tab)].tapgyr}
id={params.quiz_id}
tapgyr={data.steps[Number(tab)].tapgyr}
questions={data.steps[Number(tab)].questions}
/>
)}
</div>
</section>
);
};
export default Page;

View File

@ -1,15 +1,15 @@
'use client';
import { Queries } from '@/api/queries';
import QuizQuestionList from '@/components/quiz/QuizQuestionList';
import QuizSearch from '@/components/quiz/QuizSearch';
import QuizTable from '@/components/quiz/QuizTable';
import QuizWinnerTable from '@/components/quiz/QuizWinnerTable';
import { IQuizQuestions, Question } from '@/models/quizQuestions.model';
import QuizProvider from '@/providers/QuizProvider';
import { Validator } from '@/utils/validator';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { useMediaQuery } from 'usehooks-ts';
"use client";
import { Queries } from "@/api/queries";
import QuizQuestionList from "@/components/quiz/QuizQuestionList";
import QuizSearch from "@/components/quiz/QuizSearch";
import QuizTable from "@/components/quiz/QuizTable";
import QuizWinnerTable from "@/components/quiz/QuizWinnerTable";
import { IQuizQuestions, Question } from "@/models/quizQuestions.model";
import QuizProvider from "@/providers/QuizProvider";
import { Validator } from "@/utils/validator";
import Image from "next/image";
import { useEffect, useState } from "react";
import { useMediaQuery } from "usehooks-ts";
const page = () => {
const [quizFinished, setQuizFinished] = useState<boolean>(false);
@ -18,19 +18,19 @@ const page = () => {
useEffect(() => {
Queries.getQuizQuestions().then((res) => {
setData(res);
res
? res.data.questions[res.data.questions.length - 1]?.status === 'closed'
res && res.data.questions
? res.data.questions[res.data.questions.length - 1]?.status === "closed"
? setQuizFinished(true)
: setQuizFinished(false)
: null;
});
}, []);
const mobile = useMediaQuery('(max-width: 768px)');
const mobile = useMediaQuery("(max-width: 768px)");
return (
<main className="pt-[60px] pb-[200px]">
{typeof data !== 'string' ? (
{typeof data !== "string" ? (
<div className="container flex flex-col md:gap-[200px] gap-[80px]">
<QuizProvider>
<div className="flex flex-col gap-[100px]">
@ -55,7 +55,7 @@ const page = () => {
? data.data.banner_mobile
: data.data.banner
}
alt={'banner'}
alt={"banner"}
unoptimized
unselectable="off"
fill
@ -64,7 +64,7 @@ const page = () => {
) : (
<Image
src={data?.data.banner}
alt={'banner'}
alt={"banner"}
unoptimized
unselectable="off"
fill
@ -80,7 +80,9 @@ const page = () => {
) : null}
</div>
{data?.data.id && quizFinished ? <QuizSearch quizId={data?.data.id} /> : null}
{data?.data.id && quizFinished ? (
<QuizSearch quizId={data?.data.id} />
) : null}
<div className="flex flex-col md:gap-[160px] gap-[80px]">
{data?.data ? (
@ -96,9 +98,8 @@ const page = () => {
) : null} */}
{data?.data.id && (
<QuizWinnerTable
smsNumber={data.data.sms_number}
quizId={data?.data.id}
quizFinished={quizFinished}
questionsData={data.data.questions}
/>
)}
</div>

12
app/(main)/quiz/page.tsx Normal file
View File

@ -0,0 +1,12 @@
import QuizMainPage from "@/components/quiz/QuizMainPage";
import React, { Suspense } from "react";
function page() {
return (
<Suspense>
<QuizMainPage />
</Suspense>
);
}
export default page;

View File

@ -1,5 +1,6 @@
import ParticipantsList from '@/components/vote/ParticipantsList';
import VoteProvider from '@/providers/VoteProvider';
import ParticipantsList from "@/components/vote/ParticipantsList";
import VoteProvider from "@/providers/VoteProvider";
import { Suspense } from "react";
const page = () => {
return (
@ -7,7 +8,9 @@ const page = () => {
<div className="container">
<VoteProvider>
<div className="flex flex-col items-center w-full">
<ParticipantsList />
<Suspense>
<ParticipantsList all />
</Suspense>
</div>
</VoteProvider>
</div>

22
app/(main)/vote/page.tsx Normal file
View File

@ -0,0 +1,22 @@
"use client";
import ParticipantsList from "@/components/vote/ParticipantsList";
import VoteProvider from "@/providers/VoteProvider";
import { Suspense } from "react";
const page = () => {
return (
<main className="pt-[60px] pb-[120px]">
<div className="container">
<VoteProvider>
<div className="flex flex-col items-center w-full">
<Suspense>
<ParticipantsList />
</Suspense>
</div>
</VoteProvider>
</div>
</main>
);
};
export default page;

View File

@ -1,42 +1,42 @@
import localFont from 'next/font/local';
import Script from 'next/script';
import localFont from "next/font/local";
import Script from "next/script";
import { Roboto } from 'next/font/google';
import { Merriweather } from 'next/font/google';
import { Merriweather_Sans } from 'next/font/google';
import { Alexandria } from 'next/font/google';
import { Roboto } from "next/font/google";
import { Merriweather } from "next/font/google";
import { Merriweather_Sans } from "next/font/google";
import { Alexandria } from "next/font/google";
import 'swiper/swiper-bundle.css';
import './globals.css';
import QueryProvider from '@/providers/QueryProvider';
import { HtmlContext } from 'next/dist/shared/lib/html-context.shared-runtime';
import "swiper/swiper-bundle.css";
import "./globals.css";
import QueryProvider from "@/providers/QueryProvider";
import { HtmlContext } from "next/dist/shared/lib/html-context.shared-runtime";
// FONTS
const aeroport = localFont({
src: '../fonts/Aeroport.otf',
variable: '--font-aeroport',
src: "../fonts/Aeroport.otf",
variable: "--font-aeroport",
});
const roboto = Roboto({
subsets: ['latin'],
weight: ['300', '400', '700'],
variable: '--font-roboto',
subsets: ["latin"],
weight: ["300", "400", "700"],
variable: "--font-roboto",
});
const mw = Merriweather({
subsets: ['cyrillic', 'cyrillic-ext', 'latin', 'latin-ext'],
weight: '700',
variable: '--font-mw',
subsets: ["cyrillic", "cyrillic-ext", "latin", "latin-ext"],
weight: "700",
variable: "--font-mw",
});
const mw_sans = Merriweather_Sans({
subsets: ['cyrillic-ext', 'latin', 'latin-ext'],
weight: ['300', '400', '700'],
variable: '--font-mwsans',
subsets: ["cyrillic-ext", "latin", "latin-ext"],
weight: ["300", "400", "700"],
variable: "--font-mwsans",
});
const alexandria = Alexandria({
subsets: ['latin', 'latin-ext'],
variable: '--font-alexandria',
subsets: ["latin", "latin-ext"],
variable: "--font-alexandria",
});
export const metadata = {
title: 'Turkmen TV',
title: "Turkmen TV",
};
interface IProps {
@ -47,12 +47,14 @@ export default function RootLayout({ children }: IProps) {
return (
<html
lang="tm"
className={`${aeroport.variable} ${mw.variable} ${roboto.variable} ${mw_sans.variable} ${alexandria.variable}`}>
className={`${aeroport.variable} ${mw.variable} ${roboto.variable} ${mw_sans.variable} ${alexandria.variable}`}
>
<head>
<link rel="icon" href="/logo.png" sizes="any" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no;"></meta>
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no;"
></meta>
</head>
<body className="relative overflow-x-hidden">
<QueryProvider>{children}</QueryProvider>
@ -61,7 +63,8 @@ export default function RootLayout({ children }: IProps) {
<Script
id="ganalytics-import"
async
src="https://www.googletagmanager.com/gtag/js?id=G-F2267QXY9T"></Script>
src="https://www.googletagmanager.com/gtag/js?id=G-F2267QXY9T"
></Script>
<Script id="ganalytics-body">
{`
window.dataLayer = window.dataLayer || [];

View File

@ -1,12 +1,12 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useContext, useEffect, useRef, useState } from 'react';
import GlobalContext from '@/context/GlobalContext';
import close from '@/public/close-white.svg';
"use client";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useContext, useEffect, useRef, useState } from "react";
import GlobalContext from "@/context/GlobalContext";
import close from "@/public/close-white.svg";
const MobileMenu = () => {
const path = usePathname() ?? '/';
const path = usePathname() ?? "/";
const { burgerOpen, setBurgerOpen } = useContext(GlobalContext).burgerContext;
const onClickCloseBurgerHandler = () => {
@ -18,17 +18,20 @@ const MobileMenu = () => {
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setDropDownOpened(false);
}
};
// Attach the event listener to the document
document.addEventListener('click', handleOutsideClick);
document.addEventListener("click", handleOutsideClick);
// Cleanup the event listener when the component unmounts
return () => {
document.removeEventListener('click', handleOutsideClick);
document.removeEventListener("click", handleOutsideClick);
};
}, []);
@ -39,10 +42,10 @@ const MobileMenu = () => {
<div className="container">
<div className="w-full h-screen flex flex-col gap-10">
<div className="flex items-center justify-between">
<Link href={'/'} className="logo">
<Link href={"/"} className="logo">
<Image
priority
src={'/logo.png'}
src={"/logo.png"}
alt="logo"
unoptimized
unselectable="off"
@ -53,7 +56,8 @@ const MobileMenu = () => {
</Link>
<div
className="relative w-[24px] h-[24px] cursor-pointer "
onClick={() => setBurgerOpen(false)}>
onClick={() => setBurgerOpen(false)}
>
<Image src={close} fill alt="menu" />
</div>
</div>
@ -69,41 +73,47 @@ const MobileMenu = () => {
</li> */}
<li>
<Link
href={'/treasury'}
href={"/treasury"}
onClick={() => onClickCloseBurgerHandler()}
className="block text-3xl text-white transition-all font-roboto font-bold dark:text-white"
style={
path.includes('treasury') || path.includes('video/')
? { color: '#FFAB48' }
path.includes("treasury") || path.includes("video/")
? { color: "#FFAB48" }
: {}
}>
}
>
Hazyna
</Link>
</li>
<li>
<Link
href={'/live'}
href={"/live"}
onClick={() => onClickCloseBurgerHandler()}
className="block text-3xl text-white transition-all font-roboto font-bold dark:text-white"
style={path.includes('live') ? { color: '#FFAB48' } : {}}>
style={path.includes("live") ? { color: "#FFAB48" } : {}}
>
Göni Ýaýlym
</Link>
</li>
<li>
<Link
href={'/advert'}
href={"/advert"}
onClick={() => onClickCloseBurgerHandler()}
className="block text-3xl text-white transition-all font-roboto font-bold dark:text-white"
style={path.includes('advert') ? { color: '#FFAB48' } : {}}>
style={path.includes("advert") ? { color: "#FFAB48" } : {}}
>
Mahabat
</Link>
</li>
<li>
<Link
href={'/contact_us'}
href={"/contact_us"}
onClick={() => onClickCloseBurgerHandler()}
style={path.includes('contact_us') ? { color: '#FFAB48' } : {}}
className="font-roboto font-bold text-white text-3xl dark:text-white transition-all">
style={
path.includes("contact_us") ? { color: "#FFAB48" } : {}
}
className="font-roboto font-bold text-white text-3xl dark:text-white transition-all"
>
Habarlaşmak üçin
</Link>
</li>
@ -111,7 +121,8 @@ const MobileMenu = () => {
<div
ref={dropdownRef}
className="flex flex-col cursor-pointer relative"
onClick={() => setDropDownOpened(!dropDownOpened)}>
onClick={() => setDropDownOpened(!dropDownOpened)}
>
<div className="flex items-center gap-[8px]">
<span className="block text-3xl text-white transition-all font-roboto font-bold">
Interaktiw
@ -123,8 +134,9 @@ const MobileMenu = () => {
viewBox="0 0 24 24"
fill="none"
className={`${
dropDownOpened ? '' : 'rotate-180'
} transition-all ease-in duration-150`}>
dropDownOpened ? "" : "rotate-180"
} transition-all ease-in duration-150`}
>
<path
d="M7.41 15.41L12 10.83L16.59 15.41L18 14L12 8L6 14L7.41 15.41Z"
fill="#fff"
@ -135,56 +147,68 @@ const MobileMenu = () => {
<div
className={` flex flex-col gap-4 rounded-[8px] transition-all duration-150 ${
dropDownOpened
? 'h-fit opacity-100 pointer-events-auto py-[16px] px-[24px]'
: ' h-0 opacity-0 pointer-events-none'
}`}>
? "h-fit opacity-100 pointer-events-auto py-[16px] px-[24px]"
: " h-0 opacity-0 pointer-events-none"
}`}
>
<Link
href={'/quiz'}
href={"/quiz"}
className="block text-2xl text-white transition-all font-roboto font-bold "
style={path.includes('quiz') ? { color: '#FFAB48' } : {}}
style={
path.includes("quiz") ? { color: "#FFAB48" } : {}
}
onClick={() => {
setDropDownOpened(false);
onClickCloseBurgerHandler();
}}>
}}
>
Bäsleşik
</Link>
<Link
href={'/vote'}
href={"/vote"}
className="block text-2xl text-white transition-all font-roboto font-bold "
style={path.includes('vote') ? { color: '#FFAB48' } : {}}
style={
path.includes("vote") ? { color: "#FFAB48" } : {}
}
onClick={() => {
setDropDownOpened(false);
onClickCloseBurgerHandler();
}}>
}}
>
Ses bermek
</Link>
<Link
href={'https://shop.turkmentv.gov.tm/'}
href={"https://shop.turkmentv.gov.tm/"}
className="block text-2xl text-white transition-all font-roboto font-bold"
onClick={() => {
setDropDownOpened(false);
onClickCloseBurgerHandler();
}}>
}}
>
TV market
</Link>
<Link
href={'/prizes/auth'}
href={"/prizes/auth"}
className="block text-2xl text-white transition-all font-roboto font-bold"
style={path.includes('prizes') ? { color: '#FFAB48' } : {}}
style={
path.includes("prizes") ? { color: "#FFAB48" } : {}
}
onClick={() => {
setDropDownOpened(false);
onClickCloseBurgerHandler();
}}>
}}
>
Sowgatlar
</Link>
<Link
href={'/lottery/auth'}
href={"/b"}
className="block text-2xl text-white transition-all font-roboto font-bold"
style={path.includes('lottery') ? { color: '#FFAB48' } : {}}
style={path.includes("b") ? { color: "#FFAB48" } : {}}
onClick={() => {
setDropDownOpened(false);
onClickCloseBurgerHandler();
}}>
}}
>
Bije
</Link>
</div>
@ -193,9 +217,10 @@ const MobileMenu = () => {
<li>
<Link
target="_blank"
href={'https://turkmentv.gov.tm/mahabat/admin/user/login'}
href={"https://turkmentv.gov.tm/mahabat/admin/user/login"}
onClick={() => onClickCloseBurgerHandler()}
className="font-roboto font-bold text-white text-3xl dark:text-white transition-all p-2 bg-red-500 rounded-lg ">
className="font-roboto font-bold text-white text-3xl dark:text-white transition-all p-2 bg-red-500 rounded-lg "
>
Ýaýlym tertibi
</Link>
</li>

View File

@ -2,8 +2,6 @@
import Image from 'next/image';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { AiOutlineUser } from 'react-icons/ai';
import ThemeSwitch from './home/ThemeSwitch';
import { useContext, useEffect, useRef, useState } from 'react';
import GlobalContext from '@/context/GlobalContext';
import burger from '@/public/menu-outline.svg';
@ -140,9 +138,9 @@ const Nav = () => {
Sowgatlar
</Link>
<Link
href={'/lottery/auth'}
href={'/b'}
className="block min-w-fit text-lg text-white transition-all font-roboto font-bold"
style={path.includes('lottery') ? { color: '#FFAB48' } : {}}
style={path.includes('b') ? { color: '#FFAB48' } : {}}
onClick={() => setDropDownOpened(false)}>
Bije
</Link>

View File

@ -28,11 +28,13 @@ const VideoPlayer = ({ maxHeight, maxWidth, video_id }: IProps) => {
queryKey: ["video", `video:${video_id}`],
queryFn: async () => {
const response = await Queries.getVideo(video_id);
if (response.data.is_downloadable === 0) {
setCanDownload(false);
} else {
setCanDownload(true);
} // Set canDownload from API
// console.log(response);
// if (response.data.is_downloadable == 1) {
// setCanDownload(true);
// } else {
// setCanDownload(false);
// } // Set canDownload from API
return response;
},
});
@ -67,13 +69,18 @@ const VideoPlayer = ({ maxHeight, maxWidth, video_id }: IProps) => {
<div className="lg:w-[700px] md:w-[550px] w-full h-[200px] sm:h-[250px] md:h-[350px] lg:h-[420px]">
<video
controls
controlsList={canDownload ? "" : "nodownload"} // Conditionally enable/disable download
src={data!.data.video_stream_url}
controlsList={
data?.data.is_downloadable === 0 ? "nodownload" : ""
} // Conditionally enable/disable download
poster={data?.data.banner_url}
playsInline
itemType="video/mp4"
onPlay={() => onPlayHandler()}
></video>
>
<source
src={data!.data.video_stream_url}
type="video/mp4"
onPlay={() => onPlayHandler()}
/>
</video>
</div>
) : (
<div className="flex flex-col gap-4 h-fit">
@ -89,7 +96,9 @@ const VideoPlayer = ({ maxHeight, maxWidth, video_id }: IProps) => {
</div>
<audio
controls
controlsList={canDownload ? "" : "nodownload"} // Conditionally enable/disable download
controlsList={
data?.data.is_downloadable === 0 ? "nodownload" : ""
} // Conditionally enable/disable download
className="w-full rounded bg-white"
onPlay={() => onPlayHandler()}
>

View File

@ -19,7 +19,7 @@ const Confetti = () => {
const mobile = useMediaQuery("(max-width: 426px)");
useEffect(() => {
setTimeout(() => setRecycle(false), 30000);
setTimeout(() => setRecycle(false), 10000);
}, [recycle]);
return (

View File

@ -1,5 +1,5 @@
import Image from 'next/image';
import InfoDateAllert from '../common/InfoDateAllert';
import Image from "next/image";
import InfoDateAllert from "../common/InfoDateAllert";
interface LotteryHeaderProps {
title: string;
@ -9,7 +9,13 @@ interface LotteryHeaderProps {
startDate: string;
}
const LotteryHeader = ({ title, description, image, smsCode, startDate }: LotteryHeaderProps) => {
const LotteryHeader = ({
title,
description,
image,
smsCode,
startDate,
}: LotteryHeaderProps) => {
return (
<section className="container">
<div className="flex flex-col md:gap-[32px] gap-[24px]">
@ -17,7 +23,9 @@ const LotteryHeader = ({ title, description, image, smsCode, startDate }: Lotter
<h1 className="sm:font-display-1-regular text-[32px] leading-[40px] text-center">
{title}
</h1>
<p className="text-center text-textLarge leading-textLarge">{description}</p>
<p className="text-center text-textLarge leading-textLarge">
{description}
</p>
<InfoDateAllert date={startDate} text="Senesi:" />
</div>
{image && (
@ -27,7 +35,7 @@ const LotteryHeader = ({ title, description, image, smsCode, startDate }: Lotter
width={1416}
height={177}
alt="banner"
className="rounded-[12px] object-cover h-[177px]"
className="rounded-[12px] h-[177px] object-cover"
/>
</div>
)}

View File

@ -1,29 +1,37 @@
"use client";
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 { useRouter } from "next/navigation";
import { getLotteryStatus } from "@/lib/actions";
import LotteryWinners from "./LotteryWinners";
import { useEffect, useState } from "react";
import { useMediaQuery } from "usehooks-ts";
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 = () => {
const [lotteryData, setLotteryData] = useState<any>();
const router = useRouter();
const mobile = useMediaQuery("(max-width: 768px)");
const LotteryMain = async () => {
const lotteryData = await getData();
useEffect(() => {
async function getData() {
const phone = localStorage.getItem("phoneNumber");
const key = localStorage.getItem("key");
if (phone && key) {
const res = await authenticateLottery(phone, key);
setLotteryData(res);
} else {
localStorage.clear();
router.push("/b/auth");
}
}
const status = await getLotteryStatus(
getData();
}, []);
const status = getLotteryStatus(
lotteryData?.data.start_time,
lotteryData?.data.end_time
);
@ -33,41 +41,45 @@ const LotteryMain = async () => {
<h1 className="text-[50px]">{lotteryData.errorMessage}</h1>
</div>
) : (
<div className="flex flex-col md:gap-[128px] gap-[80px] font-roboto md:pt-[64px] sm:pt-[48px] pt-[40px] ms:pb-[128px] pb-[80px] text-lightOnSurface">
<div className="flex flex-col sm:gap-[64px] gap-[40px]">
<LotteryHeader
title={lotteryData.data.title}
description={lotteryData.data.description}
image={lotteryData.data.image}
smsCode={lotteryData.data.sms_code}
startDate={lotteryData.data.start_time}
/>
lotteryData?.data && (
<div className="flex flex-col font-roboto md:pt-[64px] sm:pt-[48px] pt-[40px] ms:pb-[128px] pb-[80px] text-lightOnSurface">
<div className="flex flex-col sm:gap-[64px] gap-[40px] pb-[80px]">
<LotteryHeader
title={lotteryData?.data.title}
description={lotteryData?.data.description}
image={
mobile ? lotteryData?.data.image_mobile : lotteryData?.data.image
}
smsCode={lotteryData?.data.sms_code}
startDate={lotteryData?.data.start_time}
/>
{status === "Upcoming" && (
<div className="container">
<LotteryCountDown
lotteryStatus={status}
endDate={lotteryData.data.end_time}
startDate={lotteryData.data.start_time}
/>
</div>
)}
</div>
<LotteryRulesSection data={lotteryData} />
<div className="flex flex-col gap-10">
<LotteryWinners data={lotteryData} lotteryStatus={status} />
<div className="w-full">
<div className="container">
<Link
href="/lottery/auth"
className="sm:text-textLarge sm:leading-textLarge text-[16px] rounded-full leading-[24px] sm:py-[12px] py-[8px] w-full flex justify-center items-center border-2 border-lightPrimary hover:bg-lightPrimary font-medium text-lightPrimary hover:text-lightOnPrimary disabled:opacity-50 transition-all duration-300"
>
Çykmak
</Link>
{status === "Upcoming" && (
<div className="container">
<LotteryCountDown
lotteryStatus={status}
endDate={lotteryData.data.end_time}
startDate={lotteryData.data.start_time}
/>
</div>
)}
</div>
<LotteryRulesSection data={lotteryData} />
<div className="flex flex-col gap-10 mt-[40px]">
<LotteryWinners data={lotteryData} lotteryStatus={status} />
<div className="w-full">
<div className="container">
<Link
href="/b/auth"
className="sm:text-textLarge sm:leading-textLarge text-[16px] rounded-full leading-[24px] sm:py-[12px] py-[8px] w-full flex justify-center items-center border-2 border-lightPrimary hover:bg-lightPrimary font-medium text-lightPrimary hover:text-lightOnPrimary disabled:opacity-50 transition-all duration-300"
>
Çykmak
</Link>
</div>
</div>
</div>
</div>
</div>
)
);
};

View File

@ -1,5 +1,4 @@
import { useState, useEffect } from "react";
import { useLotteryAuth } from "@/store/useLotteryAuth";
import { LotteryWinnerDataSimplified } from "@/typings/lottery/lottery.types";
import LotteryWinnersList from "./winners/LotteryWinnersList";
import LotterySlotCounter from "./slotCounter/LotterySlotCounter";
@ -83,10 +82,12 @@ const LotteryWinnersSection = ({ data }: { data: any }) => {
setIsProcessing(true); // Lock processing
const message = messageQueue[0]; // Get the first message in the queue
try {
await handleMessage(message);
} catch (error) {
console.error("Error processing message:", error);
if (message?.winner_no) {
try {
await handleMessage(message);
} catch (error) {
console.error("Error processing message:", error);
}
}
setMessageQueue((prevQueue) => prevQueue.slice(1)); // Remove the processed message from the queue
@ -131,13 +132,13 @@ const LotteryWinnersSection = ({ data }: { data: any }) => {
<div className="container">
<div
className="flex flex-col items-center rounded-[32px] gap-[40px]"
className="flex flex-col items-center rounded-[32px] gap-[60px] pt-[20px]"
style={{
background: "linear-gradient(180deg, #F0ECF4 0%, #E1E0FF 43.5%)",
}}
>
<AnimatePresence>
<div className="flex items-center justify-center w-full sm:min-h-[240px] pt-6">
<div className="flex items-center justify-center w-full pt-6">
{winnerSelectingStatus === "not-selected" ? (
<AnimatedText
key={topText}
@ -187,7 +188,7 @@ const LotteryWinnersSection = ({ data }: { data: any }) => {
/>
)}
</div>
<div className="flex gap-6 rounded-[12px] flex-1 w-full items-center justify-center sm:pb-[62px] pb-[32px] px-4">
<div className="flex gap-6 rounded-[12px] flex-1 w-full items-center justify-center pb-[32px] px-4">
{winners.length > 0 && <LotteryWinnersList winners={winners} />}
</div>
</div>

View File

@ -36,10 +36,9 @@ const LotteryAuthForm = () => {
if (response.errorMessage) {
setError(response.errorMessage);
} else {
console.log(response);
document.cookie = `phoneNumber=${phoneNumber};path=/`;
document.cookie = `key=${key};path=/`;
router.replace("/lottery");
localStorage.setItem("phoneNumber", phoneNumber);
localStorage.setItem("key", key);
router.replace("/b");
}
} catch (err) {
setError("Telefon belgisi ýa-da açar nädogry");

View File

@ -57,10 +57,10 @@ const LotteryCountDown: React.FC<LotteryCountDownProps> = ({
<div className="bg-lightPrimaryContainer sm:p-6 py-3 flex flex-col w-full md:gap-2 rounded-[12px] sm:gap-3 gap-0 text-lightOnPrimaryContainer">
<h3 className="text-center md:font-heading-1-regular sm:text-[32px] sm:leading-[40px] text-[20px] leading-[28px] text-lightOnSurface">
{status === "Ongoing"
? "Bije dowam edýär"
? "Çeklis dowam edýär"
: status === "Finished"
? "Bije tamamlandy"
: "Bije"}
? "Çeklisx tamamlandy"
: null}
</h3>
{/* LotteryCountDown */}
{status === "Upcoming" && (
@ -69,9 +69,6 @@ const LotteryCountDown: React.FC<LotteryCountDownProps> = ({
<h3 className="md:text-[80px] sm:text-[56px] text-[28px] md:leading-[88px] sm:leading-[64px] leading-[36px] -tracking-[1%]">
{timeLeft.hours}
</h3>
<h4 className="font-medium md:text-[20px] sm:text-[18px] text-[14px] sm:leading-[28px] leading-[20px] -tracking-[1%] text-lightOnSurfaceVariant">
sagat
</h4>
</div>
{/* Dots */}
@ -84,9 +81,6 @@ const LotteryCountDown: React.FC<LotteryCountDownProps> = ({
<h3 className="md:text-[80px] sm:text-[56px] text-[28px] md:leading-[88px] sm:leading-[64px] leading-[36px] -tracking-[1%]">
{timeLeft.minutes}
</h3>
<h4 className="font-medium md:text-[20px] sm:text-[18px] text-[14px] sm:leading-[28px] leading-[20px] -tracking-[1%] text-lightOnSurfaceVariant">
minut
</h4>
</div>
{/* Dots */}
@ -99,22 +93,9 @@ const LotteryCountDown: React.FC<LotteryCountDownProps> = ({
<h3 className="md:text-[80px] sm:text-[56px] text-[28px] md:leading-[88px] sm:leading-[64px] leading-[36px] -tracking-[1%]">
{timeLeft.seconds}
</h3>
<h4 className="font-medium md:text-[20px] sm:text-[18px] text-[14px] sm:leading-[28px] leading-[20px] -tracking-[1%] text-lightOnSurfaceVariant">
sekunt
</h4>
</div>
</div>
)}
<div className="flex items-center justify-center text-lightOnSurfaceVariant md:font-heading-1-regular md:text-[20px] sm:text-[18px] sm:leading-[28px] text-[14px] leading-[20px]">
<span>
{status === "Upcoming"
? "- den başlar"
: status === "Ongoing"
? "girmek üçin aşakda kodyňyzy giriziň"
: "netijeleri görmek üçin aşakda kodyňyzy giriziň"}
</span>
</div>
</div>
);
};

View File

@ -1,5 +1,6 @@
"use client";
import { useWebsocketLottery } from "@/hooks/useWebSocketLottery";
import clsx from "clsx";
import { useEffect, useState } from "react";
interface IProps {
@ -19,11 +20,6 @@ const LotteryRulesSection = ({ show = true, data }: IProps) => {
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) => {
@ -62,46 +58,62 @@ const LotteryRulesSection = ({ show = true, data }: IProps) => {
<section>
<div className="container">
<div className="flex flex-col md:gap-8 gap-6">
<h2 className="md:font-heading-1-regular sm:text-[32px] text-[26px] sm:leading-[40px] leading-[34px]">
Düzgünleri:
</h2>
<div className="grid md:grid-cols-2 gap-6">
<div className="flex flex-col bg-lightSurfaceContainer sm:py-4 md:px-8 sm:px-6 py-3 px-4 rounded-[12px] w-full">
<ul className="list-disc flex flex-col md:gap-4 gap-2 pl-[16px]">
{data?.data.rules?.map((item: any, i: number) => (
<li className="font-small-regular" key={i}>
{item.title}
</li>
))}
</ul>
<div
className={clsx(
"grid gap-6",
show ? "md:grid-cols-3" : "md:grid-cols-2"
)}
>
<div className="flex flex-col">
<h2 className="md:font-heading-1-regular text-[22px] sm:leading-[40px] leading-[34px]">
Düzgünleri:
</h2>
<div className="flex flex-1 flex-col bg-lightSurfaceContainer sm:py-4 md:px-8 sm:px-6 py-3 px-4 rounded-[12px] w-full">
<ul className="list-disc flex flex-col md:gap-4 gap-2 pl-[16px]">
{data?.data.rules?.map((item: any, i: number) => (
<li className="font-small-regular" key={i}>
{item.title}
</li>
))}
</ul>
</div>
</div>
<div className="bg-lightSurfaceContainer flex flex-col gap-4 px-4 py-[12px] rounded-[12px]">
<h1 className="md:font-heading-5-regular sm:text-[20px] text-[18px] sm:leading-[24px] leading-[28px]">
Umumy gatnaşyjylaryň sany:
<div className="flex flex-col">
<h1 className="md:font-heading-1-regular text-[22px] sm:leading-[40px] leading-[34px]">
Gatnaşyjylaryň sany:
</h1>
<p className="text-[24px]">{totalParticipants}</p>
<div className="bg-lightSurfaceContainer flex flex-1 items-center justify-center gap-4 px-4 py-[12px] rounded-[12px]">
<p
className={clsx(
data?.data.rules?.length > 4
? `text-[28px] sm:text-[56px] md:text-[120px]`
: "text-[24px] md:text-[32px]", "font-bold"
)}
>
{totalParticipants}
</p>
</div>
</div>
</div>
{show && (
<div className="flex flex-col md:gap-4 sm:gap-2 gap-4 bg-lightSurfaceContainer sm:py-4 md:px-8 sm:px-6 py-3 px-4 rounded-[12px] w-full">
<h3 className="md:font-heading-5-regular sm:text-[20px] text-[18px] sm:leading-[24px] leading-[28px]">
Siziň bijeli sanynyz:
</h3>
<ul className="flex flex-col items-center md:gap-4 gap-2">
{data?.user_lottery_numbers.map((item: any, i: number) => (
<li
className="text-[24px] text-[#46464F] md:text-[48px] lg:text-[80px] list-none"
key={i}
>
{item}
</li>
))}
</ul>
</div>
)}
{show && (
<div className="flex flex-col">
<h3 className="md:font-heading-1-regular text-[22px] sm:leading-[40px] leading-[34px]">
Siziň bijeli sanyňyz:
</h3>
<ul className="flex flex-col flex-1 md:gap-4 gap-2 bg-lightSurfaceContainer sm:py-4 md:px-8 sm:px-6 py-3 px-4 rounded-[12px] w-full">
{data?.user_lottery_numbers.map((item: any, i: number) => (
<li
className="text-[24px] text-[#46464F] list-none"
key={i}
>
{item}
</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
</section>

View File

@ -1,6 +1,6 @@
'use client';
"use client";
import { motion } from 'framer-motion';
import { motion } from "framer-motion";
interface IProps {
phone: string;
@ -14,14 +14,14 @@ const LotteryWinner = ({ phone, ticket, isNew, winnerNumber }: IProps) => {
<motion.div
layout
initial={isNew ? { opacity: 0, translateY: 20 } : false}
animate={{ opacity: 1, translateY: 0 }}
exit={{ opacity: 0, translateY: -20 }}
animate={isNew ? { opacity: 1, translateY: 0 } : false}
transition={{ duration: 0.5 }}
className="flex flex-col gap-2 md:pb-4 pb-3 border-b w-full border-[#CECCFF]">
className="flex flex-col gap-2 md:pb-4 pb-3 border-b w-full border-[#CECCFF]"
>
<h4 className="md:font-heading-6-regular text-[20px] leading-[28px]">
{/* The winner of the {winnerNumber} stage: */}
{winnerNumber}
{' - nji ýeňiji'}
{" - nji ýeňiji"}
</h4>
<div className="flex items-center gap-4">
<p className="md:font-base-medium font-base-regular">{phone}</p>

View File

@ -0,0 +1,285 @@
"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import aydym from "@/public/aydym-com.webp";
import horjun from "@/public/horjun.png";
import belet from "@/public/belet.jpg";
import { v4 } from "uuid";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { Queries } from "@/api/queries";
import Loader from "../Loader";
import { Button } from "../ui/button";
const PlaylistVideos = ({ id, data }: { id: string; data: any }) => {
const searchParams = useSearchParams();
const videoId = searchParams.get("video");
const nextVideo = (Number(videoId) + 1) % data?.data?.length;
return (
<div className="video-item-inner">
<div className="video-item-wrapper flex flex-col md:flex-row md:items-start items-center gap-10 relative pb-14 w-full">
<InfoBlock
video_id={data?.data[Number(videoId)].id}
nextId={nextVideo}
/>
<div className="video-item-inner flex flex-col gap-4 grow-0">
{data?.data.map(
(item: any, i: number) =>
i !== Number(videoId) && (
<Link href={`/playlist/${id}?video=${i}`} key={v4()}>
<VideoItem
id={item.id}
img={item.banner_url}
title={item.title}
/>
</Link>
)
)}
</div>
</div>
</div>
);
};
export default PlaylistVideos;
const InfoBlock = ({
video_id,
nextId,
}: {
video_id: number;
nextId: number;
}) => {
const { data, isFetching, error } = useQuery({
queryKey: ["video", video_id],
queryFn: () => Queries.getVideo(video_id),
});
const router = useRouter();
const pathName = usePathname();
if (isFetching)
return (
<div className="w-full h-[500px] sm:h-[667px] md:h-[600px] l flex items-center justify-center">
<Loader height={700} />
</div>
);
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="flex gap-6 flex-1 w-full justify-between h-full">
<div className="flex flex-col gap-6 w-full h-full">
<div className="flex justify-between gap-[32px] w-full h-full">
<div className="w-full flex flex-col gap-6 h-full">
<div className="flex flex-col gap-[40px] h-full w-full">
<div className="flex gap-[40px] flex-col h-full w-full">
<div className="w-full">
<VideoPlayer content={data?.data} nextId={nextId} />
</div>
<div className="flex flex-col gap-6">
{data?.data?.desc ? (
<p className="font-roboto text-lg w-full text-black">
{data?.data.desc}
</p>
) : null}
<Button
onClick={() => router.push(`${pathName}?video=${nextId}`)}
className="w-fit bg-blue-500 text-white flex gap-[5px] items-center"
>
<span>Indiki</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="lucide lucide-play"
>
<polygon points="6 3 20 12 6 21 6 3" />
</svg>
</Button>
{data?.data?.aydym_com_url ||
data?.data?.horjun_content_url ||
data?.data?.belet_url ? (
<div className="flex flex-col gap-6">
<h2 className="text-2xl font-semibold">
Beýleki platformalarda seret:
</h2>
<div className="flex gap-11 items-center">
{data?.data.aydym_com_url ? (
<Link
href={data?.data.aydym_com_url}
target="_blank"
className="flex flex-col items-center justify-center gap-2"
>
<div className="relative h-16 w-16 flex items-center justify-center overflow-hidden border-[#00AEFF] border-[1.5px] p-2 rounded-full">
<Image
src={aydym}
alt="Aydym.com"
className="rounded-full"
/>
</div>
<h3>Aydym.com</h3>
</Link>
) : null}
{data?.data.horjun_content_url ? (
<Link
href={data?.data.horjun_content_url}
target="_blank"
className="flex flex-col items-center justify-center gap-2"
>
<div className="relative h-16 w-16 flex items-center justify-center overflow-hidden border-[#00AEFF] border-[1.5px] p-2 rounded-full">
<Image
src={horjun}
alt="HorjunTv"
className="rounded-full"
/>
</div>
<h3>HorjunTv</h3>
</Link>
) : null}
{data?.data.belet_url ? (
<Link
href={data?.data.belet_url}
target="_blank"
className="flex flex-col items-center justify-center gap-2"
>
<div className="relative h-16 w-16 flex items-center justify-center overflow-hidden border-[#00AEFF] border-[1.5px] p-2 rounded-full">
<Image
src={belet}
alt="BeletTv"
className="rounded-full"
/>
</div>
<h3>BeletFilm</h3>
</Link>
) : null}
</div>
</div>
) : null}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
const VideoPlayer = ({ content, nextId }: { content: any; nextId: number }) => {
const [hasWindow, setHasWindow] = useState<boolean>(false);
const [data, setData] = useState<any>(content);
const router = useRouter();
const pathName = usePathname();
useEffect(() => {
if (typeof window !== "undefined") {
setHasWindow(true);
}
}, []);
useEffect(() => {
setData(content);
}, [content]);
return (
<div className="w-full h-full">
{hasWindow ? (
data?.content_url.endsWith(".mp4") ? (
<div className="lg:w-full md:w-[550px] w-full h-[200px] sm:h-[250px] md:h-[350px] lg:h-[620px]">
<video
controls
controlsList={data?.is_downloadable === 0 ? "nodownload" : ""} // Conditionally enable/disable download
poster={data?.banner_url}
playsInline
autoPlay={nextId !== 1}
onEnded={() => router.push(`${pathName}?video=${nextId}`)}
>
<source src={data?.video_stream_url} type="video/mp4" />
</video>
</div>
) : (
<div className="flex flex-col gap-4 h-fit">
<div className="relative w-full h-[200px] sm:h-[400px] md:h-[420px]">
{data?.banner_url ? (
<Image
src={data.banner_url}
fill
alt={"image"}
className="object-cover"
/>
) : null}
</div>
<audio
controls
controlsList={data?.is_downloadable === 0 ? "nodownload" : ""} // Conditionally enable/disable download
className="w-full rounded bg-white"
onEnded={() => router.push(`${pathName}?video=${nextId}`)}
autoPlay={nextId !== 1}
>
<source src={data?.content_url} />
</audio>
</div>
)
) : null}
</div>
);
};
const VideoItem = ({
img,
title,
id,
}: {
img: any;
title: string;
id: number;
}) => {
return (
<div className="video-list-item flex flex-col gap-2 lg:w-[250px] md:w-[200px] sm:w-[200px] w-[300px] ">
{img ? (
<>
<div className="relative sm:h-[150px] h-[200px] w-full overflow-hidden rounded-five">
<Image
src={img}
alt={title}
unoptimized
unselectable="off"
className="top-0 left-0 w-full h-full object-cover z-0 absolute pointer-events-none"
width={280}
height={160}
/>
<Image
src={"/play.svg"}
alt={"play icon"}
unoptimized
unselectable="off"
className="top-[50%] left-[50%] w-[52px] h-[52px] object-cover z-10 -translate-x-[50%] -translate-y-[50%] absolute"
width={52}
height={52}
/>
</div>
</>
) : null}
{/* {premium ? <Premium /> : null} */}
<div className="flex gap-3 justify-start items-center">
<div className="flex flex-col">
<h3 className="clamped font-mw_sans font-bold text-lg text-black transition-all dark:text-white overflow-hidden w-full">
{title}
</h3>
{/* <span className="font-roboto text-lg font-normal text-black transition-all dark:text-white">{`${views} Views`}</span> */}
</div>
</div>
</div>
);
};

View File

@ -1,12 +1,12 @@
'use client';
import { useContext, useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Queries } from '@/api/queries';
import Loader from '../Loader';
import { IQuizQuestionsHistory } from '@/models/quizQuestionHistory.model';
import { Validator } from '@/utils/validator';
import { v4 } from 'uuid';
import QuizContext from '@/context/QuizContext';
"use client";
import { useContext, useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Queries } from "@/api/queries";
import Loader from "../Loader";
import { IQuizQuestionsHistory } from "@/models/quizQuestionHistory.model";
import { Validator } from "@/utils/validator";
import { v4 } from "uuid";
import QuizContext from "@/context/QuizContext";
type TProps = {
finished: string;
@ -15,13 +15,19 @@ type TProps = {
const QuizAccordion = ({ finished, questionId }: TProps) => {
const [data, setData] = useState<IQuizQuestionsHistory>();
const [opened, setOpened] = useState<boolean>(finished === 'closed' ? false : true);
const [opened, setOpened] = useState<boolean>(
finished === "closed" ? false : true
);
const { quizSearchData } = useContext(QuizContext).quizSearchContext;
const { searchActive } = useContext(QuizContext).quizSearchActiveContext;
useEffect(() => {
if (quizSearchData && Object.values(quizSearchData.data).length != 0 && searchActive) {
if (
quizSearchData &&
Object.values(quizSearchData.data).length != 0 &&
searchActive
) {
setOpened(true);
} else {
setOpened(false);
@ -56,10 +62,14 @@ const QuizAccordion = ({ finished, questionId }: TProps) => {
<>
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}>
<div className="flex bg-fillTableHead md:p-0 gap-3 md:gap-0 border-b border-fillTableStrokeTableHead p-[8px] w-full">
transition={{ duration: 0.3 }}
>
<div
onClick={() => setOpened(!opened)}
className="flex bg-fillTableHead md:p-0 gap-3 md:gap-0 border-b border-fillTableStrokeTableHead p-[8px] w-full cursor-pointer"
>
<div className="block text-[12px] sm:text-base text-textBlack leading-[125%] font-semibold max-w-[15px] sm:max-w-[100px] w-full sm:px-6 sm:py-5">
<span></span>
</div>
@ -83,9 +93,12 @@ const QuizAccordion = ({ finished, questionId }: TProps) => {
{data.data.map((user, id) => (
<div
className={`flex border-b border-fillTableStrokeTableRow last:border-none ${
id % 2 === 0 ? ' bg-fillTableRow' : 'bg-fillTableRow2'
id % 2 === 0
? " bg-fillTableRow"
: "bg-fillTableRow2"
}`}
key={v4()}>
key={v4()}
>
<div className="block text-base text-textBlack leading-[125%] max-w-[100px] w-[100%] px-6 py-5">
<span>{id + 1}</span>
</div>
@ -111,9 +124,12 @@ const QuizAccordion = ({ finished, questionId }: TProps) => {
{data.data.map((user, id) => (
<div
className={`flex items-center justify-center flex-col border-b border-fillTableStrokeTableRow last:border-none p-[8px] gap-1 ${
id % 2 === 0 ? ' bg-fillTableRow' : 'bg-fillTableRow2'
id % 2 === 0
? " bg-fillTableRow"
: "bg-fillTableRow2"
}`}
key={v4()}>
key={v4()}
>
<div className="flex gap-[12px] w-full items-center">
<div className="block text-[12px] text-textBlack leading-[125%] max-w-[15px] w-full">
<span>{id + 1}</span>
@ -147,9 +163,12 @@ const QuizAccordion = ({ finished, questionId }: TProps) => {
<div key={v4()}>
<div
className={`sm:flex border-b border-fillTableStrokeTableRow last:border-none hidden ${
answerId % 2 === 0 ? ' bg-fillTableRow' : 'bg-fillTableRow2'
answerId % 2 === 0
? " bg-fillTableRow"
: "bg-fillTableRow2"
}`}
key={v4()}>
key={v4()}
>
<div className="block text-base leading-[125%] max-w-[100px] w-[100%] px-6 py-5">
{answer.serial_number_for_correct != null ? (
<span className="text-fillGreen">
@ -177,12 +196,16 @@ const QuizAccordion = ({ finished, questionId }: TProps) => {
<div
className={`sm:hidden flex items-center justify-center flex-col border-b border-fillTableStrokeTableRow last:border-none p-[8px] gap-1 ${
answerId % 2 === 0 ? ' bg-fillTableRow' : 'bg-fillTableRow2'
answerId % 2 === 0
? " bg-fillTableRow"
: "bg-fillTableRow2"
}`}
key={v4()}>
key={v4()}
>
<div className="flex gap-[12px] w-full items-center">
<div className="block text-[12px] leading-[125%] max-w-[15px] w-full">
{answer.serial_number_for_correct != null ? (
{answer.serial_number_for_correct !=
null ? (
<span className="text-fillGreen">
{answer.serial_number_for_correct}
</span>
@ -204,12 +227,14 @@ const QuizAccordion = ({ finished, questionId }: TProps) => {
</div>
</div>
<div className="text-[12px] text-textDarkt leading-[125%] max-w-[289px] w-[100%] text-center">
<span>Wagty: {Validator.parseDate(answer.dt)}</span>
<span>
Wagty: {Validator.parseDate(answer.dt)}
</span>
</div>
</div>
</div>
))
: null,
: null
)
: null}
</motion.div>
@ -217,12 +242,15 @@ const QuizAccordion = ({ finished, questionId }: TProps) => {
)}
</AnimatePresence>
) : null}
{finished === 'closed' ? (
{finished === "closed" ? (
<button
onClick={() => setOpened(!opened)}
className={`w-full ${
opened ? 'bg-fillRed text-white' : 'bg-fillButtonLowContrastDefault text-textDarkt'
} flex items-center justify-center text-xs md:text-base uppercase leading-[150%] p-[8px] md:py-5 gap-[5px] md:gap-[10px] transition-all delay-[0.2s] font-medium `}>
opened
? "bg-fillRed text-white"
: "bg-fillButtonLowContrastDefault text-textDarkt"
} flex items-center justify-center text-xs md:text-base uppercase leading-[150%] p-[8px] md:py-5 gap-[5px] md:gap-[10px] transition-all delay-[0.2s] font-medium `}
>
Netijeler
<div>
<svg
@ -231,10 +259,11 @@ const QuizAccordion = ({ finished, questionId }: TProps) => {
height="24"
viewBox="0 0 24 24"
fill="none"
className="w-[20px] h-[20px] md:w-[24px] md:h-[24px]">
className="w-[20px] h-[20px] md:w-[24px] md:h-[24px]"
>
<path
d="M7.41 15.41L12 10.83L16.59 15.41L18 14L12 8L6 14L7.41 15.41Z"
fill={`${opened ? '#fff' : '#636370'}`}
fill={`${opened ? "#fff" : "#636370"}`}
/>
</svg>
</div>

View File

@ -0,0 +1,45 @@
"use client";
import { Data } from "@/models/quizQuestions.model";
import Image from "next/image";
import React from "react";
const QuizHeader = ({ data }: { data: Data | undefined }) => {
return (
<header className="flex flex-col gap-[40px]">
<article>
<span className="text-[#46464F]">{data?.date}</span>
<h1 className="text-[44px] font-[500] text-[#1B1B21] leading-10">
<span className="md:hidden">Bäsleşik </span>
<span className="hidden md:inline">"Bäsleşigiň" netijeleri</span>
</h1>
<p className="text-[#46464F] mt-[8px]">{data?.description}</p>
</article>
{(data?.banner || data?.banner_mobile) && (
<>
{data.banner_mobile && (
<div className="w-full relative bg-[#E1E0FF] h-[100px] rounded-[8px] overflow-hidden md:hidden object-cover">
<Image
src={data.banner_mobile ? data.banner_mobile : data.banner}
alt="banner"
unselectable="off"
className="object-cover"
fill
/>
</div>
)}
<div className="w-full h-[100px] hidden md:block bg-[#E1E0FF] rounded-[8px] relative overflow-hidden">
<Image
src={data.banner}
alt="banner"
unselectable="off"
className="object-cover"
fill
/>
</div>
</>
)}
</header>
);
};
export default QuizHeader;

View File

@ -0,0 +1,197 @@
"use client";
import { Queries } from "@/api/queries";
import Loader from "@/components/Loader";
import QuizQuestionList from "@/components/quiz/QuizQuestionList";
import QuizSearch from "@/components/quiz/QuizSearch";
import QuizTable from "@/components/quiz/QuizTable";
import QuizWinnerTable from "@/components/quiz/QuizWinnerTable";
import GradientTitle from "@/components/vote/GradientTitle";
import { IQuizQuestions } from "@/models/quizQuestions.model";
import QuizProvider from "@/providers/QuizProvider";
import { useQuizSearchActive, useSteps } from "@/store/store";
import { Validator } from "@/utils/validator";
import Image from "next/image";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { useMediaQuery } from "usehooks-ts";
const QuizMainPage = () => {
const searchParams = useSearchParams();
const router = useRouter();
const [quizFinished, setQuizFinished] = useState<boolean>(false);
const [data, setData] = useState<IQuizQuestions>();
const { active } = useQuizSearchActive();
const { step, setStep } = useSteps();
const id = searchParams.get("d");
useEffect(() => {
if (id) {
Queries.getQuizByUUID(id).then((res) => {
setData(res);
if (res.data.questions) {
res.data.questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
} else if (res.data.steps && res.data.steps?.length > 0) {
setStep(res.data.steps[0].tapgyr);
for (let i = 0; i < res.data.steps.length; i++) {
res.data.steps[i].questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
}
}
});
} else {
router.push("/quiz/active");
}
}, []);
const mobile = useMediaQuery("(max-width: 768px)");
if (data) {
if (!data.data) {
return (
<main className="h-full py-[200px]">
<div className="container">
<GradientTitle title={data?.message} size="big" />
</div>
</main>
);
}
return (
<main className="pt-[60px] pb-[200px]">
{typeof data !== "string" ? (
<div className="container flex flex-col md:gap-[200px] gap-[80px]">
<QuizProvider>
<div className="flex flex-col gap-[100px]">
<div className="flex flex-col gap-[45px]">
<div className="flex flex-col gap-[10px] md:gap-[5px]">
<h3 className="text-base md:text-[14px] text-textLight font-semibold md:font-normal">
{data ? Validator.reveseDate(data?.data.date) : null}
</h3>
<h1 className="text-textBlack text-[32px] md:text-[60px] leading-[100%] font-semibold">
{data?.data.title}
</h1>
<h3 className="text-base font-medium leading-[125%] md:text-[14px] text-textDarkt mt-[5px] max-w-[600px]">
{data?.data.description}
</h3>
</div>
{data?.data.banner ? (
<div className="relative w-full md:min-h-[150px] md:h-auto h-[100px]">
{mobile ? (
<Image
src={
data.data.banner_mobile !== null
? data.data.banner_mobile
: data.data.banner
}
alt={"banner"}
unoptimized
unselectable="off"
fill
className="rounded-[8px]"
/>
) : (
<Image
src={data?.data.banner}
alt={"banner"}
unoptimized
unselectable="off"
fill
className="rounded-[8px]"
/>
)}
</div>
) : null}
</div>
{data?.data.rules && data.data.notes ? (
<QuizTable
rules={data?.data.rules}
notes={data?.data.notes}
/>
) : null}
</div>
<div className="flex flex-col md:gap-[160px] gap-[80px]">
{data.data.has_steps !== 0 &&
data.data.steps &&
data.data.steps?.length > 0 && (
<div className="flex flex-col gap-4 items-center w-full">
<h1 className="text-textBlack md:text-[60px] leading-[100%] font-semibold">
Tapgyr
</h1>
<div className="flex w-full md:w-1/2 gap-[10px]">
{data.data.steps.map((item) => (
<button
onClick={() => {
setStep(item.tapgyr);
}}
key={item.tapgyr}
className={`flex-1 py-[5px] rounded-lg transition-all duration-300 ${
step === item.tapgyr
? "bg-lightPrimary text-white"
: "bg-lightPrimaryContainer text-textLight"
}`}
>
{item.tapgyr}
</button>
))}
<Link
href={`/quiz/${data.data.id}/results`}
className={`flex-1 py-[5px] rounded-lg transition-all duration-300 bg-lightPrimaryContainer text-center text-textLight`}
>
Netije
</Link>
</div>
</div>
)}
{data?.data && !active ? (
<QuizQuestionList
paramsId={String(data.data.id)}
initialQuestionsData={data}
setQuizFinished={setQuizFinished}
quizFinished={quizFinished}
/>
) : null}
{data?.data.id && quizFinished && data.data.has_steps === 0 ? (
<QuizSearch quizId={data?.data.id} />
) : null}
{data?.data.id && data.data.has_steps === 0 && (
<QuizWinnerTable
quizId={data?.data.id}
questionsData={data.data.questions}
/>
)}
</div>
</QuizProvider>
</div>
) : (
<div className="container text-[40px] flex items-center justify-center font-bold text-textLight min-h-[30vh]">
Непредвиденная ошибка. Нет активной викторины.
</div>
)}
</main>
);
} else {
return (
<main className="h-full py-[200px]">
<div className="container">
<Loader />
</div>
</main>
);
}
};
export default QuizMainPage;

View File

@ -1,8 +1,8 @@
import React, { useContext } from 'react';
import QuizAccordion from './QuizAccordion';
import { Validator } from '@/utils/validator';
import QuizContext from '@/context/QuizContext';
import { v4 } from 'uuid';
import React, { useContext } from "react";
import QuizAccordion from "./QuizAccordion";
import { Validator } from "@/utils/validator";
import QuizContext from "@/context/QuizContext";
import { v4 } from "uuid";
type TProps = {
finished: string;
@ -15,26 +15,26 @@ type TProps = {
};
const numbers = [
'Birinji sowal',
'Ikinji sowal',
'Üçünji sowal',
'Dördünji sowal',
'Bäşinji sowal',
'Altynjy sowal',
'Ýedinji sowal',
'Sekizinji sowal',
'Dokuzynjy sowal',
'Onunjy sowal',
'On Birinji sowal',
'On Ikinji sowal',
'On Üçünji sowal',
'On Dördünji sowal',
'On Bäşinji sowal',
'On Altynji sowal',
'On Ýeddi sowal',
'On Sekiznji sowal',
'On Dokuzunji sowal',
'Ýigrinci sowal',
"Birinji sowal",
"Ikinji sowal",
"Üçünji sowal",
"Dördünji sowal",
"Bäşinji sowal",
"Altynjy sowal",
"Ýedinji sowal",
"Sekizinji sowal",
"Dokuzynjy sowal",
"Onunjy sowal",
"On Birinji sowal",
"On Ikinji sowal",
"On Üçünji sowal",
"On Dördünji sowal",
"On Bäşinji sowal",
"On Altynji sowal",
"On Ýeddi sowal",
"On Sekiznji sowal",
"On Dokuzunji sowal",
"Ýigrinci sowal",
];
const QuizQuestion = ({
@ -51,20 +51,21 @@ const QuizQuestion = ({
Object.values(quizSearchData.data).map((userQuestion, id) =>
userQuestion.question_id === questionId ? (
<div className="flex flex-col gap-[20px]" key={v4()}>
<div className="flex flex-col gap-[30px] max-w-[700px]">
<div className="flex flex-col gap-[30px]">
<div className="flex flex-col gap-[20px] md:gap-[10px]">
<h2 className="text-[24px] md:text-[32px] leading-[100%] font-semibold text-textBlack text-center md:text-left">
{numbers.map((number, id) => (id === questionNumber ? number : ''))}:
<h2 className="text-[24px] md:text-[32px] leading-[120%] font-semibold text-textBlack">
{question}
</h2>
<div className="flex flex-wrap justify-center md:justify-start md:gap-5 gap-[10px]">
<div className="flex flex-wrap md:gap-5 gap-[10px]">
<div className="flex gap-2 items-center">
<div
className={`w-[18px] h-[18px] md:w-4 md:h-4 ${
finished === 'closed' ? 'bg-fillRed' : 'bg-fillGreen'
} rounded-full`}></div>
finished === "closed" ? "bg-fillRed" : "bg-fillGreen"
} rounded-full`}
></div>
<span className="text-textLight text-sm md:text-base">
{finished === 'closed' ? 'ýapyk' : 'açyk'}
{finished === "closed" ? "ýapyk" : "açyk"}
</span>
</div>
<div className="flex gap-2 items-center">
@ -73,14 +74,16 @@ const QuizQuestion = ({
width="16"
height="16"
viewBox="0 0 16 16"
fill="none">
fill="none"
>
<path
d="M7.99967 13.3334C10.933 13.3334 13.333 10.9334 13.333 8.00004C13.333 5.06671 10.933 2.66671 7.99967 2.66671C5.06634 2.66671 2.66634 5.06671 2.66634 8.00004C2.66634 10.9334 5.06634 13.3334 7.99967 13.3334ZM7.99967 1.33337C11.6663 1.33337 14.6663 4.33337 14.6663 8.00004C14.6663 11.6667 11.6663 14.6667 7.99967 14.6667C4.33301 14.6667 1.33301 11.6667 1.33301 8.00004C1.33301 4.33337 4.33301 1.33337 7.99967 1.33337ZM10.1997 10.8L9.33301 11.3334L7.33301 7.86671V4.66671H8.33301V7.60004L10.1997 10.8Z"
fill="#878799"
/>
</svg>
<span className="text-textLight text-sm md:text-base">
{Validator.parseDate(startsAt)}-den {Validator.parseDate(endsAt)}-e çenli
{Validator.parseDate(startsAt)}-den{" "}
{Validator.parseDate(endsAt)}-e çenli
</span>
</div>
<div className="flex items-center gap-[5px]">
@ -89,7 +92,8 @@ const QuizQuestion = ({
width="18"
height="18"
viewBox="0 0 18 18"
fill="none">
fill="none"
>
<path
d="M9 5.25C8.5875 5.25 8.25 5.5875 8.25 6V8.25H6C5.5875 8.25 5.25 8.5875 5.25 9C5.25 9.4125 5.5875 9.75 6 9.75H8.25V12C8.25 12.4125 8.5875 12.75 9 12.75C9.4125 12.75 9.75 12.4125 9.75 12V9.75H12C12.4125 9.75 12.75 9.4125 12.75 9C12.75 8.5875 12.4125 8.25 12 8.25H9.75V6C9.75 5.5875 9.4125 5.25 9 5.25ZM9 1.5C4.8675 1.5 1.5 4.8675 1.5 9C1.5 13.1325 4.8675 16.5 9 16.5C13.1325 16.5 16.5 13.1325 16.5 9C16.5 4.8675 13.1325 1.5 9 1.5ZM9 15C5.6925 15 3 12.3075 3 9C3 5.6925 5.6925 3 9 3C12.3075 3 15 5.6925 15 9C15 12.3075 12.3075 15 9 15Z"
fill="#878799"
@ -101,33 +105,31 @@ const QuizQuestion = ({
</div>
</div>
</div>
<p className="text-base md:text-xl text-textDarkt italic md:text-start text-center">
«{question}»
</p>
</div>
{finished === 'closed' ? (
{finished === "closed" ? (
<QuizAccordion finished={finished} questionId={questionId} />
) : null}
</div>
) : null,
) : null
)
) : (
<div className="flex flex-col gap-[20px]">
<div className="flex flex-col gap-[30px] max-w-[700px]">
<div className="flex flex-col gap-[30px]">
<div className="flex flex-col gap-[20px] md:gap-[10px]">
<h2 className="text-[24px] md:text-[32px] leading-[100%] font-semibold text-textBlack text-center md:text-left">
{numbers.map((number, id) => (id === questionNumber ? number : ''))}:
<h2 className="text-[18px] md:text-[32px] leading-[120%] font-semibold text-textBlack">
{question}
</h2>
<div className="flex flex-wrap justify-center md:justify-start md:gap-5 gap-[10px]">
<div className="flex flex-wrap md:gap-5 gap-[10px]">
<div className="flex gap-2 items-center">
<div
className={`w-[18px] h-[18px] md:w-4 md:h-4 ${
finished === 'closed' ? 'bg-fillRed' : 'bg-fillGreen'
} rounded-full`}></div>
finished === "closed" ? "bg-fillRed" : "bg-fillGreen"
} rounded-full`}
></div>
<span className="text-textLight text-sm md:text-base">
{finished === 'closed' ? 'ýapyk' : 'açyk'}
{finished === "closed" ? "ýapyk" : "açyk"}
</span>
</div>
<div className="flex gap-2 items-center">
@ -136,14 +138,16 @@ const QuizQuestion = ({
width="16"
height="16"
viewBox="0 0 16 16"
fill="none">
fill="none"
>
<path
d="M7.99967 13.3334C10.933 13.3334 13.333 10.9334 13.333 8.00004C13.333 5.06671 10.933 2.66671 7.99967 2.66671C5.06634 2.66671 2.66634 5.06671 2.66634 8.00004C2.66634 10.9334 5.06634 13.3334 7.99967 13.3334ZM7.99967 1.33337C11.6663 1.33337 14.6663 4.33337 14.6663 8.00004C14.6663 11.6667 11.6663 14.6667 7.99967 14.6667C4.33301 14.6667 1.33301 11.6667 1.33301 8.00004C1.33301 4.33337 4.33301 1.33337 7.99967 1.33337ZM10.1997 10.8L9.33301 11.3334L7.33301 7.86671V4.66671H8.33301V7.60004L10.1997 10.8Z"
fill="#878799"
/>
</svg>
<span className="text-textLight text-sm md:text-base">
{Validator.parseDate(startsAt)}-den {Validator.parseDate(endsAt)}-e çenli
{Validator.parseDate(startsAt)}-den{" "}
{Validator.parseDate(endsAt)}-e çenli
</span>
</div>
<div className="flex items-center gap-[5px]">
@ -152,7 +156,8 @@ const QuizQuestion = ({
width="18"
height="18"
viewBox="0 0 18 18"
fill="none">
fill="none"
>
<path
d="M9 5.25C8.5875 5.25 8.25 5.5875 8.25 6V8.25H6C5.5875 8.25 5.25 8.5875 5.25 9C5.25 9.4125 5.5875 9.75 6 9.75H8.25V12C8.25 12.4125 8.5875 12.75 9 12.75C9.4125 12.75 9.75 12.4125 9.75 12V9.75H12C12.4125 9.75 12.75 9.4125 12.75 9C12.75 8.5875 12.4125 8.25 12 8.25H9.75V6C9.75 5.5875 9.4125 5.25 9 5.25ZM9 1.5C4.8675 1.5 1.5 4.8675 1.5 9C1.5 13.1325 4.8675 16.5 9 16.5C13.1325 16.5 16.5 13.1325 16.5 9C16.5 4.8675 13.1325 1.5 9 1.5ZM9 15C5.6925 15 3 12.3075 3 9C3 5.6925 5.6925 3 9 3C12.3075 3 15 5.6925 15 9C15 12.3075 12.3075 15 9 15Z"
fill="#878799"
@ -164,12 +169,11 @@ const QuizQuestion = ({
</div>
</div>
</div>
<p className="text-base md:text-xl text-textDarkt italic md:text-start text-center">
«{question}»
</p>
</div>
{finished === 'closed' ? <QuizAccordion finished={finished} questionId={questionId} /> : null}
{finished === "closed" ? (
<QuizAccordion finished={finished} questionId={questionId} />
) : null}
</div>
);
};

View File

@ -1,10 +1,11 @@
'use client';
import QuizQuestion from './QuizQuestion';
import { Queries } from '@/api/queries';
import { v4 } from 'uuid';
import { Dispatch, useContext, useEffect, useState } from 'react';
import { IQuizQuestions, Question } from '@/models/quizQuestions.model';
import QuizContext from '@/context/QuizContext';
"use client";
import QuizQuestion from "./QuizQuestion";
import { Queries } from "@/api/queries";
import { v4 } from "uuid";
import { Dispatch, useContext, useEffect, useState } from "react";
import { IQuizQuestions, Question } from "@/models/quizQuestions.model";
import QuizContext from "@/context/QuizContext";
import { useSteps } from "@/store/store";
interface IProps {
setQuizFinished: Dispatch<boolean>;
@ -19,32 +20,38 @@ const QuizQuestionList = ({
initialQuestionsData,
paramsId,
}: IProps) => {
const [data, setData] = useState<IQuizQuestions>(initialQuestionsData);
const { quizSearchData } = useContext(QuizContext).quizSearchContext;
const { setQuestionsData } = useContext(QuizContext).quizQuestionsContext;
const { step } = useSteps();
const [questionData, setQuestionsData] = useState<Question[] | undefined>(
initialQuestionsData.data.questions
? initialQuestionsData.data.questions
: initialQuestionsData.data.steps &&
initialQuestionsData.data.steps.length > 0
? initialQuestionsData.data.steps[0].questions
: []
);
useEffect(() => {
// Queries.getQuizQuestions().then((res) => setData(res));
setQuestionsData(initialQuestionsData.data.questions);
// data?.data.questions.map((question) =>
// question.status === 'active' || question.status === 'new'
// ? setQuizFinished(false)
// : setQuizFinished(true),
// );
if (paramsId && !quizFinished) {
const interval = setInterval(() => {
Queries.getQuiz(paramsId).then((res) => {
setData(res);
setQuestionsData(res.data.questions);
res.data.questions.map((question) =>
question.status === 'active' || question.status === 'new'
? setQuizFinished(false)
: setQuizFinished(true),
);
if (res.data.questions) {
setQuestionsData(res.data.questions);
res.data.questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
} else if (res.data.steps && res.data.steps?.length > 0) {
setQuestionsData(res.data.steps[0].questions);
for (let i = 0; i < res.data.steps.length; i++) {
res.data.steps[i].questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
}
}
});
}, 60000);
return () => clearInterval(interval);
@ -53,66 +60,47 @@ const QuizQuestionList = ({
if (!paramsId && !quizFinished) {
const interval = setInterval(() => {
Queries.getQuizQuestions().then((res) => {
setData(res);
setQuestionsData(res.data.questions);
res.data.questions.map((question) =>
question.status === 'active' || question.status === 'new'
? setQuizFinished(false)
: setQuizFinished(true),
);
if (res.data.questions) {
setQuestionsData(res.data.questions);
res.data.questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
} else if (res.data.steps && res.data.steps?.length > 0) {
setQuestionsData(res.data.steps[0].questions);
for (let i = 0; i < res.data.steps.length; i++) {
res.data.steps[i].questions.map((question) =>
question.status === "active" || question.status === "new"
? setQuizFinished(false)
: setQuizFinished(true)
);
}
}
});
// const isActive = data?.data.questions.some(
// (question) => question.status === 'active' || question.status === 'new',
// );
// data.data.questions.map((question) =>
// question.status === 'active' || question.status === 'new'
// ? setQuizFinished(false)
// : setQuizFinished(true),
// );
}, 60000);
return () => clearInterval(interval);
// Queries.getQuizQuestions().then((res) => {
// setData(res);
// setQuestionsData(res.data.questions);
// setSmsNumber(res.data.sms_number);
// res.data.questions.map((question) =>
// question.status === 'active' || question.status === 'new'
// ? setQuizFinished(false)
// : setQuizFinished(true),
// );
// });
}
}, [quizFinished]);
useEffect(() => {
if (initialQuestionsData.data.steps) {
const tapgyrQuestions = initialQuestionsData.data.steps.find(
(item) => item.tapgyr === step
);
setQuestionsData(tapgyrQuestions?.questions);
}
}, [step]);
return (
<div className="flex flex-col gap-[40px] md:gap-[160px]">
{data && !quizSearchData ? (
data.data.questions.map((question, paramsId) =>
question.status !== 'new' ? (
<QuizQuestion
score={question.score}
questionId={question.id}
questionNumber={paramsId}
finished={question.status}
question={question.question}
key={v4()}
startsAt={question.starts_at}
endsAt={question.ends_at}
/>
) : null,
)
) : quizSearchData && Object.values(quizSearchData.data).length === 0 ? (
{quizSearchData && Object.values(quizSearchData.data).length === 0 ? (
<h2 className="text-textDarkt text-center text-[28px] ms:text-[32px] flex items-center justify-center bg-fillBGBlockbg px-[20px] py-[40px] md:px-[40px] md:py-[80px] rounded-[24px]">
Вы не участвовали ни в одном вопросе
</h2>
) : data ? (
data.data.questions.map((question, id) =>
question.status !== 'new' ? (
) : questionData ? (
questionData.map((question, id) =>
question.status !== "new" ? (
<QuizQuestion
score={question.score}
questionId={question.id}
@ -123,7 +111,7 @@ const QuizQuestionList = ({
startsAt={question.starts_at}
endsAt={question.ends_at}
/>
) : null,
) : null
)
) : null}
</div>

View File

@ -0,0 +1,93 @@
"use client";
import { useQuizResults, useResultsLoading } from "@/store/store";
import { X } from "lucide-react";
import React, { useState } from "react";
const QuizResultsSearch = ({ id }: { id: string }) => {
const [phone, setPhone] = useState<string>("");
const { setError, setResultData, error, resultData } = useQuizResults();
const { setLoading } = useResultsLoading();
const handleSearchSubmit = async (event: any) => {
if (event.key === "Enter" && phone.length === 8) {
event.preventDefault();
try {
setLoading(true);
const response = await fetch(
`https://sms.turkmentv.gov.tm/api/quiz/${id}/search_netije`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ phone }),
}
);
// Handle the response as needed
const data = await response.json();
setLoading(false);
if (!data.error) {
setResultData([data.data]);
} else {
setResultData([]);
setError("Telefon belgisi tapylmady");
}
} catch (error) {
console.log(error);
}
}
};
return (
<div className="flex flex-col w-full items-center gap-4">
<h1 className="text-[28px] leading-[120%] font-semibold text-textBlack text-center md:text-left">
Öz jogaplaryňyzy görüň
</h1>
<div className="flex items-center gap-[14px] relative md:w-1/2 w-full">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute left-4"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.93853 0.608964C5.90914 0.206926 6.94943 0 8 0C9.05058 0 10.0909 0.206926 11.0615 0.608964C12.0321 1.011 12.914 1.60028 13.6569 2.34315C14.3997 3.08601 14.989 3.96793 15.391 4.93853C15.7931 5.90914 16 6.94943 16 8C16 9.05057 15.7931 10.0909 15.391 11.0615C15.1172 11.7226 14.7565 12.3425 14.3196 12.9054L19.7071 18.2929C20.0976 18.6834 20.0976 19.3166 19.7071 19.7071C19.3166 20.0976 18.6834 20.0976 18.2929 19.7071L12.9054 14.3196C12.3425 14.7565 11.7226 15.1172 11.0615 15.391C10.0909 15.7931 9.05057 16 8 16C6.94943 16 5.90914 15.7931 4.93853 15.391C3.96793 14.989 3.08601 14.3997 2.34315 13.6569C1.60028 12.914 1.011 12.0321 0.608964 11.0615C0.206926 10.0909 0 9.05058 0 8C0 6.94942 0.206926 5.90914 0.608964 4.93853C1.011 3.96793 1.60028 3.08601 2.34315 2.34315C3.08601 1.60028 3.96793 1.011 4.93853 0.608964ZM8 2C7.21207 2 6.43185 2.15519 5.7039 2.45672C4.97595 2.75825 4.31451 3.20021 3.75736 3.75736C3.20021 4.31451 2.75825 4.97595 2.45672 5.7039C2.15519 6.43185 2 7.21207 2 8C2 8.78793 2.15519 9.56815 2.45672 10.2961C2.75825 11.0241 3.20021 11.6855 3.75736 12.2426C4.31451 12.7998 4.97595 13.2417 5.7039 13.5433C6.43185 13.8448 7.21207 14 8 14C8.78793 14 9.56815 13.8448 10.2961 13.5433C11.0241 13.2417 11.6855 12.7998 12.2426 12.2426C12.7998 11.6855 13.2417 11.0241 13.5433 10.2961C13.8448 9.56815 14 8.78793 14 8C14 7.21207 13.8448 6.43185 13.5433 5.7039C13.2417 4.97595 12.7998 4.31451 12.2426 3.75736C11.6855 3.20021 11.0241 2.75825 10.2961 2.45672C9.56815 2.15519 8.78793 2 8 2Z"
fill="#46464F"
/>
</svg>
<input
type="tel"
className="text-[#46464F] focus:outline-[#1B1B21] flex-1 bg-[#E4E1E9] rounded-[12px] py-4 pr-4 pl-12 w-full"
placeholder="63879809"
required
value={phone}
onChange={(e) => setPhone(e.target.value)}
onKeyDown={(e) => handleSearchSubmit(e)}
maxLength={8}
minLength={8}
/>
{phone && (
<button
className="absolute right-4"
onClick={() => {
setPhone("");
if (resultData.length || error) {
setResultData([]);
setError("");
}
}}
>
<X />
</button>
)}
</div>
</div>
);
};
export default QuizResultsSearch;

View File

@ -0,0 +1,50 @@
import { Question } from "@/models/quizQuestions.model";
import React from "react";
interface IQuizTabProps {
steps: {
tapgyr: number;
questions: Question[];
}[];
setStep: React.Dispatch<React.SetStateAction<number | string>>;
tab: number | string;
loading: boolean;
}
function QuizResultsTabs({ steps, setStep, tab, loading }: IQuizTabProps) {
return (
!loading && (
<div className="flex gap-[10px] w-full md:w-1/2 self-center">
{steps.map((item) => (
<button
onClick={() => {
setStep(item.tapgyr - 1);
}}
key={item.tapgyr}
className={`flex-1 py-[5px] rounded-lg transition-all duration-300 ${
tab === item.tapgyr - 1
? "bg-lightPrimary text-white"
: "bg-lightPrimaryContainer text-textLight"
}`}
>
{item.tapgyr}
</button>
))}
<button
onClick={() => {
setStep("results");
}}
className={`flex-1 py-[5px] rounded-lg transition-all duration-300 ${
tab === "results"
? "bg-lightPrimary text-white"
: "bg-lightPrimaryContainer text-textLight"
}`}
>
Netije
</button>
</div>
)
);
}
export default QuizResultsTabs;

View File

@ -1,10 +1,12 @@
'use client';
import QuizContext from '@/context/QuizContext';
import { AnimatePresence, motion } from 'framer-motion';
import { ChangeEvent, FormEvent, useContext, useState } from 'react';
"use client";
import QuizContext from "@/context/QuizContext";
import { useQuizSearchActive } from "@/store/store";
import { AnimatePresence, motion } from "framer-motion";
import { ChangeEvent, FormEvent, useContext, useState } from "react";
const QuizSearch = ({ quizId }: { quizId: number }) => {
const [phone, setPhone] = useState<string>('');
const [phone, setPhone] = useState<string>("");
const { setActive } = useQuizSearchActive();
const { setSearchActive } = useContext(QuizContext).quizSearchActiveContext;
@ -12,8 +14,9 @@ const QuizSearch = ({ quizId }: { quizId: number }) => {
const handleCleanSearch = () => {
setQuizSearchData(undefined);
setPhone('');
setPhone("");
setSearchActive(false);
setActive(false);
};
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
@ -21,7 +24,7 @@ const QuizSearch = ({ quizId }: { quizId: number }) => {
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
if (event.key === "Enter") {
// Prevent the default form submission behavior
event.preventDefault();
// Call the function to handle the form submission
@ -29,23 +32,27 @@ const QuizSearch = ({ quizId }: { quizId: number }) => {
}
};
const handleSearchSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
const handleSearchSubmit = async (
event: React.FormEvent<HTMLFormElement>
) => {
event.preventDefault();
try {
const response = await fetch(`https://sms.turkmentv.gov.tm/api/quiz/${quizId}/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ phone }),
});
const response = await fetch(
`https://sms.turkmentv.gov.tm/api/quiz/${quizId}/search`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ phone }),
}
);
// Handle the response as needed
const data = await response.json();
setQuizSearchData(data);
setSearchActive(true);
setActive(true);
} catch (error) {}
};
@ -57,9 +64,10 @@ const QuizSearch = ({ quizId }: { quizId: number }) => {
<div className="max-w-[500px] w-full flex flex-col gap-[10px] items-center ">
<form
onSubmit={handleSearchSubmit}
className="bg-fillFormRest flex border w-full rounded-xl shadow-quizButton">
className="bg-fillFormRest flex border w-full rounded-xl shadow-quizButton"
>
<div className="relative rounded-lg rounded-e-none w-full">
{' '}
{" "}
<input
type="tel"
className="block w-full h-full rounded-lg rounded-e-none py-3 px-2 md:px-4 text-sm md:text-base leading-[150%] text-textCaptioninform bg-transparent"
@ -79,13 +87,15 @@ const QuizSearch = ({ quizId }: { quizId: number }) => {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={handleCleanSearch}>
onClick={handleCleanSearch}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none">
fill="none"
>
<path
d="M11.9998 13.4L7.0998 18.3C6.91647 18.4834 6.68314 18.575 6.3998 18.575C6.11647 18.575 5.88314 18.4834 5.6998 18.3C5.51647 18.1167 5.4248 17.8834 5.4248 17.6C5.4248 17.3167 5.51647 17.0834 5.6998 16.9L10.5998 12L5.6998 7.10005C5.51647 6.91672 5.4248 6.68338 5.4248 6.40005C5.4248 6.11672 5.51647 5.88338 5.6998 5.70005C5.88314 5.51672 6.11647 5.42505 6.3998 5.42505C6.68314 5.42505 6.91647 5.51672 7.0998 5.70005L11.9998 10.6L16.8998 5.70005C17.0831 5.51672 17.3165 5.42505 17.5998 5.42505C17.8831 5.42505 18.1165 5.51672 18.2998 5.70005C18.4831 5.88338 18.5748 6.11672 18.5748 6.40005C18.5748 6.68338 18.4831 6.91672 18.2998 7.10005L13.3998 12L18.2998 16.9C18.4831 17.0834 18.5748 17.3167 18.5748 17.6C18.5748 17.8834 18.4831 18.1167 18.2998 18.3C18.1165 18.4834 17.8831 18.575 17.5998 18.575C17.3165 18.575 17.0831 18.4834 16.8998 18.3L11.9998 13.4Z"
fill="#BCBCD6"
@ -97,13 +107,15 @@ const QuizSearch = ({ quizId }: { quizId: number }) => {
</div>
<button
className="bg-fillButtonAccentDefault px-4 py-3 rounded-lg rounded-l-none"
type="submit">
type="submit"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none">
fill="none"
>
<path
d="M9.5 3C11.2239 3 12.8772 3.68482 14.0962 4.90381C15.3152 6.12279 16 7.77609 16 9.5C16 11.11 15.41 12.59 14.44 13.73L14.71 14H15.5L20.5 19L19 20.5L14 15.5V14.71L13.73 14.44C12.5505 15.4468 11.0507 15.9999 9.5 16C7.77609 16 6.12279 15.3152 4.90381 14.0962C3.68482 12.8772 3 11.2239 3 9.5C3 7.77609 3.68482 6.12279 4.90381 4.90381C6.12279 3.68482 7.77609 3 9.5 3ZM9.5 5C7 5 5 7 5 9.5C5 12 7 14 9.5 14C12 14 14 12 14 9.5C14 7 12 5 9.5 5Z"
fill="#fff"
@ -112,7 +124,7 @@ const QuizSearch = ({ quizId }: { quizId: number }) => {
</button>
</form>
<h4 className="text-sm md:text-base text-textLight">
Her soragyň aşagynda siziň ugradan jogaplaryňyz görkeziler{' '}
Her soragyň aşagynda siziň ugradan jogaplaryňyz görkeziler{" "}
</h4>
</div>
</section>

View File

@ -0,0 +1,184 @@
"use client";
import { getNextQuizNetijeData, getQuizNetijeData } from "@/api/queries";
import { Datum, ISearchNetije } from "@/models/quizQuestionsWinners.model";
import { notFound } from "next/navigation";
import React, { useEffect, useState } from "react";
import Loader from "../Loader";
import { useQuizResults, useResultsLoading } from "@/store/store";
const padding = "py-4";
const QuizTapgyrResults = ({ id, steps }: { id: string; steps: string[] }) => {
const [data, setData] = useState<Datum[] | ISearchNetije[]>([]);
const { error, resultData } = useQuizResults();
const [loading, setLoading] = useState<boolean>(false);
const searchLoading = useResultsLoading((state) => state.loading);
const [total, setTotal] = useState<number>(0);
const [nextPageQueries, setQueries] = useState<{
limit: number;
offset: number;
}>({
limit: 0,
offset: 0,
});
async function getData(next?: boolean) {
if (next) {
const res = await getNextQuizNetijeData(
id,
nextPageQueries.limit,
nextPageQueries.offset
);
if (res) {
setQueries({
limit: res.per_page,
offset: nextPageQueries.offset + res.per_page,
});
setData([...data, ...res.data]);
}
} else {
setLoading(true);
const res = await getQuizNetijeData(id);
if (res) {
setTotal(res.total);
setQueries({
limit: res.per_page,
offset: res.per_page,
});
setData(res.data);
setLoading(false);
} else {
notFound();
}
}
}
useEffect(() => {
if (!resultData.length && !error) {
getData();
} else if (resultData.length) {
setData(resultData);
setTotal(0);
} else if (error) {
setTotal(0);
setData([]);
}
}, [resultData]);
return (
<article className="flex flex-col gap-[24px]">
<header className="flex justify-center">
<h1 className="text-[28px] text-[#1B1B21] md:text-[36px] text-center md:text-start">
Netijeler
</h1>
</header>
{data.length > 0 && !loading && !searchLoading ? (
<div className="flex flex-col bg-[#F6F2FA] rounded-[12px] overflow-hidden max-w-[700px] self-center w-full">
{/* Table Head */}
<div className="flex justify-between bg-[#EAE7EF] p-[20px]">
{((data[0] as Datum).client?.id ||
(data[0] as ISearchNetije).place) && (
<span className={`${padding} max-w-[20px] w-full text-center`}>
Ýeri
</span>
)}
{((data[0] as Datum)?.client?.phone || data[0].phone) && (
<span className={`${padding} max-w-[150px] w-full text-center`}>
Telefon beligisi
</span>
)}
{((data[0] as Datum).correct_answers_time ||
(data[0] as ISearchNetije).total_nobat) && (
<span
className={`${padding} min-w-[50px] text-center md:min-w-[115px] max-w-[150px] w-full flex justify-center`}
>
Nobatlaryň jemi
</span>
)}
{((data[0] as Datum).total_score_of_client ||
(data[0] as ISearchNetije).total_score) && (
<span
className={`${padding} min-w-[50px] text-center md:min-w-[115px] max-w-[150px] w-full flex justify-center`}
>
Utuklaryň jemi
</span>
)}
</div>
{/* Table body */}
<div className="text-[#46464F]">
{data.map((winner, id) => (
<React.Fragment key={id}>
<div
key={id}
className={`${
id !== data.length - 1 && "border-b border-[#C7C5D0]"
} flex justify-between items-center px-[20px]`}
>
<span
className={`${padding} max-w-[20px] w-full text-center`}
>
{id > 0 &&
(winner as Datum).correct_answers_time ===
(data[id - 1] as Datum).correct_answers_time
? id
: (winner as ISearchNetije).place
? (winner as ISearchNetije).place
: id + 1}
</span>
<span>
+
{(winner as Datum).client?.phone
? (winner as Datum).client?.phone
: (winner as ISearchNetije).phone
? (winner as ISearchNetije).phone
: "-"}
</span>
<div
className={`min-w-[50px] md:min-w-[115px] max-w-[150px] w-full flex flex-col md:flex-row justify-center ${padding}`}
>
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[180px] w-[100%]">
<span className="border border-[#2C7CDA] text-[#2C7CDA] rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] ">
{(winner as Datum).correct_answers_time
? (winner as Datum).correct_answers_time
: (winner as ISearchNetije).total_nobat
? (winner as ISearchNetije).total_nobat
: "-"}
</span>
</div>
</div>
<div
className={`min-w-[50px] md:min-w-[115px] max-w-[150px] w-full flex flex-col md:flex-row justify-center ${padding}`}
>
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[180px] w-[100%]">
<span className="bg-fillOrange rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] text-white">
{(winner as Datum).total_score_of_client
? (winner as Datum).total_score_of_client
: (winner as ISearchNetije).total_score
? (winner as ISearchNetije).total_score
: "-"}
</span>
</div>
</div>
</div>
</React.Fragment>
))}
</div>
</div>
) : !error ? (
<Loader />
) : (
<div className="text-center w-full">{error}</div>
)}
{data.length < total && !error && (
<button
onClick={() => getData(true)}
className="py-[5px] self-center px-[10px] md:w-fit rounded-md bg-blue-500 text-white border border-blue-500 lg:hover:bg-white lg:hover:text-blue-500 transition-all duration-300"
>
Dowamy
</button>
)}
</article>
);
};
export default QuizTapgyrResults;

View File

@ -0,0 +1,318 @@
"use client";
import { getNextQuizWinnners, getQuizWinnersById } from "@/api/queries";
import { Question } from "@/models/quizQuestions.model";
import {
Datum,
ISearchNetije,
} from "@/models/quizQuestionsWinners.model";
import { notFound } from "next/navigation";
import React, { useEffect, useState } from "react";
import Loader from "../Loader";
import { useQuizResults, useResultsLoading } from "@/store/store";
const padding = "py-4";
interface IProps {
id: string;
tapgyr: number;
questions: Question[];
}
const QuizTapgyrWinners = ({ id, tapgyr, questions }: IProps) => {
const [data, setData] = useState<Datum[] | ISearchNetije[]>([]);
const { error, resultData } = useQuizResults();
const [loading, setLoading] = useState<boolean>(false);
const searchLoading = useResultsLoading((state) => state.loading);
const [total, setTotal] = useState<number>(0);
const [nextPageQueries, setQueries] = useState<{
limit: number;
offset: number;
}>({
limit: 0,
offset: 0,
});
async function getData(next?: boolean) {
if (next) {
const res = await getNextQuizWinnners(
Number(id),
nextPageQueries.limit,
nextPageQueries.offset,
tapgyr
);
if (res) {
setQueries({
limit: res.meta.per_page,
offset: nextPageQueries.offset + res.meta.per_page,
});
setData([...(data as Datum[]), ...res.data]);
}
} else {
setLoading(true);
const res = await getQuizWinnersById(Number(id), tapgyr);
if (res) {
setTotal(res.meta.total);
setQueries({
limit: res.meta.per_page,
offset: res.meta.per_page,
});
setData(res.data);
setLoading(false);
} else {
notFound();
}
}
}
useEffect(() => {
if (!resultData.length && !error) {
getData();
} else if (resultData.length) {
setData(resultData);
setTotal(0);
} else if (error) {
setTotal(0);
setData([]);
}
}, [resultData]);
return (
<article className="flex flex-col gap-[24px]">
<header className="flex justify-center">
<h1 className="text-[28px] text-[#1B1B21] md:text-[36px]">
Gepleşik {tapgyr}
</h1>
</header>
{data.length > 0 && !loading && !searchLoading ? (
<table className="bg-[#F6F2FA] rounded-[12px] overflow-hidden">
<thead className="bg-[#EAE7EF] p-[20px]">
<tr>
{((data[0] as Datum).client_id ||
(data[0] as ISearchNetije).place) && (
<th scope="col" className={`${padding} text-center`}>
Ýeri
</th>
)}
{data[0].phone && (
<th scope="col" className={`${padding} text-center`}>
Telefon beligisi
</th>
)}
{((data[0] as Datum).answers?.length > 0 ||
(data[0] as ISearchNetije).tapgyr_breakdown?.length) && (
<th
scope="col"
className={`${padding} text-center hidden md:inline-block md:w-full`}
>
Jogap beriş nobatlary
</th>
)}
{((data[0] as Datum).correct_answers_time ||
(data[0] as ISearchNetije).total_nobat) && (
<th scope="col" className={`${padding} text-center`}>
Nobatlaryň jemi
</th>
)}
{((data[0] as Datum).total_score_of_client ||
(data[0] as ISearchNetije).total_score) && (
<th scope="col" className={`${padding} text-center`}>
Utuklaryň jemi
</th>
)}
</tr>
</thead>
<tbody className=" text-[#46464F]">
{data.map((winner, id) => (
<React.Fragment key={id}>
<tr
className={`${
id !== data.length - 1 && "md:border-b md:border-[#C7C5D0]"
}`}
>
{/* Yeri */}
<th scope="row" className={`${padding} text-center`}>
{id > 0 &&
(winner as Datum).correct_answers_time ===
(data[id - 1] as Datum).correct_answers_time
? id
: (winner as ISearchNetije).tapgyr_breakdown
? (winner as ISearchNetije).tapgyr_breakdown.find(
(item) => item.tapgyr === tapgyr
)?.tapgyr_place
: id + 1}
</th>
{/* Phone number */}
<td className={`${padding} text-center`}>
+{winner.phone ? winner.phone : "-"}
</td>
{/* Jogap berish nobaty */}
<td className={`${padding} hidden md:block`}>
<div className="w-full flex gap-[5px] justify-center">
{questions.map((question) => {
const matchingAnswer =
(winner as Datum).answers?.find(
(answer) =>
answer.question_id === question.id &&
answer.score > 0
) ||
(winner as Datum).answers?.find(
(answer) => answer.question_id === question.id
) ||
((winner as ISearchNetije).tapgyr_breakdown &&
(winner as ISearchNetije).tapgyr_breakdown
.find((step) => step.tapgyr === tapgyr)
?.answers.find(
(answer) =>
answer.question_id === question.id &&
answer.score > 0
)) ||
((winner as ISearchNetije).tapgyr_breakdown &&
(winner as ISearchNetije).tapgyr_breakdown
.find((step) => step.tapgyr === tapgyr)
?.answers.find(
(answer) => answer.question_id === question.id
));
return (
<span
key={question.id}
className={`text-sm font-semibold leading-[125%] ${
matchingAnswer &&
matchingAnswer.serial_number_for_correct !== 0
? "text-fillGreen"
: matchingAnswer &&
matchingAnswer?.serial_number_for_correct ===
0
? "text-fillRed"
: "text-textLight"
}`}
>
{matchingAnswer && matchingAnswer.score !== 0 ? (
matchingAnswer.serial_number_for_correct
) : matchingAnswer &&
matchingAnswer?.score === 0 ? (
"X"
) : (
<span className="text-[20px]">-</span>
)}
</span>
);
})}
</div>
</td>
{/* Answer time */}
<td className={`${padding}`}>
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] w-[100%]">
<span className="border border-[#2C7CDA] text-[#2C7CDA] rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] ">
{(winner as Datum).correct_answers_time
? (winner as Datum).correct_answers_time
: (winner as ISearchNetije).tapgyr_breakdown.find(
(item) => item.tapgyr === tapgyr
)?.tapgyr_total_nobat
? (winner as ISearchNetije).tapgyr_breakdown.find(
(item) => item.tapgyr === tapgyr
)?.tapgyr_total_nobat
: "-"}
</span>
</div>
</td>
{/* Total score */}
<td className={`${padding}`}>
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] w-[100%]">
<span className="bg-fillOrange rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] text-white">
{(winner as Datum).total_score_of_client
? (winner as Datum).total_score_of_client
: (winner as ISearchNetije).tapgyr_breakdown.find(
(item) => item.tapgyr === tapgyr
)?.tapgyr_total_score
? (winner as ISearchNetije).tapgyr_breakdown.find(
(item) => item.tapgyr === tapgyr
)?.tapgyr_total_score
: "-"}
</span>
</div>
</td>
</tr>
{/* Mobile jogab berish nobatlary */}
<tr
className={`${
id !== data.length - 1 && "border-b border-[#C7C5D0]"
} md:hidden`}
>
<th scope="row" colSpan={2}>
Jogap beriş nobatlary
</th>
<td colSpan={2}>
<div className="w-full flex gap-[5px] justify-center">
{questions.map((question) => {
const matchingAnswer =
(winner as Datum).answers?.find(
(answer) =>
answer.question_id === question.id &&
answer.score > 0
) ||
(winner as Datum).answers?.find(
(answer) => answer.question_id === question.id
) ||
((winner as ISearchNetije).tapgyr_breakdown &&
(winner as ISearchNetije).tapgyr_breakdown
.find((step) => step.tapgyr === tapgyr)
?.answers.find(
(answer) =>
answer.question_id === question.id &&
answer.score > 0
)) ||
((winner as ISearchNetije).tapgyr_breakdown &&
(winner as ISearchNetije).tapgyr_breakdown
.find((step) => step.tapgyr === tapgyr)
?.answers.find(
(answer) => answer.question_id === question.id
));
return (
<span
key={question.id}
className={`text-sm font-semibold leading-[125%] ${
matchingAnswer &&
matchingAnswer.serial_number_for_correct !== 0
? "text-fillGreen"
: matchingAnswer &&
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"}
</span>
);
})}
</div>
</td>
</tr>
</React.Fragment>
))}
</tbody>
</table>
) : !error ? (
<Loader />
) : (
<div className="w-full text-center">{error}</div>
)}
{data.length < total && !error && (
<button
onClick={() => getData(true)}
className="py-[5px] self-center px-[10px] md:w-fit rounded-md bg-blue-500 text-white border border-blue-500 lg:hover:bg-white lg:hover:text-blue-500 transition-all duration-300"
>
Dowamy
</button>
)}
</article>
);
};
export default QuizTapgyrWinners;

View File

@ -1,193 +1,76 @@
"use client";
import { Queries } from "@/api/queries";
import {
getNextQuizWinnners,
getQuizWinnersById,
Queries,
} from "@/api/queries";
import { v4 } from "uuid";
import { useState, useEffect, useContext } from "react";
import { IQuizQuestionsWinners } from "@/models/quizQuestionsWinners.model";
import {
Datum,
IQuizQuestionsWinners,
} from "@/models/quizQuestionsWinners.model";
import QuizContext from "@/context/QuizContext";
interface Message {
answer: string;
score: number;
date: string;
serial_number: number;
serial_number_for_correct: number;
starred_src: string;
quiz_id: number;
question_id: number;
}
interface Winner {
total_score_of_client: string;
correct_answers_time: string;
client_id: number;
client: {
id: number;
phone: string;
answers: {
id: number;
question_id: number;
score: number;
serial_number_for_correct: number;
client_id: number;
}[];
};
}
import { useQuizSearchActive } from "@/store/store";
import { Question } from "@/models/quizQuestions.model";
interface IProps {
quizId: number;
quizFinished: boolean;
smsNumber: string;
questionsData: Question[] | undefined
}
const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
// const [questionsData, setQuestionsData] = useState<IQuizQuestions>();
const [winnersData, setWinnersData] = useState<IQuizQuestionsWinners>();
const { questionsData } = useContext(QuizContext).quizQuestionsContext;
const QuizWinnerTable = ({ quizId, questionsData }: IProps) => {
const [winnersData, setWinnersData] = useState<Datum[] | []>([]);
const [winnersTotal, setWinnersTotal] = useState<number>(0);
const [nextPageQueries, setQueries] = useState<{
limit: number;
offset: number;
}>({
limit: 0,
offset: 0,
});
const { quizSearchData } = useContext(QuizContext).quizSearchContext;
const { active } = useQuizSearchActive();
const [socket, setSocket] = useState<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
async function getData(next?: boolean) {
if (next) {
const res = await getNextQuizWinnners(
quizId,
nextPageQueries.limit,
nextPageQueries.offset
);
if (res) {
setWinnersData([...winnersData, ...res.data]);
setQueries({
limit: res?.meta.per_page,
offset: nextPageQueries.offset + res?.meta.per_page,
});
}
} else {
const res = await getQuizWinnersById(quizId);
if (res) {
setWinnersTotal(res.meta.total);
setWinnersData(res.data);
setQueries({
limit: res?.meta.per_page,
offset: res?.meta.per_page,
});
}
}
}
useEffect(() => {
Queries.getQuizWinners(quizId).then((res) => {
setWinnersData(res);
});
}, [quizId]);
// 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/quiz?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);
// handleOnMessage(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 && winnersData) {
// connectWebSocket();
// }
// return () => {
// if (socket) {
// socket.close();
// }
// if (reconnectTimeout) {
// clearTimeout(reconnectTimeout);
// }
// if (pingInterval) {
// clearInterval(pingInterval);
// }
// };
// }, [smsNumber]);
// Function to handle incoming WebSocket message and update winnersData
const handleOnMessage = (message: Message) => {
if (!winnersData) {
console.error("winnersData is undefined");
return;
if (!active) {
// Queries.getQuizWinners(quizId).then((res) => {
// setWinnersData(res);
// });
getData();
} else if (active) {
setWinnersData([]);
}
}, [quizId, active]);
console.log("updating winnersData");
// Update the winnersData by matching phone with starred_src from the message
setWinnersData((prevWinnersData) => {
if (!prevWinnersData) {
return prevWinnersData;
}
return {
...prevWinnersData,
data: prevWinnersData.data.map((winner) => {
if (winner.client.phone === message.starred_src) {
const updatedAnswers = [
...winner.client.answers,
{
id: message.question_id,
question_id: message.question_id,
score: message.score,
serial_number_for_correct: message.serial_number_for_correct,
client_id: winner.client.id,
},
];
// 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
)
.toString();
return {
...winner,
client: {
...winner.client,
answers: updatedAnswers,
},
total_score_of_client: (
parseInt(winner.total_score_of_client) + message.score
).toString(),
correct_answers_time: updatedCorrectAnswersTime, // Update correct_answers_time
};
}
return winner;
}),
};
});
console.log("winnersData is updated");
};
return winnersData?.data.length !== 0 ? (
return quizSearchData?.data || (winnersData && winnersData?.length) !== 0 ? (
<div className="flex flex-col justify-center items-center gap-[60px]">
<div className="flex flex-col gap-5 justify-center items-center w-full">
<h2 className="text-textBlack text-[28px] text-center md:text-left md:text-[32px] font-semibold">
@ -198,30 +81,30 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
<div className="table-desktop hidden sm:flex flex-col bg-fillTableHead rounded-[25px] shadow-quizButton overflow-hidden max-w-[1000px] w-full">
{/* Table Head */}
<div className="flex border-b border-fillTableStrokeTableHead">
{winnersData?.data[0].client_id ? (
{winnersData[0].client_id || quizSearchData?.data ? (
<div className="text-center flex justify-center items-center text-base text-textBlack leading-[125%] font-semibold max-w-[54px] w-[100%] pl-6 pr-3 py-5">
<span></span>
<span>Ýeri</span>
</div>
) : null}
{winnersData?.data[0].client.phone ? (
{winnersData[0].phone || quizSearchData?.data ? (
<div className="text-center flex justify-center items-center text-base text-textBlack leading-[125%] font-semibold max-w-[176px] w-[100%] px-3 py-5">
<span>Gatnaşyjynyň tel. Beligisi</span>
<span>Telefon beligisi</span>
</div>
) : null}
{winnersData?.data[0].client.answers.length !== 0 ? (
{winnersData[0].answers.length || quizSearchData?.data ? (
<div className="text-center flex justify-center items-center text-base text-textBlack leading-[125%] font-semibold w-[100%] px-3 py-5">
<span>Soraglara jogap berilişiň nobaty</span>
<span>Jogap beriş nobatlary</span>
</div>
) : null}
{winnersData?.data[0].total_score_of_client ? (
{winnersData[0].total_score_of_client || quizSearchData?.data ? (
<div className="text-center flex justify-center items-center text-base text-textBlack leading-[125%] font-semibold max-w-[180px] w-[100%] px-3 py-5">
<span>Nobatlaryň jemi</span>
</div>
) : null}
{winnersData?.data[0].total_score_of_client ? (
{winnersData[0].total_score_of_client || quizSearchData?.data ? (
<div className="text-center flex justify-center items-center text-base text-textBlack leading-[125%] font-semibold max-w-[180px] w-[100%] px-3 py-5">
<span>Utuklaryň jemi</span>
</div>
@ -230,34 +113,111 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
{/* Table Body */}
<div className="">
{winnersData?.data.map((winner, id) => (
<div
className={`flex border-b border-fillTableStrokeTableRow ${
id % 2 === 0 ? "bg-fillTableRow" : "bg-fillTableRow2"
}`}
key={v4()}
>
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[54px] w-[100%] pl-6 pr-3 py-5">
<span>{id + 1}</span>
</div>
{winnersData.data[0].client.phone ? (
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] font-semibold max-w-[176px] w-[100%] px-3 py-5">
<span>+{winner.client.phone}</span>
{winnersData
? winnersData.map((winner, id) => (
<div
className={`flex border-b border-fillTableStrokeTableRow ${
id % 2 === 0 ? "bg-fillTableRow" : "bg-fillTableRow2"
}`}
key={v4()}
>
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[54px] w-[100%] pl-6 pr-3 py-5">
<span>
{id > 0 &&
winner.correct_answers_time ===
winnersData[id - 1].correct_answers_time
? id
: id + 1}
</span>
</div>
{winnersData[0].phone ? (
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] font-semibold max-w-[176px] w-[100%] px-3 py-5">
<span>+{winner.phone}</span>
</div>
) : null}
{winnersData[0].answers.length !== 0 ? (
<div className="flex justify-center items-center gap-6 text-base text-textGray leading-[125%] w-[100%] px-3 py-5">
{questionsData
? questionsData.map((question) => {
const matchingAnswer =
winner.answers.find(
(answer) =>
answer.question_id === question.id &&
answer.score > 0
) ||
winner.answers.find(
(answer) => answer.question_id === question.id
);
return (
<span
key={v4()}
className={`text-sm font-semibold leading-[125%] ${
matchingAnswer &&
matchingAnswer.serial_number_for_correct !==
0
? "text-fillGreen"
: matchingAnswer &&
matchingAnswer?.serial_number_for_correct ===
0
? "text-fillRed"
: "text-textLight"
}`}
>
{matchingAnswer && matchingAnswer.score !== 0
? matchingAnswer.serial_number_for_correct
: matchingAnswer &&
matchingAnswer?.score === 0
? "X"
: "-"}
</span>
);
})
: null}
</div>
) : null}
{winnersData[0].total_score_of_client ? (
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[180px] w-[100%] px-3 py-3">
<span className="border border-[#2C7CDA] text-[#2C7CDA] rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] ">
{winner.correct_answers_time}
</span>
</div>
) : null}
{winnersData[0].total_score_of_client ? (
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[180px] w-[100%] px-3 py-3">
<span className="bg-fillOrange rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] text-white">
{winner.total_score_of_client}
</span>
</div>
) : null}
</div>
) : null}
{winnersData.data[0].client.answers.length !== 0 ? (
<div className="flex justify-center items-center gap-6 text-base text-textGray leading-[125%] w-[100%] px-3 py-5">
{questionsData
? questionsData.map((question) => {
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
);
))
: quizSearchData && (
<div
className={`flex border-b border-fillTableStrokeTableRow bg-fillTableRow2`}
>
{/* Place of the client */}
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[54px] w-[100%] pl-6 pr-3 py-5">
<span>{quizSearchData.result.place}</span>
</div>
{/* Client phone number */}
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] font-semibold max-w-[176px] w-[100%] px-3 py-5">
<span>
+
{Object.keys(quizSearchData.data).map(
(questionId, i) =>
i === 0 &&
quizSearchData.data[questionId].answers[0].client
)}
</span>
</div>
{/* Serial number answer to questions */}
<div className="flex justify-center items-center gap-6 text-base text-textGray leading-[125%] w-[100%] px-3 py-5">
{Object.keys(quizSearchData.data)
.map((quistionId) => quizSearchData.data[quistionId])
.map((question) => {
const matchingAnswer = question.answers[0];
return (
<span
@ -277,30 +237,24 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
? matchingAnswer.serial_number_for_correct
: matchingAnswer && matchingAnswer?.score === 0
? "X"
: "0"}
: "-"}
</span>
);
})
: null}
</div>
) : null}
})}
</div>
{winnersData.data[0].total_score_of_client ? (
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[180px] w-[100%] px-3 py-3">
<span className="border border-[#2C7CDA] text-[#2C7CDA] rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] ">
{winner.correct_answers_time}
</span>
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[180px] w-[100%] px-3 py-3">
<span className="border border-[#2C7CDA] text-[#2C7CDA] rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] ">
{quizSearchData.result.total_serial}
</span>
</div>
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[180px] w-[100%] px-3 py-3">
<span className="bg-fillOrange rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] text-white">
{quizSearchData.result.total_score}
</span>
</div>
</div>
) : null}
{winnersData.data[0].total_score_of_client ? (
<div className="flex justify-center items-center text-base text-textBlack leading-[125%] max-w-[180px] w-[100%] px-3 py-3">
<span className="bg-fillOrange rounded-full w-[36px] h-[36px] flex justify-center items-center text-base leading-[125%] text-white">
{winner.total_score_of_client}
</span>
</div>
) : null}
</div>
))}
)}
</div>
</div>
@ -308,85 +262,164 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
<div className="sm:hidden flex flex-col bg-fillTableHead rounded-[13px] shadow-quizButton overflow-hidden max-w-[1000px] w-full">
{/* Table Head */}
<div className="flex border-b border-fillTableStrokeTableHead p-2 gap-[8px]">
{winnersData?.data[0].client_id ? (
{winnersData[0].client_id || quizSearchData?.data ? (
<div className="text-center flex items-center text-xs text-textBlack leading-[125%] font-semibold max-w-[14px] w-[100%]">
<span></span>
<span>Ýeri</span>
</div>
) : null}
{winnersData?.data[0].client.phone ? (
{winnersData[0].phone || quizSearchData?.data ? (
<div className="text-center flex justify-center items-center text-xs text-textBlack leading-[125%] font-semibold max-w-[107px] w-[100%]">
<span>Gatnaşyjynyň tel. Beligisi</span>
<span>Telefon beligisi</span>
</div>
) : null}
{winnersData?.data[0].total_score_of_client ? (
{winnersData[0].total_score_of_client || quizSearchData?.data ? (
<div className="text-center flex justify-center items-center text-xs text-textBlack leading-[125%] font-semibold max-w-[75px] w-[100%]">
<span>Soraglara jogap berilişiň nobaty </span>
<span>Nobatlaryň jemi </span>
</div>
) : null}
{winnersData?.data[0].total_score_of_client ? (
{winnersData[0].total_score_of_client || quizSearchData?.data ? (
<div className="text-center flex justify-center items-center text-xs text-textBlack leading-[125%] font-semibold max-w-[99px] w-[100%]">
<span>Nobatlaryň jemi</span>
<span>Utuklaryň jemi</span>
</div>
) : null}
</div>
{/* Table Body */}
<div className="">
{winnersData?.data.map((winner, id) => (
<div
className={`flex border-b border-fillTableStrokeTableRow items-center p-[8px] gap-[8px] ${
id % 2 === 0 ? "bg-fillTableRow" : "bg-fillTableRow2"
}`}
key={v4()}
>
<div className="flex items-center text-base text-textBlack leading-[125%] max-w-[14px] w-[100%] ">
<span>{id + 1}</span>
</div>
{winnersData
? winnersData.map((winner, id) => (
<div
className={`flex border-b border-fillTableStrokeTableRow items-center p-[8px] gap-[8px] ${
id % 2 === 0 ? "bg-fillTableRow" : "bg-fillTableRow2"
}`}
key={v4()}
>
<div className="flex items-center text-base text-textBlack leading-[125%] max-w-[14px] w-[100%] ">
<span>{id + 1}</span>
</div>
<div className="flex flex-col gap-[8px] w-full">
<div className="flex gap-[8px] items-center">
{winnersData.data[0].client.phone ? (
<div className="flex items-center text-xs text-textBlack leading-[125%] font-semibold max-w-[107px] w-full">
<span>+{winner.client.phone}</span>
</div>
) : null}
<div className="flex flex-col gap-[8px] w-full">
<div className="flex gap-[8px] items-center">
{winnersData[0].phone ? (
<div className="flex items-center text-xs text-textBlack leading-[125%] font-semibold max-w-[107px] w-full">
<span>+{winner.phone}</span>
</div>
) : null}
{winnersData.data[0].total_score_of_client ? (
<div className="flex justify-center items-center text-xs text-textBlack leading-[125%] max-w-[75px] w-full">
<span className="border border-[#2C7CDA] text-[#2C7CDA] rounded-full w-[24px] h-[24px] flex justify-center items-center text-xs leading-[125%] ">
{winner.correct_answers_time}
</span>
{winnersData[0].total_score_of_client ? (
<div className="flex justify-center items-center text-xs text-textBlack leading-[125%] max-w-[75px] w-full">
<span className="border border-[#2C7CDA] text-[#2C7CDA] rounded-full w-[24px] h-[24px] flex justify-center items-center text-xs leading-[125%] ">
{winner.correct_answers_time}
</span>
</div>
) : null}
{winnersData[0].total_score_of_client ? (
<div className="flex justify-center items-center text-xs text-textBlack leading-[125%] max-w-[99px] w-full">
<span className="bg-fillOrange rounded-full w-[24px] h-[24px] flex justify-center items-center text-xs leading-[125%] text-white">
{winner.total_score_of_client}
</span>
</div>
) : null}
</div>
) : null}
{winnersData.data[0].total_score_of_client ? (
<div className="flex justify-center items-center text-xs text-textBlack leading-[125%] max-w-[99px] w-full">
<span className="bg-fillOrange rounded-full w-[24px] h-[24px] flex justify-center items-center text-xs leading-[125%] text-white">
{winner.total_score_of_client}
</span>
<div className="flex gap-[8px] items-center">
{winnersData[0].answers.length !== 0 ? (
<div className="flex justify-center items-center text-xs text-textLight leading-[125%] font-semibold w-fit">
<span>Jogap beriş nobatlary:</span>
</div>
) : null}
{winnersData[0].answers.length !== 0 ? (
<div className="flex justify-center items-center gap-[4px] text-xs text-textGray leading-[125%] w-fit">
{questionsData
? questionsData.map((question) => {
const matchingAnswer =
winner.answers.find(
(answer) =>
answer.question_id === question.id &&
answer.score > 0
) ||
winner.answers.find(
(answer) =>
answer.question_id === question.id
);
return (
<span
key={v4()}
className={`text-sm font-semibold leading-[125%] ${
matchingAnswer &&
matchingAnswer.serial_number_for_correct !==
0
? "text-fillGreen"
: matchingAnswer &&
matchingAnswer.serial_number_for_correct ===
0
? "text-fillRed"
: "text-textLight"
}`}
>
{matchingAnswer &&
matchingAnswer.score !== 0
? matchingAnswer.serial_number_for_correct
: matchingAnswer &&
matchingAnswer?.score === 0
? "X"
: "-"}
</span>
);
})
: null}
</div>
) : null}
</div>
) : null}
</div>
</div>
<div className="flex gap-[8px] items-center">
{winnersData?.data[0].client.answers.length !== 0 ? (
<div className="flex justify-center items-center text-xs text-textLight leading-[125%] font-semibold w-fit">
<span>Soraglara näçinji jogap berdi :</span>
))
: quizSearchData && (
<div
className={`flex border-b border-fillTableStrokeTableRow items-center p-[8px] gap-[8px] bg-fillTableRow2`}
>
<div className="flex items-center text-base text-textBlack leading-[125%] max-w-[14px] w-[100%] ">
<span>{quizSearchData.result.place}</span>
</div>
<div className="flex flex-col gap-[8px] w-full">
<div className="flex gap-[8px] items-center">
<div className="flex items-center text-xs text-textBlack leading-[125%] font-semibold max-w-[107px] w-full">
<span>
+
{Object.keys(quizSearchData.data).map(
(questionId, i) =>
i === 0 &&
quizSearchData.data[questionId].answers[0]
.client
)}
</span>
</div>
<div className="flex justify-center items-center text-xs text-textBlack leading-[125%] max-w-[75px] w-full">
<span className="border border-[#2C7CDA] text-[#2C7CDA] rounded-full w-[24px] h-[24px] flex justify-center items-center text-xs leading-[125%] ">
{quizSearchData.result.total_serial}
</span>
</div>
<div className="flex justify-center items-center text-xs text-textBlack leading-[125%] max-w-[99px] w-full">
<span className="bg-fillOrange rounded-full w-[24px] h-[24px] flex justify-center items-center text-xs leading-[125%] text-white">
{quizSearchData.result.total_score}
</span>
</div>
</div>
) : null}
{winnersData.data[0].client.answers.length !== 0 ? (
<div className="flex justify-center items-center gap-[4px] text-xs text-textGray leading-[125%] w-fit">
{questionsData
? questionsData.map((question) => {
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
);
<div className="flex gap-[8px] items-center">
<div className="flex justify-center items-center text-xs text-textLight leading-[125%] font-semibold w-fit">
<span>Jogap beriş nobatlary:</span>
</div>
<div className="flex justify-center items-center gap-[4px] text-xs text-textGray leading-[125%] w-fit">
{Object.keys(quizSearchData.data)
.map(
(quistionId) => quizSearchData.data[quistionId]
)
.map((question) => {
const matchingAnswer = question.answers[0];
return (
<span
key={v4()}
@ -396,7 +429,7 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
0
? "text-fillGreen"
: matchingAnswer &&
matchingAnswer.serial_number_for_correct ===
matchingAnswer?.serial_number_for_correct ===
0
? "text-fillRed"
: "text-textLight"
@ -407,21 +440,28 @@ const QuizWinnerTable = ({ quizId, quizFinished, smsNumber }: IProps) => {
: matchingAnswer &&
matchingAnswer?.score === 0
? "X"
: "0"}
: "-"}
</span>
);
})
: null}
})}
</div>
</div>
) : null}
</div>
</div>
</div>
</div>
))}
)}
</div>
</div>
</div>
{winnersData.length < winnersTotal && (
<button
onClick={() => getData(true)}
className="py-[5px] px-[10px] rounded-md bg-blue-500 text-white border border-blue-500 lg:hover:bg-white lg:hover:text-blue-500 transition-all duration-300"
>
Dowamy
</button>
)}
{/* Rules block */}
<div className="flex flex-col gap-[20px] p-5 border border-strokeLightGray1 rounded-[25px] max-w-[1000px] w-full items-center justify-center">
<h3 className="text-[26px] text-textBlack font-semibold leading-[124%]">

View File

@ -1,20 +1,26 @@
"use client";
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";
import { useEffect, useState } from "react";
import { useMediaQuery } from "usehooks-ts";
const TossPage = async ({
type,
id,
}: {
type: "bije" | "cekilis";
id: string;
}) => {
const tossData = await getTossData({ type, id });
const TossPage = ({ type, id }: { type: "bije" | "cekilis"; id: string }) => {
const [tossData, setTossData] = useState<any>();
const mobile = useMediaQuery("(max-width: 768px)");
const status = await getLotteryStatus(
useEffect(() => {
const getData = async () => {
setTossData(await getTossData({ type, id }));
};
getData();
}, []);
const status = getLotteryStatus(
tossData?.data?.start_time,
tossData?.data?.end_time
);
@ -22,13 +28,15 @@ const TossPage = async ({
return (
<>
{tossData?.data ? (
<div className="flex flex-col md:gap-[128px] gap-[80px] font-roboto md:pt-[64px] sm:pt-[48px] pt-[40px] ms:pb-[128px] pb-[80px] text-lightOnSurface">
<div className="flex flex-col font-roboto md:pt-[64px] sm:pt-[48px] pt-[40px] ms:pb-[128px] pb-[80px] text-lightOnSurface">
{tossData && (
<div className="flex flex-col sm:gap-[64px] gap-[40px]">
<div className="flex flex-col sm:gap-[64px] gap-[40px] pb-[40px]">
<LotteryHeader
title={tossData.data.title}
description={tossData.data.description}
image={tossData.data.image}
image={
mobile ? tossData?.data.image_mobile : tossData?.data.image
}
smsCode={tossData.data.sms_code}
startDate={tossData.data.start_time}
/>
@ -47,7 +55,7 @@ const TossPage = async ({
<LotteryRulesSection show={false} data={tossData} />
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-10 mt-[40px]">
<LotteryWinners data={tossData} lotteryStatus={status} />
</div>
</div>

View File

@ -1,14 +1,15 @@
'use client';
"use client";
import Image from 'next/image';
import placeholder from '@/public/person placeholder.svg';
import clsx from 'clsx';
import { motion, usePresence, Variant, Transition } from 'framer-motion';
import RollingCounter from 'react-slot-counter';
import Image from "next/image";
import placeholder from "@/public/person placeholder.svg";
import clsx from "clsx";
import { motion, usePresence, Variant, Transition } from "framer-motion";
import RollingCounter from "react-slot-counter";
interface IProps {
number: number;
name: string | null;
description: string | null;
photo: string | null;
votes: number;
progress: number;
@ -24,6 +25,7 @@ interface IProps {
const ParticipantCard = ({
number,
name,
description,
photo,
votes,
progress,
@ -37,7 +39,7 @@ const ParticipantCard = ({
const substractedProgress = progress > 99 ? progress - 2 : progress;
const transition: Transition = {
type: 'spring',
type: "spring",
stiffness: 500,
damping: 50,
mass: 1,
@ -48,11 +50,11 @@ const ParticipantCard = ({
const animations = {
layout: true,
initial: 'out',
initial: "out",
style: {
position: (isPresent ? 'static' : 'absolute') as 'static' | 'absolute', // Corrected cast
position: (isPresent ? "static" : "absolute") as "static" | "absolute", // Corrected cast
},
animate: isPresent ? 'in' : 'out',
animate: isPresent ? "in" : "out",
variants: {
in: { scaleY: 1, opacity: 1 },
out: { scaleY: 0, opacity: 0, zIndex: -1 },
@ -66,7 +68,8 @@ const ParticipantCard = ({
////////////////////////////////////////////// Winner card
<motion.div
// {...animations}
className="flex flex-col overflow-hidden bg-fillNavyBlue max-w-[940px] w-full group">
className="flex flex-col overflow-hidden bg-fillNavyBlue max-w-[940px] w-full group"
>
<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}
@ -78,8 +81,9 @@ const ParticipantCard = ({
fill
src={photo}
alt={name}
className={clsx(' object-cover', {
'group-hover:scale-110 transition-all duration-300 ease-out': hasUrl,
className={clsx(" object-cover", {
"group-hover:scale-110 transition-all duration-300 ease-out":
hasUrl,
})}
unoptimized
unselectable="off"
@ -91,9 +95,10 @@ const ParticipantCard = ({
<Image
fill
src={placeholder}
alt={'placeholder'}
className={clsx(' object-cover', {
'group-hover:scale-110 transition-all duration-300 ease-out': hasUrl,
alt={"placeholder"}
className={clsx(" object-cover", {
"group-hover:scale-110 transition-all duration-300 ease-out":
hasUrl,
})}
unoptimized
unselectable="off"
@ -106,17 +111,31 @@ const ParticipantCard = ({
{name ? (
<h2
className={clsx(
'text-[18px] transition-all sm:text-[24px] leading-[100%] font-bold text-white',
"text-[18px] transition-all sm:text-[24px] leading-[100%] font-bold text-white",
{
// 'group-hover:text-[28px] transition-all duration-300 ease-out': hasUrl,
},
)}>
}
)}
>
{name}
</h2>
) : null}
{description ? (
<p
className={clsx(
"text-[14px] transition-all font-[400] text-[#B7B7D1]",
{
// 'group-hover:text-[28px] transition-all duration-300 ease-out': hasUrl,
}
)}
>
{description}
</p>
) : null}
{/* If we have voteCode and voting not closed, then show badge with code. Else dont show */}
{voteCode && voteStatus !== 'closed' ? (
{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
@ -146,13 +165,13 @@ const ParticipantCard = ({
</div>
{/* If we have voteCode and voting not closed, then show badge with code. Else dont show */}
{voteCode && voteStatus !== 'closed' ? (
{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{' '}
Ses bermek üçin{" "}
<span className="inline-block w-fit px-1 py-[4px] mx-[4px] font-bold text-white text-[16px] border rounded-md">
{voteCode}
</span>{' '}
</span>{" "}
ugrat
</p>
) : null}
@ -164,15 +183,18 @@ const ParticipantCard = ({
// {...animations}
>
<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>
<h3 className="w-[24px] text-[16px] sm:text-[20px] leading-[100%] font-bold">
{number}
</h3>
{photo && name ? (
<div className="relative min-w-[50px] rounded-[10px] overflow-hidden sm:min-w-[80px] h-[50px] sm:h-[80px]">
<div className="relative min-w-[50px] rounded-[10px] overflow-hidden sm:min-w-[104px] h-[50px] sm:h-[104px]">
<Image
fill
src={photo}
alt={name}
className={clsx(' object-cover', {
'group-hover:scale-110 transition-all duration-300 ease-out': hasUrl,
className={clsx(" object-cover", {
"group-hover:scale-110 transition-all duration-300 ease-out":
hasUrl,
})}
unoptimized
unselectable="off"
@ -183,9 +205,10 @@ const ParticipantCard = ({
<Image
fill
src={placeholder}
alt={'placeholder'}
className={clsx(' object-cover', {
'group-hover:scale-110 transition-all duration-300 ease-out': hasUrl,
alt={"placeholder"}
className={clsx(" object-cover", {
"group-hover:scale-110 transition-all duration-300 ease-out":
hasUrl,
})}
unoptimized
unselectable="off"
@ -198,13 +221,28 @@ const ParticipantCard = ({
{name ? (
<h2
className={clsx(
'text-textBlack text-[16px] sm:text-[18px] leading-[100%] font-bold',
)}>
"text-textBlack text-[16px] sm:text-[18px] leading-[100%] font-bold"
)}
>
{name}
</h2>
) : null}
{description ? (
<p
className={clsx(
"text-[14px] transition-all font-[400] text-[#4D4D4D]",
{
// 'group-hover:text-[28px] transition-all duration-300 ease-out': hasUrl,
}
)}
>
{description}
</p>
) : null}
{/* If we have voteCode and voting not closed, then show badge with code. Else dont show */}
{voteCode && voteStatus !== 'closed' ? (
{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
@ -234,13 +272,13 @@ const ParticipantCard = ({
</div>
{/* If we have voteCode and voting not closed, then show badge with code. Else dont show */}
{voteCode && voteStatus !== 'closed' ? (
{voteCode && voteStatus !== "closed" ? (
// Mobile version
<p className="block sm:hidden text-[#9393DA] text-[10px] leading-[125%] w-full">
Ses bermek üçin{' '}
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">
{voteCode}
</span>{' '}
</span>{" "}
ugrat
</p>
) : null}

View File

@ -1,22 +1,23 @@
'use client';
import React, { useContext, useEffect, useState } from 'react';
import GradientTitle from './GradientTitle';
import ParticipantCard from './ParticipantCard';
import { v4 } from 'uuid';
import { IAllVotes, VotingItem } from '@/models/allVotes.model';
import { Queries } from '@/api/queries';
import Loader from '../Loader';
import VoteContext from '@/context/VoteContext';
import PageBage from './PageBage';
import Image from 'next/image';
import { useMediaQuery } from 'usehooks-ts';
import Countdown from './Countdown';
import Link from 'next/link';
import { useWindowSize } from 'react-use';
import Confetti from '../common/Confetti';
"use client";
import React, { useContext, useEffect, useState } from "react";
import GradientTitle from "./GradientTitle";
import ParticipantCard from "./ParticipantCard";
import { v4 } from "uuid";
import { IAllVotes, VotingItem } from "@/models/allVotes.model";
import { Queries } from "@/api/queries";
import Loader from "../Loader";
import VoteContext from "@/context/VoteContext";
import PageBage from "./PageBage";
import Image from "next/image";
import { useMediaQuery } from "usehooks-ts";
import Countdown from "./Countdown";
import Link from "next/link";
import Confetti from "../common/Confetti";
import { useRouter, useSearchParams } from "next/navigation";
interface IParams {
vote_id?: string;
all?: boolean;
}
interface ISocketMessage {
@ -27,11 +28,13 @@ interface ISocketMessage {
date: string;
}
const ParticipantsList = ({ vote_id }: IParams) => {
const ParticipantsList = ({ vote_id, all }: IParams) => {
const searchParams = useSearchParams();
const router = useRouter();
const [data, setData] = useState<IAllVotes>();
const [participantsData, setParticipantsData] = useState<VotingItem[]>([]);
const [voteStatus, setVoteStatus] = useState<string>();
const [eventStatus, setEventStatus] = useState<string>('Not started');
const [eventStatus, setEventStatus] = useState<string>("Not started");
const [manualClose, setManualClose] = useState(false); // Track manual closure
const [winnersCount, setWinnersCount] = useState<number>(0);
@ -41,13 +44,29 @@ const ParticipantsList = ({ vote_id }: IParams) => {
const [socket, setSocket] = useState<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const mobile = useMediaQuery('(max-width: 768px)');
const { width, height } = useWindowSize();
const mobile = useMediaQuery("(max-width: 768px)");
const { setVoteDescription } = useContext(VoteContext).voteDescriptionContext;
useEffect(() => {
if (!vote_id) {
const id = searchParams.get("d");
if (id) {
Queries.getVoteByUUID(id).then((res) => {
setData(res);
setParticipantsData(res.data.voting_items);
setVoteDescription(res.data.description);
setVoteStatus(res.data.status);
setSmsNumber(res.data.sms_number);
});
} else if (vote_id) {
Queries.getVote(vote_id).then((res) => {
setData(res);
setParticipantsData(res.data.voting_items);
setVoteDescription(res.data.description);
setVoteStatus(res.data.status);
setSmsNumber(res.data.sms_number);
});
} else if (all) {
Queries.getAllVotes().then((res) => {
setData(res);
setParticipantsData([...res.data.voting_items]);
@ -56,13 +75,7 @@ const ParticipantsList = ({ vote_id }: IParams) => {
setSmsNumber(res.data.sms_number);
});
} else {
Queries.getVote(vote_id).then((res) => {
setData(res);
setParticipantsData(res.data.voting_items);
setVoteDescription(res.data.description);
setVoteStatus(res.data.status);
setSmsNumber(res.data.sms_number);
});
router.push("/vote/active");
}
if (participantsData) {
@ -79,19 +92,21 @@ const ParticipantsList = ({ vote_id }: IParams) => {
try {
// Only connect if manualClose is false
if (!manualClose) {
socket = new WebSocket(`wss://sms.turkmentv.gov.tm/ws/voting?dst=${smsNumber}`);
socket = new WebSocket(
`wss://sms.turkmentv.gov.tm/ws/voting?dst=${smsNumber}`
);
setSocket(socket);
socket.onopen = () => {
console.log('WebSocket is connected');
console.log("WebSocket is connected");
setIsConnected(true);
pingInterval = setInterval(() => {
if (socket?.readyState === WebSocket.OPEN) {
try {
socket.send(JSON.stringify({ type: 'ping' }));
socket.send(JSON.stringify({ type: "ping" }));
} catch (error) {
console.error('Error sending ping:', error);
console.error("Error sending ping:", error);
}
}
}, 25000); // Ping every 25 seconds
@ -102,23 +117,23 @@ const ParticipantsList = ({ vote_id }: IParams) => {
const message = JSON.parse(event.data);
handleWebSocketMessage(message);
} catch (error) {
console.error('Error processing message:', error);
console.error("Error processing message:", error);
}
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
console.error("WebSocket error:", error);
if (!manualClose && !reconnectTimeout) {
reconnectTimeout = setTimeout(() => {
console.log('Attempting to reconnect WebSocket after error...');
console.log("Attempting to reconnect WebSocket after error...");
connectWebSocket();
}, 5000); // Reconnect after 5 seconds
}
};
socket.onclose = () => {
console.log('WebSocket is closed');
console.log("WebSocket is closed");
setIsConnected(false);
if (pingInterval) {
@ -132,12 +147,12 @@ const ParticipantsList = ({ vote_id }: IParams) => {
};
}
} catch (error) {
console.error('WebSocket connection error:', error);
console.error("WebSocket connection error:", error);
}
};
// WebSocket connection only if eventStatus is 'Started'
if (smsNumber && eventStatus === 'Started' && !manualClose) {
if (smsNumber && eventStatus === "Started" && !manualClose) {
connectWebSocket();
}
@ -161,7 +176,9 @@ const ParticipantsList = ({ vote_id }: IParams) => {
// Update the corresponding voting item
const updatedItems = prevVotingItems.map((item, index) =>
item.id === message.voting_item_id ? { ...item, votes_count: item.votes_count + 1 } : item,
item.id === message.voting_item_id
? { ...item, votes_count: item.votes_count + 1 }
: item
);
// Sort the updated items array by votes_count in descending order
@ -175,10 +192,10 @@ const ParticipantsList = ({ vote_id }: IParams) => {
// Update the corresponding voting item
const updatedItems = prevVotingItems.map((item, index) =>
index === 1 ? { ...item, votes_count: item.votes_count + 1 } : item,
index === 1 ? { ...item, votes_count: item.votes_count + 1 } : item
);
console.log('votes updated');
console.log("votes updated");
console.log(updatedItems.sort((a, b) => b.votes_count - a.votes_count));
// Sort the updated items array by votes_count in descending order
return updatedItems.sort((a, b) => b.votes_count - a.votes_count);
@ -188,7 +205,10 @@ const ParticipantsList = ({ vote_id }: IParams) => {
const winnersCountHandle = (winners: VotingItem[]) => {
let count = 0;
winners.map((winner) => {
if (winner.votes_percents === 100 && winner.votes_count === winners[0].votes_count) {
if (
winner.votes_percents === 100 &&
winner.votes_count === winners[0].votes_count
) {
count++;
setWinnersCount(count);
}
@ -202,23 +222,29 @@ const ParticipantsList = ({ vote_id }: IParams) => {
if (!data?.data) {
return (
<div className="py-12">
<GradientTitle title={'No voting to show on the site'} size="big" />
<GradientTitle title={"No voting to show on the site"} size="big" />
</div>
);
}
return (
<div className="flex flex-col gap-[20px] sm:gap-[40px] w-full items-center">
{data.data.description ? <PageBage title={data.data.description} /> : null}
{data.data.description ? (
<PageBage title={data.data.description} />
) : null}
{eventStatus === 'Finished' && <Confetti showConfetti={true} />}
{eventStatus === "Finished" && <Confetti />}
{data.data.banner ? (
<div className="relative w-full md:min-h-[150px] md:h-auto h-[100px] ">
{mobile ? (
<Image
fill
src={data.data.banner_mobile !== null ? data.data.banner_mobile : data.data.banner}
src={
data.data.banner_mobile !== null
? data.data.banner_mobile
: data.data.banner
}
alt={data.data.title}
unselectable="off"
unoptimized
@ -251,24 +277,31 @@ const ParticipantsList = ({ vote_id }: IParams) => {
) : null}
<div className="flex w-full flex-col items-center gap-[10px] sm:gap-[20px]">
{winnersCount > 1 ? <GradientTitle title="победители" size="small" /> : null}
{winnersCount > 1 ? (
<GradientTitle title="победители" size="small" />
) : null}
{participantsData && participantsData[0].votes_count > 0 ? (
{participantsData &&
participantsData.length > 0 &&
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, index) =>
participant.votes_count === participantsData[0].votes_count ? (
participant.votes_count ===
participantsData[0].votes_count ? (
participant.url ? (
<Link
href={participant.url ? participant.url : ''}
href={participant.url ? participant.url : ""}
target="_blank"
className="w-full"
key={v4()}>
key={v4()}
>
<ParticipantCard
index={index}
hasUrl={true}
voteStatus={voteStatus ? voteStatus : ''}
voteStatus={voteStatus ? voteStatus : ""}
isFirst={index === 0 ? true : false}
name={participant.title}
description={participant.description}
progress={participant.votes_percents}
votes={participant.votes_count}
voteCode={participant.vote_code}
@ -283,9 +316,10 @@ const ParticipantsList = ({ vote_id }: IParams) => {
key={v4()}
index={index}
hasUrl={false}
voteStatus={voteStatus ? voteStatus : ''}
voteStatus={voteStatus ? voteStatus : ""}
isFirst={index === 0 ? true : false}
name={participant.title}
description={participant.description}
progress={participant.votes_percents}
votes={participant.votes_count}
voteCode={participant.vote_code}
@ -295,12 +329,14 @@ const ParticipantsList = ({ vote_id }: IParams) => {
winner={true}
/>
)
) : null,
) : null
)}
</div>
) : null}
{winnersCount > 1 ? <div className="w-full h-[1px] bg-[#3636A3]"></div> : null}
{winnersCount > 1 ? (
<div className="w-full h-[1px] bg-[#3636A3]"></div>
) : null}
</div>
<div className="flex flex-col items-center max-w-[940px] w-full gap-5 justify-center mx-auto">
@ -309,16 +345,18 @@ const ParticipantsList = ({ vote_id }: IParams) => {
!hasVotes ? (
participant.url ? (
<Link
href={participant.url ? participant.url : ''}
href={participant.url ? participant.url : ""}
target="_blank"
className="w-full mx-auto"
key={v4()}>
key={v4()}
>
<ParticipantCard
index={index}
hasUrl={true}
voteStatus={voteStatus ? voteStatus : ''}
voteStatus={voteStatus ? voteStatus : ""}
isFirst={index === 0 ? true : false}
name={participant.title}
description={participant.description}
progress={participant.votes_percents}
votes={participant.votes_count}
voteCode={participant.vote_code}
@ -333,9 +371,10 @@ const ParticipantsList = ({ vote_id }: IParams) => {
hasUrl={false}
key={v4()}
index={index}
voteStatus={voteStatus ? voteStatus : ''}
voteStatus={voteStatus ? voteStatus : ""}
isFirst={index === 0 ? true : false}
name={participant.title}
description={participant.description}
progress={participant.votes_percents}
votes={participant.votes_count}
voteCode={participant.vote_code}
@ -346,19 +385,22 @@ const ParticipantsList = ({ vote_id }: IParams) => {
/>
)
) : (
participant.votes_count !== participantsData[0].votes_count &&
participant.votes_count !==
participantsData[0].votes_count &&
(participant.url ? (
<Link
href={participant.url ? participant.url : ''}
href={participant.url ? participant.url : ""}
target="_blank"
className="w-full mx-auto"
key={v4()}>
key={v4()}
>
<ParticipantCard
index={index}
hasUrl={true}
voteStatus={voteStatus ? voteStatus : ''}
voteStatus={voteStatus ? voteStatus : ""}
isFirst={index === 0 ? true : false}
name={participant.title}
description={participant.description}
progress={participant.votes_percents}
votes={participant.votes_count}
voteCode={participant.vote_code}
@ -373,9 +415,10 @@ const ParticipantsList = ({ vote_id }: IParams) => {
hasUrl={false}
key={v4()}
index={index}
voteStatus={voteStatus ? voteStatus : ''}
voteStatus={voteStatus ? voteStatus : ""}
isFirst={index === 0 ? true : false}
name={participant.title}
description={participant.description}
progress={participant.votes_percents}
votes={participant.votes_count}
voteCode={participant.vote_code}
@ -385,7 +428,7 @@ const ParticipantsList = ({ vote_id }: IParams) => {
winner={false}
/>
))
),
)
)
: null}
</div>

View File

@ -1,6 +1,4 @@
"use server";
export async function getLotteryStatus(startTime: string, endTime: string) {
export function getLotteryStatus(startTime: string, endTime: string) {
const now = new Date();
const start = new Date(startTime);
const end = new Date(endTime);

View File

@ -20,6 +20,7 @@ export interface VotingItem {
votes_count: number;
vote_code: string;
title: null | string;
description: null | string;
photo: null | string;
votes_percents: number;
url?: string;

View File

@ -5,6 +5,7 @@ export interface IQuizQuestions {
export interface Data {
id: number;
uuid?: number;
title: string;
date: string;
banner: string;
@ -13,7 +14,12 @@ export interface Data {
description: string;
rules: Note[];
notes: Note[];
questions: Question[];
questions?: Question[];
has_steps: 0 | 1;
steps?: {
tapgyr: number;
questions: Question[];
}[];
}
export interface Note {

View File

@ -1,24 +1,54 @@
export interface IQuizQuestionsWinners {
data: Datum[];
meta: IMeta;
}
export interface Datum {
total_score_of_client: string;
correct_answers_time: string;
client_id: number;
client: Client;
}
export interface Client {
id: number;
phone: string;
answers: Answer[];
client?: {
id: number;
phone: string;
};
tapgyr_breakdown?: {
tapgyr: number;
tapgyr_correct_time: number;
tapgyr_score: number;
}[];
}
export interface Answer {
id: number;
question_id: number;
score: number;
serial_number_for_correct: number;
client_id: number;
tapgyr?: number;
quiz_id?: number;
}
interface IMeta {
current_page: number;
from: number;
last_page: number;
path: string;
per_page: number;
to: number;
total: number;
}
export interface ISearchNetije {
total_nobat: number;
total_score: number;
place: number;
phone: string;
tapgyr_breakdown: {
tapgyr: number;
tapgyr_total_score: number;
tapgyr_total_nobat: number;
tapgyr_place: number;
answers: Answer[];
}[];
}

View File

@ -1,5 +1,10 @@
export interface IQuizSearchData {
data: { [key: string]: Datum };
result: {
total_score: number;
total_serial: number;
place: number;
};
}
export interface Datum {

View File

@ -20,6 +20,7 @@ export interface VotingItem {
votes_count: number;
vote_code: string;
title: null | string;
description: null | string;
photo: null | string;
votes_percents: number;
}

View File

@ -1,6 +1,5 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
reactStrictMode: false,
images: {

0
notes.txt Normal file
View File

View File

@ -22,8 +22,8 @@
"@tanstack/react-query": "^4.32.0",
"@tanstack/react-query-devtools": "^4.32.0",
"@types/node": "18.15.13",
"@types/react": "^18.2.15",
"@types/react-dom": "18.0.11",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@types/uuid": "^9.0.1",
"autoprefixer": "10.4.14",
"axios": "^1.5.1",
@ -33,13 +33,13 @@
"dayjs": "^1.11.7",
"framer-motion": "^10.12.16",
"lucide-react": "^0.408.0",
"next": "^14.1.0",
"next": "15.1.6",
"next-seo": "^6.0.0",
"postcss": "8.4.23",
"react": "^18.2.0",
"react": "19.0.0",
"react-confetti": "^6.1.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-dom": "19.0.0",
"react-fast-marquee": "^1.3.5",
"react-hook-form": "^7.54.2",
"react-icons": "^4.8.0",
@ -60,6 +60,10 @@
"devDependencies": {
"@types/date-fns": "^2.6.0",
"eslint": "8.49.0",
"eslint-config-next": "^14.1.0"
"eslint-config-next": "15.1.6"
},
"overrides": {
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3"
}
}

View File

@ -11,13 +11,16 @@ export default {
// Quiz ==============================================================
getQuizQuestions: `/quiz/active`,
getQuiz: (quiz_id: string) => `/quiz/${quiz_id}`,
getQuizUUID: (quiz_id: string) => `/quiz/uuid/${quiz_id}`,
getQuizQuestionsWinners: (id: number) => `/quiz/${id}/winners`,
getQuizNetijeWinners: (id: string) => `/quiz/${id}/netije`,
getQuizQuestionHistory: (id: number) => `/question/${id}/history`,
// ===================================================================
// Votes ================================================================
allVotes: "/voting/show_on_site",
vote: (vote_id: string) => `/voting/${vote_id}`,
voteUUID: (vote_id: string) => `/voting/uuid/${vote_id}`,
// ======================================================================
// Lottery ================================================================

View File

@ -1,3 +1,4 @@
import { ISearchNetije } from "@/models/quizQuestionsWinners.model";
import { create } from "zustand";
interface ILotteryStatus {
@ -5,8 +6,52 @@ interface ILotteryStatus {
setStatus: (value: "Upcoming" | "Finished" | "Ongoing") => void;
}
interface IQuizSearch {
active: boolean;
setActive: (value: boolean) => void;
}
interface IStep {
step: number | null;
setStep: (value: number | null) => void;
}
interface ILoading {
loading: boolean;
setLoading: (value: boolean) => void;
}
interface IQuizResults {
resultData: ISearchNetije[];
setResultData: (value: ISearchNetije[]) => void;
error: string;
setError: (value: string) => void;
}
export const useLotteryStatus = create<ILotteryStatus>((set) => ({
status: "Upcoming",
setStatus: (value: "Upcoming" | "Finished" | "Ongoing") =>
set({ status: value }),
}));
export const useQuizSearchActive = create<IQuizSearch>((set) => ({
active: false,
setActive: (value: boolean) => set({ active: value }),
}));
export const useSteps = create<IStep>((set) => ({
step: null,
setStep: (value: number | null) => set({ step: value }),
}));
export const useQuizResults = create<IQuizResults>((set) => ({
resultData: [],
setResultData: (value: ISearchNetije[]) => set({ resultData: value }),
error: "",
setError: (value: string) => set({ error: value }),
}));
export const useResultsLoading = create<ILoading>((set) => ({
loading: false,
setLoading: (value: boolean) => set({ loading: value }),
}));

View File

@ -1,6 +1,5 @@
import { IQuizSearchData } from '@/models/quizSearchData.model';
import { Dispatch, SetStateAction } from 'react';
import { IQuizSearchData } from "@/models/quizSearchData.model";
import { Dispatch, SetStateAction } from "react";
export interface IQuizSearch {
quizSearchData: IQuizSearchData | undefined;
setQuizSearchData: Dispatch<SetStateAction<IQuizSearchData | undefined>>;