This commit is contained in:
Kakabay 2024-08-19 17:44:56 +05:00
parent f7fdf51526
commit 26d5647782
241 changed files with 21330 additions and 0 deletions

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
# /out/
# /out/
# production
# /out
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.vscode

222
api/queries.ts Normal file
View File

@ -0,0 +1,222 @@
import baseUrl from '@/baseUrl';
import { AddPostModel } from '@/models/add.post.model';
import { IAllVotes } from '@/models/allVotes.model';
import { BannerModel } from '@/models/banner.model';
import { CategoriesModel } from '@/models/categories.model';
import { ChannelsModel } from '@/models/channels.model';
import { HomeModel } from '@/models/home.model';
import { LiveDescriptionModel } from '@/models/liveDescription.model';
import { MarqueeModel } from '@/models/marquee.model';
import { NewsModel } from '@/models/news.model';
import { NewsItemModel } from '@/models/newsItem.model';
import { PageItemModel } from '@/models/pageItem.model';
import { PlansModel } from '@/models/plans.model';
import { PropertiesModel } from '@/models/properties.model';
import { IQuizQuestionsHistory } from '@/models/quizQuestionHistory.model';
import { IQuizQuestions } from '@/models/quizQuestions.model';
import { IQuizQuestionsWinners } from '@/models/quizQuestionsWinners.model';
import { MessagesByTvAdmin } from '@/models/sms/messagesByTvAdmis.model';
import { IMyTvAdmins } from '@/models/sms/my.tv.admins.model';
import { VideoModel } from '@/models/video.model';
import { VideosModel } from '@/models/videos.model';
import { IVote } from '@/models/vote.model';
import routes from '@/routes';
import { CloudFog } from 'lucide-react';
export class Queries {
public static async getNews(
page: number,
{ perPage = 8 }: { perPage?: number },
): Promise<NewsModel> {
return await fetch(
`${baseUrl.NEWS_SRC}${routes.news}?locale=tm&count=${perPage}&page=${page}`,
{
next: { revalidate: 3600 },
},
).then((res) => res.json().then((res) => res as NewsModel));
}
public static async getlastNews(): Promise<NewsModel> {
return await fetch(`${baseUrl.NEWS_SRC}${routes.news}?locale=tm&count=5`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as NewsModel));
}
public static async getNewsItem(id: string): Promise<NewsItemModel> {
return await fetch(`${baseUrl.NEWS_SRC}${routes.newsItem(id)}?locale=tm`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as NewsItemModel));
}
public static async getCategories(): Promise<CategoriesModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.categories}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as CategoriesModel));
}
public static async getChannels(): Promise<ChannelsModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.channels}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as ChannelsModel));
}
public static async getVideos(search: string): Promise<VideosModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.videos(search)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as VideosModel));
}
public static async getLastVideos(): Promise<VideosModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.lastVideos}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as VideosModel));
}
public static async getVideo(id: number): Promise<VideoModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.video(id)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as VideoModel));
}
public static async getPage(id: string): Promise<PageItemModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.pageItem(id)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as PageItemModel));
}
public static async getHome(): Promise<HomeModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.home}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as HomeModel));
}
public static async getSmallSlider1(): Promise<HomeModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.homeSmallSlider_1}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as HomeModel));
}
public static async getSmallSlider2(): Promise<HomeModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.homeSmallSlider_2}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as HomeModel));
}
public static async getSmallSlider3(): Promise<HomeModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.homeSmallSlider_1}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as HomeModel));
}
public static async getSmallSlider4(): Promise<HomeModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.homeSmallSlider_2}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as HomeModel));
}
public static async getMarquee(): Promise<MarqueeModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.marquee}?on_morquee=1`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as MarqueeModel));
}
public static async getBanner(): Promise<BannerModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.banner}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as BannerModel));
}
public static async getLiveDescription(channel: number): Promise<LiveDescriptionModel> {
return await fetch(`${baseUrl.MATERIALS_SRC}${routes.channelItem(channel)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as LiveDescriptionModel));
}
public static async getProperties(): Promise<PropertiesModel> {
return await fetch(`${baseUrl.API_SRC}${routes.properties}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as PropertiesModel));
}
public static async getPlans(property_id: number): Promise<PlansModel> {
return await fetch(`${baseUrl.API_SRC}${routes.plans(String(property_id))}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as PlansModel));
}
public static async postAdvert(data: AddPostModel): Promise<Response> {
return await fetch(`${baseUrl.API_SRC}${routes.addPost}`, {
headers: { 'Content-Type': 'application/json' },
cache: 'no-cache',
body: JSON.stringify(data),
method: 'POST',
});
}
// Quiz fetching ===========================================================================
public static async getQuizQuestions(): Promise<IQuizQuestions> {
return await fetch(`${baseUrl.QUIZ_SRC}${routes.getQuizQuestions}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as IQuizQuestions));
}
public static async getQuiz(quiz_id: string): Promise<IQuizQuestions> {
return await fetch(`${baseUrl.QUIZ_SRC}${routes.getQuiz(quiz_id)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as IQuizQuestions));
}
public static async getQuizHistory(id: number): Promise<IQuizQuestionsHistory> {
return await fetch(`${baseUrl.QUIZ_SRC}${routes.getQuizQuestionHistory(id)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as any));
}
public static async getQuizWinners(id: number): Promise<IQuizQuestionsWinners> {
return await fetch(`${baseUrl.QUIZ_SRC}${routes.getQuizQuestionsWinners(id)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as IQuizQuestionsWinners));
}
// ======================================================================================
// Votes ================================================================================
public static async getAllVotes(): Promise<IAllVotes> {
return await fetch(`${baseUrl.QUIZ_SRC}${routes.allVotes}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as IAllVotes));
}
public static async getVote(vote_id: string): Promise<IVote> {
return await fetch(`${baseUrl.QUIZ_SRC}${routes.vote(vote_id)}`, {
next: { revalidate: 3600 },
}).then((res) => res.json().then((res) => res as IVote));
}
// ============================================================================================
// Sms ========================================================================================
public static async getAdmins(): Promise<IMyTvAdmins> {
const token = localStorage.getItem('access_token');
return await fetch(`${baseUrl.SMS_SRC}${routes.myTvAdmins}`, {
headers: { Authorization: `Bearer ${token}` },
}).then((res) => res.json().then((res) => res as IMyTvAdmins));
}
public static async getMessages(
id: number,
current_page: number,
dateValue: string,
activeSort: string,
searchFetch: string,
): Promise<MessagesByTvAdmin> {
const token = localStorage.getItem('access_token');
return await fetch(
`${baseUrl.SMS_SRC}${routes.messagesByTvAdmin(id)}?per_page=60&page=${current_page}${
dateValue ? '&filter_by_date=' + dateValue.toString() : ''
}${searchFetch ? '&search=' + searchFetch : ''}&order=${activeSort}`,
{
headers: { Authorization: `Bearer ${token}` },
},
).then((res) => res.json().then((res) => res as MessagesByTvAdmin));
}
}

View File

@ -0,0 +1,92 @@
'use client';
import { Queries } from '@/api/queries';
import Loader from '@/components/Loader';
import PageTitle from '@/components/PageTitle';
import MainNews from '@/components/news/MainNews';
import NewsGrid from '@/components/news/NewsGrid';
import Item from '@/components/news/NewsItem';
import GlobalContext from '@/context/GlobalContext';
import MainProvider from '@/providers/MainProvider';
import Hydrate from '@/utils/HydrateClient';
import getQueryClient from '@/utils/getQueryClient';
import { dehydrate, useQuery } from '@tanstack/react-query';
import { data } from 'autoprefixer';
import { NextSeo } from 'next-seo';
import Image from 'next/image';
import { useContext } from 'react';
import { useMediaQuery } from 'usehooks-ts';
interface IParams {
params: {
page_id: string;
};
}
const PageItem = ({ params }: IParams) => {
const responsive = useMediaQuery('(max-width: 425px)');
const { data, isFetching, error } = useQuery({
queryKey: ['page_item'],
queryFn: () => Queries.getPage(params.page_id),
});
if (isFetching) return <Loader height={'100%'} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="container">
<div className="flex flex-col gap-8 py-6">
<NextSeo title={data!.data.title} description={data!.data.content} />
<div className="flex flex-col gap-2">
{data?.data.title ? <PageTitle title={data?.data.title} /> : <Loader />}
</div>
<div className="main-news flex md:flex-row flex-col gap-6">
{data?.data.image && data.data.title ? (
responsive && data.data.mobile_image ? (
<div className="w-full h-[300px] relative ">
<Image
src={data?.data.image}
alt={data?.data.title}
unoptimized
unselectable="off"
fill
priority
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="w-full lg:h-[800px] md:h-[650px] h-[350px] relative lg:max-w-[500px] md:max-w-[300px]">
<Image
src={data?.data.image}
alt={data?.data.title}
unoptimized
unselectable="off"
fill
priority
className="w-full h-full object-cover"
/>
</div>
)
) : (
<Loader />
)}
<div className="flex flex-col gap-3 text-black text-lg">
{data?.data.content ? (
<p
className="font-roboto font-normal flex flex-col gap-4"
dangerouslySetInnerHTML={{ __html: data?.data.content }}></p>
) : (
<Loader />
)}
{/* <p
className="font-roboto font-normal flex flex-col gap-4"
dangerouslySetInnerHTML={{ __html: data!.data.content_html }}></p> */}
</div>
</div>
</div>
</div>
);
};
export default PageItem;

View File

@ -0,0 +1,68 @@
const AboutUs = () => {
return (
<div className="about-us">
<h1 className="hidden">About us</h1>
<div className="container">
<div className="about-us-inner flex gap-x-8">
<div className="py-11 flex flex-col gap-6">
<div className="flex flex-col gap-14 pb-5 border-b border-[#BBBBBB] dark:border-b-black border-solid">
<h2 className="font-aeroport text-[32px] font-bold text-black transition-all dark:text-white">
Biz Barada
</h2>
<p className="font-mw_sans text-lg font-light text-black transition-all dark:text-white">
The State Committee of Turkmenistan for Television, Radio Broadcasting and
Cinematography was established on October 17, 2011 by a decree of the President of
Turkmenistan. The main task of the Committee is to implement the state policy of
Turkmenistan in the field of television, radio and film industry.
</p>
</div>
<div className="flex flex-col gap-4 border-b border-[#BBBBBB] dark:border-b-black border-solid pb-5">
<h5 className="font-aeroport font-normal text-[22px] text-black transition-all dark:text-white">
There are 8 TV channels within the Committee:
</h5>
<ul className="flex flex-col gap-2 font-mw_sans font-light text-black text-lg list-disc pl-7 transition-all dark:text-white">
<li>Arkadag;</li>
<li>Altyn Asyr: Turkmenistan;</li>
<li>Yashlyk;</li>
<li>Miras;</li>
<li>Turkmenistan;</li>
<li>Turkmen Owazy;</li>
<li>Ashgabat;</li>
<li>Sport.</li>
</ul>
</div>
<div className="flex flex-col gap-4 border-b border-[#BBBBBB] dark:border-b-black border-solid pb-5">
<h5 className="font-aeroport font-normal text-black transition-all dark:text-white text-[22px]">
4 radio channels:
</h5>
<ul className="flex flex-col gap-2 font-mw_sans font-light text-black transition-all dark:text-white text-lg list-disc pl-7">
<li>Watan;</li>
<li>Chartarapdan;</li>
<li>Miras;</li>
<li>Owaz.</li>
</ul>
</div>
{/* <div className="flex flex-col gap-4 border-b border-[#BBBBBB] dark:border-b-black border-solid pb-5"> */}
<div className="flex flex-col gap-4">
<p className="font-mw_sans text-lg font-light text-black transition-all dark:text-white">
In the prosperous period of our sovereign state, it broadcasts widely to promote the
national values of the Turkmen people, to further develop our culture and art;
</p>
<h5 className="font-aeroport font-normal text-black transition-all dark:text-white text-[22px]">
There are also 4 economic settlements under the State Committee:
</h5>
<ul className="flex flex-col gap-2 font-mw_sans text-black transition-all dark:text-white text-lg list-disc pl-7">
<li> Oguzhan Turkmenfilm Association</li>
<li>Teleradiomerkezi State Enterprise</li>
<li>Mahabat advertising company</li>
<li>School of production</li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
};
export default AboutUs;

View File

@ -0,0 +1,18 @@
import AdvertWindow from '@/components/advert/AdvertWindow';
import StepsProvider from '@/providers/StepsProvider';
const Advert = () => {
return (
<div className="advert-add">
<div className="container">
<div className="inner flex justify-center w-full">
<StepsProvider>
<AdvertWindow />
</StepsProvider>
</div>
</div>
</div>
);
};
export default Advert;

View File

@ -0,0 +1,15 @@
interface IProps {
children: React.ReactNode;
}
function RootLayout({ children }: IProps) {
return (
<div className="h-screen">
<div className="container h-full">
<div className="inner h-full">{children}</div>
</div>
</div>
);
}
export default RootLayout;

View File

@ -0,0 +1,11 @@
import LoginForm from "@/components/auth/LoginForm";
const Login = () => {
return (
<div className="login h-full flex items-center justify-center">
<LoginForm />
</div>
);
};
export default Login;

View File

@ -0,0 +1,11 @@
import RecoveryForm from "@/components/auth/RecoveryForm";
const Recovery = () => {
return (
<div className="login h-full flex items-center justify-center">
<RecoveryForm />
</div>
);
};
export default Recovery;

View File

@ -0,0 +1,11 @@
import SignUpForm from "@/components/auth/SignUpForm";
const SignUp = () => {
return (
<div className="login h-full flex items-center justify-center">
<SignUpForm />
</div>
);
};
export default SignUp;

View File

@ -0,0 +1,12 @@
import TarifWindow from '@/components/auth/TarifWindow';
const Login = () => {
return (
<div className="flex flex-row justify-center items-center w-full h-full gap-[42px]">
<TarifWindow folder={10} days={10} price={10} />
<TarifWindow folder={1} days={5} price={1} />
</div>
);
};
export default Login;

View File

@ -0,0 +1,17 @@
import ContactForm from '@/components/contact_us/ContactForm';
import ContactMap from '@/components/contact_us/ContactMap';
const Contact = () => {
return (
<div>
<div className="container">
<div className="inner py-8 max-w-[1000px] w-full m-auto">
<ContactForm />
<ContactMap />
</div>
</div>
</div>
);
};
export default Contact;

30
app/(main)/layout.tsx Normal file
View File

@ -0,0 +1,30 @@
import Buble from '@/components/Buble';
import Footer from '@/components/Footer';
import MobileMenu from '@/components/MobileMenu';
import Nav from '@/components/Nav';
import GlobalContext from '@/context/GlobalContext';
import MainProvider from '@/providers/MainProvider';
import { useContext } from 'react';
interface IProps {
children: React.ReactNode;
}
const RootLayout = ({ children }: IProps) => {
return (
<div className="z-20 relative">
<MainProvider>
<Buble />
<div className="bg-white dark:bg-black transition-all h-full">
<h1 className="hidden">Turkmen TV</h1>
<Nav />
<main>{children}</main>
<Footer />
<MobileMenu />
</div>
</MainProvider>
</div>
);
};
export default RootLayout;

105
app/(main)/live/page.tsx Normal file
View File

@ -0,0 +1,105 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import ReactPlayer from 'react-player';
import { v4 } from 'uuid';
import { Queries } from '@/api/queries';
import Loader from '@/components/Loader';
import { useQuery } from '@tanstack/react-query';
import channels from '@/channels';
import { useLocalStorage } from 'usehooks-ts';
import { ParseString } from '@/utils/parseString';
import Banner from '@/components/live/Banner';
const page = () => {
const [activeChannel, setActiveChanell] = useLocalStorage('activeChannel', {
channelName: 'Arkadag',
channelNumber: 1,
});
const { data, isFetching, error } = useQuery({
queryKey: ['channel_description', activeChannel],
queryFn: () => Queries.getLiveDescription(activeChannel.channelNumber),
});
// if (isFetching) return <Loader height={'100%'} />;
// if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="live">
<div className="container w-full">
<div className="live-wrapper wrapper flex gap-8 pt-8 pb-14">
<div className="live-main flex flex-col mb-14 w-full h-fit ">
<div className="live-player relative md:h-[550px] sm:h-full h-[250px] w-full border-black border-2">
{channels.map((channel) =>
channel.name === activeChannel.channelName ? (
<ReactPlayer
height={'100%'}
width={'100%'}
controls
url={channel.source}
playing={true}
key={v4()}
/>
) : null,
)}
</div>
<div className="bg-black flex flex-col">
<div className="live-cahnnels flex flex-wrap md:justify-between gap-6 p-5">
{channels
? channels.map((channel) => {
return (
<Link
key={channel.id}
onClick={() =>
setActiveChanell({
channelName: channel.name,
channelNumber: channel.channel,
})
}
className={`channels-img flex flex-col justify-center items-center gap-2 hover:brightness-100 transition-all ${
channel.name === activeChannel.channelName
? 'brightness-100'
: 'brightness-50'
}`}
href={''}>
<div className="relative py-3 px-3 md:w-20 md:h-20 w-16 h-16 rounded-full bg-white overflow-hidden flex flex-col items-center justify-center">
<Image
src={channel.img}
unoptimized
alt={channel.name}
fill
priority
className="w-full h-full object-contain"
/>
</div>
<h3 className="text-white md:text-base text-xs">{channel.name}</h3>
</Link>
);
})
: null}
</div>
{data?.data.length ? (
<div className="flex items-center gap-10 pt-5 w-full text-white border-t-[1px] border-[#7f7f7f] p-5">
{/* <h3 className="font-bold text-base">
{`${ParseString.parseTime(data.data[0].start)} - ${ParseString.parseTime(
data.data[0].end,
)}`}
</h3> */}
<h4 className="text-base">{data.data[0].content}</h4>
</div>
) : null}
</div>
</div>
<Banner />
</div>
</div>
</div>
);
};
export default page;

View File

@ -0,0 +1,86 @@
import { Queries } from '@/api/queries';
import PageTitle from '@/components/PageTitle';
import MainNews from '@/components/news/MainNews';
import NewsGrid from '@/components/news/NewsGrid';
import Item from '@/components/news/NewsItem';
import Hydrate from '@/utils/HydrateClient';
import getQueryClient from '@/utils/getQueryClient';
import { dehydrate } from '@tanstack/react-query';
import Image from 'next/image';
import StaticImage from '@/public/staticPageImage.jpg';
export async function generateStaticParams() {
const news = await Queries.getNews(1, { perPage: 20 });
return news.data.map((item) => ({
slug: item.id.toString(),
}));
}
interface IParams {
params: {
slug: string;
};
}
const NewsItemStatic = async ({ params }: IParams) => {
const queryClient = getQueryClient();
const dehydratedState = dehydrate(queryClient);
return (
<div className="news-item">
<div className="container">
<Hydrate state={{ dehydratedState }}>
<div className="news-body py-11">
<div className="flex flex-col gap-8">
{/* <NextSeo title={data!.data.title} description={data!.data.excerpt} /> */}
<div className="flex flex-col gap-2">
<PageTitle title={'MHB books'} />
{/* <p className="text-lg">{data?.data.published_at}</p> */}
</div>
<div className="main-news flex flex-col gap-6">
<div className="w-full lg:h-[600px] md:h-[400px] h-[250px] relative">
<Image
src={StaticImage}
alt={'kitaplar'}
unoptimized
unselectable="off"
fill
priority
className="w-full object-cover h-[600px]"
/>
</div>
<div className="flex flex-col gap-3 text-black text-lg">
{/* <h2 className="font-mw_sans font-bold">{data!.data.title}</h2> */}
<p className="font-roboto font-normal flex flex-col gap-4 md:text-xl text-lg">
Mahabat müdirliginiň neşir önümleri:
<br /> 1. 3+ we 5+ ýaşly çagalar üçin Zehin soraglary. Bahasy 23 manat;
<br /> 2. Çagalar üçin reňkleme kitaplary. Bahasy: 13 manat;
<br /> 3. Ulylar üçin "Sözýetim" güýmenjesi. Bahasy: 10 manat;
<br /> 4. Çagalara kompýuter programirleme diline giriş "Başarjaň". Bahasy: 38
manat;
<br /> 5. Elwan depderim reňkleme kitaby. Bahasy: 28 manat;
<br /> 6. 7 ýaşdan ýokary çagalar üçin niýetlenen erteki kitaplary. Bahasy: 8
manat;
<br />
<br />{' '}
<a
href="https://forms.gle/g6qm76tZihd4conb8"
target="_blank"
className="text-[#337AB7] font-bold md:text-xl text-lg">
Satyn almak üçin şu düwmä basyň!
</a>
</p>
</div>
</div>
</div>
</div>
</Hydrate>
</div>
</div>
);
};
export default NewsItemStatic;

View File

@ -0,0 +1,53 @@
import { Queries } from "@/api/queries";
import PageTitle from "@/components/PageTitle";
import MainNews from "@/components/news/MainNews";
import NewsGrid from "@/components/news/NewsGrid";
import Item from "@/components/news/NewsItem";
import Hydrate from "@/utils/HydrateClient";
import getQueryClient from "@/utils/getQueryClient";
import { dehydrate } from "@tanstack/react-query";
export async function generateStaticParams() {
const news = await Queries.getNews(1, { perPage: 20 });
return news.data.map((item) => ({
slug: item.id.toString(),
}));
}
interface IParams {
params: {
slug: string;
};
}
const NewsItem = async ({ params }: IParams) => {
const queryClient = getQueryClient();
await queryClient.prefetchQuery({
queryKey: ["news_item", params.slug],
queryFn: () => Queries.getNewsItem(params.slug),
});
await queryClient.prefetchInfiniteQuery({
queryKey: ["news", "infinite"],
queryFn: ({ pageParam = 1 }) => Queries.getNews(pageParam, {}),
});
const dehydratedState = dehydrate(queryClient);
return (
<div className="news-item">
<div className="container">
<Hydrate state={{ dehydratedState }}>
<div className="news-body py-11">
<Item id={params.slug} />
<div className="pt-8">
<NewsGrid isSlides perPage={20} title="Habarlar" />
</div>
</div>
</Hydrate>
</div>
</div>
);
};
export default NewsItem;

29
app/(main)/news/page.tsx Normal file
View File

@ -0,0 +1,29 @@
import { Queries } from '@/api/queries';
import NewsGrid from '@/components/news/NewsGrid';
import Hydrate from '@/utils/HydrateClient';
import getQueryClient from '@/utils/getQueryClient';
import { dehydrate } from '@tanstack/react-query';
const News = async () => {
const queryClient = getQueryClient();
await queryClient.prefetchInfiniteQuery({
queryKey: ['news', 'infinite'],
queryFn: ({ pageParam = 1 }) => Queries.getNews(pageParam, {}),
});
const dehydratedState = dehydrate(queryClient);
return (
<div className="">
<div className="container">
<div className="inner flex flex-col gap-14 py-11">
<Hydrate state={dehydratedState}>
<NewsGrid isExtendable />
</Hydrate>
</div>
</div>
</div>
);
};
export default News;

View File

@ -0,0 +1,160 @@
'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';
interface IParams {
params: {
quiz_id: string;
};
}
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,
// },
// );
useEffect(() => {
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;
});
} 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;
});
}
}, []);
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>
{data?.data.id && quizFinished ? <QuizSearch quizId={data?.data.id} /> : null}
<div className="flex flex-col md:gap-[160px] gap-[80px]">
{data?.data ? (
<QuizQuestionList
dynamic
id={params.quiz_id}
initialQuestionsData={data}
setQuizFinished={setQuizFinished}
quizFinished={quizFinished}
/>
) : null}
{data?.data.id && quizFinished ? (
<QuizWinnerTable quizId={data?.data.id} quizFinished={quizFinished} />
) : null}
</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 page;

View File

@ -0,0 +1,120 @@
'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 { 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);
const [data, setData] = useState<IQuizQuestions>();
// const { data, error, isFetching } = useQuery(
// ['quiz_questions'],
// () => Queries.getQuizQuestions(),
// {
// keepPreviousData: true,
// },
// );
useEffect(() => {
Queries.getQuizQuestions().then((res) => {
setData(res);
res
? res.data.questions[res.data.questions.length - 1]?.status === 'closed'
? setQuizFinished(true)
: setQuizFinished(false)
: null;
});
}, []);
const mobile = useMediaQuery('(max-width: 768px)');
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>
{data?.data.id && quizFinished ? <QuizSearch quizId={data?.data.id} /> : null}
<div className="flex flex-col md:gap-[160px] gap-[80px]">
{data?.data ? (
<QuizQuestionList
id="active"
initialQuestionsData={data}
setQuizFinished={setQuizFinished}
quizFinished={quizFinished}
/>
) : null}
{data?.data.id && quizFinished ? (
<QuizWinnerTable quizId={data?.data.id} quizFinished={quizFinished} />
) : null}
</div>
</QuizProvider>
</div>
) : (
<div className="container text-[40px] flex items-center justify-center font-bold text-textLight min-h-[30vh]">
Непредвиденная ошибка. Нет активной викторины.
</div>
)}
</main>
);
};
export default page;

View File

@ -0,0 +1,12 @@
import Dashboard from '@/components/dashboard/Dashboard';
import { SmsProvider } from '@/context/SmsContext';
const page = () => {
return (
<SmsProvider>
<Dashboard />
</SmsProvider>
);
};
export default page;

15
app/(main)/sms/layout.tsx Normal file
View File

@ -0,0 +1,15 @@
'use client';
// src/app/layout.tsx
import { AuthProvider } from '@/context/AuthContext';
import { SmsContext, SmsProvider } from '@/context/SmsContext';
import { PropsWithChildren, useState } from 'react';
export default function SmsLayout({ children }: PropsWithChildren) {
return (
<main className="min-h-screen">
<div className="container">
<AuthProvider>{children}</AuthProvider>
</div>
</main>
);
}

View File

@ -0,0 +1,81 @@
'use client';
// src/app/sms/sign_up/page.tsx
import { useState, FormEvent, useContext, useEffect } from 'react';
import { AuthContext } from '@/context/AuthContext';
import Loader from '@/components/Loader';
const Page = () => {
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
const authContext = useContext(AuthContext);
const [isLoading, setIsLoading] = useState(true);
if (!authContext) {
throw new Error('AuthContext must be used within an AuthProvider');
}
const { login: loginUser, userLogedIn } = authContext;
const { checkUserLoggedIn } = authContext;
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
loginUser(login, password);
setLogin('');
setPassword('');
};
useEffect(() => {
checkUserLoggedIn();
}, []);
return (
<main className="container flex justify-center h-screen items-center ">
<form onSubmit={handleSubmit} id="create-course-form">
<div className="bg-[#F5F5FA] p-10 rounded-[25px] w-[522px] -mt-28 shadow-tableShadow">
<h2 className="mb-10 text-[40px] leading-none text-#242429 font-semibold">SMS ulgamy</h2>
<div className="flex flex-col gap-2 mb-5 leading-[150%]">
<label htmlFor="login" className="text-[16px] font-semibold text-[#242429]">
Login
</label>
<input
id="login"
type="text"
className="input-style"
value={login}
onChange={(e) => setLogin(e.target.value)}
placeholder="Login giriziň"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="password" className="text-[16px] font-semibold text-[#242429]">
Açar sözi
</label>
<input
id="password"
type="password"
className="input-style"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Açar sözi giriziň"
/>
</div>
<button
type="submit"
className="p-3 bg-[#7A7ACC] text-[18px] leading-[150%] font-medium text-white w-full rounded-xl mt-[30px]">
Ulgama gir
</button>
{userLogedIn === false ? (
<p className="text-center pt-[16px] text-[16px] font-[600] text-red-400">
Login ýa-da açar sözi ýalňyş
</p>
) : null}
</div>
</form>
</main>
);
};
export default Page;

64
app/(main)/test/page.tsx Normal file
View File

@ -0,0 +1,64 @@
'use client';
import { Calendar } from '@/components/ui/calendar';
import { clsx } from 'clsx';
import { useEffect, useRef, useState } from 'react';
const page = () => {
const [date, setDate] = useState<Date>();
const calendarRef = useRef<HTMLDivElement | null>(null);
const mount = useRef(false);
useEffect(() => {
if (mount.current) {
if (calendarRef.current) {
const calendarFooter = document.createElement('div');
calendarFooter.classList.add(
'calendar-footer',
'flex',
'justify-between',
'items-center',
'mt-6',
'px-3',
);
const calendar = document.querySelector('.calendar');
const btn1 = document.createElement('div');
btn1.classList.add('btn1', 'text-textLight', 'cursor-pointer', 'text-[14px]');
btn1.textContent = 'Отменить';
const btn2 = document.createElement('button');
btn2.classList.add('btn2');
btn2.textContent = 'Выбрать';
calendarFooter.appendChild(btn1);
calendarFooter.appendChild(btn2);
calendar?.appendChild(calendarFooter);
}
}
mount.current = true;
}, []);
return (
<section className="container">
<div className="" ref={calendarRef}>
<Calendar
mode="single"
selected={date}
onSelect={setDate}
initialFocus
// onDayClick={() => setActiveDay(true)}
className={clsx(
'my-20 calendar bg-white w-fit rounded-[8px] shadow-[0_2px_32px_rgba(0,0,0,0.3)] transition-all',
// { 'day-styles': activeDay },
)}
/>
</div>
</section>
);
};
export default page;

View File

@ -0,0 +1,73 @@
import VideoList from '@/components/VideoList';
import VideoPlayer from '@/components/VideoPlayer';
import InfoBlock from '@/components/InfoBlock';
import getQueryClient from '@/utils/getQueryClient';
import { Queries } from '@/api/queries';
import Hydrate from '@/utils/HydrateClient';
import { dehydrate, useMutation } from '@tanstack/react-query';
import SectionTitle from '@/components/SectionTitle';
interface IParams {
params: {
video_id: number;
category_id: string;
content_url: string;
banner_url: string;
};
}
const VideoItem = async ({ params }: IParams) => {
const queryClient = getQueryClient();
// queryClient.prefetchQuery({
// queryKey: ['video', `video:${params.video_id}`],
// queryFn: () => Queries.getVideo(params.video_id),
// });
// queryClient.prefetchQuery({
// queryKey: ['video', 'all'],
// queryFn: () => Queries.getVideos(''),
// });
await queryClient.prefetchQuery({
queryKey: ['video', `video:${params.video_id}`],
queryFn: () => Queries.getVideo(params.video_id),
});
await queryClient.prefetchInfiniteQuery({
queryKey: ['video', 'all'],
queryFn: () => Queries.getVideos(''),
});
await queryClient.prefetchInfiniteQuery({
queryKey: ['videos', 'infinite', ''],
queryFn: ({ pageParam = 1 }) =>
Queries.getVideos(
'?' +
String(
new URLSearchParams({
page: pageParam,
per_page: '8',
}),
),
),
});
const dehydratedState = dehydrate(queryClient);
return (
<div className="video-item mt-6">
<div className="container">
<Hydrate state={dehydratedState}>
<div className="video-item-inner">
<div className="video-item-wrapper flex flex-col gap-10 relative pb-14">
<InfoBlock video_id={params.video_id} />
<div className="video-item-inner w-full flex flex-col gap-4">
<SectionTitle title={'Beylekiler'} />
<VideoList isSlides />
</div>
</div>
</div>
</Hydrate>
</div>
</div>
);
};
export default VideoItem;

View File

@ -0,0 +1,15 @@
import MaterialsProvider from "@/providers/MaterialsProvider";
interface IProps {
children: React.ReactNode;
}
function RootLayout({ children }: IProps) {
return (
<div>
<MaterialsProvider>{children}</MaterialsProvider>
</div>
);
}
export default RootLayout;

View File

@ -0,0 +1,66 @@
import { Queries } from '@/api/queries';
import Aside from '@/components/Aside';
import Categories from '@/components/Categories';
import SearchBar from '@/components/SearchBar';
import VideoList from '@/components/VideoList';
import Hydrate from '@/utils/HydrateClient';
import getQueryClient from '@/utils/getQueryClient';
import { dehydrate } from '@tanstack/react-query';
export async function generateStaticParams() {
const categories = await Queries.getCategories();
return categories.data.map((category) => ({
category_id: String(category.id),
}));
}
const Treasury = async () => {
const queryClient = getQueryClient();
await queryClient.prefetchQuery({
queryKey: ['categories'],
queryFn: () => Queries.getCategories(),
});
await queryClient.prefetchQuery({
queryKey: ['channels'],
queryFn: () => Queries.getChannels(),
});
await queryClient.prefetchInfiniteQuery({
queryKey: ['videos', 'infinite', ''],
queryFn: ({ pageParam = 1 }) =>
Queries.getVideos(
'?' +
String(
new URLSearchParams({
page: pageParam,
per_page: '8',
}),
),
),
});
const dehydratedState = dehydrate(queryClient);
return (
<div className="treasury relative">
<div className="container">
<div className="treasury-inner pt-8 pb-14">
<h1 className="hidden">Treasury</h1>
<Hydrate state={dehydratedState}>
<div className="treasury-wrapper flex md:flex-row gap-x-8 flex-col ">
<Aside line />
<div className="treasury-top flex flex-col gap-6 w-full">
<SearchBar />
<Categories />
<VideoList isExtendable />
</div>
</div>
</Hydrate>
</div>
</div>
</div>
);
};
export default Treasury;

View File

@ -0,0 +1,25 @@
'use client';
import ParticipantsList from '@/components/vote/ParticipantsList';
import VoteProvider from '@/providers/VoteProvider';
interface IParams {
params: {
vote_id: string;
};
}
const page = ({ params }: IParams) => {
return (
<main className="pt-[60px] pb-[120px]">
<div className="container">
<VoteProvider>
<div className="flex flex-col items-center w-full">
<ParticipantsList vote_id={params.vote_id} />
</div>
</VoteProvider>
</div>
</main>
);
};
export default page;

View File

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

34
app/failed/page.tsx Normal file
View File

@ -0,0 +1,34 @@
import Image from 'next/image';
import Link from 'next/link';
import errorIcon from '@/public/error-icon.svg';
const Failed = () => {
return (
<main className="success flex items-center justify-center h-screen">
<div className="flex flex-col gap-4 items-center justify-center h-screen">
<Image
src={errorIcon}
alt="close icon"
width={100}
height={100}
className="w-[100px] h-[100px] object-contain"
/>
<h1 className="text-black font-mw_sans font-bold text-[32px]"> Näsazlyk ýüze çykdy</h1>
{/* <p className="font-roboto text-black text-lg text-center">Something went wrong.</p> */}
<Link
href={'/advert'}
className="flex items-center justify-center gap-2 text-white text-lg font-roboto text-center rounded-[30px] bg-mlightblue py-4 min-w-[480px]">
<Image
src={'/arrow-left.svg'}
alt="arrow"
width={32}
height={14}
className="w-8 h-[14px] object-contain"
/>
<p>Yza geç</p>
</Link>
</div>
</main>
);
};
export default Failed;

209
app/globals.css Normal file
View File

@ -0,0 +1,209 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
/* @layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
} */
/* <<<<<<< HEAD * {
scroll-behavior: smooth;
}
=======>>>>>>>2c1eddb85d62e4709470605eb8df7458c2d6c37e body, */
* {
scroll-behavior: smooth;
}
html {
overflow-x: hidden;
/* min-height: 50vh; */
}
.container {
max-width: 1316px;
padding: 0 30px;
width: 100%;
margin: 0 auto;
/* overflow-x: hidden; */
}
.text-stroke {
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
}
.big-swiper .swiper-pagination-bullet {
width: 14px;
height: 14px;
background: white;
border: none;
transition: 0.3 all ease;
}
.small-swiper .swiper-pagination-bullet {
width: 6px;
height: 6px;
background: white;
opacity: 0.7;
transition: 0.3 all ease;
}
.big-swiper .swiper-pagination-bullet-active,
.small-swiper .swiper-pagination-bullet-active {
opacity: 1;
background: #010431;
transition: 0.3 all ease;
}
.small-swiper .swiper-button-next:after {
color: white;
content: url('/arrow-right-small.svg');
}
.small-swiper .swiper-button-prev:after {
color: white;
content: url('/arrow-left-small.svg');
}
.big-swiper .swiper-button-next:after {
color: white;
content: url('/arrow-right-big.svg');
}
.big-swiper .swiper-button-prev:after {
color: white;
content: url('/arrow-left-big.svg');
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
main,
nav,
footer {
z-index: 10;
position: relative;
}
button:disabled {
transition: all 0.3s ease;
opacity: 0.7;
}
.MuiInputBase-root.MuiOutlinedInput-root {
font-family: var(--font-roboto);
border-radius: 0;
border: 2px solid rgba(139, 218, 255, 0.24);
font-size: 18px;
}
p a {
display: inline;
overflow-wrap: break-word;
}
big {
font-size: inherit !important;
}
.clamped {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
text-overflow: ellipsis;
}
@layer utilities {
.input-style {
@apply bg-[#E6E6FA] rounded-xl py-3 px-4 placeholder:text-[#BCBCD6] outline-none;
}
.calendar [aria-label='Go to next month'] {
@apply shadow-sm transition-all;
}
.calendar [aria-label='Go to previous month'] {
@apply shadow-sm transition-all;
}
.day-styles [name='day'] {
@apply p-4 text-textDarkt leading-[140%] bg-purple-600 rounded-full;
}
.btn2 {
@apply text-textBlack leading-[140%] py-1 px-3 rounded-[8px] bg-white border border-[#E6E6FA] font-semibold shadow-sm;
}
.pagination-item {
@apply px-3 py-1 rounded-md border border-textBlack/20 text-textBlack/80 text-[12px] cursor-pointer hover:bg-textBlack transition-all hover:text-white;
}
.pagination-current {
@apply px-3 py-1 rounded-md border bg-fillButtonAccentDefault text-white text-[12px];
}
}

71
app/layout.tsx Normal file
View File

@ -0,0 +1,71 @@
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 'swiper/swiper-bundle.css';
import './globals.css';
import QueryProvider from '@/providers/QueryProvider';
// FONTS
const aeroport = localFont({
src: '../fonts/Aeroport.otf',
variable: '--font-aeroport',
});
const roboto = 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',
});
const mw_sans = Merriweather_Sans({
subsets: ['cyrillic-ext', 'latin', 'latin-ext'],
weight: ['300', '400', '700'],
variable: '--font-mwsans',
});
const alexandria = Alexandria({
subsets: ['latin', 'latin-ext'],
variable: '--font-alexandria',
});
export const metadata = {
title: 'Turkmen TV',
};
interface IProps {
children: React.ReactNode;
}
export default function RootLayout({ children }: IProps) {
return (
<html
lang="tm"
className={`${aeroport.variable} ${mw.variable} ${roboto.variable} ${mw_sans.variable} ${alexandria.variable}`}>
<head>
<link rel="icon" href="/logo.png" sizes="any" />
</head>
<body className="-z-0 relative">
<QueryProvider>{children}</QueryProvider>
</body>
<Script
id="ganalytics-import"
async
src="https://www.googletagmanager.com/gtag/js?id=G-F2267QXY9T"></Script>
<Script id="ganalytics-body">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-F2267QXY9T');`}
</Script>
</html>
);
}

