load more completed

This commit is contained in:
2024-11-06 17:45:34 +05:00
parent 73f6845ede
commit 08aa0f72ec
31 changed files with 217 additions and 246 deletions
@@ -0,0 +1,19 @@
'use server';
import { getClient } from '@/lib/apolloClientForSC';
import {
GetArticlesDocument,
GetArticlesQuery,
} from '@/queries/articles/getArticles';
export async function getArticlesWithPaginationAction(
offset: number = 0,
limit: number = 5,
tags: string[] = [],
) {
return (
await getClient().query<GetArticlesQuery>({
query: GetArticlesDocument,
variables: { offset, limit, tags },
})
).data.articles;
}
+5 -28
View File
@@ -1,45 +1,22 @@
import { getArticlesWithPaginationAction } from '@/actions/articles/getArticlesWithPaginationAction';
import { ArticlesList } from '@/components/pages/BlogPage/ArticlesList';
import { ArticlesPageHeader } from '@/components/pages/BlogPage/ArticlesPageHeader';
import { ArticlesResult } from '@/generated/graphql';
import { getClient } from '@/lib/apolloClientForSC';
import { GetArticlesDocument } from '@/queries/articles/getArticles';
import { GetArticlesCountDocument } from '@/queries/articles/getArticlesCount';
export default async function BlogPage({
searchParams: { tags = [], limit = 5 },
searchParams: { tags = [] },
}: {
searchParams: {
tags?: string[];
limit: number;
};
}) {
const client = getClient();
const data = await getArticlesWithPaginationAction(0, 5, tags);
const {
data: { articles },
} = await client.query<{
articles: ArticlesResult;
}>({
query: GetArticlesDocument,
variables: {
tags,
limit: +limit,
},
});
const {
data: { articlesCount },
} = await client.query<{ articlesCount: number }>({
query: GetArticlesCountDocument,
variables: {
tags,
},
});
if (data.__typename !== 'Articles') return <div>error</div>;
return (
<>
<ArticlesPageHeader />
<ArticlesList articlesData={articles} articlesCount={articlesCount} />
<ArticlesList articles={data.articles} />
</>
);
}
+1
View File
@@ -4,6 +4,7 @@
@layer base {
.no-tailwindcss-base {
@apply w-full;
word-break: normal;
h1 {
@apply text-[32px] font-medium leading-[35.2px];
+8 -6
View File
@@ -1,6 +1,6 @@
import { cities } from '@/consts/cities';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
export function CitySelector() {
const { push } = useRouter();
@@ -21,6 +21,12 @@ export function CitySelector() {
useEffect(() => setValue(params.get('city') as string), [params]);
function handleChangeSelect(e: ChangeEvent<HTMLSelectElement>) {
if (!e.target.value) params.delete('city');
else params.set('city', e.target.value);
push(pathname + '?' + params.toString());
}
return (
<div className="relative flex items-center border border-[#3D425C] rounded-full 2xl:col-start-11 2xl:col-span-2 lg:col-start-10 nice-cock-2 lg:col-span-3 md:col-start-9 md:col-span-4 sm:col-start-8 sm:col-span-5 self-start w-full">
<p
@@ -40,11 +46,7 @@ export function CitySelector() {
: `url("data:image/svg+xml;utf8,<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12.0001 17.707L19.7072 9.99992L18.293 8.58571L12.0001 14.8786L5.70718 8.58571L4.29297 9.99992L12.0001 17.707Z' fill='white'/></svg>")`,
}}
className="bg-[#14161F] flex-1 appearance-none [-webkit-appearance:none] [-moz-appearance:none] rounded-full p-[10px] outline-none bg-no-repeat bg-[right_10px_center]"
onChange={e => {
if (!e.target.value) params.delete('city');
else params.set('city', e.target.value);
push(pathname + '?' + params.toString());
}}
onChange={handleChangeSelect}
>
{!!ref.current && (
<>
@@ -23,7 +23,7 @@ export function FilterItem({
const params = new URLSearchParams(useSearchParams());
function clickHandler() {
function handleClick() {
if (isAll) {
params.delete(type);
} else {
@@ -44,7 +44,7 @@ export function FilterItem({
? 'bg-[#798FFF] border-[#798FFF]'
: 'border-[#3D425C] nice-cock-2'
}`}
onClick={clickHandler}
onClick={handleClick}
>
{value}
{!!count && (
+1 -3
View File
@@ -15,9 +15,7 @@ export function ProductsList({ inBurger = false }: { inBurger?: boolean }) {
return (
<button
ref={ref}
onClick={() => {
setModal(<MenuModal />, 'menu');
}}
onClick={() => setModal(<MenuModal />, 'menu')}
className={
`btn-text font-medium flex gap-x-2 items-center border-[#3D425C] nice-cock-2 hover:text-white group outline-none ${name === 'menu' ? 'text-white' : ''}` +
(name === 'products' ? ' relative z-[101]' : '') +
+13 -9
View File
@@ -6,7 +6,7 @@ import {
getCountryCallingCode,
} from 'libphonenumber-js';
import Image from 'next/image';
import { useRef, useState } from 'react';
import { SyntheticEvent, useRef, useState } from 'react';
import { useOnClickOutside } from 'usehooks-ts';
import { ChevronDownIcon } from '../icons/ChevronDownIcon';
import { ChevronUpIcon } from '../icons/ChevronUpIcon';
@@ -24,14 +24,21 @@ export function SelectPhoneCode({
useOnClickOutside(ref, () => setOpen(false));
function handleExpand(e: SyntheticEvent) {
e.preventDefault();
setOpen(prev => !prev);
}
function pickPhoneCode(phoneCode: string, country: CountryCode) {
onClick([phoneCode, country as CountryCode]);
setOpen(false);
}
return (
<div ref={ref} className="relative flex flex-col sm:w-1/3 max-w-[350px]">
<button
className="relative flex items-center justify-between gap-x-1"
onClick={e => {
e.preventDefault();
setOpen(prev => !prev);
}}
onClick={handleExpand}
>
<Image
width={16}
@@ -58,10 +65,7 @@ export function SelectPhoneCode({
<div
key={country}
className="flex items-center gap-x-1 hover:bg-[#3D425C] px-1"
onClick={() => {
onClick([phoneCode, country as CountryCode]);
setOpen(false);
}}
onClick={() => pickPhoneCode(phoneCode, country as CountryCode)}
>
<Image
src={countries.find(c => c.iso === country)?.flag || ''}
@@ -1,4 +1,5 @@
import { Reorder } from 'framer-motion';
import { SyntheticEvent } from 'react';
import { UseFieldArrayRemove, UseFormSetValue } from 'react-hook-form';
import { CloseIcon } from '../icons/CloseIcon';
import { MediaUploader } from '../MediaUploader';
@@ -19,6 +20,11 @@ export function ArticleSliderImageInput({
remove,
setValue,
}: IArticleSliderImageInputProps) {
function handleClickClose(e: SyntheticEvent) {
e.preventDefault();
remove(imgIndex);
}
return (
<Reorder.Item value={item} className="flex items-center" drag>
<MediaUploader
@@ -28,13 +34,7 @@ export function ArticleSliderImageInput({
item={item}
label="Выберите изображение"
/>
<button
className="self-start z-[1]"
onClick={e => {
e.preventDefault();
remove(imgIndex);
}}
>
<button className="self-start z-[1]" onClick={handleClickClose}>
<CloseIcon />
</button>
</Reorder.Item>
@@ -1,6 +1,7 @@
import { ISlider } from '@/types/IArticle';
import { reorderFields } from '@/utils/reorderFields';
import { Reorder } from 'framer-motion';
import { SyntheticEvent } from 'react';
import {
Control,
useFieldArray,
@@ -32,26 +33,26 @@ export function ArticleSliderInput({
name: `blocks.${index}.images`,
});
function handleAddSlide(e: SyntheticEvent) {
e.preventDefault();
append({ img: '' });
}
function handleRemoveSlider(e: SyntheticEvent) {
e.preventDefault();
removeSlider(index);
}
return (
<Reorder.Item
value={item}
className="border border-[#3D425C] rounded-3xl p-4 col-span-full space-y-4"
>
<div className="flex gap-x-4 justify-center">
<button
onClick={e => {
e.preventDefault();
append({ img: '' });
}}
>
<button onClick={handleAddSlide}>
<PlusIcon />
</button>
<button
onClick={e => {
e.preventDefault();
removeSlider(index);
}}
>
<button onClick={handleRemoveSlider}>
<TrashIcon />
</button>
</div>
@@ -33,7 +33,7 @@ export function ArticleContentEditorModal({
}}
init={{
content_style:
'body {color: #fff; background: #14161f; font-size:16px;display:grid;grid-template-columns:repeat(4,1fr)} body > * {grid-column-start: 2;grid-column-end:4',
'body {color: #fff; background: #14161f; font-size:16px;display:grid;grid-template-columns:repeat(4,1fr);} body > * {grid-column-start: 2;grid-column-end:4',
height: '100%',
font_size_formats: '10px 12px 14px 16px 18px 20px 24px 28px 30px',
video_template_callback: (data: { source: string }) =>
+18 -15
View File
@@ -30,6 +30,23 @@ export function ArticleFormModal({ defaultValues, set }: IArticleFormProps) {
defaultValues,
});
function onSubmit({
cardImage,
createdAt,
description,
tags,
title,
}: Omit<IArticleFormInput, 'blocks'>) {
setModal(null, '');
if (set) {
set('title', title);
set('description', description);
set('tags', tags);
set('cardImage', cardImage);
set('createdAt', createdAt);
}
}
return (
<div className="text-black bg-white p-4 rounded-lg relative top-10 space-y-3">
<div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4">
@@ -41,21 +58,7 @@ export function ArticleFormModal({ defaultValues, set }: IArticleFormProps) {
<CloseIcon />
</button>
</div>
<form
onSubmit={handleSubmit(
({ cardImage, description, tags, title, createdAt }) => {
setModal(null, '');
if (set) {
set('title', title);
set('description', description);
set('tags', tags);
set('cardImage', cardImage);
set('createdAt', createdAt);
}
},
)}
className="space-y-4"
>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<label htmlFor="blog_title" className="flex flex-col">
Название статьи
<input
@@ -7,15 +7,6 @@ import { CloseIcon } from '../icons/CloseIcon';
export function DeleteArticleModal({ id }: { id: number }) {
const { setModal } = useModalStore();
// const [deleteArticle] = useDeleteArticleMutation({
// variables: { id },
// refetchQueries: ['GetArticleById', 'GetArticlesCount'],
// onCompleted: () => {
// setModal(null, '');
// window.location.href = '/blog';
// },
// });
const client = useApolloClient();
async function handleDeleteArticle() {
@@ -1,6 +1,6 @@
'use client';
import { FilterItem } from '@/components/TagFilterItem';
import { FilterItem } from '@/components/FilterItem';
import { PostTags } from '@/consts/PostTags';
import { useGetArticlesCountQuery } from '@/queries/articles/getArticlesCount';
import { Vertical } from '@/ui/Vertical';
@@ -8,6 +8,7 @@ import { useSearchParams } from 'next/navigation';
export function ArticlesFilters() {
const params = useSearchParams();
const chosedTags = params.getAll('tags');
const { data } = useGetArticlesCountQuery({ variables: { tags: [] } });
+49 -45
View File
@@ -1,64 +1,68 @@
'use client';
import { ArticlesResult } from '@/generated/graphql';
import { getArticlesWithPaginationAction } from '@/actions/articles/getArticlesWithPaginationAction';
import { Article } from '@/generated/graphql';
import { useGetArticlesCountQuery } from '@/queries/articles/getArticlesCount';
import { Block } from '@/types/IArticle';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { ArticleCard } from './ArticleCard';
export function ArticlesList({
articlesData,
articlesCount,
}: {
articlesData: ArticlesResult;
articlesCount: number;
}) {
const { push } = useRouter();
export function ArticlesList({ articles }: { articles: Article[] }) {
const params = useSearchParams();
const pathname = usePathname();
const [nextOffset, setNextOffset] = useState(5);
const [currentArticles, setCurrentArticles] = useState(articles);
// eslint-disable-next-line react-hooks/exhaustive-deps
const params = new URLSearchParams(useSearchParams());
const [limit, setLimit] = useState(5);
const { data } = useGetArticlesCountQuery({
variables: { tags: params.getAll('tags') },
});
useEffect(() => {
params.set('limit', limit.toString());
push(pathname + '?' + params.toString());
}, [limit, params, pathname, push]);
setCurrentArticles(articles);
}, [params, articles]);
function handleShowMore() {
if (articlesData.__typename === 'Articles' && limit < articlesCount)
setLimit(prev => prev + 5);
}
const handleLoadMore = useCallback(async () => {
const data = await getArticlesWithPaginationAction(
nextOffset,
5,
params.getAll('tags'),
);
if (data.__typename === 'Articles') {
setCurrentArticles(prev => [...prev, ...data.articles]);
setNextOffset(prev => prev + 5);
}
}, [nextOffset, params]);
return (
<div className="lg:pb-6">
{articlesData.__typename === 'Articles' &&
articlesData.articles.length === 0 ? (
{currentArticles.length === 0 ? (
<p className="text-center mt-10 h3 font-medium">Постов не найдено</p>
) : (
<div className="lg:mb-6 sm:mb-5 mb-4">
{articlesData?.__typename === 'Articles' &&
articlesData?.articles?.map(article => (
<ArticleCard
key={article.id}
{...article}
blocks={
article.blocks.map(block => ({
...block,
type: block.__typename,
})) as Block[]
}
/>
))}
<div className="lg:pl-[calc(25vw-40px)] mt-10">
<button
onClick={handleShowMore}
className="lg:opacity-80 lg:hover:opacity-100 btn-text font-semibold border border-[#3D425C] rounded-[32px] lg:py-[14.5px] sm:py-6 py-4 w-full transition-all nice-cock-2"
>
Показать еще
</button>
</div>
{currentArticles?.map(article => (
<ArticleCard
key={article.id}
{...article}
blocks={
article.blocks.map(block => ({
...block,
type: block.__typename,
})) as Block[]
}
/>
))}
{data?.articlesCount &&
currentArticles.length < data?.articlesCount && (
<div className="lg:pl-[calc(25vw-40px)] mt-10">
<button
onClick={handleLoadMore}
className="lg:opacity-80 lg:hover:opacity-100 btn-text font-semibold border border-[#3D425C] rounded-[32px] lg:py-[14.5px] sm:py-6 py-4 w-full transition-all nice-cock-2"
>
Показать еще
</button>
</div>
)}
</div>
)}
</div>
+2 -17
View File
@@ -1,3 +1,4 @@
import { awards } from '@/consts/awards';
import { Title } from '@/ui/Title';
import Image from 'next/image';
@@ -5,23 +6,7 @@ export function Awards() {
return (
<div className="grid lg:grid-cols-2 sm:max-lg:grid-rows-[104px_1fr_1fr_1fr] lg:py-40 sm:py-20 py-12">
<Title className="sm:max-lg:mb-16 max-sm:mb-12">Награды</Title>
{[
{
title: 'Победители BuildUP 2023',
description: 'в номинации IT',
image: '/img/pages/home/awards/BuildUP.png',
},
{
title: '1 место',
description: 'WOW AWARDS 2024 ',
image: '/img/pages/home/awards/wow_awards.png',
},
{
title: 'Лучшее на Dprofile',
description: 'а еще за UI награда',
image: '/img/pages/home/awards/dpui.png',
},
].map((awards, index) => (
{awards.map((awards, index) => (
<AwardsItem key={index} {...awards} />
))}
</div>
+8 -10
View File
@@ -34,6 +34,13 @@ export function Projects({ projects }: { projects: ProjectsResult }) {
setModal(null, '');
}
function handleAddProject() {
setModal(
<ProjectFormModal action={'create'} onSubmit={onSubmit} />,
'addProject',
);
}
return (
<>
<div className="border-y border-[#3D425C] py-6 grid lg:grid-cols-4 sm:gap-y-8 gap-y-6">
@@ -52,16 +59,7 @@ export function Projects({ projects }: { projects: ProjectsResult }) {
{authData?.checkAuth.__typename === 'CheckAuthResponse' &&
authData?.checkAuth.isAuth && (
<div className="m-auto">
<Button
onClick={() =>
setModal(
<ProjectFormModal action={'create'} onSubmit={onSubmit} />,
'addProject',
)
}
>
Добавить проект
</Button>
<Button onClick={handleAddProject}>Добавить проект</Button>
</div>
)}
<Link
+11 -9
View File
@@ -10,6 +10,16 @@ export function Showreel() {
const videoRef = useRef<HTMLVideoElement>(null);
function handleFullScreenClick() {
setModal(
<VideoModal
currentTime={videoRef.current?.currentTime ?? 0}
link={'/videos/pages/home/showreel.mp4'}
/>,
'video',
);
}
return (
<div className="lg:mb-[200px] sm:mb-[120px] mb-20 w-full relative lg:aspect-[1551/616] flex justify-center items-center group">
<video
@@ -23,15 +33,7 @@ export function Showreel() {
/>
<button
className="absolute z-[8] p-8 rounded-full border group-hover:block hidden bg-[#14161F33]"
onClick={() => {
setModal(
<VideoModal
currentTime={videoRef.current?.currentTime ?? 0}
link={'/videos/pages/home/showreel.mp4'}
/>,
'video',
);
}}
onClick={handleFullScreenClick}
>
<FullScreenIcon />
</button>
@@ -1,6 +1,6 @@
'use client';
import { FilterItem } from '@/components/TagFilterItem';
import { FilterItem } from '@/components/FilterItem';
import { Devices } from '@/consts/ProjectTags';
import { useGetProjectsCountQuery } from '@/queries/projects/getProjectsCount';
import { Vertical } from '@/ui/Vertical';
-9
View File
@@ -1,9 +0,0 @@
export const PostYears = [
'2024',
'2023',
'2022',
'2021',
'2020',
'2019',
'2018',
];
-1
View File
@@ -1 +0,0 @@
export const Products = [];
-15
View File
@@ -1,15 +0,0 @@
export const ProjectYears = [
'2024',
'2023',
'2022',
'2021',
'2020',
'2019',
'2018',
'2017',
'2016',
'2015',
'2014',
'2013',
'2012',
];
+19
View File
@@ -0,0 +1,19 @@
import { IAward } from '@/types/IAward';
export const awards: IAward[] = [
{
title: 'Победители BuildUP 2023',
description: 'в номинации IT',
image: '/img/pages/home/awards/BuildUP.png',
},
{
title: '1 место',
description: 'WOW AWARDS 2024 ',
image: '/img/pages/home/awards/wow_awards.png',
},
{
title: 'Лучшее на Dprofile',
description: 'а еще за UI награда',
image: '/img/pages/home/awards/dpui.png',
},
];
-3
View File
@@ -1,3 +0,0 @@
import { PhoneCode } from '@/types/PhoneCode';
export const phoneCodes: PhoneCode[] = ['+7', '+375', '+380', '+44'];
+1
View File
@@ -191,6 +191,7 @@ export type QueryArticleArgs = {
export type QueryArticlesArgs = {
limit?: InputMaybe<Scalars['Int']['input']>;
offset?: InputMaybe<Scalars['Int']['input']>;
tags?: Array<Scalars['String']['input']>;
};
@@ -6,6 +6,7 @@ const defaultOptions = {} as const;
export type GetArticlesQueryVariables = Types.Exact<{
tags: Array<Types.Scalars['String']['input']> | Types.Scalars['String']['input'];
limit?: Types.InputMaybe<Types.Scalars['Int']['input']>;
offset?: Types.InputMaybe<Types.Scalars['Int']['input']>;
}>;
@@ -13,8 +14,8 @@ export type GetArticlesQuery = { __typename?: 'Query', articles: { __typename?:
export const GetArticlesDocument = gql`
query GetArticles($tags: [String!]!, $limit: Int) {
articles(tags: $tags, limit: $limit) {
query GetArticles($tags: [String!]!, $limit: Int, $offset: Int) {
articles(tags: $tags, limit: $limit, offset: $offset) {
... on Error {
message
}
@@ -58,6 +59,7 @@ export const GetArticlesDocument = gql`
* variables: {
* tags: // value for 'tags'
* limit: // value for 'limit'
* offset: // value for 'offset'
* },
* });
*/
@@ -1,5 +1,5 @@
query GetArticles($tags: [String!]!, $limit: Int) {
articles(tags: $tags, limit: $limit) {
query GetArticles($tags: [String!]!, $limit: Int, $offset: Int) {
articles(tags: $tags, limit: $limit, offset: $offset) {
... on Error {
message
}
+5
View File
@@ -0,0 +1,5 @@
export interface IAward {
title: string;
description: string;
image: string;
}
-1
View File
@@ -1 +0,0 @@
export type PhoneCode = '+7' | '+375' | '+380' | '+44';
-3
View File
@@ -1,3 +0,0 @@
export function Select({ text }: { text: string }) {
return <div>Select</div>;
}
+24 -32
View File
@@ -38,6 +38,28 @@ export const SliderControls = forwardRef<
right: () => nextBtnRef.current?.click(),
}));
function handleLeftClick() {
if (slide === 0) {
rectRef.current!.classList.remove('transition-all', 'duration-300');
rectRef.current!.setAttribute('stroke-dasharray', `${length} 0`);
delay(() => {
rectRef.current!.classList.add('transition-all', 'duration-300');
}, 1);
}
onLeftClick();
}
function handleRightClick() {
if (slide + 1 === slidesCount) {
rectRef.current!.classList.remove('transition-all', 'duration-300');
rectRef.current!.setAttribute('stroke-dasharray', `0 ${length}`);
delay(() => {
rectRef.current!.classList.add('transition-all', 'duration-300');
}, 1);
}
onRightClick();
}
return (
<div className={'flex items-center gap-2 ' + className}>
<div className="relative flex justify-center max-sm:order-2">
@@ -77,44 +99,14 @@ export const SliderControls = forwardRef<
</div>
<button
ref={prevBtnRef}
onClick={() => {
if (slide === 0) {
rectRef.current!.classList.remove(
'transition-all',
'duration-300',
);
rectRef.current!.setAttribute('stroke-dasharray', `${length} 0`);
delay(() => {
rectRef.current!.classList.add(
'transition-all',
'duration-300',
);
}, 1);
}
onLeftClick();
}}
onClick={handleLeftClick}
className="rounded-full sm:p-5 p-4 border border-[#3D425C] bg-[#14161F] nice-cock-2"
>
<ArrowLeftIcon />
</button>
<button
onClick={() => {
if (slide + 1 === slidesCount) {
rectRef.current!.classList.remove(
'transition-all',
'duration-300',
);
rectRef.current!.setAttribute('stroke-dasharray', `0 ${length}`);
delay(() => {
rectRef.current!.classList.add(
'transition-all',
'duration-300',
);
}, 1);
}
onRightClick();
}}
ref={nextBtnRef}
onClick={handleRightClick}
className="rounded-full sm:p-5 p-4 border border-[#3D425C] bg-[#14161F] max-sm:order-2 nice-cock-2"
>
<ArrowRightIcon />
+1 -3
View File
@@ -98,9 +98,7 @@ export function SliderWithScaling<T extends { title: string; id: number }>({
<div {...handlers} className="h-full">
<div
className={`flex items-${alignItems} gap-x-4 -mr-6 select-none h-full`}
onTransitionEnd={() => {
setTransiting(false);
}}
onTransitionEnd={() => setTransiting(false)}
style={{
minHeight: minHeightScaled,
transform: `translateX(${sliderOffset}px)`,