added reviews

This commit is contained in:
Batyr 2025-11-21 15:53:33 +05:00
parent 0f4cd2b374
commit 88c1a2f935
7 changed files with 249 additions and 4 deletions

View File

@ -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">

View File

@ -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>

View File

@ -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;
}

View File

@ -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>
);
};

View File

@ -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>
);

69
hooks/useFetch.ts Normal file
View File

@ -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;
}

32
lib/types/Reviews.type.ts Normal file
View File

@ -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;
}