change
This commit is contained in:
parent
506a03318e
commit
dbf2291534
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import clsx from "clsx";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
import triangle from "@/public/assets/icons/drop-icon.svg";
|
||||
import { useAppSelector, useAppDispatch } from "@/redux/hooks";
|
||||
import { activeLangType, selectHeader } from "@/redux/slices/headerSlice";
|
||||
import { setActiveLang } from "@/redux/slices/headerSlice";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export const lang = [
|
||||
{
|
||||
title: "Русский",
|
||||
localization: "ru",
|
||||
},
|
||||
// {
|
||||
// title: 'Tm',
|
||||
// localization: 'tm',
|
||||
// },
|
||||
{
|
||||
title: "English",
|
||||
localization: "en",
|
||||
},
|
||||
];
|
||||
|
||||
export const LangMenu = () => {
|
||||
const { activeLang } = useAppSelector(selectHeader);
|
||||
const { refresh } = useRouter();
|
||||
const [active, setActive] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const setLang = (lang: activeLangType) => {
|
||||
// Устанавливаем cookie через document.cookie
|
||||
document.cookie = `lang=${lang.localization}; path=/; max-age=31536000`;
|
||||
dispatch(setActiveLang(lang));
|
||||
refresh();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const cookieLang = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("lang="))
|
||||
?.split("=")[1];
|
||||
|
||||
if (cookieLang) {
|
||||
const foundLang = lang.find((l) => l.localization === cookieLang);
|
||||
if (foundLang) dispatch(setActiveLang(foundLang));
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
setActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", handleClick);
|
||||
|
||||
return () => document.removeEventListener("click", handleClick);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="relative w-[90px] cursor-pointer flex items-center gap-x-5"
|
||||
onClick={() => {
|
||||
setActive(!active);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center px-[12px]">
|
||||
<p>{activeLang.title}</p>
|
||||
<Image
|
||||
src={triangle}
|
||||
alt="arrow"
|
||||
className={clsx("transition-rotate duration-300 img-auto", {
|
||||
"rotate-180": active,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{active && (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
}}
|
||||
className="absolute rounded-[4px] w-[112px] overflow-hidden custom-shadow z-10 flex flex-col top-[27px] bg-[#EEF1F0] transition-all duration-300"
|
||||
>
|
||||
{lang
|
||||
.filter((item) => item.title !== activeLang.title)
|
||||
.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => setLang(item)}
|
||||
className={clsx(
|
||||
"p-3 pr-[22px] py-4 text-extraSm transition-all",
|
||||
{
|
||||
"hover:bg-ON_SECONDARY_CONTAINER/[8%]":
|
||||
item.title === item.title,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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