32
app/page.tsx Normal file
View File

@ -0,0 +1,32 @@
import { Queries } from '@/api/queries';
import MainSwiper from '@/components/home/MainSwiper';
import Marque from '@/components/home/Marque';
import SmallSwipers from '@/components/home/SmallSwipers';
import Toolbar from '@/components/home/Toolbar';
import Hydrate from '@/utils/HydrateClient';
import getQueryClient from '@/utils/getQueryClient';
import { dehydrate } from '@tanstack/react-query';
const Home = async () => {
const queryClient = getQueryClient();
queryClient.prefetchQuery({
queryKey: ['home'],
queryFn: () => Queries.getHome(),
});
const dehydratedState = dehydrate(queryClient);
return (
<main className="main">
<Hydrate state={dehydratedState}>
<div className="grid lg:grid-cols-home_custom grid-cols-1">
{/* <Toolbar /> */}
<MainSwiper />
<SmallSwipers />
<Marque />
</div>
</Hydrate>
</main>
);
};
export default Home;

36
app/success/page.tsx Normal file
View File

@ -0,0 +1,36 @@
import Image from 'next/image';
import Link from 'next/link';
const Success = () => {
return (
<main className="success flex items-center justify-center h-screen">
<div className="flex flex-col gap-4 items-center justify-center h-screen">
<Image
src={'/success.svg'}
alt="success icon"
width={100}
height={100}
className="w-[100px] h-[100px] object-contain"
/>
<h1 className="text-black font-mw_sans font-bold text-[32px]">Gutlaýarys!</h1>
<p className="font-roboto text-black text-lg text-center">
Siziň sargydyňyz kabul edildi, mahabat@turkmentv.gov.tm elektron poçtasyna
rekwizitleriňizi ugratmagyňyzy haýyş edýäris!
</p>
<Link
href={'/advert'}
className="flex items-center justify-center gap-2 text-white text-lg font-roboto text-center rounded-[30px] bg-mlightblue py-4 min-w-[480px]">
<Image
src={'/arrow-left.svg'}
alt="arrow"
width={32}
height={14}
className="w-8 h-[14px] object-contain"
/>
<p>Yza geç</p>
</Link>
</div>
</main>
);
};
export default Success;

