added reviews
This commit is contained in:
parent
0f4cd2b374
commit
88c1a2f935
|
|
@ -3,7 +3,7 @@ import React from "react";
|
|||
|
||||
const CompanyLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="lg:bg-blueBg bg-transparent">
|
||||
<div className="lg:bg-blueBg bg-transparent pt-12">
|
||||
<div className="container">
|
||||
<div className="flex w-full">
|
||||
<aside className="w-[25%] hidden tab:block">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Events } from "@/components/shared/Events";
|
||||
import { News } from "@/components/shared/News";
|
||||
import { Partners } from "@/components/shared/Partners";
|
||||
import { Reviews } from "@/components/shared/Reviews";
|
||||
import { Slider } from "@/components/shared/Slider";
|
||||
import { Video } from "@/components/shared/Video";
|
||||
import Loader from "@/components/ui/Loader";
|
||||
import { cookies } from "next/headers";
|
||||
import { Suspense } from "react";
|
||||
|
|
@ -18,6 +18,7 @@ export default async function HomePage() {
|
|||
<Events lang={lang} />
|
||||
</Suspense>
|
||||
<News />
|
||||
<Reviews />
|
||||
{/* <Video /> */}
|
||||
<Partners />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -416,6 +416,22 @@ p img {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #d1d5db;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.dot-active {
|
||||
@apply bg-PRIMARY;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply flex h-14 rounded-[2px] p-4 focus:ring-2 border-0 focus:outline-none focus:ring-white transition-all hover:ring-white ring-slate-300 ring-[1px] bg-[#E3E8E5] text-base file:border-0 file:bg-SECONDARY_CONTAINER file:outline-none file:text-sm file:w-fit file:text-ON_SECONDARY_CONTAINER file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
"use client";
|
||||
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import { Autoplay, Pagination } from "swiper/modules";
|
||||
|
||||
import "swiper/css";
|
||||
import "swiper/css/pagination";
|
||||
import { useFetch } from "@/hooks/useFetch";
|
||||
import { ReviewsType } from "@/lib/types/Reviews.type";
|
||||
import { useAppSelector } from "@/redux/hooks";
|
||||
import { selectHeader } from "@/redux/slices/headerSlice";
|
||||
import { Title } from "../ui/title";
|
||||
|
||||
export const Reviews = () => {
|
||||
const { data } = useFetch<ReviewsType>("reviews");
|
||||
|
||||
const {
|
||||
activeLang: { localization },
|
||||
} = useAppSelector(selectHeader);
|
||||
|
||||
const title = localization === "en" ? "Feedback" : "Обратная связь";
|
||||
|
||||
return (
|
||||
<section className="container my-20">
|
||||
<Title text={title} className="!text-center mb-10" />
|
||||
|
||||
<div className="max-w-[710px] mx-auto">
|
||||
<Swiper
|
||||
modules={[Autoplay, Pagination]}
|
||||
autoplay={{
|
||||
delay: 6000,
|
||||
disableOnInteraction: false,
|
||||
}}
|
||||
speed={1000}
|
||||
pagination={{
|
||||
clickable: true,
|
||||
el: ".custom-pagination",
|
||||
bulletClass:
|
||||
"dot transition-all duration-300 bg-gray-300 rounded-full w-3 h-3 mx-1",
|
||||
bulletActiveClass: "dot-active !bg-PRIMARY scale-125",
|
||||
}}
|
||||
spaceBetween={50}
|
||||
slidesPerView={1}
|
||||
className="testimonials-swiper"
|
||||
>
|
||||
{data?.data?.map((item, i) => (
|
||||
<SwiperSlide key={item?.id ?? i}>
|
||||
<article className="relative flex flex-col text-center gap-3 justify-center items-center min-h-[300px]">
|
||||
{/* Левая кавычка */}
|
||||
<div className="md:block hidden">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#059784"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="absolute left-0 rotate-180 md:bottom-32 bottom-60"
|
||||
>
|
||||
<path d="M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" />
|
||||
<path d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" />
|
||||
</svg>
|
||||
|
||||
{/* Правая кавычка */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#059784"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="absolute right-0 bottom-2"
|
||||
>
|
||||
<path d="M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" />
|
||||
<path d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="size-[100px]">
|
||||
{item?.image?.path && (
|
||||
<img
|
||||
src={item.image.path}
|
||||
alt={item.name}
|
||||
className="size-[100px] object-contain bg-white rounded-full border border-gray-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-bold text-base">{item.name}</h3>
|
||||
<h4 className="text-base text-text-secondary">
|
||||
{item.job_title}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<p className="text-base text-text-secondary md:px-14">
|
||||
{item.text}
|
||||
</p>
|
||||
</article>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
|
||||
{/* Кастомная пагинация */}
|
||||
<div className="custom-pagination flex justify-center items-center mt-10"></div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +1,20 @@
|
|||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
export const Title = ({ text }: { text: string | boolean }) => {
|
||||
export const Title = ({
|
||||
text,
|
||||
className,
|
||||
}: {
|
||||
text: string | boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<h2 className={`text-[26px] sm:text-[34px] leading-[115%] font-normal`}>
|
||||
<h2
|
||||
className={clsx(
|
||||
`text-[26px] sm:text-[34px] leading-[115%] font-normal`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</h2>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
"use client";
|
||||
|
||||
import { baseAPI } from "@/lib/API";
|
||||
import { useAppSelector } from "@/redux/hooks";
|
||||
import { selectHeader } from "@/redux/slices/headerSlice";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface FetchState<T> {
|
||||
data: T | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const useFetch = <T>(
|
||||
url: string,
|
||||
options?: RequestInit
|
||||
): FetchState<T> => {
|
||||
const [state, setState] = useState<FetchState<T>>({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const {
|
||||
activeLang: { localization },
|
||||
} = useAppSelector(selectHeader);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setState((prev) => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
const response = await fetch(baseAPI + url, {
|
||||
headers: {
|
||||
"Accept-Language": localization,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as T;
|
||||
|
||||
setState({
|
||||
data: result,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
setState({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [url, localization]); // Добавьте options в зависимости, если они динамические
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
// Пример использования
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
export interface Image {
|
||||
id: number;
|
||||
disk_name: string;
|
||||
file_name: string;
|
||||
file_size: number;
|
||||
content_type: string;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
field: string;
|
||||
sort_order: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
path: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
// Тип для элемента отзыва
|
||||
export interface Review {
|
||||
id: number;
|
||||
name: string;
|
||||
job_title: string;
|
||||
text: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
image: Image | null;
|
||||
}
|
||||
|
||||
// Основной тип ответа API
|
||||
export interface ReviewsType {
|
||||
status: "success" | "error";
|
||||
data: Review[] | null;
|
||||
}
|
||||
Loading…
Reference in New Issue