connected backend to service page and detail
This commit is contained in:
@@ -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 />
|
||||||
|
|||||||
280
app/[locale]/services/detail/page.tsx
Normal file
280
app/[locale]/services/detail/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
72
components/pages/services/empty.tsx
Normal file
72
components/pages/services/empty.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
components/pages/services/loading.tsx
Normal file
46
components/pages/services/loading.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
206
components/pages/services/ourService.tsx
Normal file
206
components/pages/services/ourService.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
27
lib/api/demoapi/operationalSystems.ts
Normal file
27
lib/api/demoapi/operationalSystems.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "Отправьте нам свой номер",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
BIN
public/images/services/foam.jpg
Normal file
BIN
public/images/services/foam.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
public/images/services/gss.webp
Normal file
BIN
public/images/services/gss.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
public/images/services/sprinkler.jpg
Normal file
BIN
public/images/services/sprinkler.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -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) =>
|
||||||
|
|||||||
14
store/useService.ts
Normal file
14
store/useService.ts
Normal 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,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user