22
baseUrl.ts Normal file
View File

@ -0,0 +1,22 @@
export default {
API_SRC: 'https://turkmentv.gov.tm/v2/api',
// API_SRC: 'http://turkmentv.gov.tm:8080/api',
// MATERIALS_SRC: 'http://turkmentv.gov.tm:8082/api',
MATERIALS_SRC: 'https://turkmentv.gov.tm/v2/api',
// NEWS_SRC: 'http://216.250.11.231:8083/api/v1',
NEWS_SRC: 'https://turkmenistaninfo.gov.tm/app/api/v1',
// Quiz and voting
QUIZ_SRC: 'https://sms.turkmentv.gov.tm/api',
// SMS
SMS_SRC: 'https://extra.turkmentv.gov.tm/api',
};
// export default {
// API_SRC: 'http://turkmentv.gov.tm:8080/api',
// MATERIALS_SRC: 'http://turkmentv.gov.tm/v2/api',
// NEWS_SRC: 'http://216.250.11.231/habar/api/v1',
// };

63
channels.ts Normal file
View File

@ -0,0 +1,63 @@
import { IChannels } from '@/typings/channels.type';
const channels: IChannels[] = [
{
id: 1,
img: '/video_arkadag.jpg',
name: 'Arkadag',
source: 'https://alpha.tv.online.tm/hls/ch000.m3u8',
channel: 1,
},
{
id: 2,
img: '/video_altyn_asyr.jpg',
name: 'Altyn Asyr',
source: 'https://alpha.tv.online.tm/hls/ch001.m3u8',
channel: 2,
},
{
id: 3,
img: '/video_yaslyk.jpg',
name: 'Ýaşlyk',
source: 'https://alpha.tv.online.tm/hls/ch002.m3u8',
channel: 3,
},
{
id: 4,
img: '/video_miras.jpg',
name: 'Miras',
source: 'https://alpha.tv.online.tm/hls/ch003.m3u8',
channel: 4,
},
{
id: 5,
img: '/video_turkmenistan.jpg',
name: 'Türkmenistan',
source: 'https://alpha.tv.online.tm/hls/ch007.m3u8',
channel: 5,
},
{
id: 6,
img: '/video_turkmen_owaz.jpg',
name: 'Türkmen Owazy',
source: 'https://alpha.tv.online.tm/hls/ch005.m3u8',
channel: 6,
},
{
id: 7,
img: '/video_ashgabat.jpg',
name: 'Aşgabat',
source: 'https://alpha.tv.online.tm/hls/ch006.m3u8',
channel: 7,
},
{
id: 8,
img: '/video_turkmen_sport.jpg',
name: 'Türkmen Sporty',
source: 'https://alpha.tv.online.tm/hls/ch004.m3u8',
channel: 8,
},
];
export default channels;

17
components.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

83
components/Aside.tsx Normal file
View File

@ -0,0 +1,83 @@
'use client';
import { v4 } from 'uuid';
import { useQuery } from '@tanstack/react-query';
import { Queries } from '@/api/queries';
import Image from 'next/image';
import Loader from './Loader';
import closeIcon from '@/public/close-outline.svg';
import { useContext, useState } from 'react';
import MaterialsContext from '@/context/MaterialsContext';
import { motion } from 'framer-motion';
interface IProps {
line: boolean;
}
const Aside = ({ line = true }: IProps) => {
const { data, error, isFetching } = useQuery({
queryKey: ['channels'],
queryFn: () => Queries.getChannels(),
});
const { params, setParams } = useContext(MaterialsContext);
const [channelActive, setChannelActive] = useState(false);
const channelHandler = (id: number, state: boolean) => {
setChannelActive(state);
setParams({ ...params, channel_id: String(id) });
};
if (isFetching) return <Loader />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<aside
className={`aside flex flex-wrap md:flex-col md:gap-3 gap-3 md:pr-7 h-fit w-fit pb-5 sticky top-5 justify-between ${
line &&
'md:border md:border-t-0 md:border-l-0 md:border-b-0 md:border-r-gray-400 md:transition-all md:dark:border-r-black'
}`}>
{data
? data.data.map((image) => (
<motion.div
onClick={() => channelHandler(image.id, true)}
className="aside-img w-14 h-14 rounded-full overflow-hidden border bg-white border-gray-500 cursor-pointer"
key={v4()}
initial={{ border: 'solid 1px rgb(107, 114, 128)' }}
animate={
params.channel_id === String(image.id) ? { border: 'solid 3px #FFAB48' } : {}
}>
<Image
src={image.image}
alt={image.name}
unoptimized
unselectable="off"
className="h-full w-full object-contain rounded-full"
width={44}
height={44}
/>
</motion.div>
))
: null}
{channelActive && (
<motion.div
onClick={() => channelHandler(0, false)}
className="aside-img w-14 h-14 rounded-full overflow-hidden border bg-white border-gray-500 cursor-pointer p-2"
key={v4()}
initial={{ border: 'solid 1px rgb(107, 114, 128)' }}
animate={params.channel_id === String(0) ? { border: 'solid 3px #FFAB48' } : {}}>
<Image
src={closeIcon}
alt="close"
unoptimized
unselectable="off"
className="h-full w-full object-contain rounded-full"
width={44}
height={44}
/>
</motion.div>
)}
</aside>
);
};
export default Aside;
// export default Aside;

19
components/AsideAdd.tsx Normal file
View File

@ -0,0 +1,19 @@
import Image from "next/image";
const AsideAdd = () => {
return (
<aside className="w-full">
<Image
className="w-full h-80% object-cover"
src={"/aside-add.jpg"}
alt="aside-add"
unoptimized
unselectable="off"
width={100}
height={200}
/>
</aside>
);
};
export default AsideAdd;

10
components/Buble.tsx Normal file
View File

@ -0,0 +1,10 @@
const Buble = () => {
return (
<div className="-z-0 relative pointer-events-none">
<div className="buble absolute top-10 left-28 w-96 h-96 bg-[#bfa6e7] rounded-full mix-blend-multiply filter blur-3xl opacity-70 animate-buble transition-colors dark:bg-[#3448FF]"></div>
<div className="buble absolute bottom-20 right-28 w-96 h-96 bg-[#bfa6e7] rounded-full mix-blend-multiply filter blur-3xl opacity-70 animate-buble transition-colors dark:bg-[#3448FF]"></div>
</div>
);
};
export default Buble;

24
components/Button.tsx Normal file
View File

@ -0,0 +1,24 @@
interface IProps {
name: string;
width?: string;
type: 'submit' | 'button' | 'reset';
onClick?: () => void;
disabled?: boolean;
}
const Button = (props: IProps) => {
return (
<div className="flex justify-end">
<button
disabled={props.disabled}
type={props.type}
onClick={props.onClick}
style={props.width ? { width: props.width } : {}}
className={`min-w-[200px] bg-mlightblue font-roboto font-bold text-lg text-white text-center rounded-five py-2 px-5 flex justify-center flex-shrink-0 h-fit`}>
{props.name}
</button>
</div>
);
};
export default Button;

29
components/Categories.tsx Normal file
View File

@ -0,0 +1,29 @@
"use client";
import Category from "./Category";
import { v4 } from "uuid";
import { useQuery } from "@tanstack/react-query";
import { Queries } from "@/api/queries";
import Loader from "./Loader";
const Categories = () => {
const { data, error, isFetching } = useQuery({
queryKey: ["categories"],
queryFn: () => Queries.getCategories(),
});
if (isFetching) return <Loader />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<ul className="flex flex-wrap gap-x-2 gap-y-3">
<Category id={0} name={"Hemme"} key={v4()} isInitial />
{data
? data.data.map((category) => (
<Category id={category.id} name={category.name} key={v4()} />
))
: null}
</ul>
);
};
export default Categories;

46
components/Category.tsx Normal file
View File

@ -0,0 +1,46 @@
'use client';
import { useContext } from 'react';
import { ICategory } from '@/typings/category.type';
import GlobalContext from '@/context/GlobalContext';
import { usePathname } from 'next/navigation';
import MaterialsContext from '@/context/MaterialsContext';
interface IProps extends ICategory {
isInitial?: boolean;
}
const Category = ({ id, name, isInitial }: IProps) => {
const path = usePathname() ?? '/';
const context = useContext(GlobalContext);
const { params, setParams } = useContext(MaterialsContext);
const category = params.category_id;
const { theme } = context.themeContext;
return (
<li className="flex-1 max-w-[180px] cursor-pointer">
<div
onClick={() => {
setParams({ ...params, category_id: String(id) });
}}
className="bg-categorybg rounded-five px-3 py-2 flex justify-center w-full whitespace-nowrap font-roboto transition-all text-black dark:text-white dark:bg-[#38383880]"
style={
category
? category === String(id)
? {
backgroundColor: theme === 'light' ? '#FFAB48' : '#FFFFFF',
color: 'black',
}
: {}
: path.endsWith('/treasury') && isInitial
? {
backgroundColor: theme === 'light' ? '#FFAB48' : '#FFFFFF',
color: 'black',
}
: {}
}>
{name}
</div>
</li>
);
};
export default Category;

View File

@ -0,0 +1,12 @@
const ContactDetails = () => {
return (
<div className="flex flex-col pl-6 md:text-[22px] text-base text-black font-light font-mw_sans border-l-[6px] border-mblue h-fit min-w-[350px] dark:text-white">
<h2 className="md:text-[32px] text-xl font-normal">MAHABAT MÜDIRLIGI</h2>
<a href="tel:+99312493705">Phone: +993 12 493705</a>
<span>Aşgabat şäheri Oguzhan 13</span>
<a href="mailto:mahabat@turkmentv.gov.tm">mahabat@turkmentv.gov.tm</a>
</div>
);
};
export default ContactDetails;

View File

@ -0,0 +1,80 @@
'use client';
import { ChangeEvent, HTMLInputTypeAttribute, useState } from 'react';
interface ICustomInput {
name: string;
type?: HTMLInputTypeAttribute;
label?: string;
placeholder?: string;
isTextArea?: boolean;
required?: boolean;
isBlack?: boolean;
value: {
value: string;
setValue: (newValue: ICustomInput['value']['value']) => void;
};
validate?: {
isValid: boolean;
errMsg: string;
};
}
const CustomInput = ({
value,
name,
isTextArea,
label,
placeholder,
type,
required,
isBlack,
validate,
}: ICustomInput) => {
const [isTouched, setIsTouched] = useState<boolean>(false);
return (
<div className="custom-input relative">
{label ? (
<label
htmlFor={name}
className="text-black md:text-lg text-base px-4 bg-white font-mw_sans absolute -top-[14px] left-[2%]"
style={isBlack ? { background: '#1a1a1a', color: '#fff' } : {}}>
{label}
</label>
) : null}
{!isTextArea ? (
<div className="flexs flex-col gap-2">
<input
type={type}
name={name}
id={name}
required={required}
placeholder={placeholder}
className="p-5 border border-solid border-[#B2B1B1] rounded-[5px] font-roboto text-[#9A9A9A] md:text-[22px] text-base w-full"
style={isBlack ? { background: '#1a1a1a', color: '#fff' } : {}}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
if (!isTouched) {
setIsTouched(true);
}
value.setValue(e.target.value);
}}
/>
{isTouched && validate && !validate.isValid ? (
<span className="text-mred text-sm font-bold font-roboto">{validate.errMsg}</span>
) : null}
</div>
) : (
<textarea
name={name}
id={name}
required={required}
placeholder={placeholder}
rows={5}
className="resize-none p-5 border border-solid border-[#B2B1B1] rounded-[5px] font-roboto text-[#9A9A9A] md:text-[22px] text-base w-full"
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => value.setValue(e.target.value)}
style={isBlack ? { background: '#1a1a1a', color: '#fff' } : {}}></textarea>
)}
</div>
);
};
export default CustomInput;

View File

@ -0,0 +1,90 @@
'use client';
import { useRef, useState } from 'react';
import { motion } from 'framer-motion';
import { v4 } from 'uuid';
import { BiSolidDownArrow } from 'react-icons/bi';
import { useOnClickOutside } from 'usehooks-ts';
interface ICustomSelect {
name: string;
label?: string;
placeholder?: string;
required?: boolean;
items: string[];
isBlack?: boolean;
setValue: (newValue: string) => void;
}
const CustomSelect = ({
name,
label,
placeholder,
required,
items,
setValue,
isBlack,
}: ICustomSelect) => {
const [open, setOpen] = useState<boolean>(false);
const ref = useRef<HTMLDivElement>(null);
const [input, setInput] = useState<string>();
useOnClickOutside(ref, () => setOpen(false));
return (
<div ref={ref} className="custom-input relative cursor-pointer" onClick={() => setOpen(!open)}>
{label ? (
<label
htmlFor={name}
className="text-black text-lg px-4 bg-white font-mw_sans absolute -top-[14px] left-[2%]"
style={isBlack ? { background: '#1a1a1a', color: '#fff' } : {}}>
{label}
</label>
) : null}
<input
readOnly
type={'text'}
name={name}
id={name}
required={required}
placeholder={placeholder}
className="p-5 border border-solid border-[#B2B1B1] rounded-[5px] font-roboto text-[#9A9A9A] text-[22px] w-full pointer-events-none"
style={isBlack ? { background: '#1a1a1a', color: '#fff' } : {}}
value={input}
/>
<motion.div
className="absolute top-[40%] right-4"
initial={{ rotate: '0deg' }}
animate={
open
? {
rotate: ' 180deg',
transition: { type: 'tween', duration: 0.4 },
}
: { rotate: '0deg', transition: { type: 'tween', duration: 0.4 } }
}>
<BiSolidDownArrow color="#B2B1B1" />
</motion.div>
<motion.ul
className="absolute top-[100%] left-0 w-full h-fit bg-white z-20 rounded-[5px] border border-solid overflow-hidden border-[#B2B1B1] max-h-52 overflow-y-auto"
initial={{ height: '0%' }}
animate={
open ? { height: 'fit-content', opacity: '100%' } : { height: '0%', opacity: '0%' }
}>
{items.map((item) => (
<motion.li
className="p-5 text-[20px] text-black w-full z-30 relative cursor-pointer border-b border-solid border-[#B2B1B1]"
style={isBlack ? { color: '#fff' } : {}}
initial={!isBlack ? { background: '#fff' } : { background: '#1a1a1a' }}
whileHover={!isBlack ? { background: '#dadada' } : { background: '#151515' }}
onClick={() => {
setValue(item);
setInput(item);
}}
key={v4()}>
{item}
</motion.li>
))}
</motion.ul>
</div>
);
};
export default CustomSelect;

120
components/Footer.tsx Normal file
View File

