Horizontal scrolling in mobile mode for projects and blogs and add favicon
@@ -14,7 +14,8 @@
|
||||
"next-intl": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-leaflet": "^5.0.0"
|
||||
"react-leaflet": "^5.0.0",
|
||||
"swiper": "^11.2.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@@ -5892,6 +5893,25 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/swiper": {
|
||||
"version": "11.2.9",
|
||||
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.9.tgz",
|
||||
"integrity": "sha512-X4lP396wfb85t5GclSlXmZyeDgcV66JK9+HDfqlk+8wJ5Bp85ZMv2fxu9psulsm22dLQxbnN4kADxK8jXTnSuw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/swiperjs"
|
||||
},
|
||||
{
|
||||
"type": "open_collective",
|
||||
"url": "http://opencollective.com/swiper"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"next-intl": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-leaflet": "^5.0.0"
|
||||
"react-leaflet": "^5.0.0",
|
||||
"swiper": "^11.2.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="450" height="200" viewBox="0 0 450 200"><image width="450" height="200" xlink:href="data:image/png;base64,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"></image><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
||||
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
||||
</style></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Pasargad Bricks",
|
||||
"short_name": "Pasargad Bricks",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -18,7 +18,9 @@ export async function generateMetadata({ params }: { params: IPageParams }) {
|
||||
|
||||
export default async function Blog({ params }: { params: IPageParams }) {
|
||||
const paramsData = await params;
|
||||
const t = await getTranslations({ locale: paramsData.locale });
|
||||
const { locale } = paramsData;
|
||||
|
||||
const t = await getTranslations({ locale });
|
||||
|
||||
const { data: posts, pagination } = await getPosts();
|
||||
|
||||
@@ -28,7 +30,9 @@ export default async function Blog({ params }: { params: IPageParams }) {
|
||||
title={t('pages_blog_title')}
|
||||
imageSrc={images.blogBanner.src}
|
||||
/>
|
||||
<div id="postList" className="w-full">
|
||||
<PostsGrid initialPosts={posts} initialPagination={pagination} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB |
@@ -57,7 +57,22 @@ export default async function RootLayout({
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:site_name" content="کارخانه آجرپزی پاسارگاد" />
|
||||
<meta name="author" content="پاسارگاد" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
{/* <link rel="icon" href="/favicon.ico" /> */}
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href="/favicon/favicon-96x96.png"
|
||||
sizes="96x96"
|
||||
/>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/favicon/apple-touch-icon.png"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="Pasargad Bricks" />
|
||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||
</Head>
|
||||
<body className={`min-h-svh w-full overflow-x-hidden ${fontClass}`}>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import routeFactory from '@/assets/constants/routeFactory';
|
||||
import images from '@/assets/images/images';
|
||||
import InnerPageBanner from '@/components/layout/innerPages/banner';
|
||||
import SectionTitle from '@/components/layout/sectionTitle';
|
||||
import ProjectsGrid from '@/components/pages/project/ProjectGrid';
|
||||
import ProjectsGridView from '@/components/pages/project/projectGridView';
|
||||
import Button from '@/components/uikit/button';
|
||||
import { IPageParams } from '@/models/layout';
|
||||
@@ -40,7 +41,7 @@ export default async function ProjectsPage({
|
||||
<h3 className="text-xl tracking-normal max-lg:hidden">
|
||||
{t('page_projects_description')}
|
||||
</h3>
|
||||
<ProjectsGridView
|
||||
<ProjectsGrid
|
||||
initialProjects={projects.data}
|
||||
initialPagination={projects.pagination}
|
||||
/>
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,46 @@
|
||||
import { IPostThumbResponseData } from '@/app/api/blog/data';
|
||||
import routeFactory from '@/assets/constants/routeFactory';
|
||||
import Button from '@/components/uikit/button';
|
||||
import { TLocales } from '@/models/layout';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'use-intl';
|
||||
|
||||
interface Props {
|
||||
post: IPostThumbResponseData;
|
||||
locale: TLocales;
|
||||
}
|
||||
|
||||
const PostThumb = ({ post, locale }: Props) => {
|
||||
const t = useTranslations();
|
||||
const routes = routeFactory(t, locale);
|
||||
return (
|
||||
<div
|
||||
key={post.id}
|
||||
className="group flex flex-col gap-3 overflow-hidden bg-white md:gap-5"
|
||||
>
|
||||
<div className="relative aspect-[1.09] w-full overflow-hidden rounded-2xl">
|
||||
<Image
|
||||
src={post.imageSrc}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="h-auto w-full object-cover"
|
||||
/>
|
||||
|
||||
<div className="invisible absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 backdrop-blur-xs transition-all group-hover:visible group-hover:opacity-100">
|
||||
<Button link={routes.singlePost.route(post.id)} endIcon="showMore">
|
||||
{t('page_blog_details_CTA')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={routes.singlePost.route(post.id)}
|
||||
className="group-hover:text-primary mb-4 text-center text-gray-500"
|
||||
>
|
||||
<h2 className="text-base font-medium">{post.title}</h2>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostThumb;
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import Pagination from '@/components/layout/pagination';
|
||||
|
||||
import { getPosts } from '@/services/blog';
|
||||
import { IPostThumbResponseData } from '@/app/api/blog/data';
|
||||
import { IPagination } from '@/models/response';
|
||||
import routeFactory from '@/assets/constants/routeFactory';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { TLocales } from '@/models/layout';
|
||||
import { useTranslations } from 'next-intl';
|
||||
@@ -24,8 +22,6 @@ export default function PostsGrid({
|
||||
const t = useTranslations();
|
||||
const { locale } = useParams();
|
||||
|
||||
const routes = routeFactory(t, locale as TLocales);
|
||||
|
||||
const [posts, setPosts] = useState(initialPosts);
|
||||
const [pagination, setPagination] = useState(initialPagination);
|
||||
|
||||
@@ -34,6 +30,9 @@ export default function PostsGrid({
|
||||
.then((res) => {
|
||||
setPosts(res.data);
|
||||
setPagination(res.pagination);
|
||||
document
|
||||
.getElementById('postList')
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch posts:', error);
|
||||
|
||||
@@ -1,52 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { IPostThumbResponseData } from '@/app/api/blog/data';
|
||||
import routeFactory from '@/assets/constants/routeFactory';
|
||||
import Button from '@/components/uikit/button';
|
||||
import { TLocales } from '@/models/layout';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import PostThumb from './PostThumb';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default async function PostsGridView({
|
||||
export default function PostsGridView({
|
||||
posts,
|
||||
locale,
|
||||
}: {
|
||||
posts: IPostThumbResponseData[];
|
||||
locale: TLocales;
|
||||
}) {
|
||||
const t = await getTranslations({ locale });
|
||||
const t = useTranslations();
|
||||
const routes = routeFactory(t, locale);
|
||||
|
||||
return (
|
||||
<>
|
||||
{posts.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className="group flex flex-col gap-3 overflow-hidden bg-white md:gap-5"
|
||||
>
|
||||
<div className="relative aspect-[1.09] w-full overflow-hidden rounded-2xl">
|
||||
<Image
|
||||
src={post.imageSrc}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="h-auto w-full object-cover"
|
||||
/>
|
||||
|
||||
<div className="invisible absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 backdrop-blur-xs transition-all group-hover:visible group-hover:opacity-100">
|
||||
<Button
|
||||
link={routes.singlePost.route(post.id)}
|
||||
endIcon="showMore"
|
||||
>
|
||||
{t('page_blog_details_CTA')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={routes.singlePost.route(post.id)}
|
||||
className="group-hover:text-primary mb-4 text-center text-gray-500"
|
||||
>
|
||||
<h2 className="text-base font-medium">{post.title}</h2>
|
||||
</Link>
|
||||
</div>
|
||||
<PostThumb key={post.id} post={post} locale={locale} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { FreeMode } from 'swiper/modules';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/free-mode';
|
||||
import { IPostThumbResponseData } from '@/app/api/blog/data';
|
||||
import { useTranslations } from 'use-intl';
|
||||
import routeFactory from '@/assets/constants/routeFactory';
|
||||
import { TLocales } from '@/models/layout';
|
||||
import PostThumb from './PostThumb';
|
||||
|
||||
interface Props {
|
||||
posts: IPostThumbResponseData[];
|
||||
locale: TLocales;
|
||||
}
|
||||
|
||||
export default function PostsGridViewSlider({ posts, locale }: Props) {
|
||||
const t = useTranslations();
|
||||
const routes = routeFactory(t, locale);
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Swiper
|
||||
modules={[FreeMode]}
|
||||
freeMode
|
||||
spaceBetween={16}
|
||||
breakpoints={{
|
||||
0: { slidesPerView: 1.1 },
|
||||
768: { slidesPerView: posts.length, spaceBetween: 24 },
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{posts.map((post) => (
|
||||
<SwiperSlide key={post.id}>
|
||||
<PostThumb post={post} locale={locale} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { TLocales } from '@/models/layout';
|
||||
import Button from '@/components/uikit/button';
|
||||
import routeFactory from '@/assets/constants/routeFactory';
|
||||
import { getPosts } from '@/services/blog';
|
||||
import PostsGridView from '../blog/PostsGridView';
|
||||
import PostsGridViewSlider from '../blog/postsGridViewSlider';
|
||||
interface Props {
|
||||
locale: TLocales;
|
||||
className?: string;
|
||||
@@ -38,8 +38,11 @@ export default async function HomePageBlog({ locale, className }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3 lg:gap-8">
|
||||
<PostsGridView posts={posts.data?.slice(0, 3)} locale={locale} />
|
||||
<div className="mt-10">
|
||||
<PostsGridViewSlider
|
||||
posts={posts.data?.slice(0, 3)}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
link={routes.blog.route()}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { getProjects } from '@/services/projects';
|
||||
import ProjectsGrid from '../project/ProjectGrid';
|
||||
import Button from '@/components/uikit/button';
|
||||
import routeFactory from '@/assets/constants/routeFactory';
|
||||
import ProjectsGridView from '../project/projectGridView';
|
||||
import ProjectsGridSlider from '../project/projectsGridSlider';
|
||||
interface Props {
|
||||
locale: TLocales;
|
||||
}
|
||||
@@ -36,8 +38,13 @@ export default async function HomePageProjects({ locale }: Props) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<ProjectsGridSlider
|
||||
projects={projects.data?.slice(0, 3)}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ProjectsGrid projects={projects.data?.slice(3)} className="mt-10" />
|
||||
<Button
|
||||
link={routes.projects.route()}
|
||||
endIcon="showMore"
|
||||
|
||||
@@ -1,61 +1,49 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { IProjectCardProps } from './project';
|
||||
import Button from '@/components/uikit/button';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import routeFactory from '@/assets/constants/routeFactory';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { TLocales } from '@/models/layout';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import ProjectsGridView from './projectGridView';
|
||||
import Pagination from '@/components/layout/pagination';
|
||||
import { IProjectThumbResponseData } from '@/app/api/projects/data';
|
||||
import { IPagination } from '@/models/response';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { getProjects } from '@/services/projects';
|
||||
export interface IProjectsGridProps {
|
||||
initialProjects: IProjectThumbResponseData[];
|
||||
initialPagination: IPagination;
|
||||
}
|
||||
export default function ProjectsGrid({
|
||||
projects,
|
||||
className = '',
|
||||
}: {
|
||||
projects: IProjectCardProps[];
|
||||
className?: string;
|
||||
}) {
|
||||
initialProjects,
|
||||
initialPagination,
|
||||
}: IProjectsGridProps) {
|
||||
const t = useTranslations();
|
||||
const { locale } = useParams();
|
||||
|
||||
const routes = routeFactory(t, locale as TLocales);
|
||||
const [projects, setProjects] = useState(initialProjects);
|
||||
const [pagination, setPagination] = useState(initialPagination);
|
||||
|
||||
const fetchPage = (page: number) => {
|
||||
getProjects(page)
|
||||
.then((res) => {
|
||||
setProjects(res.data);
|
||||
setPagination(res.pagination);
|
||||
|
||||
document
|
||||
.getElementById('projectList')
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch Projects:', error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-8 xl:grid-cols-3 xl:gap-10 ${className}`}
|
||||
>
|
||||
{projects.map((project, index) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className="group flex flex-col gap-3 overflow-hidden bg-white md:gap-5"
|
||||
>
|
||||
<div className="relative aspect-[1.4] w-full overflow-hidden rounded-2xl">
|
||||
<Image
|
||||
src={project.imageSrc}
|
||||
alt={project.title}
|
||||
fill
|
||||
className="h-auto w-full object-cover"
|
||||
/>
|
||||
<div className="invisible absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 backdrop-blur-xs transition-all group-hover:visible group-hover:opacity-100">
|
||||
<Button
|
||||
endIcon="showMore"
|
||||
link={routes.project.route(project.id)}
|
||||
>
|
||||
{t('com_home_projects_details_CTA')}
|
||||
</Button>
|
||||
<div className="container mx-auto px-4 py-14 md:py-28">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<ProjectsGridView projects={projects} locale={locale as TLocales} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={routes.project.route(project.id)}
|
||||
key={index}
|
||||
className="group-hover:text-primary mb-4 text-center text-gray-500"
|
||||
>
|
||||
<h2 className="text-base font-medium">{project.title}</h2>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<Pagination {...pagination} onPageChange={fetchPage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import Button from '@/components/uikit/button';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import routeFactory from '@/assets/constants/routeFactory';
|
||||
import { IProjectThumbResponseData } from '@/app/api/projects/data';
|
||||
import { TLocales } from '@/models/layout';
|
||||
|
||||
interface Props {
|
||||
project: IProjectThumbResponseData;
|
||||
locale: TLocales;
|
||||
}
|
||||
|
||||
const ProjectThumb = ({ project, locale }: Props) => {
|
||||
const t = useTranslations();
|
||||
const routes = routeFactory(t, locale);
|
||||
return (
|
||||
<div className="group flex flex-col gap-3 overflow-hidden bg-white md:gap-5">
|
||||
<div className="relative aspect-[1.4] w-full overflow-hidden rounded-2xl">
|
||||
<Image
|
||||
src={project.imageSrc}
|
||||
alt={project.title}
|
||||
fill
|
||||
className="h-auto w-full object-cover"
|
||||
/>
|
||||
<div className="invisible absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 backdrop-blur-xs transition-all group-hover:visible group-hover:opacity-100">
|
||||
<Button endIcon="showMore" link={routes.project.route(project.id)}>
|
||||
{t('com_home_projects_details_CTA')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={routes.project.route(project.id)}
|
||||
className="group-hover:text-primary mb-4 text-center text-gray-500"
|
||||
>
|
||||
<h2 className="text-base font-medium">{project.title}</h2>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectThumb;
|
||||
@@ -1,37 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { getProjects } from '@/services/projects';
|
||||
import { IProjectGridViewProps } from './project';
|
||||
import ProjectsGrid from './ProjectGrid';
|
||||
import Pagination from '@/components/layout/pagination';
|
||||
import { useState } from 'react';
|
||||
import ProjectThumb from './ProjectThumb';
|
||||
import { IProjectThumbResponseData } from '@/app/api/projects/data';
|
||||
import { TLocales } from '@/models/layout';
|
||||
|
||||
export default function ProjectsGridView({
|
||||
initialProjects,
|
||||
initialPagination,
|
||||
}: IProjectGridViewProps) {
|
||||
const [projects, setProjects] = useState(initialProjects);
|
||||
const [pagination, setPagination] = useState(initialPagination);
|
||||
|
||||
const fetchPage = (page: number) => {
|
||||
getProjects(page)
|
||||
.then((res) => {
|
||||
setProjects(res.data);
|
||||
setPagination(res.pagination);
|
||||
|
||||
document
|
||||
.getElementById('projectList')
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch Projects:', error);
|
||||
});
|
||||
};
|
||||
interface Props {
|
||||
projects: IProjectThumbResponseData[];
|
||||
locale: TLocales;
|
||||
}
|
||||
|
||||
export default function ProjectsGridView({ projects, locale }: Props) {
|
||||
return (
|
||||
<>
|
||||
{projects?.length ? <ProjectsGrid projects={projects} /> : <></>}
|
||||
<Pagination {...pagination} onPageChange={fetchPage} />
|
||||
{projects?.map((project) => (
|
||||
<ProjectThumb key={project.id} project={project} locale={locale} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { FreeMode } from 'swiper/modules';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/free-mode';
|
||||
import ProjectThumb from './ProjectThumb';
|
||||
import { TLocales } from '@/models/layout';
|
||||
import { IProjectThumbResponseData } from '@/app/api/projects/data';
|
||||
|
||||
export default function ProjectsGridSlider({
|
||||
projects,
|
||||
locale,
|
||||
className = '',
|
||||
}: {
|
||||
projects: IProjectThumbResponseData[];
|
||||
locale: TLocales;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Swiper
|
||||
modules={[FreeMode]}
|
||||
freeMode
|
||||
spaceBetween={16}
|
||||
breakpoints={{
|
||||
0: { slidesPerView: 1.1 },
|
||||
768: { slidesPerView: projects.length, spaceBetween: 24 },
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{projects.map((project) => (
|
||||
<SwiperSlide key={project.id}>
|
||||
<ProjectThumb project={project} locale={locale} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||