connected backend to service page and detail

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-02-16 15:57:19 +05:00
parent 91fe13f9bf
commit 1d34ea1d47
21 changed files with 820 additions and 158 deletions

View File

@@ -61,7 +61,7 @@ export default function SlugPage() {
const features = product.features.map((item: any) => item.name); const features = product.features.map((item: any) => item.name);
return ( return (
<div className="min-h-screen bg-[#1e1d1c] px-4 md:px-8"> <div className="min-h-screen bg-[#1e1d1c] px-4 md:px-8 pb-35">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<div className="pt-30 pb-10"> <div className="pt-30 pb-10">
<Breadcrumb /> <Breadcrumb />

View File

@@ -0,0 +1,280 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import {
getOperationalSystems,
SystemFeature,
} from "@/lib/api/demoapi/operationalSystems";
import { Breadcrumb } from "@/components/breadCrumb";
export default function OperationalSystemsPage() {
const t = useTranslations("operationalSystems");
const [data, setData] = useState<SystemFeature[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
// Demo data - fallback ma'lumotlar
const demoData: SystemFeature[] = [
{
id: "1",
title: t("systems.sprinkler.title"),
shortDesc:t("systems.sprinkler.short-desc"),
description: t("systems.sprinkler.description"),
features: [
t("systems.sprinkler.features.0"),
t("systems.sprinkler.features.1"),
t("systems.sprinkler.features.2"),
t("systems.sprinkler.features.3"),
],
image: "/images/services/sprinkler.jpg",
},
];
const fetchData = async () => {
try {
setLoading(true);
setError(false);
const result = await getOperationalSystems();
// Agar backend ma'lumot qaytarsa
if (result && result.length > 0) {
setData(result);
} else {
// Backend bo'sh ma'lumot qaytarsa, demo ma'lumotlarni ishlatamiz
setData(demoData);
}
} catch (err) {
setError(true);
// Xatolik bo'lsa, demo ma'lumotlarni ishlatamiz
setData(demoData);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
// Animation variants
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.22, 1, 0.36, 1] as const,
},
},
};
const cardVariants = {
hidden: { opacity: 0, scale: 0.95 },
visible: {
opacity: 1,
scale: 1,
transition: {
duration: 0.5,
ease: [0.22, 1, 0.36, 1] as const,
},
},
};
// Loading state
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center space-y-6"
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-16 h-16 border-4 border-red-500 border-t-transparent rounded-full mx-auto"
/>
<p className="text-white text-xl font-medium font-unbounded">
{t("loading")}
</p>
</motion.div>
</div>
);
}
// Error state with retry
if (error && !data) {
return (
<div className="min-h-screen flex items-center justify-center bg-black px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center space-y-6 max-w-md"
>
<div className="text-7xl"></div>
<p className="text-white text-xl font-medium">{t("error")}</p>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
className="px-6 py-3 bg-red-500 text-white rounded-lg font-medium hover:bg-red-600 transition-colors"
>
{t("retry")}
</motion.button>
</motion.div>
</div>
);
}
return (
<div className="pt-20 md:pt-30 pb-35 max-w-6xl mx-auto w-full px-4">
{/* Header */}
{/* <motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="flex md:flex-row flex-col items-start justify-between gap-6 md:gap-8 w-full mb-16 md:mb-24"
>
<motion.div variants={itemVariants} className="space-y-4 md:space-y-8 flex-1">
<h1 className="bg-linear-to-br from-white via-white to-gray-400 text-transparent bg-clip-text text-3xl md:text-4xl lg:text-5xl font-semibold font-almarai">
{t('title')}
</h1>
<p className="text-white font-medium font-unbounded text-base sm:text-lg md:text-xl max-w-2xl leading-relaxed">
{t('subtitle')}
</p>
</motion.div>
<motion.div
variants={itemVariants}
whileHover={{ scale: 1.05, rotate: 3 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="shrink-0 relative"
>
<Image
src="/images/home/redShlang.png"
alt="Fire hose"
fill
priority
className="w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 lg:w-52 lg:h-52 object-contain"
/>
</motion.div>
</motion.div> */}
<div className="mb-5">
<Breadcrumb />
</div>
{/* Main Content */}
{!data || data.length === 0 ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-20"
>
<p className="text-gray-400 text-xl font-unbounded">{t("noData")}</p>
</motion.div>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-12 md:space-y-20"
>
{data.map((system, index) => (
<motion.div
key={system.id}
variants={cardVariants}
whileHover={{ y: -8 }}
className={`flex flex-col gap-8 md:gap-12 items-center`}
>
{/* Image Section */}
<motion.div
whileHover={{ scale: 1.03 }}
transition={{ duration: 0.3 }}
className="w-full relative h-64 md:h-80 lg:h-96 rounded-xl overflow-hidden"
>
<Image
src={system.image}
alt={system.title}
fill
className="object-cover w-full h-auto"
/>
<div className="absolute inset-0 bg-linear-to-t from-black/10 to-transparent" />
</motion.div>
{/* Content Section */}
<div className="w-full space-y-6">
<motion.h2
initial={{ opacity: 0, x: index % 2 === 0 ? -20 : 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-2xl md:text-3xl lg:text-4xl font-bold text-white font-almarai"
>
{system.title}
</motion.h2>
<motion.p
initial={{ opacity: 0, x: index % 2 === 0 ? -20 : 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-gray-300 text-sm md:text-base leading-relaxed font-unbounded"
>
{system.shortDesc}
</motion.p>
<motion.p
initial={{ opacity: 0, x: index % 2 === 0 ? -20 : 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-gray-300 text-sm md:text-base leading-relaxed font-unbounded"
>
{system.description}
</motion.p>
<div className="space-y-4">
<ul className="space-y-3">
{system.features.map((feature, featureIndex) => (
<motion.li
key={featureIndex}
initial={{ opacity: 0, x: index % 2 === 0 ? -20 : 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{
duration: 0.5,
delay: 0.3 + featureIndex * 0.1,
}}
className="flex items-start gap-3 text-gray-300"
>
<span className="text-sm md:text-base font-unbounded">
{feature}
</span>
</motion.li>
))}
</ul>
</div>
</div>
</motion.div>
))}
</motion.div>
)}
</div>
);
}

View File

@@ -140,6 +140,7 @@ export function Breadcrumb({
if (segment === "special_product") { if (segment === "special_product") {
return product.name; return product.name;
} }
if(segment === 'detail') return '';
return t(`breadcrumb.${segment}`); return t(`breadcrumb.${segment}`);
} catch { } catch {
// Aks holda, segment nomini formatlash // Aks holda, segment nomini formatlash

View File

@@ -61,7 +61,10 @@ export function Footer() {
const handleSubscribe = (e: React.FormEvent) => { const handleSubscribe = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (email) { if (email) {
formRequest.mutate({ number: email }); // Telefon raqamni tozalash (faqat raqamlar)
const cleanPhone = email.replace(/\D/g, "");
console.log("without 998: ",cleanPhone.slice(3))
formRequest.mutate({ number: Number(cleanPhone.slice(3))});
} }
}; };
@@ -239,7 +242,9 @@ export function Footer() {
<div className="border-t border-gray-800 px-4 py-8"> <div className="border-t border-gray-800 px-4 py-8">
<div className="mx-auto max-w-6xl"> <div className="mx-auto max-w-6xl">
<div className="font-almarai flex flex-col justify-center gap-4 text-lg text-gray-400 md:flex-row md:items-center"> <div className="font-almarai flex flex-col justify-center gap-4 text-lg text-gray-400 md:flex-row md:items-center">
<a href="http://felix-its.uz/" className="hover:text-red-600">{t("footer.create", { name: "Felix-its.uz" })}</a> <a href="http://felix-its.uz/" className="hover:text-red-600">
{t("footer.create", { name: "Felix-its.uz" })}
</a>
{/* <div className="flex gap-6"> {/* <div className="flex gap-6">
<a href="#terms" className="hover:text-white"> <a href="#terms" className="hover:text-white">
Terms & Conditions Terms & Conditions

View File

@@ -5,12 +5,10 @@ import { ChevronDown, Phone, Menu, X } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import LanguageSelectRadix from "../languageSwitcher"; import LanguageSelectRadix from "../languageSwitcher";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import NavbarLogo from "./navbarLogo/navbarLogo";
export function Navbar() { export function Navbar() {
const locale = useLocale(); const locale = useLocale();
const t = useTranslations(); const t = useTranslations();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
@@ -42,7 +40,12 @@ export function Navbar() {
<Link href={`/${locale}/home`} className="hover:cursor-pointer"> <Link href={`/${locale}/home`} className="hover:cursor-pointer">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className=" flex items-center justify-center"> <div className=" flex items-center justify-center">
<Image src={'/images/IGNUM/PNG/1.@6x.png'} alt="logo image" width={80} height={80} /> <Image
src={"/images/IGNUM/PNG/1.@6x.png"}
alt="logo image"
width={80}
height={80}
/>
</div> </div>
</div> </div>
</Link> </Link>
@@ -103,8 +106,8 @@ export function Navbar() {
</span> </span>
<div> <div>
<div className="text-white text-sm font-bold"> <div className="text-white text-sm font-bold">
<div>+998-55-055-21-21</div>
<div>+998-77-372-21-21</div> <div>+998-77-372-21-21</div>
<div>+998-98-099-21-21</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@ export { Banner } from "./banner";
export { Statistics } from "./statistics"; export { Statistics } from "./statistics";
export { AboutUs } from "./about"; export { AboutUs } from "./about";
export { Video } from "./video"; export { Video } from "./video";
export { OurService } from "./ourService"; export { OurService } from "../services/ourService";
export { Testimonial } from "./testimonal"; export { Testimonial } from "./testimonal";
export { Line } from "./line"; export { Line } from "./line";
export { Blog } from "./blog/blog"; export { Blog } from "./blog/blog";

View File

@@ -1,122 +0,0 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { ChevronRight } from "lucide-react";
import { useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
export function OurService() {
const t = useTranslations();
return (
<div className="bg-[#1e1d1c] py-10 md:py-16 lg:py-20 mb-30">
<div className="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="space-y-4 md:space-y-6">
<div className="font-almarai flex items-center justify-center gap-2 text-base sm:text-lg md:text-xl text-white font-bold">
<DotAnimatsiya />
{t("home.services.title")}
</div>
<h1 className="uppercase font-unbounded text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl tracking-wider lg:tracking-[5px] font-bold bg-linear-to-br from-white via-white to-gray-400 text-transparent bg-clip-text text-center w-full">
{t("home.services.subtitle")}
</h1>
<p className="font-almarai text-center text-sm sm:text-base md:text-lg text-gray-400 max-w-4xl mx-auto px-4">
{t("home.services.description")}
</p>
</div>
{/* cards */}
<div className="max-w-250 w-full mx-auto flex sm:flex-row flex-col items-center gap-5 my-10">
<div className="relative space-y-4 py-6 px-8 rounded-xl sm:w-[55%] w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{t("home.services.services.operation.title")}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{t("home.services.services.operation.description")}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src="/images/home/gruop.png"
alt="images"
width={200}
height={100}
className="object-contain sm:absolute bottom-0 right-2 z-10"
/>
</div>
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl sm:w-[45%] w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{t("home.services.services.suppression.title")}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{t("home.services.services.suppression.description")}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src="/images/home/redShlang.png"
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-4 -right-4 z-10"
/>
</div>
</div>
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-5 mt-5 w-full mx-auto">
<div className="relative rounded-xl sm:w-[40%] w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
<Image
src="/images/home/ambulance.png"
alt="images"
width={300}
height={200}
className="object-contain mt-5"
/>
<div className="space-y-4 py-6 px-8">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{t("home.services.services.safety.title")}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{t("home.services.services.safety.description")}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
</div>
</div>
<div className="sm:w-[60%] w-full">
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{t("home.services.services.monitoring.title")}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{t("home.services.services.monitoring.description")}
</p>
<button className="font-almarai sm:mt-38 mt-0 text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src="/images/home/balon.png"
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
/>
</div>
<div className="py-8 px-8 rounded-xl mt-5 w-full p-5 bg-[linear-gradient(to_top_right,#000000,#190b00,#542604,#8f4308)] flex sm:flex-row flex-col gap-5 items-center justify-between">
<h2 className="font-unbounded sm:text-3xl text-xl font-semibold font-armanai text-white">
{t("home.services.viewMoreServices")}
</h2>
<Link
href="/services"
className="font-almarai shadow-[0px_0px_2px_6px_#a60404ad] bg-red-600 hover:bg-red-700 text-white font-bold sm:py-3 sm:px-8 px-8 py-2 rounded-full transition duration-300 transform hover:scale-105 w-fit"
>
{t("home.services.viewMore")}
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,72 @@
import { useTranslations } from "next-intl";
import { motion } from "framer-motion";
// Empty State Component
export function EmptyServices() {
const t = useTranslations();
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="max-w-250 w-full mx-auto py-20"
>
<div className="bg-linear-to-br from-gray-900 to-gray-800 rounded-2xl p-10 md:p-16 text-center border border-gray-700/50">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="mb-8"
>
<div className="w-24 h-24 mx-auto bg-red-500/10 rounded-full flex items-center justify-center">
<svg
className="w-12 h-12 text-red-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
</div>
</motion.div>
<motion.h3
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="text-2xl md:text-3xl font-bold text-white font-unbounded mb-4"
>
{t("home.services.noData.title") || "Xizmatlar topilmadi"}
</motion.h3>
<motion.p
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="text-gray-400 font-almarai text-base md:text-lg mb-8 max-w-md mx-auto"
>
{t("home.services.noData.description") || "Hozircha hech qanday xizmat mavjud emas. Tez orada yangi xizmatlar qo'shiladi."}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
>
<button
onClick={() => window.location.reload()}
className="font-almarai bg-red-600 hover:bg-red-700 text-white font-semibold py-3 px-8 rounded-full transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-red-500/50"
>
{t("home.services.noData.reload") || "Qayta yuklash"}
</button>
</motion.div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,46 @@
import { motion } from "framer-motion";
// Loading Skeleton Component
function ServiceCardSkeleton() {
return (
<div className="animate-pulse space-y-4 py-6 px-8 rounded-xl bg-linear-to-br from-gray-800 to-gray-900">
<div className="h-6 bg-gray-700 rounded w-3/4"></div>
<div className="h-4 bg-gray-700 rounded w-full"></div>
<div className="h-4 bg-gray-700 rounded w-5/6"></div>
<div className="h-8 bg-gray-700 rounded w-32"></div>
</div>
);
}
// Loading Component
export function ServicesLoading() {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="max-w-250 w-full mx-auto space-y-5"
>
{/* Top Cards Skeleton */}
<div className="flex sm:flex-row flex-col items-center gap-5">
<div className="sm:w-[55%] w-full">
<ServiceCardSkeleton />
</div>
<div className="sm:w-[45%] w-full">
<ServiceCardSkeleton />
</div>
</div>
{/* Bottom Cards Skeleton */}
<div className="flex sm:flex-row flex-col items-start justify-between gap-5">
<div className="sm:w-[40%] w-full">
<ServiceCardSkeleton />
</div>
<div className="sm:w-[60%] w-full space-y-5">
<ServiceCardSkeleton />
<div className="h-24 bg-gray-800 rounded-xl animate-pulse"></div>
</div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,206 @@
"use client"
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import { ChevronRight } from "lucide-react";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { motion } from "framer-motion";
import { ServicesLoading } from "./loading";
import { EmptyServices } from "./empty";
export function OurService() {
const t = useTranslations();
const locale = useLocale();
// get request
const { data, isLoading, isError } = useQuery({
queryKey: ['firesafety'],
queryFn: () => httpClient(endPoints.services.all)
});
console.log("service data: ", data);
// Animation variants
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: [0.22, 1, 0.36, 1] as const,
},
},
};
return (
<div className="bg-[#1e1d1c] py-10 md:py-16 lg:py-20 mb-30">
<div className="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="space-y-4 md:space-y-6"
>
<div className="font-almarai flex items-center justify-center gap-2 text-base sm:text-lg md:text-xl text-white font-bold">
<DotAnimatsiya />
{t("home.services.title")}
</div>
<h1 className="uppercase font-unbounded text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl tracking-wider lg:tracking-[5px] font-bold bg-linear-to-br from-white via-white to-gray-400 text-transparent bg-clip-text text-center w-full">
{t("home.services.subtitle")}
</h1>
<p className="font-almarai text-center text-sm sm:text-base md:text-lg text-gray-400 max-w-4xl mx-auto px-4">
{t("home.services.description")}
</p>
</motion.div>
{/* Conditional Rendering */}
{isLoading ? (
<div className="my-10">
<ServicesLoading />
</div>
) : !data || (Array.isArray(data) && data.length === 0) ? (
<div className="my-10">
<EmptyServices />
</div>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* cards */}
<div className="max-w-250 w-full mx-auto flex sm:flex-row flex-col items-center gap-5 my-10">
<motion.div variants={cardVariants} className="sm:w-[55%] w-full">
<Link
href={`/${locale}/services/detail`}
className="block hover:cursor-pointer relative space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{t("home.services.services.operation.title")}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{t("home.services.services.operation.description")}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src="/images/home/gruop.png"
alt="images"
width={200}
height={100}
className="object-contain sm:absolute bottom-0 right-2 z-10"
/>
</Link>
</motion.div>
<motion.div variants={cardVariants} className="sm:w-[45%] w-full">
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{t("home.services.services.suppression.title")}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{t("home.services.services.suppression.description")}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src="/images/home/redShlang.png"
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-4 -right-4 z-10"
/>
</div>
</motion.div>
</div>
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-5 mt-5 w-full mx-auto">
<motion.div variants={cardVariants} className="sm:w-[40%] w-full">
<Link
href={`/${locale}/services/detail`}
className="block hover:cursor-pointer relative rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<Image
src="/images/home/ambulance.png"
alt="images"
width={300}
height={200}
className="object-contain mt-5"
/>
<div className="space-y-4 py-6 px-8">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{t("home.services.services.safety.title")}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{t("home.services.services.safety.description")}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
</div>
</Link>
</motion.div>
<div className="sm:w-[60%] w-full">
<motion.div variants={cardVariants}>
<Link href={`/${locale}/services/detail`}>
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{t("home.services.services.monitoring.title")}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{t("home.services.services.monitoring.description")}
</p>
<button className="font-almarai sm:mt-38 mt-0 text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src="/images/home/balon.png"
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
/>
</div>
</Link>
</motion.div>
<motion.div
variants={cardVariants}
className="py-8 px-8 rounded-xl mt-5 w-full p-5 bg-[linear-gradient(to_top_right,#000000,#190b00,#542604,#8f4308)] flex sm:flex-row flex-col gap-5 items-center justify-between hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<h2 className="font-unbounded sm:text-3xl text-xl font-semibold font-armanai text-white">
{t("home.services.viewMoreServices")}
</h2>
<Link
href="/services"
className="font-almarai shadow-[0px_0px_2px_6px_#a60404ad] bg-red-600 hover:bg-red-700 text-white font-bold sm:py-3 sm:px-8 px-8 py-2 rounded-full transition duration-300 transform hover:scale-105 w-fit"
>
{t("home.services.viewMore")}
</Link>
</motion.div>
</div>
</div>
</motion.div>
)}
</div>
</div>
);
}

View File

@@ -12,7 +12,7 @@ import { toast } from "react-toastify";
interface FormType { interface FormType {
name: string; name: string;
product: number; product: number;
phone: number; // ✅ String bo'lishi kerak number: number; // ✅ String bo'lishi kerak
} }
export function PriceModal() { export function PriceModal() {
@@ -21,12 +21,12 @@ export function PriceModal() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
phone: "+998 ", number: "+998 ",
}); });
const [errors, setErrors] = useState({ const [errors, setErrors] = useState({
name: "", name: "",
phone: "", number: "",
}); });
const formRequest = useMutation({ const formRequest = useMutation({
@@ -35,7 +35,7 @@ export function PriceModal() {
onSuccess: () => { onSuccess: () => {
setFormData({ setFormData({
name: "", name: "",
phone: "+998 ", number: "+998 ",
}); });
toast.success(t("success") || "Muvaffaqiyatli yuborildi!"); toast.success(t("success") || "Muvaffaqiyatli yuborildi!");
closeModal(); closeModal();
@@ -51,11 +51,11 @@ export function PriceModal() {
if (!isOpen) { if (!isOpen) {
setFormData({ setFormData({
name: "", name: "",
phone: "+998 ", number: "+998 ",
}); });
setErrors({ setErrors({
name: "", name: "",
phone: "", number: '',
}); });
} }
}, [isOpen]); }, [isOpen]);
@@ -90,9 +90,9 @@ export function PriceModal() {
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatPhoneNumber(e.target.value); const formatted = formatPhoneNumber(e.target.value);
setFormData({ ...formData, phone: formatted }); setFormData({ ...formData, number: formatted });
if (errors.phone) { if (errors.number) {
setErrors({ ...errors, phone: "" }); setErrors({ ...errors, number: "" });
} }
}; };
@@ -107,7 +107,7 @@ export function PriceModal() {
const validateForm = () => { const validateForm = () => {
const newErrors = { const newErrors = {
name: "", name: "",
phone: "", number: "",
}; };
// Name validation // Name validation
@@ -116,17 +116,17 @@ export function PriceModal() {
} }
// Phone validation // Phone validation
const phoneNumbers = formData.phone.replace(/\D/g, ""); const phoneNumbers = formData.number.replace(/\D/g, "");
if (phoneNumbers.length !== 12) { if (phoneNumbers.length !== 12) {
newErrors.phone = newErrors.number =
t("validation.phoneRequired") || "To'liq telefon raqam kiriting"; t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
} else if (!phoneNumbers.startsWith("998")) { } else if (!phoneNumbers.startsWith("998")) {
newErrors.phone = newErrors.number =
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam"; t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
} }
setErrors(newErrors); setErrors(newErrors);
return !newErrors.name && !newErrors.phone; return !newErrors.name && !newErrors.number;
}; };
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
@@ -137,11 +137,11 @@ export function PriceModal() {
} }
// Telefon raqamni tozalash (faqat raqamlar) // Telefon raqamni tozalash (faqat raqamlar)
const cleanPhone = formData.phone.replace(/\D/g, ""); const cleanPhone = formData.number.replace(/\D/g, "");
const sendedData: FormType = { const sendedData: FormType = {
name: formData.name, name: formData.name,
phone: Number(cleanPhone), // ✅ String sifatida yuborish number: Number(cleanPhone.slice(3)), // ✅ String sifatida yuborish
product: product?.id || 0, product: product?.id || 0,
}; };
@@ -236,16 +236,16 @@ export function PriceModal() {
type="tel" type="tel"
id="phone" id="phone"
name="phone" name="phone"
value={formData.phone} value={formData.number}
onChange={handlePhoneChange} onChange={handlePhoneChange}
className={`w-full px-4 py-3 bg-[#1e1e1e] border ${ className={`w-full px-4 py-3 bg-[#1e1e1e] border ${
errors.phone ? "border-red-500" : "border-gray-700" errors.number ? "border-red-500" : "border-gray-700"
} rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-red-700 transition`} } rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-red-700 transition`}
placeholder="+998 90 123 45 67" placeholder="+998 90 123 45 67"
maxLength={17} maxLength={17}
/> />
{errors.phone && ( {errors.number && (
<p className="mt-1 text-sm text-red-500">{errors.phone}</p> <p className="mt-1 text-sm text-red-500">{errors.number}</p>
)} )}
</div> </div>

View File

@@ -0,0 +1,27 @@
import axios from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://api.example.com';
export interface SystemFeature {
id: string;
title: string;
shortDesc?:string;
description: string;
features: string[];
image: string;
}
export const getOperationalSystems = async (): Promise<SystemFeature[]> => {
try {
const response = await axios.get(`${API_BASE_URL}/operational-systems`, {
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('Error fetching operational systems:', error);
throw error;
}
};

View File

@@ -225,6 +225,48 @@
"address": "Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39", "address": "Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39",
"create": "Created by {name}" "create": "Created by {name}"
}, },
"operationalSystems": {
"title": "Operating Systems",
"subtitle": "Automatic fire detection and extinguishing systems. Latest technological achievements.",
"loading": "Loading...",
"error": "An error occurred. Please try again.",
"noData": "No data found",
"retry": "Retry",
"features": "Features",
"systems": {
"sprinkler": {
"title": "Sprinkler Fire Suppression System",
"short-desc": "Automatic fire detection and extinguishing systems. The latest achievements in technology.",
"description": "The sprinkler fire suppression system controls and extinguishes fires at an early stage through automatic water spraying. The system activates automatically when temperature rises and operates only in the fire detection area.",
"features": [
"Automatic activation mechanism",
"Covers only the affected area with water",
"Suitable for industrial and commercial buildings",
"Reliable and widely used system"
]
},
"gas": {
"title": "Gas Fire Suppression System",
"description": "The gas fire suppression system extinguishes fires using special inert or chemical gases. This system is used in places where water cannot be used — server rooms, data centers, and areas with electrical equipment.",
"features": [
"Does not damage electrical equipment",
"Fast and effective suppression",
"Leaves no residue",
"Ideal for server and IT rooms"
]
},
"foam": {
"title": "Foam Fire Suppression System",
"description": "The foam fire suppression system effectively extinguishes fires involving flammable liquids and petroleum products. The foam covers the combustible material, blocks oxygen access, and quickly suppresses the flames.",
"features": [
"Effective for flammable liquids",
"Used in oil depots and warehouses",
"Quickly isolates fire",
"High safety level"
]
}
}
},
"rasmlar": "Images", "rasmlar": "Images",
"fotogalereya": "Photo Gallery", "fotogalereya": "Photo Gallery",
"contactTitle": "Send us your phone number", "contactTitle": "Send us your phone number",

View File

@@ -225,6 +225,48 @@
"address": "г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39", "address": "г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39",
"create": "Разработано {name}" "create": "Разработано {name}"
}, },
"operationalSystems": {
"title": "Операционные системы",
"subtitle": "Системы автоматического обнаружения и тушения пожаров. Последние достижения технологий.",
"loading": "Загрузка...",
"error": "Произошла ошибка. Пожалуйста, попробуйте снова.",
"noData": "Данные не найдены",
"retry": "Повторить",
"features": "Характеристики",
"systems": {
"sprinkler": {
"title": "Спринклерная система пожаротушения",
"short-desc":"Системы автоматического обнаружения и тушения пожаров. Новейшие достижения технологий.",
"description": "Спринклерная система пожаротушения контролирует и тушит пожар на начальной стадии путем автоматического распыления воды. Система автоматически активируется при повышении температуры и работает только в зоне обнаружения пожара.",
"features": [
"Автоматический механизм активации",
"Покрывает водой только поврежденную зону",
"Подходит для промышленных и торговых зданий",
"Надежная и широко применяемая система"
]
},
"gas": {
"title": "Газовая система пожаротушения",
"description": "Газовая система пожаротушения тушит пожар с помощью специальных инертных или химических газов. Эта система применяется в местах, где нельзя использовать воду — серверные комнаты, дата-центры и помещения с электрооборудованием.",
"features": [
"Не наносит вреда электрооборудованию",
"Быстрое и эффективное тушение",
"Не оставляет остатков",
"Идеально для серверных и IT помещений"
]
},
"foam": {
"title": "Пенная система пожаротушения",
"description": "Пенная система пожаротушения эффективно тушит пожары, связанные с горючими жидкостями и нефтепродуктами. Пена покрывает горючее вещество, перекрывает доступ кислорода и быстро подавляет огонь.",
"features": [
"Эффективна для горючих жидкостей",
"Применяется на нефтебазах и складах",
"Быстро изолирует пожар",
"Высокий уровень безопасности"
]
}
}
},
"rasmlar": "Изображения", "rasmlar": "Изображения",
"fotogalereya": "Фотогалерея", "fotogalereya": "Фотогалерея",
"contactTitle": "Отправьте нам свой номер", "contactTitle": "Отправьте нам свой номер",

View File

@@ -225,6 +225,48 @@
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy", "address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
"create": "{name} - Jamoasi tomonidan ishlab chiqilgan" "create": "{name} - Jamoasi tomonidan ishlab chiqilgan"
}, },
"operationalSystems": {
"title": "Operatsion tizimlar",
"subtitle": "Yong'inni avtomatik aniqlash va o'chirish tizimlari. Texnologiyaning eng so'nggi yutuqlari.",
"loading": "Yuklanmoqda...",
"error": "Xatolik yuz berdi. Iltimos, qaytadan urinib ko'ring.",
"noData": "Ma'lumotlar topilmadi",
"retry": "Qayta urinish",
"features": "Hususiyatlari",
"systems": {
"sprinkler": {
"title": "Sprinklerli yong'in o'chirish tizimi",
"short-desc":"Yong'inni avtomatik aniqlash va o'chirish tizimlari. Texnologiyaning eng so'nggi yutuqlari.",
"description": "Sprinklerli yong'in o'chirish tizimi avtomatik suv purkash orqali yong'inni dastlabki bosqichida nazorat qiladi va o'chiradi. Tizim harorat oshganda avtomatik ishga tushadi va faqat yong'in aniqlangan hududda faoliyat ko'rsatadi.",
"features": [
"Avtomatik ishga tushish mexanizmi",
"Faqat zararlangan hududni suv bilan qoplaydi",
"Sanoat va savdo binolari uchun mos",
"Ishonchli va keng qo'llaniladigan tizim"
]
},
"gas": {
"title": "Gazli yong'in o'chirish tizimi",
"description": "Gazli yong'in o'chirish tizimi maxsus inert yoki kimyoviy gazlar yordamida yong'inni o'chiradi. Bu tizim suv ishlatish mumkin bo'lmagan joylarda — server xonalari, data markazlar va elektr jihozlari mavjud hududlarda qo'llaniladi.",
"features": [
"Elektr jihozlariga zarar yetkazmaydi",
"Tez va samarali o'chirish",
"Qoldiq modda qoldirmaydi",
"Server va IT xonalari uchun ideal"
]
},
"foam": {
"title": "Ko'pikli yong'in o'chirish tizimi",
"description": "Ko'pikli yong'in o'chirish tizimi yonuvchi suyuqliklar va neft mahsulotlari bilan bog'liq yong'inlarni samarali o'chiradi. Ko'pik yonuvchi modda ustini qoplab, kislorod kirishini to'sadi va olovni tezda bostiradi.",
"features": [
"Yonuvchi suyuqliklar uchun samarali",
"Neft bazalari va omborlarda qo'llaniladi",
"Yong'inni tez izolyatsiya qiladi",
"Yuqori xavfsizlik darajasi"
]
}
}
},
"rasmlar": "Rasmlar", "rasmlar": "Rasmlar",
"fotogalereya": "Fotogalereya", "fotogalereya": "Fotogalereya",
"contactTitle": "Bizga raqamingizni yuboring", "contactTitle": "Bizga raqamingizni yuboring",
@@ -262,7 +304,7 @@
"about": "Biz haqimizda", "about": "Biz haqimizda",
"services": "Xizmatlar", "services": "Xizmatlar",
"catalog_page": "Mahsulotlar", "catalog_page": "Mahsulotlar",
"subCategory":"{subCategory}", "subCategory": "{subCategory}",
"contact": "Bog'lanish", "contact": "Bog'lanish",
"blog": "Blog", "blog": "Blog",
"fire-safety": "Yong'in xavfsizligi", "fire-safety": "Yong'in xavfsizligi",
@@ -271,9 +313,9 @@
"installation": "O'rnatish", "installation": "O'rnatish",
"maintenance": "Texnik xizmat" "maintenance": "Texnik xizmat"
}, },
"filter":{ "filter": {
"category":"Kategoriyalar", "category": "Kategoriyalar",
"catalog":"Bo'lim", "catalog": "Bo'lim",
"size":"O'lchamlar" "size": "O'lchamlar"
} }
} }