@ -0,0 +1,120 @@
import Link from 'next/link';
import Image from 'next/image';
import apple from '@/public/App Store Button Black.svg';
import play from '@/public/Google Play Button Black.svg';
import app1 from '@/public/tv-mobile.png';
import app2 from '@/public/tv-mobile2.png';
const Footer = () => {
const year = new Date().getFullYear();
return (
<footer className="footer w-full bg-[#121268] z-auto">
<div className="container">
<div className="footer-inner py-[22px] md:px-8 md:grid md:grid-cols-footer flex flex-col gap-6 overflow-x-hidden">
<div className="flex flex-col md:gap-14 gap-6">
<ul className="grid grid-cols-2 gap-3">
<li>
<Link
href={'/news'}
className="w-full h-full text-left font-roboto text-lg text-white">
Habarlar
</Link>
</li>
<li>
<Link
href={'/advert'}
className="w-full h-full text-left font-roboto text-lg text-white">
Mahabat
</Link>
</li>
<li>
<Link
href={'/treasury'}
className="w-full h-full text-left font-roboto text-lg text-white">
Hazyna
</Link>
</li>
<li>
<Link
href={'/about_us'}
className="w-full h-full text-left font-roboto text-lg text-white">
Biz barada
</Link>
</li>
<li>
<Link
href={'/live'}
className="w-full h-full text-left font-roboto text-lg text-white">
Göni Ýaýlym
</Link>
</li>
<li>
<Link
href={'/contact_us'}
className="w-full h-full text-left font-roboto text-lg text-white">
Habarlaşmak üçin
</Link>
</li>
</ul>
<div className="flex md:flex-row flex-col md:gap-12 gap-6">
<div className="flex items-center gap-6">
<div className="relative w-20 h-20 rounded-full overflow-hidden">
<Image src={app2} alt="asf" fill />
</div>
<div className="flex flex-col gap-3 w-fit">
<Link
href="https://apps.apple.com/ru/app/t%C3%BCrkmen-radio/id1622189361"
className="relative h-12 w-40 block"
target="_blank">
<Image src={apple} alt="app store" fill />
</Link>
<Link
href="https://play.google.com/store/apps/details?id=com.app.telecom_radio"
className="relative h-12 w-40 block"
target="_blank">
<Image src={play} alt="google play" fill />
</Link>
</div>
</div>
<div className="flex items-center gap-6">
<div className="relative w-20 h-20 rounded-lg overflow-hidden">
<Image src={app1} alt="asf" fill />
</div>
<div className="flex flex-col gap-3 w-fit">
<Link
href="https://apps.apple.com/ru/app/t%C3%BCrkmen-tv/id915833573"
className="relative h-12 w-40 block"
target="_blank">
<Image src={apple} alt="app store" fill />
</Link>
<Link
href="https://play.google.com/store/apps/details?id=tm.ykjam.turkmentv"
className="relative h-12 w-40 block"
target="_blank">
<Image src={play} alt="google play" fill />
</Link>
</div>
</div>
</div>
<p className="font-alexandria text-[#5151CF] md:block hidden">
{year} © TurkmenTV. All rights reserved.
</p>
</div>
<div className="flex flex-col gap-9">
<p className="relative font-roboto font-normal opacity-60 text-white flex flex-col gap-3 pl-[18px]">
<a href="tel:+99312493705">Phone: +993 12 493705</a>
<span>Aşgabat şäheri Oguzhan 13</span>
<a href="mailto:mahabat@turkmentv.gov.tm">mahabat@turkmentv.gov.tm</a>
<span className="absolute top-0 left-0 w-[5px] h-full bg-white"></span>
</p>
</div>
<p className="font-alexandria text-[#5151CF] md:hidden block">
{year} © TurkmenTV. All rights reserved.
</p>
</div>
</div>
</footer>
);
};
export default Footer;

24
components/Icon.tsx Normal file
View File

@ -0,0 +1,24 @@
import Image from "next/image";
interface IProps {
width: number;
height: number;
element: string;
}
const Icon = ({ width, height, element }: IProps) => {
return (
<div style={{ width, height }}>
<Image
src={element}
unoptimized
unselectable="off"
alt={`icon_${element}`}
width={width}
height={height}
></Image>
</div>
);
};
export default Icon;

187
components/InfoBlock.tsx Normal file
View File

@ -0,0 +1,187 @@
'use client';
// NextJs components
import Image from 'next/image';
import Link from 'next/link';
// React query
import { useMutation, useQuery } from '@tanstack/react-query';
import { Queries } from '@/api/queries';
// Components
import Loader from './Loader';
import VideoPlayer from './VideoPlayer';
import SectionTitle from './SectionTitle';
// Images and cions
import { BiLike, BiSolidDislike, BiSolidLike, BiDislike } from 'react-icons/bi';
import { SlEye } from 'react-icons/sl';
import aydym from '@/public/aydym-com.webp';
import horjun from '@/public/horjun.png';
import belet from '@/public/belet.jpg';
import { useState } from 'react';
import { useLocalStorage } from 'usehooks-ts';
import axios from 'axios';
interface IProps {
video_id: number;
}
const InfoBlock = ({ video_id }: IProps) => {
const [isLikedId, setIsLikedId] = useLocalStorage<number[]>('isLiked', []) || [];
const [isDisLikedId, setIsDisLikedId] = useLocalStorage<number[]>('isDisliked', []) || [];
async function addLikes() {
return axios.post(`https://turkmentv.gov.tm/v2/api/material/${video_id}/likes`, {
like: isLikedId.includes(video_id) ? true : false,
});
}
async function addDisLikes() {
return axios.post(`https://turkmentv.gov.tm/v2/api/material/${video_id}/dislikes`, {
dislike: isDisLikedId.includes(video_id) ? true : false,
});
}
const mutationLikes = useMutation(() => addLikes());
const mutationDisLikes = useMutation(() => addDisLikes());
const onLikeHandler = () => {
if (isLikedId.includes(video_id)) {
setIsLikedId(isLikedId.filter((id) => id !== video_id));
} else {
if (isDisLikedId.includes(video_id)) {
setIsDisLikedId(isDisLikedId.filter((id) => id !== video_id));
mutationDisLikes.mutate();
}
setIsLikedId([...isLikedId, video_id]);
}
mutationLikes.mutate();
};
const onDisLikeHandler = () => {
if (isDisLikedId.includes(video_id)) {
setIsDisLikedId(isDisLikedId.filter((id) => id !== video_id));
} else {
if (isLikedId.includes(video_id)) {
setIsLikedId(isLikedId.filter((id) => id !== video_id));
mutationLikes.mutate();
}
setIsDisLikedId([...isDisLikedId, video_id]);
}
mutationDisLikes.mutate();
};
const { data, isFetching, error } = useQuery({
queryKey: ['video', video_id, mutationLikes, mutationDisLikes],
queryFn: () => Queries.getVideo(video_id),
});
if (isFetching) return <Loader height={200} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="flex gap-6 max-w-[1220px] 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-2">
<SectionTitle title={data!.data.title} />
<div className="flex gap-2 items-center">
<SlEye
style={{ transition: '150ms all cubic-bezier(0.4, 0, 0.2, 1)' }}
width={30}
height={18}
className="w-[30px] h-[18px]"
/>
<span className="font-roboto text-lg font-normal text-[#494949] transition-all dark:text-white">
{data!.data.view}
</span>
</div>
</div>
<div className="flex flex-col gap-[40px] h-full w-full">
<div className="flex gap-[40px] md:flex-row flex-col h-full w-full">
<div>
<VideoPlayer video_id={video_id} maxHeight="700px" maxWidth="100%" />
</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}
<div className="flex items-center bg-slate-50 rounded-full overflow-hidden w-fit">
<div
className="flex items-center gap-4 cursor-pointer py-4 px-6 hover:bg-slate-100"
onClick={() => onLikeHandler()}>
<button>
{isLikedId.includes(video_id) ? (
<BiSolidLike size={30} />
) : (
<BiLike size={30} />
)}
</button>
<span className="block text-xl ">{data?.data.likes}</span>
</div>
<div
className="flex items-center gap-4 cursor-pointer py-4 px-6 hover:bg-slate-100"
onClick={() => onDisLikeHandler()}>
<button>
{isDisLikedId.includes(video_id) ? (
<BiSolidDislike size={30} />
) : (
<BiDislike size={30} />
)}
</button>
<span className="block text-xl ">{data?.data.dislikes}</span>
</div>
</div>
{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>
);
};
export default InfoBlock;

23
components/Loader.tsx Normal file
View File

@ -0,0 +1,23 @@
import Image from 'next/image';
import { CSSProperties } from 'react';
interface IProps {
height?: CSSProperties['height'];
}
const Loader = ({ height = 'auto' }: IProps) => {
return (
<div className="loader my-6 w-full flex justify-center items-center" style={{ height }}>
<Image
src={'/spin-blue.svg'}
alt="loader"
width={38}
height={38}
unoptimized
className="object-contain w-[50px] h-[50px]"
/>
</div>
);
};
export default Loader;

21
components/Map.tsx Normal file
View File

@ -0,0 +1,21 @@
import Image from 'next/image';
import map from '@/public/mahabat-map.png';
const Map = () => {
return (
<div className="w-full h-full border-2 border-black rounded-five p-4">
<Image
src={map}
width={600}
height={200}
unoptimized
unselectable="off"
alt="map"
className="w-full h-full object-contain"
/>
</div>
);
};
export default Map;

192
components/MobileMenu.tsx Normal file
View File

@ -0,0 +1,192 @@
'use client';
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 close from '@/public/close-white.svg';
const MobileMenu = () => {
const path = usePathname() ?? '/';
const { burgerOpen, setBurgerOpen } = useContext(GlobalContext).burgerContext;
const onClickCloseBurgerHandler = () => {
setBurgerOpen(false);
};
const [dropDownOpened, setDropDownOpened] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setDropDownOpened(false);
}
};
// Attach the event listener to the document
document.addEventListener('click', handleOutsideClick);
// Cleanup the event listener when the component unmounts
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, []);
return (
<>
{burgerOpen && (
<div className="absolute top-0 left-0 w-full h-fit bg-[#131369] z-50 py-1">
<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">
<Image
priority
src={'/logo.png'}
alt="logo"
unoptimized
unselectable="off"
width={60}
height={60}
className="w-[60px] h-[60px] object-contain"
/>
</Link>
<div
className="relative w-[24px] h-[24px] cursor-pointer "
onClick={() => setBurgerOpen(false)}>
<Image src={close} fill alt="menu" />
</div>
</div>
<ul className="flex flex-col gap-10 items-start justify-center p-6">
<li>
<Link
className="block text-3xl text-white transition-all font-roboto font-bold dark:text-white"
onClick={() => onClickCloseBurgerHandler()}
href={'/news'}
style={path.includes('news') ? { color: '#FFAB48' } : {}}>
Habarlar
</Link>
</li>
<li>
<Link
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' }
: {}
}>
Hazyna
</Link>
</li>
<li>
<Link
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' } : {}}>
Göni Ýaýlym
</Link>
</li>
<li>
<Link
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' } : {}}>
Mahabat
</Link>
</li>
<li>
<Link
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">
Habarlaşmak üçin
</Link>
</li>
<li>
<div
ref={dropdownRef}
className="flex flex-col cursor-pointer relative"
onClick={() => setDropDownOpened(!dropDownOpened)}>
<div className="flex items-center gap-[8px]">
<span className="block text-3xl text-white transition-all font-roboto font-bold">
Interaktiw
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
className={`${
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"
/>
</svg>
</div>
<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'
}`}>
<Link
href={'/quiz'}
className="block text-2xl text-white transition-all font-roboto font-bold "
style={path.includes('quiz') ? { color: '#FFAB48' } : {}}
onClick={() => {
setDropDownOpened(false);
onClickCloseBurgerHandler();
}}>
Bäsleşik
</Link>
<Link
href={'/vote'}
className="block text-2xl text-white transition-all font-roboto font-bold "
onClick={() => {
setDropDownOpened(false);
onClickCloseBurgerHandler();
}}>
Ses bermek
</Link>
<Link
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>
</div>
</div>
</li>
<li>
<Link
target="_blank"
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 ">
Ýaýlym tertibi
</Link>
</li>
</ul>
</div>
</div>
</div>
)}
</>
);
};
export default MobileMenu;

29
components/MoreBtn.tsx Normal file
View File

@ -0,0 +1,29 @@
'use client';
import Image from 'next/image';
import { FiMoreHorizontal } from 'react-icons/fi';
interface IProps {
onClick: () => void;
disabled?: boolean;
isFetching?: boolean;
}
const MoreBtn = ({ onClick, disabled, isFetching }: IProps) => {
return (
<button
disabled={disabled}
onClick={onClick}
type="button"
className="flex gap-2 rounded-five py-2 px-2 w-[180px] bg-mlightblue items-center justify-center">
{isFetching ? (
<Image src="/spin.svg" alt="spin" width={38} height={38} unoptimized />
) : (
<>
<FiMoreHorizontal color="white" width={14} height={7} />
<span className="font-roboto text-lg font-bold text-white">Ýene gör</span>
</>
)}
</button>
);
};
export default MoreBtn;

198
components/Nav.tsx Normal file
View File

@ -0,0 +1,198 @@
'use client';
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';
const Nav = () => {
const path = usePathname() ?? '/';
const { burgerOpen, setBurgerOpen } = useContext(GlobalContext).burgerContext;
const [dropDownOpened, setDropDownOpened] = useState(false);
// const dropdownRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setDropDownOpened(false);
}
};
// Attach the event listener to the document
document.addEventListener('click', handleOutsideClick);
// Cleanup the event listener when the component unmounts
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, []);
return (
<nav className="nav py-1 border-b-2 border-solid border-[#D9D9D9] bg-transparent dark:bg-black transition-all dark:border-black z-50 ">
<div className="container">
<div className="nav-inner flex justify-between gap-4 items-center">
<div className="flex gap-11 items-center justify-start">
<Link href={'/'} className="logo">
<Image
priority
src={'/logo.png'}
alt="logo"
unoptimized
unselectable="off"
width={60}
height={60}
className="w-[60px] h-[60px] object-contain"
/>
</Link>
<ul className="md:flex gap-5 items-center justify-start hidden">
<li>
<Link
className="block text-lg text-black transition-all font-roboto font-bold dark:text-white"
href={'/news'}
style={path.includes('news') ? { color: '#FFAB48' } : {}}>
Habarlar
</Link>
</li>
<li>
<Link
href={'/treasury'}
className="block text-lg text-black transition-all font-roboto font-bold dark:text-white"
style={
path.includes('treasury') || path.includes('video/') ? { color: '#FFAB48' } : {}
}>
Hazyna
</Link>
</li>
<li>
<Link
href={'/live'}
className="block text-lg text-black transition-all font-roboto font-bold dark:text-white"
style={path.includes('live') ? { color: '#FFAB48' } : {}}>
Göni Ýaýlym
</Link>
</li>
<li>
<Link
href={'/advert'}
className="block text-lg text-black transition-all font-roboto font-bold dark:text-white"
style={path.includes('advert') ? { color: '#FFAB48' } : {}}>
Mahabat
</Link>
</li>
<li>
<div
ref={dropdownRef}
className="flex items-center cursor-pointer relative w-full"
onClick={() => setDropDownOpened(!dropDownOpened)}>
<span className="block text-lg text-black transition-all font-roboto font-bold dark:text-white">
Interaktiw
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className={`${
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="#4D4D4D"
/>
</svg>
<div
className={`absolute top-10 left-1/2 -translate-x-1/2 bg-[#353598] flex flex-col gap-4 p-[24px] rounded-[8px] transition-all duration-300 w-[150px] ${
dropDownOpened
? 'opacity-100 translate-y-0 pointer-events-auto'
: ' opacity-0 translate-y-2 pointer-events-none'
}`}>
<Link
href={'/quiz/active'}
className="block min-w-fit text-lg text-white transition-all font-roboto font-bold "
style={path.includes('quiz/active') ? { color: '#FFAB48' } : {}}
onClick={() => setDropDownOpened(false)}>
Bäsleşik
</Link>
<Link
href={'/vote/active'}
className="block min-w-fit text-lg text-white transition-all font-roboto font-bold "
style={path.includes('vote/active') ? { color: '#FFAB48' } : {}}
onClick={() => setDropDownOpened(false)}>
Ses bermek
</Link>
<Link
target="_blank"
href={'https://shop.turkmentv.gov.tm/'}
className="block min-w-fit text-lg text-white transition-all font-roboto font-bold"
onClick={() => setDropDownOpened(false)}>
TV market
</Link>
<Link
href={'/sms/sign_up'}
className="block min-w-fit text-lg text-white transition-all font-roboto font-bold"
onClick={() => setDropDownOpened(false)}>
SMS ulgamy
</Link>
</div>
</div>
</li>
<li>
<Link
href={'https://turkmentv.gov.tm/mahabat/admin/user/login'}
target="_blank"
className="block text-lg p-2 bg-red-500 rounded-lg text-white transition-all font-roboto font-bold dark:text-white">
Ýaýlym tertibi
</Link>
</li>
</ul>
</div>
<div className="flex gap-4 items-center">
<ul className="md:flex gap-4 items-center hidden">
<li>
<Link
href={'/contact_us'}
style={path.includes('contact_us') ? { color: '#FFAB48' } : {}}
className="font-roboto font-normal text-black text-lg dark:text-white transition-all">
Habarlaşmak üçin
</Link>
</li>
{/* <li>
<span className="text-[#B2B1B1]">|</span>
</li>
<li>
<AiOutlineUser
style={{
transitionProperty: 'all',
transitionDuration: '150ms',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
}}
color={theme === 'dark' ? '#fff' : '#000'}
className="w-[22px] h-[22px]"
/>
</li>
<li>
<span className="text-[#B2B1B1]">|</span>
</li>
<li>
<ThemeSwitch />
</li> */}
</ul>
<div
className="md:hidden block relative w-[24px] h-[24px] cursor-pointer p-4"
onClick={() => setBurgerOpen(true)}>
<Image src={burger} fill alt="menu" />
</div>
</div>
</div>
</div>
</nav>
);
};
export default Nav;

8
components/PageTitle.tsx Normal file
View File

@ -0,0 +1,8 @@
interface IProps {
title: string;
}
const PageTitle = ({ title }: IProps) => {
return <h1 className="font-mw_sans font-bold text-black md:text-[32px] text-[24px]">{title}</h1>;
};
export default PageTitle;

20
components/PickDate.tsx Normal file
View File

@ -0,0 +1,20 @@
'use client';
import { useState } from 'react';
import { DatePicker } from '@mui/x-date-pickers';
import dayjs from 'dayjs';
const PickDate = () => {
const [date, setDate] = useState<dayjs.Dayjs>(dayjs('2022-05-16'));
return (
<div className="date-picker max-w-[500px] w-full bg-fancy-input font-roboto text-lg font-normal">
<DatePicker
value={date}
onChange={(newValue) => (newValue ? setDate(newValue) : null)}
format="MMM DD, YYYY"
className="w-full bg-transparent"
/>
</div>
);
};
export default PickDate;

9
components/Premium.tsx Normal file
View File

@ -0,0 +1,9 @@
const Premium = () => {
return (
<div className="premium w-24 h-8 bg-[#E20000] rounded-five text-center text-white py-1 px-3 right-2 top-2 absolute z-10">
Premium
</div>
);
};
export default Premium;

52
components/SearchBar.tsx Normal file
View File

@ -0,0 +1,52 @@
'use client';
import { FormEvent, useContext, useState } from 'react';
import { FiSearch } from 'react-icons/fi';
import GlobalContext from '@/context/GlobalContext';
import MaterialsContext from '@/context/MaterialsContext';
const SearchBar = () => {
const { theme } = useContext(GlobalContext).themeContext;
const { params, setParams } = useContext(MaterialsContext);
const [searchValue, setSearchValue] = useState<string>('');
return (
<form
className=" border-2 border-[rgba(139, 218, 255, 0.24)] grid grid-cols-search"
style={{
background:
'linear-gradient(90.91deg, rgba(55, 171, 225, 0.07) 16.09%, rgba(55, 171, 225, 0.19) 115.87%)',
}}
onSubmit={(e: FormEvent) => e.preventDefault()}>
<div>
<input
value={searchValue}
onChange={(e) => {
setParams({ ...params, search: searchValue || '' });
setSearchValue(e.target.value);
}}
type="text"
className="font-roboto py-[10px] px-5 text-[#373737] text-lg border-r border-white w-full h-full bg-transparent outline-none transition-all dark:text-white"
placeholder="Gözlemek"
/>
</div>
<button
className="flex items-center justify-center px-4 py-2"
onClick={() => {
setParams({ ...params, search: searchValue || '' });
}}>
<FiSearch
height={20}
width={20}
color={theme === 'light' ? '#373737' : '#fff'}
style={{ transition: '150ms all cubic-bezier(0.4, 0, 0.2, 1)' }}
className="block w-[22px] h-[22px]"
/>
</button>
</form>
);
};
export default SearchBar;

View File

@ -0,0 +1,39 @@
'use client';
import GlobalContext from '@/context/GlobalContext';
import { useContext } from 'react';
import { SlEye } from 'react-icons/sl';
interface IProps {
title: string;
views?: number;
}
const SectionTitle = ({ title, views }: IProps) => {
const { theme } = useContext(GlobalContext).themeContext;
return (
<div className="section-title flex flex-col gap-[18px] ">
<h2
className="font-aeroport font-bold transition-all dark:text-white md:text-[32px] text-[24px]"
// style={{ fontSize: views ? '38px' : '32px' }}
>
{title}
</h2>
{views ? (
<div className="flex gap-2 items-center">
<SlEye
color={theme === 'light' ? '#494949' : '#fff'}
style={{ transition: '150ms all cubic-bezier(0.4, 0, 0.2, 1)' }}
width={30}
height={18}
className="w-[30px] h-[18px]"
/>
<span className="font-roboto text-lg font-normal text-[#494949] transition-all dark:text-white">
{views}
</span>
</div>
) : null}
</div>
);
};
export default SectionTitle;

48
components/VideoItem.tsx Normal file
View File

@ -0,0 +1,48 @@
import { IVideo } from '@/typings/video.type';
import Image from 'next/image';
import Link from 'next/link';
const VideoItem = ({ img, title, id, slides }: IVideo) => {
return (
<Link
href={`${slides ? '' : 'treasury/'}${id}`}
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>
</Link>
);
};
export default VideoItem;

111
components/VideoList.tsx Normal file
View File

@ -0,0 +1,111 @@
'use client';
import { Swiper, SwiperSlide } from 'swiper/react';
import Loader from './Loader';
import MaterialsContext from '@/context/MaterialsContext';
import MoreBtn from './MoreBtn';
import { Autoplay, Navigation } from 'swiper';
import { Queries } from '@/api/queries';
import VideoItem from './VideoItem';
import { useContext } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { v4 } from 'uuid';
interface IProps {
isSlides?: boolean;
isExtendable?: boolean;
}
const VideoList = ({ isSlides }: IProps) => {
const { params } = useContext(MaterialsContext);
const { search, ...noSearchParams } = params;
const { data, isLoading, isFetchingNextPage, error, hasNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['videos', 'infinite', params ? params : ''],
queryFn: ({ pageParam = 1 }) =>
params.search !== ''
? Queries.getVideos('?' + String(new URLSearchParams({ ...params, page: pageParam })))
: Queries.getVideos(
'?' + String(new URLSearchParams({ ...noSearchParams, page: pageParam })),
),
getNextPageParam: (prevData) =>
prevData.meta.last_page > prevData.meta.current_page
? prevData.meta.current_page + 1
: null,
keepPreviousData: true,
});
if (isLoading) return <Loader height={250} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="video-list flex flex-col gap-8 w-full">
{isSlides ? (
<div className="w-full">
<Swiper
autoHeight
speed={1000}
modules={[Navigation, Autoplay]}
spaceBetween={10}
slidesPerGroup={1}
slidesPerGroupAuto
slidesPerView={'auto'}
navigation
autoplay={{ delay: 3000 }}
loop>
{data ? (
data.pages.flatMap((data) => data.data).length > 0 ? (
data.pages
.flatMap((data) => data.data)
.map((item) => (
<SwiperSlide key={v4()} className="w-fit">
<VideoItem
id={item.id}
img={item.banner_url}
title={item.title}
slides={true}
/>
</SwiperSlide>
))
) : (
<p className="text-lg text-center my-10">No news for the given filters</p>
)
) : null}
</Swiper>
</div>
) : (
<div className="flex flex-col gap-8">
<div className="grid lg:grid-cols-four gap-x-2 gap-y-6 md:grid-cols-3 sm:grid-cols-2 grid-cols-1">
{data ? (
data.pages.flatMap((data) => data.data).length > 0 ? (
data.pages
.flatMap((data) => data.data)
.map((item) => (
<VideoItem
id={item.id}
img={item.banner_url}
title={item.title}
key={v4()}
slides={false}
/>
))
) : (
<p className="text-lg text-center my-10">No news for the given filters</p>
)
) : null}
</div>
<div className="flex justify-center">
<MoreBtn
onClick={() => fetchNextPage()}
disabled={!hasNextPage}
isFetching={isFetchingNextPage}
/>
</div>
</div>
)}
</div>
);
};
export default VideoList;

View File

@ -0,0 +1,90 @@
'use client';
import { Queries } from '@/api/queries';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useState, useEffect } from 'react';
import ReactPlayer from 'react-player';
import Loader from './Loader';
import Image from 'next/image';
import axios from 'axios';
interface IProps {
maxWidth?: string;
maxHeight?: string;
video_id: number;
}
const VideoPlayer = ({ maxHeight, maxWidth, video_id }: IProps) => {
const [hasWindow, setHasWindow] = useState<boolean>(false);
const [hasStartedPlaying, setHasStartedPlaying] = useState<boolean>(false);
useEffect(() => {
if (typeof window !== 'undefined') {
setHasWindow(true);
}
}, []);
const { data, isFetching, error } = useQuery({
queryKey: ['video', `video:${video_id}`],
queryFn: () => Queries.getVideo(video_id),
});
async function addViews() {
return axios.post(`https://turkmentv.gov.tm/v2/api/material/${video_id}/views/increment`);
}
const mutation = useMutation(() => addViews());
const onPlayHandler = () => {
// Check if the video has not started playing before
if (!hasStartedPlaying) {
// Send a POST request to the server
mutation.mutate();
// Update the state to indicate that the video has started playing
setHasStartedPlaying(true);
}
};
if (isFetching) return <Loader height={700} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="w-full h-full ">
{hasWindow ? (
data?.data.content_url.endsWith('.mp4') ? (
<div className="lg:w-[700px] md:w-[550px] w-full h-[200px] sm:h-[250px] md:h-[350px] lg:h-[420px]">
{/* <ReactPlayer
height={'100%'}
width={'100%'}
controls
playsInline
playing={true}
light={data?.data.banner_url}
url={data!.data.video_stream_url}
onStart={() => onPlayHandler()}
/> */}
<video
controls
src={data!.data.video_stream_url}
poster={data?.data.banner_url}
playsInline
itemType="video/mp4"
onPlay={() => onPlayHandler()}></video>
</div>
) : (
<div className="flex flex-col gap-4 h-fit">
<div className="relative lg:w-[700px] md:w-[550px] w-full h-[200px] sm:h-[400px] md:h-[420px]">
{data?.data.banner_url ? (
<Image src={data?.data.banner_url} fill alt={'image'} />
) : null}
</div>
<audio controls className="w-full rounded bg-white" onLoadStart={() => onPlayHandler()}>
<source src={data?.data.content_url} />
</audio>
</div>
)
) : null}
</div>
);
};
export default VideoPlayer;

View File

@ -0,0 +1,67 @@
'use client';
import Step from './Step';
import { v4 } from 'uuid';
import WindowOne from './windows/WindowOne';
import { useContext, useEffect } from 'react';
import StepsContext from '@/context/StepsContext';
import { AnimatePresence } from 'framer-motion';
import WindowTwo from './windows/WindowTwo';
import WindowThree from './windows/WindowThree';
import Calculator from './Calculator';
const AdvertWindow = () => {
const { stepContext, calculatorContext } = useContext(StepsContext);
const { step } = stepContext;
const stepNames = [
'Mahabat ýerleşdirmek üçin 3 ädimi yzarlaň!',
'Mahabat ýerleşdirmek üçin 3 ädimi yzarlaň!',
'Mahabat ýerleşdirmek üçin 3 ädimi yzarlaň!',
];
useEffect(() => {
calculatorContext.calculatorOpen
? (window.document.body.style.overflowY = 'hidden')
: (window.document.body.style.overflowY = 'auto');
}, [calculatorContext.calculatorOpen]);
return (
<div className="advert-window flex flex-col gap-14 py-14 h-full max-w-[1000px] w-full">
<h1 className="font-mw_sans font-bold text-black text-[32px]">
Mahabat ýerleşdirmek üçin 3 ädimi yzarlaň!
</h1>
<div className="bg-white rounded-[10px] border border-solid border-[#D1D1D1] shadow-form md:p-8 p-4 md:block flex items-start gap-6">
<div
className="steps md:grid md:grid-cols-3 md:gap-2 h-full gap-2 flex flex-col justify-between"
// style={{
// gridTemplateColumns: 'repeat(3, 1fr) auto',
// }}
>
{stepNames.map((value, index) => (
<div className="h-full flex flex-col items-center gap-2" key={v4()}>
<Step last={index === 2} name={value} number={(index + 1) as 1 | 2 | 3} key={v4()} />
{index !== 2 ? (
index !== step ? (
<div className="min-h-[100px] md:hidden block h-full w-1 rounded bg-[#08CB9C]"></div>
) : (
<div className="min-h-[100px] md:hidden block h-full w-1 rounded bg-[#b8c4b2]"></div>
)
) : null}
</div>
))}
</div>
<div className="windows overflow-hidden">
<AnimatePresence mode="wait">
{step === 1 && <WindowOne />}
{step === 2 && <WindowTwo />}
{step === 3 && <WindowThree />}
</AnimatePresence>
{calculatorContext.calculatorOpen ? <Calculator /> : null}
</div>
</div>
</div>
);
};
export default AdvertWindow;

View File

@ -0,0 +1,20 @@
import { ButtonHTMLAttributes } from 'react';
interface IProps {
onClick: () => void;
name: string;
type?: ButtonHTMLAttributes<HTMLButtonElement>['type'];
disabled?: boolean;
}
const ButtonPrimary = ({ name, onClick, type, disabled }: IProps) => {
return (
<button
disabled={disabled}
type={type}
onClick={onClick}
className="text-center bg-[#37ABE1] text-white text-lg font-bold font-roboto p-3 rounded-[4px] border border-solid border-[#37ABE1] w-full">
{name}
</button>
);
};
export default ButtonPrimary;

View File

@ -0,0 +1,19 @@
import { ButtonHTMLAttributes } from 'react';
interface IProps {
onClick: () => void;
name: string;
type?: ButtonHTMLAttributes<HTMLButtonElement>['type'];
}
const ButtonSecondary = ({ name, onClick, type = 'button' }: IProps) => {
return (
<button
type={type}
onClick={onClick}
className="text-center text-white text-xs font-bold font-roboto p-3 w-[150px] rounded-[4px] border border-solid border-white">
{name}
</button>
);
};
export default ButtonSecondary;

View File

@ -0,0 +1,110 @@
'use client';
import CustomInput from '../CustomInput';
import { Dispatch, FormEvent, SetStateAction, useContext, useEffect } from 'react';
import StepsContext from '@/context/StepsContext';
import ButtonPrimary from './ButtonPrimary';
import { FormActionType } from '@/providers/StepsProvider';
import Loader from '../Loader';
import { Validator } from '@/utils/validator';
import Image from 'next/image';
import closeIcon from '@/public/close-white.svg';
const Calculator = () => {
const { data, stepContext, calculatorContext } = useContext(StepsContext);
const { plans } = data.plans;
const { form, dispatch } = useContext(StepsContext).addPostContext;
const validateForm = () => {
if (Validator.number(form.second) && Validator.number(form.day) && form.folder_id.length > 0)
return true;
else return false;
};
const calculate = () => {
const price = plans?.data.find((item) => item.id === Number(form.folder_id))?.total_price;
if (price)
dispatch({
type: FormActionType.TOTAL,
payload: String(Math.ceil(Number(price) * (Number(form.second) / 60) * Number(form.day))),
});
};
useEffect(() => {
if (validateForm()) {
calculate();
}
}, [form.folder_id, form.day, form.second]);
return (
<div className="fixed top-0 left-0 w-screen px-5 h-screen bg-[#0000004f] flex items-center justify-center z-50">
{plans ? (
<form
className="bg-[#1a1a1a] px-4 py-6 rounded-lg flex flex-col gap-8 max-w-[480px] h-fit"
onSubmit={(e: FormEvent) => e.preventDefault()}>
<div className="flex justify-between items-start">
<div className="flex flex-col gap-2 font-mw_sans text-white font-[500]">
<h3 className="text-[20px]">Calculator</h3>
<p className="text-base">Sargyt etmek üçin aşakdaky maglumatlary dolduryň</p>
</div>
<div
className="w-[24px] h-[24px] relative p-1 rounded-full flex items-center justify-center cursor-pointer"
onClick={() => calculatorContext.setCalculatorOpen(false)}>
<Image src={closeIcon} fill alt="close" />
</div>
</div>
<div className="flex flex-col gap-4">
<CustomInput
value={{
value: form.second,
setValue: (newValue) =>
dispatch({ type: FormActionType.SECOND, payload: newValue }),
}}
name="second"
label="Näçe sekunt"
type="text"
isBlack
validate={{
errMsg: 'This field must a be number',
isValid: Validator.number(form.second),
}}
/>
<CustomInput
value={{
value: form.day,
setValue: (newValue) => dispatch({ type: FormActionType.DAY, payload: newValue }),
}}
name="day"
label="Näçe gün"
type="text"
isBlack
validate={{
errMsg: 'This field must a be number',
isValid: Validator.number(form.day),
}}
/>
</div>
<div className="text-white text-[22px] flex flex-col">
<p className="font-mw_sans font-[500]">Jemi baha: </p>
<span className="font-redhat w-fit" style={validateForm() ? { background: 'red' } : {}}>
{form.total || 0} TMT
</span>
</div>
<div className="flex gap-3 justify-end">
<ButtonPrimary
disabled={!validateForm()}
name="Indiki"
onClick={() => {
stepContext.setStep(3);
calculatorContext.setCalculatorOpen(false);
}}
type="button"
/>
</div>
</form>
) : (
<Loader height={200} />
)}
</div>
);
};
export default Calculator;

View File

@ -0,0 +1,44 @@
'use client';
import StepsContext from '@/context/StepsContext';
import { useContext } from 'react';
interface IOption {
title: string;
description?: string | null;
id: number;
activeId: number | null;
onClick: (id: number) => void;
}
const Option = ({ title, description, id, onClick, activeId }: IOption) => {
console.log(title);
return (
<div
onClick={() => onClick(id)}
style={id === activeId ? { background: '#121268', color: 'white' } : {}}
className="cursor-pointer overflow-hidden font-roboto leading-[22px] flex flex-col justify-center items-center gap-[12px] max-w-[400px] bg-[#E7E7E7] border-[1px] border-[#BBBBBB] rounded-[5px] px-[12px] py-[24px] hover:border-[#37ABE1] transition-all">
<h2
className="md:min-h-[68px] font-bold text-[22px] md:pb-[22px] pb-4 border-b-[#BBBBBB] border-b-[1px]"
style={
!description
? {
border: 'none',
paddingBottom: 0,
padding: '34px 0',
minHeight: 'unset',
textAlign: 'center',
}
: {}
}>
{title}
</h2>
<p
className="text-[18px] w-full"
style={!description ? { display: 'none' } : {}}
dangerouslySetInnerHTML={{ __html: description || '' }}></p>
</div>
);
};
export default Option;

View File

@ -0,0 +1,5 @@
const SelectForm = () => {
return <div></div>;
};
export default SelectForm;

View File

@ -0,0 +1,50 @@
'use client';
import StepsContext from '@/context/StepsContext';
import { useCallback, useContext, useState } from 'react';
interface IStep {
number: 1 | 2 | 3;
name: string;
last: boolean;
}
const Step = ({ number, name, last }: IStep) => {
const { stepContext } = useContext(StepsContext);
const { step, setStep } = stepContext;
return (
<div
className="step flex md:flex-row md:w-full flex-col items-center h-fit w-fit justify-start"
style={last ? { gridTemplateColumns: '51px 130px' } : {}}>
<div
className="step-circle w-[45px] h-[45px] rounded-full bg-[#131369] relative transition-all flex items-center justify-center"
style={step === number ? { background: ' #131369' } : {}}>
<div
className="step-tick absolute top-0 left-0 bg-step-tick w-full h-full rounded-full opacity-0 transition-all z-10"
style={step !== number ? { opacity: 1 } : {}}></div>
<div
className="absolute top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] rounded-full w-full h-full border border-solid border-[#131369] transition-all pointer-events-none"
style={
step === number ? { width: '51px', height: '51px', opacity: 1 } : { opacity: 0 }
}></div>
<span className="font-roboto font-bold flex items-center justify-center text-2xl text-center text-white transition-all w-[45px] h-[45px]">
{number}
</span>
</div>
<p
className="step-name md:block hidden font-mw_sans font-light text-black ml-2 max-w-[130px]"
onClick={() => setStep(number)}>
{name}
</p>
{!last ? (
<div className="step-line bg-step_line bg-repeat md:block hidden md:w-full h-full w-1 md:h-0.5 overflow-hidden">
<div
className="h-full z-10 w-0 bg-[#08CB9C] transition-all"
style={step !== number ? { width: '100%' } : {}}></div>
</div>
) : null}
</div>
);
};
export default Step;

View File