View File

@@ -6,7 +6,7 @@
"build": "next build", "build": "next build",
"dev": "next dev", "dev": "next dev",
"lint": "eslint .", "lint": "eslint .",
"start": "next start" "start": "next start -p 3909"
}, },
"dependencies": { "dependencies": {
"@formatjs/intl-localematcher": "^0.8.0", "@formatjs/intl-localematcher": "^0.8.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -5,6 +5,10 @@ export const endPoints = {
subCategory: { subCategory: {
byId: (id: number) => `subCategory/?category=${id}`, byId: (id: number) => `subCategory/?category=${id}`,
}, },
services: {
all: "firesafety/",
detail: (id: number) => `/api/firesafety/${id}`,
},
product: { product: {
byCategory: (categoryId: number) => `product/?category=${categoryId}`, byCategory: (categoryId: number) => `product/?category=${categoryId}`,
bySubCategory: (subCategoryId: number) => bySubCategory: (subCategoryId: number) =>
@@ -18,10 +22,10 @@ export const endPoints = {
filter: { filter: {
size: "size/", size: "size/",
sizePageItems: "size/?page_size=500", sizePageItems: "size/?page_size=500",
sizeCategoryId:(id:number)=>`size/?category=${id}&page_size=500`, sizeCategoryId: (id: number) => `size/?category=${id}&page_size=500`,
catalog: "catalog/", catalog: "catalog/",
catalogPageItems: "catalog/?page_size=500", catalogPageItems: "catalog/?page_size=500",
catalogCategoryId:(id:number)=>`catalog/?category=${id}&page_size=500`, catalogCategoryId: (id: number) => `catalog/?category=${id}&page_size=500`,
}, },
post: { post: {
sendNumber: "callBack/", sendNumber: "callBack/",

14
store/useService.ts Normal file
View File

@@ -0,0 +1,14 @@
import { create } from "zustand";
interface ServiceIdZustandType {
serviceId: number;
setServiceId: (serviceId: number) => void;
}
export const useServiceDetail = create<ServiceIdZustandType>((set) => ({
serviceId: 0,
setServiceId: (data: number) =>
set({
serviceId: data,
}),
}));