@ -0,0 +1,96 @@
'use client';
import { FolderTable, PlansModel } from '@/models/plans.model';
import { v4 } from 'uuid';
export interface IColumn {
name: string;
propertyId: string;
}
interface IProps {
columns: FolderTable;
data: FolderTable[];
plan: number;
}
const columnName = {
time: 'Wagt aralygy',
price: 'Bahasy',
set_tv: 'TV',
set_aydym: 'aydym.com',
set_radio: 'Radio',
set_belet: 'Belet',
set_sub: 'Subtitle',
set_web: 'Website',
set_outside_monitors: 'LED',
};
const Table = ({ columns, data, plan }: IProps) => {
const cols = Object.keys(columns);
const gridCols = cols.length;
return (
<div
className="advert-table max-w-[700px] md:w-fit w-full flex flex-col gap-8 rounded-[5px] overflow-hidden border border-solid border-gray-400 pt-6"
style={{
boxShadow: '0px 4.176356792449951px 3.341085195541382px 0px rgba(0, 0, 0, 0.15)',
}}>
<div className="table-inner px-2 pb-2 overflow-auto ">
<div
className="table-cols w-full grid gap-4 border-b border-solid border-black pb-2 content-center items-center"
style={{ gridTemplateColumns: `repeat(${gridCols}, 1fr)` }}>
{cols.map((col, id) => (
<span
className={`font-mw_sans text-[14px] font-bold text-black flex justify-center items-center ${
id === 0 ? 'w-[100px]' : ''
}`}
key={v4()}>
{plan == 3 && columnName[col as keyof typeof columnName] === 'Subtitle'
? 'Flyer'
: columnName[col as keyof typeof columnName]}
</span>
))}
</div>
<div className="flex flex-col w-full gap-2 max-h-[480px] overflow-y-auto">
{data.map((item, index) => {
const keys = Object.keys(item);
return index % 2 === 0 ? (
<div
key={v4()}
className="table-data grid gap-4 pt-2 border-b border-solid border-[#DAD8D8] pb-2 content-center items-center bg-[#f9f9f9]"
style={{ gridTemplateColumns: `repeat(${gridCols}, 1fr)` }}>
{keys.map((key, id) => (
<span
className={`font-mw_sans text-[14px] font-light text-black flex justify-center items-center ${
id === 0 ? 'w-[100px]' : ''
}`}
key={v4()}>
{item[key as keyof typeof item]}
</span>
))}
</div>
) : (
<div
key={v4()}
className="table-data grid gap-4 pt-2 border-b border-solid border-[#DAD8D8] pb-2 content-center items-center"
style={{ gridTemplateColumns: `repeat(${gridCols}, 1fr)` }}>
{keys.map((key, id) => (
<span
className={`font-mw_sans text-[14px] font-light text-black flex justify-center items-center ${
id === 0 ? 'w-[100px]' : ''
}`}
key={v4()}>
{item[key as keyof typeof item]}
</span>
))}
</div>
);
})}
</div>
</div>
</div>
);
};
export default Table;

View File

@ -0,0 +1,157 @@
'use client';
import Table from '../Table';
import { v4 } from 'uuid';
import PresenceAnimator from '../../hox/PresenceAnimator';
import Option from '../Option';
import { useContext, useEffect, useState } from 'react';
import StepsContext from '@/context/StepsContext';
import { AnimatePresence } from 'framer-motion';
import { FormActionType } from '@/providers/StepsProvider';
import { PlansModel } from '@/models/plans.model';
import Button from '@/components/Button';
import Link from 'next/link';
const Plan = () => {
const { data, plansContext, addPostContext, calculatorContext } = useContext(StepsContext);
const { plan } = plansContext;
const { plans } = data.plans;
const { dispatch, form } = addPostContext;
const [folder, setFolder] = useState<PlansModel['data'][any]>();
useEffect(() => {
if (form.folder_id !== '' && plans) {
const newFolder = plans.data.find((item) => String(item.id) === form.folder_id);
if (newFolder) {
setFolder(newFolder);
}
}
}, [form.folder_id, plans]);
// useEffect(() => {
// calculatorOpen
// ? (window.document.body.style.overflowY = 'hidden')
// : (window.document.body.style.overflowY = 'auto');
// }, [calculatorOpen]);
const definePlan = (plan: number | null) => {
if (!plan) return;
switch (plan) {
case 1:
return 'on_tv';
case 2:
return 'on_radio';
case 3:
return 'on_subtitle';
case 4:
return 'on_web';
case 5:
return 'on_outside_monitors';
default:
return '';
}
};
if (!plans) return null;
if (!plan) return null;
return (
<div className="flex flex-col gap-6">
<PresenceAnimator
initial={{ x: '100%', opacity: '0%' }}
animate={{
x: '0%',
opacity: '100%',
transition: { duration: 0.5 },
}}
exit={{ x: '-100%', opacity: '0%', transition: { duration: 0.5 } }}>
<div className="flex flex-col gap-6">
<h4 className="text-black text-[22px] font-redhat font-bold pt-6 border-t border-solid border-[#E7E7E7]">
Mahabat görnüşini we bukjasyny saýlaň!
</h4>
<div className="md:grid md:grid-cols-4 flex flex-col gap-4">
{/* {plans.data.map((item) =>
item[definePlan(plan) as keyof (typeof plans)['data'][any]] ? (
<Option
activeId={Number(form.folder_id)}
id={item.id}
onClick={(id) =>
dispatch({
type: FormActionType.FOLDER,
payload: String(id),
})
}
title={item.title}
description={item.description}
key={v4()}
/>
) : null,
)} */}
{plans.data.map((item) =>
item.usage_name === definePlan(plan) ? (
<Option
activeId={Number(form.folder_id)}
id={item.id}
onClick={(id) =>
dispatch({
type: FormActionType.FOLDER,
payload: String(id),
})
}
title={item.title}
description={item.description}
key={v4()}
/>
) : null,
)}
</div>
{definePlan(plan) === 'on_outside_monitors' ? (
<Link
className="text-lg text-blue-500 font-bold underline"
href={
'https://www.google.com/maps/d/viewer?mid=1h-OonWMZ0EPNKYHMNvC5IPLMTERL_Gc&ll=37.94468794917295%2C58.377513099999994&z=12'
}
target="_blank">
LED monitorlaryň kartasy
</Link>
) : null}
{form.folder_id !== '' && folder ? (
<PresenceAnimator
initial={{ x: '100%', opacity: '0%' }}
animate={{
x: '0%',
opacity: '100%',
transition: { duration: 0.5 },
}}
exit={{
x: '-100%',
opacity: '0%',
transition: { duration: 0.5 },
}}>
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-6">
<Table
columns={folder.folder_table[0]}
plan={plan}
data={folder.folder_table}
key={v4()}
/>
</div>
<Button
onClick={() => calculatorContext.setCalculatorOpen(true)}
name="Indiki"
type="button"
disabled={calculatorContext.calculatorOpen ? true : false}
/>
</div>
</PresenceAnimator>
) : null}
</div>
</PresenceAnimator>
</div>
);
};
export default Plan;

View File

@ -0,0 +1,58 @@
'use client';
import { PlansModel } from '@/models/plans.model';
import Table from '../Table';
import { v4 } from 'uuid';
import PresenceAnimator from '../../hox/PresenceAnimator';
import Option from '../Option';
import { useState } from 'react';
interface IProps {
plans: PlansModel;
}
const PlanFour = ({ plans }: IProps) => {
const [planItem, setPlanItem] = useState<number | null>(null);
return (
<div className="flex flex-col gap-6">
<PresenceAnimator
initial={{ x: '100%', opacity: '0%' }}
animate={{
x: '0%',
opacity: '100%',
transition: { duration: 0.5 },
}}
exit={{ x: '-100%', opacity: '0%', transition: { duration: 0.5 } }}>
<h4 className="text-black text-[22px] font-roboto font-bold pt-6 border-t border-solid border-[#E7E7E7]">
Mahabat görnüşini we bukjasyny saýlaň!
</h4>
<div className="grid grid-cols-4 gap-4">
{plans.data.map((item) =>
item.on_web ? (
<Option
activeId={planItem}
id={item.id}
onClick={(id) => setPlanItem(id)}
title={item.title}
description={item.description}
key={v4()}
/>
) : null,
)}
</div>
{/* {plans.data.map((plan) =>
plan.on_web ? (
<Table
columns={plan.folder_table[0]}
data={plan.folder_table}
description={plan.description}
title={plan.title}
key={v4()}
/>
) : null
)} */}
</PresenceAnimator>
</div>
);
};
export default PlanFour;

View File

@ -0,0 +1,58 @@
'use client';
import { PlansModel } from '@/models/plans.model';
import Table from '../Table';
import { v4 } from 'uuid';
import PresenceAnimator from '../../hox/PresenceAnimator';
import Option from '../Option';
import { useState } from 'react';
interface IProps {
plans: PlansModel;
}
const PlanThree = ({ plans }: IProps) => {
const [planItem, setPlanItem] = useState<number | null>(null);
return (
<div className="flex flex-col gap-6">
<PresenceAnimator
initial={{ x: '100%', opacity: '0%' }}
animate={{
x: '0%',
opacity: '100%',
transition: { duration: 0.5 },
}}
exit={{ x: '-100%', opacity: '0%', transition: { duration: 0.5 } }}>
<h4 className="text-black text-[22px] font-roboto font-bold pt-6 border-t border-solid border-[#E7E7E7]">
Mahabat görnüşini we bukjasyny saýlaň!
</h4>
<div className="grid grid-cols-4 gap-4">
{plans.data.map((item) =>
item.on_subtitle ? (
<Option
activeId={planItem}
id={item.id}
onClick={(id) => setPlanItem(id)}
title={item.title}
description={item.description}
key={v4()}
/>
) : null,
)}
</div>
{/* {plans.data.map((plan) =>
plan.on_subtitle ? (
<Table
columns={plan.folder_table[0]}
data={plan.folder_table}
description={plan.description}
title={plan.title}
key={v4()}
/>
) : null
)} */}
</PresenceAnimator>
</div>
);
};
export default PlanThree;

View File

@ -0,0 +1,58 @@
'use client';
import { PlansModel } from '@/models/plans.model';
import Table from '../Table';
import { v4 } from 'uuid';
import PresenceAnimator from '../../hox/PresenceAnimator';
import Option from '../Option';
import { useState } from 'react';
interface IProps {
plans: PlansModel;
}
const PlanTwo = ({ plans }: IProps) => {
const [planItem, setPlanItem] = useState<number | null>(null);
return (
<div className="flex flex-col gap-6">
<PresenceAnimator
initial={{ x: '100%', opacity: '0%' }}
animate={{
x: '0%',
opacity: '100%',
transition: { duration: 0.5 },
}}
exit={{ x: '-100%', opacity: '0%', transition: { duration: 0.5 } }}>
<h4 className="text-black text-[22px] font-roboto font-bold pt-6 border-t border-solid border-[#E7E7E7]">
Mahabat görnüşini we bukjasyny saýlaň!
</h4>
<div className="grid grid-cols-4 gap-4">
{plans.data.map((item) =>
item.on_radio ? (
<Option
activeId={planItem}
id={item.id}
onClick={(id) => setPlanItem(id)}
title={item.title}
description={item.description}
key={v4()}
/>
) : null,
)}
</div>
{/* {plans.data.map((plan) =>
plan.on_radio ? (
<Table
columns={plan.folder_table[0]}
data={plan.folder_table}
description={plan.description}
title={plan.title}
key={v4()}
/>
) : null
)} */}
</PresenceAnimator>
</div>
);
};
export default PlanTwo;

View File

@ -0,0 +1,55 @@
'use client';
import { Queries } from '@/api/queries';
import Loader from '@/components/Loader';
import { useContext, useEffect } from 'react';
import Option from '../Option';
import { v4 } from 'uuid';
import Button from '@/components/Button';
import StepsContext from '@/context/StepsContext';
import PresenceAnimator from '@/components/hox/PresenceAnimator';
const WindowOne = () => {
const ctx = useContext(StepsContext);
const { stepContext, propertyContext, data } = ctx;
const { properties } = data;
useEffect(() => {
Queries.getProperties().then((data) => properties.setProperties(data));
}, []);
if (!properties.properties) return <Loader height={200} />;
return (
<PresenceAnimator
initial={{ x: '100%', opacity: '0%' }}
animate={{ x: '0%', opacity: '100%', transition: { duration: 0.5 } }}
exit={{ x: '-100%', opacity: '0%', transition: { duration: 0.5 } }}>
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-[24px] mt-[36px]">
<h2 className="font-mw_sans font-bold md:text-[22px] text-lg leading-[24px]">
Eýeçiligiň görnüşini saýlaň!
</h2>
<div className="flex md:flex-row flex-col gap-[16px]">
{properties.properties.data.map((option) => (
<Option
onClick={(id) => propertyContext.setProperty(id)}
activeId={propertyContext.property}
id={option.id}
title={option.title}
description={option.description}
key={v4()}
/>
))}
</div>
</div>
<Button
onClick={() => stepContext.setStep(2)}
name="Indiki"
type="button"
disabled={!propertyContext.property ? true : false}
/>
</div>
</PresenceAnimator>
);
};
export default WindowOne;

View File

@ -0,0 +1,130 @@
'use client';
import { useRouter } from 'next/navigation';
import PresenceAnimator from '@/components/hox/PresenceAnimator';
import CustomInput from '@/components/CustomInput';
import { FormEvent, useContext, useState } from 'react';
import StepsContext from '@/context/StepsContext';
import { FormActionType } from '@/providers/StepsProvider';
import { Queries } from '@/api/queries';
import Button from '@/components/Button';
import Loader from '@/components/Loader';
import { Validator } from '@/utils/validator';
const WindowThree = () => {
const { form, dispatch } = useContext(StepsContext).addPostContext;
const [loading, setLoading] = useState<boolean | 'error'>(false);
const router = useRouter();
const validateForm = () => {
if (
Validator.email(form.customer_email) &&
Validator.isNotEmpty(form.customer_name) &&
Validator.phone(form.customer_phone)
)
return true;
else return false;
};
if (loading == true) return <Loader height={400} />;
return (
<>
<PresenceAnimator
initial={{ x: '100%', opacity: '0%' }}
animate={{ x: '0%', opacity: '100%', transition: { duration: 0.5 } }}
exit={{ x: '-100%', opacity: '0%', transition: { duration: 0.5 } }}>
<div className="flex flex-col gap-10">
<form
className="flex flex-col gap-6"
onSubmit={async (e: FormEvent) => {
e.preventDefault();
await Queries.postAdvert(form);
}}>
<h2 className="font-mw_sans font-[500] leading-[100%] md:text-[22px] text-lg text-black">
Bukjany sargyt etmek üçin aşakdaky maglumatlary dolduryň
</h2>
<CustomInput
value={{
value: form.customer_name,
setValue: (newValue) => dispatch({ type: FormActionType.NAME, payload: newValue }),
}}
name="name"
label="Ady"
placeholder="Sargyt edijiniň ady ýa-da familiýasy"
type="text"
validate={{
errMsg: 'Name is required',
isValid: Validator.isNotEmpty(form.customer_name),
}}
/>
<CustomInput
value={{
value: form.customer_phone,
setValue: (newValue) => dispatch({ type: FormActionType.PHONE, payload: newValue }),
}}
name="phone"
label="Telefon"
placeholder="+993 61234567"
type="text"
validate={{
errMsg: 'Invalid phone number',
isValid: Validator.phone(form.customer_phone),
}}
/>
<CustomInput
value={{
value: form.customer_phone,
setValue: (newValue) => dispatch({ type: FormActionType.EMAIL, payload: newValue }),
}}
name="email"
label="E-mail"
placeholder="meselem@gmail.com"
type="email"
validate={{
errMsg: 'Invalid email address',
isValid: Validator.email(form.customer_email),
}}
/>
<CustomInput
value={{
value: form.customer_notes,
setValue: (newValue) => dispatch({ type: FormActionType.NOTES, payload: newValue }),
}}
isTextArea
name="notes"
label="Goşmaça bellikler"
placeholder="Mahabat etmak isleäýän zadyňyzy barada şu ýerik ýazyň..."
type="text"
/>
<p className="text-lg font-mw_sans leading-[100%] text-mlightblue">
Jikme-jiklikleri bize{' '}
<a href="mailto:mahabat@turkmen.gov.tm" className="underline">
mahabat@turkmen.gov.tm
</a>{' '}
adrese iberiň
</p>
</form>
<Button
onClick={() => {
setLoading(true);
Queries.postAdvert(form)
.then((res) => {
if (res.status === 201) {
router.push('/success');
}
})
.catch(() => {
router.push('/failed');
});
}}
name="Indiki"
type="button"
disabled={!validateForm()}
/>
</div>
</PresenceAnimator>
</>
);
};
export default WindowThree;

View File

@ -0,0 +1,101 @@
'use client';
import { Queries } from '@/api/queries';
import Loader from '@/components/Loader';
import { useContext, useEffect, useState } from 'react';
import Option from '../Option';
import { v4 } from 'uuid';
import Button from '@/components/Button';
import StepsContext from '@/context/StepsContext';
import PresenceAnimator from '@/components/hox/PresenceAnimator';
import { PlansModel } from '@/models/plans.model';
// import PlanOne from "../plans/PlanOne";
// import PlanTwo from "../plans/PlanTwo";
// import PlanThree from "../plans/PlanThree";
// import PlanFour from "../plans/PlanFour";
import { AnimatePresence } from 'framer-motion';
import Plan from '../plans/Plan';
const WindowTwo = () => {
const planTypes = [
{
id: 1,
name: 'TV',
property: 'on_tv',
},
{
id: 2,
name: 'Radio',
property: 'on_radio',
},
{
id: 3,
name: 'Air ticket flyer',
property: 'on_sub',
},
{
id: 4,
name: 'Website',
property: 'on_web',
},
{
id: 5,
name: 'Outdoor LED',
property: 'on_outside_monitors',
},
];
const ctx = useContext(StepsContext);
const { stepContext, propertyContext, data } = ctx;
const { plans } = data;
const { plan, setPlan } = useContext(StepsContext).plansContext;
useEffect(() => {
Queries.getPlans(propertyContext.property!).then((data) => plans.setPlans(data));
}, [propertyContext.property]);
useEffect(() => {}, []);
// console.log('Plans', plans);
// console.log('Plan', plan);
if (!plans.plans) return <Loader height={200} />;
return (
<PresenceAnimator
initial={{ x: '100%', opacity: '0%' }}
animate={{
x: '0%',
opacity: '100%',
transition: { duration: 0.5, delay: 0.6 },
}}
exit={{ x: '-100%', opacity: '0%', transition: { duration: 0.5 } }}>
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-[24px] mt-[36px]">
<h2 className="font-mw_sans font-bold md:text-[22px] text-lg leading-[24px]">
Mahabat görnüşini we bukjasyny saýlaň!
</h2>
<div className="md:grid md:grid-cols-5 flex flex-col gap-[8px]">
{planTypes.map((option) => (
<Option
onClick={(id) => setPlan(id)}
activeId={plan}
id={option.id}
title={option.name}
key={v4()}
/>
// <Option
// onClick={() => setPlan(option.id)}
// activeId={plan}
// id={option.id}
// title={option.name}
// key={v4()}
// />
))}
</div>
</div>
<Plan />
</div>
</PresenceAnimator>
);
};
export default WindowTwo;

View File

@ -0,0 +1,53 @@
'use client';
import { AiTwotoneEyeInvisible } from 'react-icons/ai';
import { AiTwotoneEye } from 'react-icons/ai';
import { useState } from 'react';
import { IAuthInput } from '@/typings/auth-input.type';
const AuthInput = ({ id, placeholder, type, required, label }: IAuthInput) => {
const [isVisible, setIsVisible] = useState<boolean>(false);
return (
<div className="auth-input relative w-full">
{label ? <label htmlFor={id}>{label}</label> : null}
<input
type={type === 'password' ? (isVisible ? 'text' : type) : type}
required={required}
name={id}
id={id}
placeholder={placeholder}
className="text-[#474747] font-mw_sans font-normal text-base bg-[#F5F5F5] rounded-[5px] p-4 w-full outline-none"
/>
{type === 'password' ? (
<>
<AiTwotoneEye
color="#474747"
width={20}
height={20}
className="w-5 h-5 absolute right-5 top-[50%] -translate-y-[50%] transition-all opacity-100 cursor-pointer"
style={
isVisible
? { opacity: 1, pointerEvents: 'all' }
: { opacity: 0, pointerEvents: 'none' }
}
onClick={() => setIsVisible(!isVisible)}
/>
<AiTwotoneEyeInvisible
color="#474747"
width={20}
height={20}
className="w-5 h-5 absolute right-5 top-[50%] -translate-y-[50%] transition-all opacity-100 cursor-pointer"
style={
!isVisible
? { opacity: 1, pointerEvents: 'all' }
: { opacity: 0, pointerEvents: 'none' }
}
onClick={() => setIsVisible(!isVisible)}
/>
</>
) : null}
</div>
);
};
export default AuthInput;

View File

@ -0,0 +1,30 @@
'use client';
import Link from 'next/link';
import { FormEvent } from 'react';
import AuthInput from './AuthInput';
import Button from '../Button';
const LoginForm = () => {
return (
<div className="py-10">
<div className="login max-w-[455px] w-full h-fit rounded-[15px] shadow-form bg-white py-12 px-11 flex flex-col gap-10">
<h1 className="text-[32px] font-lighter text-black font-mw_sans text-center uppercase">
Hasabyňyza giriň
</h1>
<form onSubmit={(e: FormEvent) => e.preventDefault()} className="flex flex-col gap-4">
<AuthInput id="tel_mail" placeholder="Telefon ýa-da Email" type="text" required />
<AuthInput id="password" placeholder="Açar söz" type="password" required />
<Button name="Hasap aç" type="submit" width="100%" onClick={() => null} />
</form>
<Link
href={'/auth/recovery'}
className="font-roboto text-base text-black flex justify-center">
<span className="border-b-2 border-black border-solid">Açar sözi unytdym!</span>
</Link>
</div>
</div>
);
};
export default LoginForm;

View File

@ -0,0 +1,29 @@
'use client';
import Link from 'next/link';
import { FormEvent } from 'react';
import AuthInput from './AuthInput';
import Button from '../Button';
const LoginForm = () => {
return (
<div className="py-10">
<div className="login max-w-[455px] w-full h-fit rounded-[15px] shadow-form bg-white py-12 px-11 flex flex-col gap-10">
<h1 className="text-[32px] font-lighter text-black font-mw_sans text-center uppercase">
Hasabyňyza giriň
</h1>
<form onSubmit={(e: FormEvent) => e.preventDefault()} className="flex flex-col gap-4">
<AuthInput id="email" placeholder="Emailiňizi ýazyň" type="email" required />
<Button name="Hasap aç" type="submit" width="100%" onClick={() => null} />
</form>
<Link
href={'/auth/signup'}
className="font-roboto text-base text-black flex justify-center">
<span className="border-b-2 border-black border-solid">Saýtda ilk gezek girýäňizmi?</span>
</Link>
</div>
</div>
);
};
export default LoginForm;

View File

@ -0,0 +1,37 @@
'use client';
import Link from 'next/link';
import { FormEvent } from 'react';
import AuthInput from './AuthInput';
import Button from '../Button';
const LoginForm = () => {
return (
<div className="py-10">
<div className="login max-w-[455px] w-full h-fit rounded-[15px] shadow-form bg-white py-12 px-11 flex flex-col gap-10">
<h1 className="text-[32px] font-lighter text-black font-mw_sans text-center uppercase">
Hasabyňyza giriň
</h1>
<form onSubmit={(e: FormEvent) => e.preventDefault()} className="flex flex-col gap-4">
<AuthInput id="name" placeholder="Ady" type="text" required />
<AuthInput id="lastname" placeholder="Familiýasy" type="text" required />
<AuthInput id="email" placeholder="Email" type="email" required />
<AuthInput id="phone" placeholder="Telefon" type="text" required />
<AuthInput id="password" placeholder="Açar söz" type="password" required />
<AuthInput
id="password_confirmation"
placeholder="Açar söz gaýtala"
type="password"
required
/>
<Button name="Ýazyl" type="submit" width="100%" onClick={() => null} />
</form>
<Link href={'/auth/login'} className="font-roboto text-base text-black flex justify-center">
<span className="border-b-2 border-black border-solid">Hasabyňyz barmy? Içeri gir.</span>
</Link>
</div>
</div>
);
};
export default LoginForm;

View File

@ -0,0 +1,22 @@
'use client';
import Button from '../Button';
interface Props {
folder: number;
days: number;
price: number;
}
const TarifWindow = ({ folder, days, price }: Props) => {
return (
<div className="min-w-[394px] pb-[16px] pt-[56px] px-[16px] flex flex-col items-center bg-white text-black gap-[62px] rounded-2xl">
<div className="flex items-center flex-col gap-[32px] pb-[22px] border-b border-b-[#CAD2DA]">
<h3 className="text-3xl">{`BUKJA ${folder}`}</h3>
<h2 className="text-[#FFAB48] font-bold text-4xl">{`${days} GÜN - ${price} TMT`}</h2>
</div>
<Button name="Satyn al" type="button" onClick={() => null} width="100%" />
</div>
);
};
export default TarifWindow;

View File

@ -0,0 +1,63 @@
'use client';
import CustomInput from '@/components/CustomInput';
import Button from '../Button';
import { FormEvent, useState } from 'react';
const ContactForm = () => {
const [input, setInput] = useState({
name: '',
email: '',
message: '',
topic: '',
});
return (
<form
className="custom-input bg-white md:py-12 py-6 md:px-3 px-6 rounded-[10px] border border-solid transition-all border-[#D1D1D1] flex flex-col md:gap-9 gap-6 shadow-form dark:border-0 dark:shadow-none dark:bg-dark"
onSubmit={(e: FormEvent) => e.preventDefault()}>
<CustomInput
value={{
value: input.name,
setValue: (value) => setInput({ ...input, name: value }),
}}
name="name"
label="Ady"
placeholder="Aman Amanow"
type="text"
/>
<CustomInput
value={{
value: input.email,
setValue: (value) => setInput({ ...input, email: value }),
}}
name="email"
label="E-mail"
placeholder="meselem@gmail.com"
type="email"
/>
<CustomInput
value={{
value: input.message,
setValue: (value) => setInput({ ...input, message: value }),
}}
name="message"
label="Hat"
placeholder="Hat"
isTextArea
/>
<CustomInput
value={{
value: input.topic,
setValue: (value) => setInput({ ...input, topic: value }),
}}
name="topic"
label="Mowzuk"
placeholder="Mowzuk"
type="text"
/>
{/* <CustomInput name="img" label="Surat" type="file" accept="image/*" /> */}
<Button name="Ugrat" onClick={() => console.log('click')} type="submit" />
</form>
);
};
export default ContactForm;

View File

@ -0,0 +1,13 @@
import ContactDetails from '../ContactDetails';
import Map from '../Map';
const ContactMap = () => {
return (
<div className="contact-map flex md:flex-row flex-col gap-9 border-t border-t-[#BBBBBB] pt-11 mt-14 dark:border-dark transition-all">
<ContactDetails />
<Map />
</div>
);
};
export default ContactMap;

View File

@ -0,0 +1,115 @@
'use client';
import { useContext, useEffect, useState } from 'react';
import { FitlerNumber } from '@/components/table/FitlerNumber';
import { Search } from '@/components/table/Search';
import { Sort } from '@/components/table/Sort';
import { Inter } from 'next/font/google';
import { AuthContext } from '@/context/AuthContext';
import ProtectedRoute from '@/components/sms/ProtectedRoute';
import { setDate } from 'date-fns';
import { Calendar } from 'lucide-react';
import { SmsContext } from '@/context/SmsContext';
import SmsTable from '@/components/sms/smsTable/SmsTable';
import Loader from '@/components/Loader';
import GradientTitle from '@/components/vote/GradientTitle';
import FilterTable from './FilterTable';
// import FilterTable from './FilterTable';
const inter = Inter({
weight: ['400', '600', '700', '500'],
subsets: ['cyrillic', 'latin'],
});
const Dashboard = () => {
const authContext = useContext(AuthContext);
if (!authContext) {
throw new Error('AuthContext must be used within an AuthProvider');
}
const { logout, userIsLoading } = authContext;
const smsContext = useContext(SmsContext);
if (!smsContext) {
throw new Error('smsContext must be used within an AuthProvider');
}
const { setCurrentPage, isError, smsTableData, currentPage } = smsContext;
useEffect(() => {
window.scrollTo(0, 0);
}, [smsTableData, currentPage]);
if (isError) {
return (
<main>
<div className="container flex flex-col justify-center items-center h-screen">
<h1
className={`text-[44px] sm:text-[80px] leading-[100%] font-bold bg-fancyTitle bg-clip-text text-transparent text-center `}>
Gysga belgi birikdirilmedi
</h1>
<button
onClick={() => {
logout();
setCurrentPage(1);
}}
type="submit"
className="p-3 bg-[#7A7ACC] text-[18px] leading-[150%] font-medium text-white w-[200px] rounded-xl mt-[30px]">
Yza
</button>
</div>
</main>
);
}
if (userIsLoading) {
return (
<main>
<div className="container flex justify-center items-center h-screen">
<Loader />
</div>
</main>
);
}
return (
<ProtectedRoute>
<div className={`pt-[100px] pb-[200px] ${inter.className}`}>
<div className="container">
<div className="flex gap-[40px]">
<div className="flex flex-col gap-[32px]">
<FitlerNumber />
<span
onClick={() => {
logout();
setCurrentPage(1);
}}
className="text-textLight text-[16px] leading-[140%] font-semibold cursor-pointer w-full py-2">
Ulgamdan çykmak
</span>
</div>
<div className="flex flex-col w-full gap-5">
<FilterTable />
<SmsTable />
</div>
{/* <div className="flex flex-col gap-5 w-full">
<div className="flex gap-8 items-end justify-end w-full">
<Sort />
<Search />
</div>
<Calendar
mode="single"
// selected={date}
// onSelect={setDate}
className="rounded-md border"
/>
</div> */}
</div>
</div>
</div>
</ProtectedRoute>
);
};
export default Dashboard;

View File

@ -0,0 +1,214 @@
'use client';
import clsx from 'clsx';
import { AnimatePresence, motion } from 'framer-motion';
import { MouseEvent, useContext, useEffect, useRef, useState } from 'react';
import { Calendar } from '../ui/calendar';
import { format } from 'date-fns';
import { SmsContext } from '@/context/SmsContext';
import Image from 'next/image';
import { useOnClickOutside } from 'usehooks-ts';
import { SlSocialGithub } from 'react-icons/sl';
export const sortData = [
{
title: 'Newest',
id: 'desc',
},
{
title: 'Oldest',
id: 'asc',
},
];
const FilterTable = () => {
const smsContext = useContext(SmsContext);
if (!smsContext) {
throw new Error('smsContext must be used within an AuthProvider');
}
const {
datee,
setDatee,
setActiveSort,
activeSort,
searchValue,
setSearchValue,
setSearchFecth,
searchFecth,
timeDate,
setTimeDate,
smsTableData,
} = smsContext;
const calendarRef = useRef<HTMLDivElement>(null);
const [calendar, setCalendar] = useState(false);
const [filterActive, setFilterActive] = useState(false);
const handleClickOutside = () => {
setCalendar(false);
};
useOnClickOutside(calendarRef, handleClickOutside);
const checkSortActive = () => {
if (activeSort === 'asc' || datee || searchFecth) {
setFilterActive(true);
} else {
setFilterActive(false);
}
};
const resetFilters = () => {
setActiveSort('desc');
setDatee(undefined);
setSearchFecth('');
setSearchValue('');
};
const resetSearch = () => {
setSearchFecth('');
setSearchValue('');
};
useEffect(() => {
checkSortActive();
}, [activeSort, datee, searchFecth]);
return (
<div className="">
<div className="flex gap-6 w-full justify-between items-start">
<div className="flex flex-col gap-[16px]">
<div className="bg-[#F0F0FA] rounded-full flex gap-6 shadow-tableShadow">
<div className="flex items-center pr-[24px] w-fit gap-[24px]">
<div
className={clsx(
'leading-[115%] text-[14px] py-[16px] px-[24px] bg-[#E1E1F5] text-black font-semibold rounded-l-full',
{},
)}>
Filtr
</div>
{sortData.map((item, i) => (
<div
key={i}
onClick={() => {
setActiveSort(item.id);
}}
className={clsx('leading-[115%] text-[14px] cursor-pointer', {
'text-fillButtonAccentDefault font-[600]': activeSort === item.id,
'text-textDarkt': activeSort !== item.id,
})}>
{item.title}
</div>
))}
<div className="relative">
<div
onClick={() => setCalendar((prev) => !prev)}
className={clsx(
'leading-[115%] text-[14px] text-textDarkt cursor-pointer w-fit ',
{},
)}>
Kalendar
</div>
<AnimatePresence>
{calendar && (
<motion.div
initial={{ opacity: 0, y: -20, pointerEvents: 'none' }}
animate={{ opacity: 1, y: 0, pointerEvents: 'all' }}
exit={{ opacity: 0, y: -20, pointerEvents: 'none' }}
transition={{ ease: 'easeInOut' }}
ref={calendarRef}
className="absolute top-5">
<Calendar
mode="single"
selected={datee}
onSelect={setDatee}
initialFocus
onDayClick={() => setCalendar(false)}
className={clsx(
'my-20 bg-[#F5F5FA] w-fit rounded-[8px] -translate-y-[50px] -translate-x-[60px] shadow-[0_2px_32px_rgba(0,0,0,0.3)] transition-all',
)}
/>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
{datee && smsTableData?.data.length !== 0 && (
<input
type="text"
placeholder="Sagat boýunça gözle... 18:1"
onChange={(e) => setTimeDate(e.target.value)}
value={timeDate}
className="w-full outline-none px-6 py-3 shadow-tableShadow bg-[#F0F0FA] rounded-full"
/>
)}
{filterActive ? (
<div
className="cursor-pointer h-fit flex items-center gap-[8px] stroke-textGray hover:stroke-fillNavyBlue text-textDarkt hover:text-fillNavyBlue"
onClick={resetFilters}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="transition-all ease-out duration-[0.3s]">
<path
d="M16 16L12 12M12 12L8 8M12 12L16 8M12 12L8 16"
stroke="current"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
)
</svg>
<h1 className=" text-[14px] font-medium w-fit">Filteri aýyr</h1>
</div>
) : null}
</div>
<div className="flex shadow-tableShadow rounded-full w-fit">
<div className="flex items-center justify-between bg-[#F0F0FA] w-fit py-3 px-4 rounded-[9999px_0_0_9999px] ">
<input
type="text"
placeholder="Gözle..."
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
className="w-full bg-transparent outline-none"
/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="cursor-pointer stroke-textGray hover:stroke-fillNavyBlue transition-all ease-out duration-[0.3s]"
onClick={resetSearch}>
{searchFecth ? (
<path
d="M16 16L12 12M12 12L8 8M12 12L16 8M12 12L8 16"
stroke="current"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
) : null}
</svg>
</div>
<div
onClick={() => setSearchFecth(searchValue)}
className="bg-fillButtonAccentDefault rounded-[0_9999px_9999px_0] px-4 py-[12px] cursor-pointer">
<Image src={'/search.svg'} alt="search" width={24} height={24} />
</div>
</div>
</div>
</div>
);
};
export default FilterTable;

View File

@ -0,0 +1,20 @@
import Link from 'next/link';
interface IProps {
href: string;
color: string;
content: string;
}
const LinkBlock = ({ color, href, content }: IProps) => {
return (
<Link
href={href}
className="flex items-center justify-center p-4 w-full lg:h-full h-[200px] font-aeroport font-bold md:text-[34px] text-[26px] text-white text-center"
style={{ backgroundColor: color }}>
{content}
</Link>
);
};
export default LinkBlock;

View File

@ -0,0 +1,52 @@
'use client';
import Image from 'next/image';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper';
import { Queries } from '@/api/queries';
import { useQuery } from '@tanstack/react-query';
import Loader from '../Loader';
import { v4 } from 'uuid';
import Link from 'next/link';
const MainSwiper = () => {
const { data, isFetching, error } = useQuery({
queryKey: ['home'],
queryFn: () => Queries.getHome(),
});
if (isFetching) return <Loader height={'100%'} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="big-swiper ">
<Swiper
modules={[Navigation, Pagination, Autoplay]}
slidesPerView={1}
navigation
autoplay={{ delay: 3200 }}
speed={700}
loop
pagination={{ clickable: true }}
className="h-full w-full">
{data!.data.map((item) => (
<SwiperSlide key={v4()} className="w-full h-full">
<Link
href={item.url ? item.url : ''}
className="flex justify-center items-center relative w-full lg:h-full h-[400px]">
<Image
src={item.image}
alt={item.title}
fill
unoptimized
unselectable="off"
className="pointer-events-none "
/>
</Link>
</SwiperSlide>
))}
</Swiper>
</div>
);
};
export default MainSwiper;

View File

@ -0,0 +1,45 @@
'use client';
import { Queries } from '@/api/queries';
import { useQuery } from '@tanstack/react-query';
import Marquee from 'react-fast-marquee';
import Loader from '../Loader';
import { v4 } from 'uuid';
import channels from '@/channels';
const Marque = () => {
const { data, isFetching, error } = useQuery({
queryKey: ['marquee'],
queryFn: () => Queries.getMarquee(),
});
if (isFetching) return <Loader height={'100%'} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return data?.data ? (
<div className="flex w-full items-center z-50 lg:fixed lg:bottom-0 lg:left-0">
{data.data.length === 0 ? null : (
<div className="relative w-full flex">
<div className="w-fit absolute top-[50%] translate-y-[-50%] left-0 bg-mred font-roboto text-white font-bold text-base z-20">
<span className=" py-1 px-4 block">Gysga Habarlar</span>
</div>
<Marquee
direction="left"
loop={0}
speed={30}
pauseOnHover
className="w-full flex gap-4 bg-[#BBBB] z-10 lg:my-[11px]"
gradient={false}>
{data?.data.map((item) => (
<div className="mr-10 py-1" key={v4()}>
{item.content}
</div>
))}
</Marquee>
</div>
)}
</div>
) : null;
};
export default Marque;

View File

@ -0,0 +1,79 @@
'use client';
import Image from 'next/image';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Scrollbar, A11y, Autoplay } from 'swiper';
import { v4 } from 'uuid';
import Link from 'next/link';
import { Queries } from '@/api/queries';
import { useQuery } from '@tanstack/react-query';
import Loader from '../Loader';
interface IProps {
sliderNumber: number;
}
const SmallSwiperAdvert = ({ sliderNumber }: IProps) => {
const { data, isFetching, error } = useQuery({
queryKey: [sliderNumber === 3 ? 'small_slider3' : sliderNumber === 4 ? 'small_slider4' : ''],
queryFn: () =>
sliderNumber === 3
? Queries.getSmallSlider3()
: sliderNumber === 4
? Queries.getSmallSlider4()
: null,
});
if (isFetching) return <Loader height={'100%'} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="small-swiper overflow-hidden">
<Swiper
className="h-full overflow-hidden"
modules={[Navigation, Pagination, Scrollbar, A11y, Autoplay]}
slidesPerView={1}
spaceBetween={0}
navigation
autoplay={{ delay: 4200 }}
speed={700}
loop
pagination={{
clickable: true,
}}>
{/* PAY ATTENTION [data] is wrapped in an array */}
{data?.data.map((item) => (
<SwiperSlide key={v4()} className="overflow-hidden">
{item.url || item.page_id ? (
<Link
// href={item.url ? item.url : `advertisment_page/${item.page_id}`}
href={item.url ? item.url : item.page_id ? `/${item.page_id}` : ''}
className="overflow-hidden w-full lg:h-full h-[200px] flex">
<Image
src={item.image}
alt="small_banner"
fill
unoptimized
unselectable="off"
className={` pointer-events-none overflow-hidden`}
/>
</Link>
) : (
<div className="overflow-hidden w-full h-full">
<Image
src={item.image}
alt="small_banner"
fill
unoptimized
unselectable="off"
className={` pointer-events-none overflow-hidden`}
/>
</div>
)}
</SwiperSlide>
))}
</Swiper>
</div>
);
};
export default SmallSwiperAdvert;

View File

@ -0,0 +1,65 @@
'use client';
import Image from 'next/image';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Scrollbar, A11y, Autoplay } from 'swiper';
import { v4 } from 'uuid';
import Link from 'next/link';
import { HomeModel } from '@/models/home.model';
import { Queries } from '@/api/queries';
import { useQuery } from '@tanstack/react-query';
import Loader from '../Loader';
import LinkBlock from './LinkBlock';
const SmallSwiperNews = () => {
const { data, isFetching, error } = useQuery({
queryKey: ['small_slider_news'],
queryFn: () => Queries.getlastNews(),
});
if (isFetching) return <Loader height={'100%'} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="small-swiper flex-1">
<Swiper
className="h-full"
modules={[Navigation, Pagination, Scrollbar, A11y, Autoplay]}
slidesPerView={1}
spaceBetween={0}
navigation
autoplay={{ delay: 4100 }}
speed={700}
loop
pagination={{
clickable: true,
}}>
{/* PAY ATTENTION [data] is wrapped in an array */}
<SwiperSlide>
<LinkBlock href="/news" content="Habarlar" color="#a554f0" />
</SwiperSlide>
{data?.data.map((item, index) => (
<SwiperSlide key={v4()} className="">
<Link href={item ? `news/${item.id}` : ''} className="relative ">
<div className="relative w-full h-full">
<Image
src={item.featured_images[0].path}
alt="small_banner"
fill
unoptimized
unselectable="off"
className={`w-full object-fill pointer-events-none h-full absolute top-0 left-0`}
/>
<div className="w-full h-full flex justify-start items-end p-4 pb-8 absolute top-0 left-0 z-30">
<h2 className="text-lg text-white font-bold">{item.title}</h2>
</div>
<div className="w-full h-full z-20 absolute top-0 left-0 bg-black opacity-40"></div>
</div>
</Link>
</SwiperSlide>
))}
</Swiper>
</div>
);
};
export default SmallSwiperNews;

View File

@ -0,0 +1,85 @@
'use client';
import Image from 'next/image';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Scrollbar, A11y, Autoplay } from 'swiper';
import { v4 } from 'uuid';
import Link from 'next/link';
import { Queries } from '@/api/queries';
import { useQuery } from '@tanstack/react-query';
import Loader from '../Loader';
import LinkBlock from './LinkBlock';
import { VideoItemModel } from '@/models/videoItem.model';
const SmallSwiperVideos = () => {
const { data, isFetching, error } = useQuery({
queryKey: ['small_slider_videos'],
queryFn: () => Queries.getLastVideos(),
});
if (isFetching) return <Loader height={'100%'} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
function chooseRandomItems(inputArray: VideoItemModel[]) {
// Check if the input array has at least 5 items
if (inputArray.length < 5) {
throw new Error('Input array must have at least 5 items');
}
// Use the Fisher-Yates (Knuth) shuffle algorithm to shuffle the array
for (let i = inputArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[inputArray[i], inputArray[j]] = [inputArray[j], inputArray[i]];
}
// Return the first 5 items from the shuffled array
return inputArray.slice(0, 5);
}
return (
<div className="small-swiper ">
<Swiper
className="h-full"
modules={[Navigation, Pagination, Scrollbar, A11y, Autoplay]}
slidesPerView={1}
spaceBetween={0}
navigation
autoplay={{ delay: 4000 }}
speed={700}
loop
pagination={{
clickable: true,
}}>
{/* PAY ATTENTION [data] is wrapped in an array */}
<SwiperSlide>
<LinkBlock href="/treasury" content="Hazyna" color="#c56540" />
</SwiperSlide>
{data?.data
? chooseRandomItems(data.data).map((item, index) => (
<SwiperSlide key={v4()}>
<Link href={item ? `treasury/${item.id}` : ''}>
{item.banner_url ? (
<div className="relative w-full h-full">
<Image
src={item.banner_url}
alt="small_banner"
fill
unoptimized
unselectable="off"
className={`w-full object-fill pointer-events-none h-full absolute top-0 left-0`}
/>
<div className="w-full h-full flex justify-start items-end p-4 pb-8 absolute top-0 left-0 z-30">
<h2 className="text-lg text-white font-bold">{item.title}</h2>
</div>
<div className="w-full h-full z-20 absolute top-0 left-0 bg-black opacity-40"></div>
</div>
) : null}
</Link>
</SwiperSlide>
))
: null}
</Swiper>
</div>
);
};
export default SmallSwiperVideos;

View File

@ -0,0 +1,27 @@
import SmallSwiper from './SmallSwiperVideos';
import LinkBlock from './LinkBlock';
import { HomeModel } from '@/models/home.model';
import { Queries } from '@/api/queries';
import { useQuery } from '@tanstack/react-query';
import Loader from '../Loader';
import SmallSwiperNews from './SmallSwiperNews';
import SmallSwiperVideos from './SmallSwiperVideos';
import SmallSwiperAdvert from './SmallSwiperAdvert';
const SmallSwipers = () => {
return (
<div className="grid grid-cols-2 lg:grid-rows-full3 lg:h-home h-max overflow-hidden">
<LinkBlock href="/advert" content="Mahabat" color="#5461F0" />
<LinkBlock href="/live" content="Göni Ýaýlym" color="#EB4765" />
{/* Is not yet in API, static for now */}
<SmallSwiperNews />
<SmallSwiperVideos />
<SmallSwiperAdvert sliderNumber={3} />
<SmallSwiperAdvert sliderNumber={4} />
{/* <SmallSwiper data={data} /> */}
</div>
);
};
export default SmallSwipers;

View File

@ -0,0 +1,33 @@
'use client';
import GlobalContext from '@/context/GlobalContext';
import { useContext } from 'react';
import { RiContrastLine } from 'react-icons/ri';
const ThemeSwitch = () => {
const { theme, setTheme } = useContext(GlobalContext).themeContext;
return (
<div className="theme-switch flex items-center gap-2">
<span
className="font-roboto font-normal text-lg transition-all text-[#D9D9D9] cursor-pointer"
onClick={() => setTheme('light')}
style={{ color: theme === 'light' ? '#121268' : '#D9D9D9' }}>
Light
</span>
<RiContrastLine
color={theme === 'dark' ? '#37ABE1' : '#121268'}
width={23}
height={23}
className="rotate-180 w-[23px] h-[23px] transition-all"
/>
<span
className="font-roboto font-normal text-lg transition-all text-[#D9D9D9] cursor-pointer"
onClick={() => setTheme('dark')}
style={{ color: theme === 'dark' ? '#37ABE1' : '#D9D9D9' }}>
Dark
</span>
</div>
);
};
export default ThemeSwitch;

View File

@ -0,0 +1,29 @@
"use client";
import Link from "next/link";
import { FiSearch } from "react-icons/fi";
import { AiOutlineUser } from "react-icons/ai";
const Toolbar = () => {
return (
<div className="bg-[#E2E2E2] rounded-[20px] py-[10px] px-3 flex items-center gap-5 fixed top-5 right-5 w-32 justify-center">
<Link href={"/"}>
<FiSearch
height={20}
width={20}
color="#121268"
className="block w-5 h-5"
/>
</Link>
<Link href={"/"}>
<AiOutlineUser
height={20}
width={20}
color="#121268"
className="block w-[22px] h-[22px]"
/>
</Link>
</div>
);
};
export default Toolbar;

View File

@ -0,0 +1,29 @@
"use client";
import { PropsWithChildren } from "react";
import { motion } from "framer-motion";
import {
VariantLabels,
AnimationControls,
TargetAndTransition,
} from "framer-motion";
interface IProps extends PropsWithChildren {
initial?: any;
animate?: VariantLabels | AnimationControls | TargetAndTransition | undefined;
exit?: VariantLabels | TargetAndTransition | undefined;
}
const PresenceAnimator = ({ children, initial, animate, exit }: IProps) => {
return (
<motion.div
className="flex flex-col justify-between min-h-[500px]"
initial={initial}
animate={animate}
exit={exit}
>
{children}
</motion.div>
);
};
export default PresenceAnimator;

View File

@ -0,0 +1,44 @@
import React from 'react';
import Image from 'next/image';
import { Queries } from '@/api/queries';
import { useQuery } from '@tanstack/react-query';
import Loader from '../Loader';
import Link from 'next/link';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper';
import { v4 } from 'uuid';
const Banner = () => {
const { data, isFetching, error } = useQuery({
queryKey: ['channel_description'],
queryFn: () => Queries.getBanner(),
});
// if (isFetching) return <Loader height={'100%'} />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<aside className="live-aside max-w-[200px] w-full lg:block hidden relative">
{data?.data ? (
<Swiper
modules={[Pagination, Autoplay]}
slidesPerView={1}
autoplay={{ delay: 3200 }}
speed={700}
loop
pagination={{ clickable: true }}
className="w-full">
{data!.data.map((item) => (
<SwiperSlide key={v4()} className="w-[200px] h-[400px]">
<Link href={item.url} className="w-full h-full" target="_blank">
<Image src={item.img_url} unoptimized alt={item.alt} fill priority className="" />
</Link>
</SwiperSlide>
))}
</Swiper>
) : null}
</aside>
);
};
export default Banner;

View File

@ -0,0 +1,44 @@
"use client";
import { Queries } from "@/api/queries";
import { useInfiniteQuery } from "@tanstack/react-query";
import Image from "next/image";
import Link from "next/link";
import Loader from "../Loader";
import baseUrl from "@/baseUrl";
const MainNews = () => {
const { data, isLoading, error } = useInfiniteQuery({
queryKey: ["news", "infinite"],
queryFn: ({ pageParam = 1 }) => Queries.getNews(pageParam, {}),
getNextPageParam: (prevData) =>
prevData.meta.last_page > prevData.meta.current_page
? prevData.meta.current_page + 1
: null,
});
const news = data!.pages.flatMap((data) => data.data)[0];
if (isLoading) return <Loader />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<Link href={`/news/${news.id}`} className="main-news flex flex-col gap-6">
<div className="w-full max-h-[250px] h-full">
<Image
src={news.featured_images[0].path}
alt={news.title}
unoptimized
unselectable="off"
width={1000}
height={250}
className="w-full object-cover h-[250px]"
/>
</div>
<div className="flex flex-col gap-3 text-black text-lg">
<h2 className="font-mw_sans font-bold">{news.title}</h2>
</div>
</Link>
);
};
export default MainNews;

33
components/news/News.tsx Normal file
View File

@ -0,0 +1,33 @@
import baseUrl from '@/baseUrl';
import { NewsModel } from '@/models/news.model';
import Image from 'next/image';
import Link from 'next/link';
interface IProps {
news: NewsModel['data'][0];
}
const News = ({ news }: IProps) => {
return (
<Link
href={`/news/${news.id}`}
className="news flex flex-col gap-2 h-fit lg:max-w-[350px] md:max-w-[300px] sm:max-w-[200px] max-w-[300px] w-full">
<div className=" w-full md:h-[200px] sm:h-[150px] h-[200px] rounded-five overflow-hidden relative">
{news.featured_images && news.featured_images[0].path ? (
<Image
src={news.featured_images[0].path}
alt={news.slug ? news.slug : news.title}
unoptimized
unselectable="off"
fill
className="w-full object-cover h-full"
/>
) : null}
</div>
<p className="font-roboto text-sm text-black font-normal clamped">{news.published_at}</p>
<p className="font-roboto text-lg leading-6 text-black font-bold clamped">{news.title}</p>
</Link>
);
};
export default News;

View File

@ -0,0 +1,67 @@
'use client';
import { v4 } from 'uuid';
import News from './News';
import MoreBtn from '../MoreBtn';
import SectionTitle from '../SectionTitle';
import { Queries } from '@/api/queries';
import { useInfiniteQuery } from '@tanstack/react-query';
import NewsSlider from './NewsSlider';
import Loader from '../Loader';
interface IProps {
title?: string;
isExtendable?: boolean;
isSlides?: boolean;
perPage?: number;
}
const NewsGrid = ({ title, isExtendable, isSlides, perPage = 8 }: IProps) => {
const { data, isLoading, isFetchingNextPage, error, hasNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['news', 'infinite'],
queryFn: ({ pageParam = 1 }) => Queries.getNews(pageParam, { perPage }),
getNextPageParam: (prevData) =>
prevData.meta.last_page > prevData.meta.current_page
? prevData.meta.current_page + 1
: null,
keepPreviousData: true,
});
// const { data, isLoading, isFetchingNextPage, error, hasNextPage, fetchNextPage } =
// useInfiniteQuery({
// queryKey: ['news', 'infinite'],
// queryFn: ({ pageParam = 1 }) => Queries.getNews(pageParam, { perPage }),
// keepPreviousData: true,
// });
if (isLoading) return <Loader />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="flex flex-col gap-8">
{title ? <SectionTitle title={title} /> : null}
{isSlides ? (
<NewsSlider data={data} />
) : (
<div className="news-grid sm:grid gap-[10px] sm:grid-rows-1 sm:items-start lg:grid-cols-4 sm:grid-cols-3 flex flex-col items-center content-center sm gap-y-6">
{data!.pages
.flatMap((data) => data.data)
.map((novelty) => (
<News news={novelty} key={v4()} />
))}
</div>
)}
{isExtendable ? (
<div className="flex justify-center">
<MoreBtn
onClick={() => fetchNextPage()}
disabled={!hasNextPage}
isFetching={isFetchingNextPage}
/>
</div>
) : null}
</div>
);
};
export default NewsGrid;

View File

@ -0,0 +1,56 @@
'use client';
import { Queries } from '@/api/queries';
import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';
import Loader from '../Loader';
import baseUrl from '@/baseUrl';
import PageTitle from '../PageTitle';
import { NextSeo } from 'next-seo';
interface IProps {
id: string;
}
const Item = ({ id }: IProps) => {
const { data, error, isFetching } = useQuery({
queryKey: ['news_item', id],
queryFn: () => Queries.getNewsItem(id),
});
if (isFetching) return <Loader />;
if (error) return <h1>{JSON.stringify(error)}</h1>;
return (
<div className="flex flex-col gap-8">
<NextSeo title={data!.data.title} description={data!.data.excerpt} />
<div className="flex flex-col gap-2">
<PageTitle title={data!.data.title} />
<p className="text-lg">{data?.data.published_at}</p>
</div>
<div className="main-news flex flex-col gap-6">
<div className="w-full lg:h-[600px] md:h-[400px] h-[250px] relative">
{data?.data.featured_images && data.data.featured_images[0].path ? (
<Image
src={data!.data.featured_images[0].path}
alt={data!.data.slug ? data!.data.slug : data!.data.title}
unoptimized
unselectable="off"
fill
priority
className="w-full object-contain h-[600px]"
/>
) : null}
</div>
<div className="flex flex-col gap-3 text-black text-lg">
{/* <h2 className="font-mw_sans font-bold">{data!.data.title}</h2> */}
<p
className="font-roboto font-normal flex flex-col gap-4"
dangerouslySetInnerHTML={{ __html: data!.data.content_html }}></p>
</div>
</div>
</div>
);
};
export default Item;

View File

@ -0,0 +1,38 @@
'use client';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Autoplay } from 'swiper';
import { InfiniteData } from '@tanstack/react-query';
import { NewsModel } from '@/models/news.model';
import { v4 } from 'uuid';
import News from './News';
interface IProps {
data: InfiniteData<NewsModel> | undefined;
}
const NewsSlider = ({ data }: IProps) => {
return (
<div className="big-swiper w-full">
<Swiper
speed={1000}
modules={[Navigation, Autoplay]}
spaceBetween={10}
slidesPerGroup={1}
slidesPerGroupAuto
slidesPerView={'auto'}
navigation
autoplay={{ delay: 3000 }}
loop>
{data!.pages
.flatMap((data) => data.data)
.map((novelty) => (
<SwiperSlide key={v4()} className="w-fit">
<News news={novelty} key={v4()} />
</SwiperSlide>
))}
</Swiper>
</div>
);
};
export default NewsSlider;

Some files were not shown because too many files have changed in this diff Show More