classify web

This commit is contained in:
Husanjonazamov
2026-02-24 12:52:49 +05:00
commit 64af77101f
310 changed files with 45449 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
"use client";
import { getPlaceholderImage } from "@/redux/reducer/settingSlice";
import Image from "next/image";
import { useState } from "react";
import { useSelector } from "react-redux";
const CustomImage = ({ src, alt, loading = "lazy", ...props }) => {
const placeholderImage = useSelector(getPlaceholderImage);
const fallback = "/assets/Transperant_Placeholder.png";
// Initial source can be string OR object (StaticImageData)
const initialSrc =
(src && (typeof src === "string" ? src.trim() : src)) ||
(placeholderImage && placeholderImage.trim?.()) ||
fallback;
const [imgSrc, setImgSrc] = useState(initialSrc);
const handleError = () => {
if (
imgSrc !== placeholderImage &&
typeof placeholderImage === "string" &&
placeholderImage.trim()
) {
setImgSrc(placeholderImage);
} else if (imgSrc !== fallback) {
setImgSrc(fallback);
}
};
return (
<Image
src={imgSrc}
alt={alt}
onError={handleError}
loading={loading} // Dynamic loading: defaults to "lazy" if not provided
{...props} // width, height, className etc can still be passed
/>
);
};
export default CustomImage;

View File

@@ -0,0 +1,29 @@
"use client";
import { getCurrentLangCode } from "@/redux/reducer/languageSlice";
import { getDefaultLanguageCode } from "@/redux/reducer/settingSlice";
import Link from "next/link";
import { useSelector } from "react-redux";
const CustomLink = ({ href, children, ...props }) => {
const defaultLangCode = useSelector(getDefaultLanguageCode);
const currentLangCode = useSelector(getCurrentLangCode);
const langCode = currentLangCode || defaultLangCode;
// Split hash (#) safely from href
const [baseHref, hash = ""] = href.split("#");
// Append lang param safely
const separator = baseHref.includes("?") ? "&" : "?";
const newHref = `${baseHref}${separator}lang=${langCode}${
hash ? `#${hash}` : ""
}`;
return (
<Link href={newHref} {...props}>
{children}
</Link>
);
};
export default CustomLink;

View File

@@ -0,0 +1,11 @@
"use client";
import { usePathname } from "next/navigation";
import LandingHeader from "../PagesComponent/LandingPage/LandingHeader";
import HomeHeader from "../PagesComponent/Home/HomeHeader";
const Header = () => {
const pathname = usePathname();
return pathname === "/landing" ? <LandingHeader /> : <HomeHeader />;
};
export default Header;

View File

@@ -0,0 +1,218 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
CurrentLanguageData,
setCurrentLanguage,
} from "@/redux/reducer/languageSlice";
import { getCityData, saveCity } from "@/redux/reducer/locationSlice";
import { getIsPaidApi, settingsData } from "@/redux/reducer/settingSlice";
import { isEmptyObject, updateStickyNoteTranslations } from "@/utils";
import { getLanguageApi, getLocationApi } from "@/utils/api";
import {
setHasFetchedCategories,
setHasFetchedSystemSettings,
} from "@/utils/getFetcherStatus";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { toast } from "sonner";
import CustomImage from "./CustomImage";
const LanguageDropdown = () => {
const IsPaidApi = useSelector(getIsPaidApi);
const router = useRouter();
const dispatch = useDispatch();
const pathname = usePathname();
const location = useSelector(getCityData);
const settings = useSelector(settingsData);
const CurrentLanguage = useSelector(CurrentLanguageData);
const currentLangCode = CurrentLanguage?.code;
const languages = settings && settings?.languages;
const isRTL = CurrentLanguage.rtl;
const searchParams = useSearchParams();
const langCode = searchParams?.get("lang");
const params = new URLSearchParams(searchParams.toString());
const setDefaultLanguage = async () => {
try {
params.set("lang", settings?.default_language.toLowerCase());
router.push(`${pathname}?${params.toString()}`, { scroll: false });
const language_code = settings?.default_language;
const res = await getLanguageApi.getLanguage({
language_code,
type: "web",
});
if (res?.data?.error === false) {
dispatch(setCurrentLanguage(res?.data?.data));
document.documentElement.lang =
res?.data?.data?.code?.toLowerCase() ||
settings?.default_language.toLowerCase();
} else {
toast.error(res?.data?.message);
}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
// Check if Redux language is empty or invalid
if (isEmptyObject(CurrentLanguage)) {
setDefaultLanguage();
return;
}
// If URL has lang parameter and languages are loaded, check if valid and update if needed
if (langCode && languages.length > 0) {
const urlLang = languages.find(
(lang) => lang.code?.toUpperCase() === langCode.toUpperCase()
);
if (
urlLang &&
currentLangCode?.toUpperCase() !== urlLang.code.toUpperCase()
) {
getLanguageData(urlLang.code);
return;
}
}
// Check if current language code is no longer valid (language was removed from settings)
if (languages && !languages.some((lang) => lang.code === currentLangCode)) {
setDefaultLanguage();
return;
}
if (!langCode) {
params.set("lang", currentLangCode);
router.push(`${pathname}?${params.toString()}`, { scroll: false });
}
}, [langCode]);
const getLanguageData = async (
language_code = settings?.default_language
) => {
try {
const res = await getLanguageApi.getLanguage({
language_code,
type: "web",
});
if (res?.data?.error === false) {
dispatch(setCurrentLanguage(res?.data?.data));
getLocationAfterLanguageChange(language_code);
document.documentElement.lang =
res?.data?.data?.code?.toLowerCase() || "en";
setHasFetchedSystemSettings(false);
setHasFetchedCategories(false);
updateStickyNoteTranslations();
} else {
toast.error(res?.data?.message);
}
} catch (error) {
console.log(error);
}
};
const getLocationAfterLanguageChange = async (language_code) => {
if (IsPaidApi) {
return;
}
// If no country/state/city/area stored, skip API call
if (
!location?.country &&
!location?.state &&
!location?.city &&
!location?.area
) {
return;
}
const response = await getLocationApi.getLocation({
lat: location?.lat,
lng: location?.long,
lang: language_code,
});
if (response?.data.error === false) {
const result = response?.data?.data;
const updatedLocation = {};
if (location?.country) updatedLocation.country = result?.country;
if (location?.state) updatedLocation.state = result?.state;
if (location?.city) updatedLocation.city = result?.city;
if (location?.area) {
updatedLocation.area = result?.area;
updatedLocation.areaId = result?.area_id;
}
updatedLocation.lat = location?.lat;
updatedLocation.long = location?.long;
// ✅ Dynamically build formattedAddress only with existing parts
const parts = [];
if (location?.area) parts.push(result?.area_translation);
if (location?.city) parts.push(result?.city_translation);
if (location?.state) parts.push(result?.state_translation);
if (location?.country) parts.push(result?.country_translation);
updatedLocation.address_translated = parts.filter(Boolean).join(", ");
saveCity(updatedLocation);
}
};
const handleLanguageSelect = (id) => {
const lang = languages?.find((item) => item.id === Number(id));
if (CurrentLanguage.id === lang.id) {
return;
}
params.set("lang", lang.code.toLowerCase()); // Store language code
// Push new URL with lang param
router.push(`${pathname}?${params.toString()}`, { scroll: false });
getLanguageData(lang?.code);
};
return (
<DropdownMenu>
<DropdownMenuTrigger className="border rounded-full py-2 px-4">
<div className="flex items-center gap-1">
<CustomImage
key={CurrentLanguage?.id}
src={CurrentLanguage?.image}
alt={CurrentLanguage?.name || "language"}
width={20}
height={20}
className="rounded-full"
/>
<span>{CurrentLanguage?.code}</span>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
className="min-w-0 max-h-[250px] overflow-y-auto"
align={isRTL ? "start" : "end"}
>
{languages &&
languages.map((lang) => (
<DropdownMenuItem
key={lang?.id}
onClick={() => handleLanguageSelect(lang.id)}
className="cursor-pointer"
>
<CustomImage
src={lang?.image}
alt={lang.name || "english"}
width={20}
height={20}
className="rounded-full"
/>
<span>{lang.code}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};
export default LanguageDropdown;

View File

@@ -0,0 +1,12 @@
const Loader = () => {
return (
<div className="h-screen flex items-center justify-center">
<div className="relative w-12 h-12">
<div className="absolute w-12 h-12 bg-primary rounded-full animate-ping"></div>
<div className="absolute w-12 h-12 bg-primary rounded-full animate-ping delay-1000"></div>
</div>
</div>
);
};
export default Loader;

View File

@@ -0,0 +1,89 @@
import { useSelector } from "react-redux";
import {
getDefaultLatitude,
getDefaultLongitude,
} from "@/redux/reducer/settingSlice";
import { useEffect, useRef } from "react";
import { MapContainer, Marker, TileLayer, useMapEvents } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
// Fix Leaflet default marker icon issue
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
iconUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
shadowUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
});
const MapClickHandler = ({ onMapClick }) => {
useMapEvents({
click: (e) => {
onMapClick(e.latlng);
},
});
return null;
};
const MapComponent = ({ getLocationWithMap, location }) => {
const latitude = useSelector(getDefaultLatitude);
const longitude = useSelector(getDefaultLongitude);
const mapRef = useRef();
const position = {
lat: Number(location?.lat) || latitude,
lng: Number(location?.long) || longitude,
};
useEffect(() => {
if (mapRef.current && position.lat && position.lng) {
mapRef.current.flyTo(
[position.lat, position.lng],
mapRef.current.getZoom()
);
}
}, [position?.lat, position?.lng]);
const containerStyle = {
width: "100%",
height: "400px",
zIndex: 0,
};
const handleMapClick = (latlng) => {
if (getLocationWithMap) {
getLocationWithMap({
lat: latlng.lat,
lng: latlng.lng,
});
}
};
return (
<>
<MapContainer
style={containerStyle}
center={[position?.lat, position?.lng]}
zoom={6}
ref={mapRef}
whenCreated={(mapInstance) => {
mapRef.current = mapInstance;
}}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapClickHandler onMapClick={handleMapClick} />
{position?.lat && position?.lng && (
<Marker position={[position?.lat, position?.lng]}></Marker>
)}
</MapContainer>
</>
);
};
export default MapComponent;

View File

@@ -0,0 +1,83 @@
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer"
import { settingsData } from "@/redux/reducer/settingSlice";
import { t } from "@/utils"
import { usePathname } from "next/navigation";
import { useSelector } from "react-redux";
import { toast } from "sonner";
const OpenInAppDrawer = ({ isOpenInApp, setIsOpenInApp }) => {
const path = usePathname()
const settings = useSelector(settingsData);
const companyName = settings?.company_name;
const scheme = settings?.deep_link_scheme;
const playStoreLink = settings?.play_store_link;
const appStoreLink = settings?.app_store_link;
function handleOpenInApp() {
var appScheme = `${scheme}://${window.location.hostname}${path}`;
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
var isAndroid = /android/i.test(userAgent);
var isIOS = /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream;
let applicationLink;
if (isAndroid) {
applicationLink = playStoreLink;
} else if (isIOS) {
applicationLink = appStoreLink;
} else {
// Fallback for desktop or other platforms
applicationLink = playStoreLink || appStoreLink;
}
// Attempt to open the app
window.location.href = appScheme;
// Set a timeout to check if app opened
setTimeout(function () {
if (document.hidden || document.webkitHidden) {
// App opened successfully
} else {
// App is not installed, ask user if they want to go to app store
if (confirm(`${companyName} ${t('appIsNotInstalled')} ${isIOS ? t('appStore') : t('playStore')}?`)) {
if (!applicationLink) {
toast.error(`${companyName} ${isIOS ? t('appStore') : t('playStore')} ${t('linkNotAvailable')}`);
return;
}
window.location.href = applicationLink;
}
}
}, 1000);
}
return (
<Drawer open={isOpenInApp} onOpenChange={setIsOpenInApp}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{`${t('viewIn')} ${companyName} ${t('app')}`}</DrawerTitle>
<DrawerDescription>
{t('getTheBestExperienceByOpeningThisInOurMobileApp')}
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button onClick={handleOpenInApp}>
{t('openInApp')}
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}
export default OpenInAppDrawer

View File

@@ -0,0 +1,14 @@
"use client"
const PageLoader = () => {
return (
<div className="h-[calc(100vh-20vh)] flex items-center justify-center">
<div className="relative w-12 h-12">
<div className="absolute w-12 h-12 bg-primary rounded-full animate-ping"></div>
<div className="absolute w-12 h-12 bg-primary rounded-full animate-ping delay-1000"></div>
</div>
</div>
)
}
export default PageLoader

View File

@@ -0,0 +1,113 @@
"use client";
import {
Pagination as PaginationContainer,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
const Pagination = ({
currentPage,
totalPages,
onPageChange,
className = "",
}) => {
const handlePageChange = (page) => {
onPageChange(page);
};
const generatePaginationItems = () => {
const items = [];
if (totalPages <= 6) {
for (let i = 1; i <= totalPages; i++) {
items.push(i);
}
} else {
if (currentPage <= 3) {
for (let i = 1; i <= 4; i++) {
items.push(i);
}
items.push(null); // Ellipsis
items.push(totalPages);
} else if (currentPage >= totalPages - 2) {
items.push(1);
items.push(null); // Ellipsis
for (let i = totalPages - 3; i <= totalPages; i++) {
items.push(i);
}
} else {
items.push(1);
items.push(null); // Ellipsis
items.push(currentPage - 1);
items.push(currentPage);
items.push(currentPage + 1);
items.push(null); // Ellipsis
items.push(totalPages);
}
}
return items;
};
// Don't render if there's only 1 page or no pages
if (totalPages <= 1) return null;
return (
<PaginationContainer className={className}>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) handlePageChange(currentPage - 1);
}}
className={
currentPage === 1 ? "pointer-events-none opacity-50" : ""
}
/>
</PaginationItem>
{generatePaginationItems().map((page, index) =>
page === null ? (
<PaginationItem key={`ellipsis-${index}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={page}>
<PaginationLink
href="#"
onClick={(e) => {
e.preventDefault();
handlePageChange(page);
}}
isActive={page === currentPage}
>
{page}
</PaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages) handlePageChange(currentPage + 1);
}}
className={
currentPage === totalPages ? "pointer-events-none opacity-50" : ""
}
/>
</PaginationItem>
</PaginationContent>
</PaginationContainer>
);
};
export default Pagination;

View File

@@ -0,0 +1,120 @@
import { formatDate, t } from "@/utils";
import { BiBadgeCheck } from "react-icons/bi";
import { FaHeart, FaRegHeart } from "react-icons/fa";
import { manageFavouriteApi } from "@/utils/api";
import { useSelector } from "react-redux";
import { userSignUpData } from "@/redux/reducer/authSlice";
import CustomLink from "@/components/Common/CustomLink";
import { toast } from "sonner";
import { setIsLoginOpen } from "@/redux/reducer/globalStateSlice";
import CustomImage from "./CustomImage";
const ProductCard = ({ item, handleLike }) => {
const userData = useSelector(userSignUpData);
const isJobCategory = Number(item?.category?.is_job_category) === 1;
const translated_item = item.translated_item;
const isHidePrice = isJobCategory
? !item?.formatted_salary_range
: !item?.formatted_price;
const price = isJobCategory
? item?.formatted_salary_range
: item?.formatted_price;
const productLink =
userData?.id === item?.user_id
? `/my-listing/${item?.slug}`
: `/ad-details/${item.slug}`;
const handleLikeItem = async (e) => {
e.preventDefault();
e.stopPropagation();
try {
if (!userData) {
setIsLoginOpen(true);
return;
}
const response = await manageFavouriteApi.manageFavouriteApi({
item_id: item?.id,
});
if (response?.data?.error === false) {
toast.success(response?.data?.message);
handleLike(item?.id);
} else {
toast.error(t("failedToLike"));
}
} catch (error) {
console.log(error);
toast.error(t("failedToLike"));
}
};
return (
<CustomLink
href={productLink}
className="border p-2 rounded-2xl flex flex-col gap-2 h-full"
>
<div className="relative">
<CustomImage
src={item?.image}
width={288}
height={249}
className="w-full aspect-square rounded object-cover"
alt="Product"
/>
{item?.is_feature && (
<div className="flex items-center gap-1 ltr:rounded-tl rtl:rounded-tr py-0.5 px-1 bg-primary absolute top-0 ltr:left-0 rtl:right-0">
<BiBadgeCheck size={16} color="white" />
<p className="text-white text-xs sm:text-sm">{t("featured")}</p>
</div>
)}
<div
onClick={handleLikeItem}
className="absolute h-10 w-10 ltr:right-2 rtl:left-2 top-2 bg-white p-2 rounded-full flex items-center justify-center text-primary"
>
{item?.is_liked ? (
<button>
<FaHeart size={24} className="like_icon" />
</button>
) : (
<button>
<FaRegHeart size={24} className="like_icon" />
</button>
)}
</div>
</div>
<div className="space-between gap-2">
{isHidePrice ? (
<p className="text-sm sm:text-base font-medium line-clamp-1">
{translated_item?.name || item?.name}
</p>
) : (
<p
className="text-sm sm:text-lg font-bold break-all text-balance line-clamp-2"
title={price}
>
{price}
</p>
)}
<p className="text-xs sm:text-sm opacity-65 whitespace-nowrap">
{formatDate(item?.created_at)}&lrm;
</p>
</div>
{!isHidePrice && (
<p className="text-sm sm:text-base font-medium line-clamp-1">
{translated_item?.name || item?.name}
</p>
)}
<p className="text-xs sm:text-sm opacity-65 line-clamp-1">
{item?.translated_address}
</p>
</CustomLink>
);
};
export default ProductCard;

View File

@@ -0,0 +1,17 @@
import { Skeleton } from "../ui/skeleton";
const ProductCardSkeleton = () => {
return (
<div className="border p-2 rounded-2xl flex flex-col gap-2">
<Skeleton className="w-full aspect-square" />
<div className="space-between">
<Skeleton className="w-1/4 h-4" />
<Skeleton className="w-1/4 h-4" />
</div>
<Skeleton className="w-3/4 h-4" />
<Skeleton className="w-2/3 h-4" />
</div>
);
};
export default ProductCardSkeleton;

View File

@@ -0,0 +1,116 @@
import { formatDate, t } from "@/utils";
import { BiBadgeCheck } from "react-icons/bi";
import { FaHeart, FaRegHeart } from "react-icons/fa";
import { manageFavouriteApi } from "@/utils/api";
import { useSelector } from "react-redux";
import { userSignUpData } from "@/redux/reducer/authSlice";
import { toast } from "sonner";
import CustomLink from "@/components/Common/CustomLink";
import { setIsLoginOpen } from "@/redux/reducer/globalStateSlice";
import CustomImage from "./CustomImage";
const ProductHorizontalCard = ({ item, handleLike }) => {
const userData = useSelector(userSignUpData);
const translated_item = item.translated_item;
const productLink =
userData?.id === item?.user_id
? `/my-listing/${item?.slug}`
: `/ad-details/${item.slug}`;
const isJobCategory = Number(item?.category?.is_job_category) === 1;
const isHidePrice = isJobCategory
? !item?.formatted_salary_range
: !item?.formatted_price;
const price = isJobCategory
? item?.formatted_salary_range
: item?.formatted_price;
const handleLikeItem = async (e) => {
e.preventDefault();
e.stopPropagation();
try {
if (!userData) {
setIsLoginOpen(true);
return;
}
const response = await manageFavouriteApi.manageFavouriteApi({
item_id: item?.id,
});
if (response?.data?.error === false) {
toast.success(response?.data?.message);
handleLike(item?.id);
} else {
toast.error(t("failedToLike"));
}
} catch (error) {
console.log(error);
toast.error(t("failedToLike"));
}
};
return (
<CustomLink
href={productLink}
className="border p-2 rounded-md flex items-center gap-2 sm:gap-4 w-full relative"
>
<CustomImage
src={item?.image}
width={219}
height={190}
alt="Product"
className="w-[100px] sm:w-[219px] h-auto aspect-square sm:aspect-[219/190] rounded object-cover"
/>
<div
onClick={handleLikeItem}
className="absolute h-8 w-8 ltr:right-2 rtl:left-2 top-2 bg-white p-1.5 rounded-full flex items-center justify-center text-primary z-10"
>
{item?.is_liked ? (
<button>
<FaHeart size={20} className="like_icon" />
</button>
) : (
<button>
<FaRegHeart size={20} className="like_icon" />
</button>
)}
</div>
<div className="flex flex-col gap-1 sm:gap-2 flex-1 relative min-w-0">
{item?.is_feature && (
<div className="flex items-center gap-1 rounded-md py-0.5 px-1 bg-primary w-fit mb-1">
<BiBadgeCheck size={16} color="white" />
<p className="text-white text-xs sm:text-sm">{t("featured")}</p>
</div>
)}
{!isHidePrice && (
<p className="text-sm sm:text-lg font-bold truncate" title={price}>
{price}
</p>
)}
<p
className="text-xs sm:text-base font-medium line-clamp-1"
title={translated_item?.name || item?.name}
>
{translated_item?.name || item?.name}
</p>
<p className="text-xs sm:text-sm opacity-65 line-clamp-1">
{item?.translated_address}
</p>
<div className="flex justify-end mt-auto">
<p className="text-xs sm:text-sm opacity-65 whitespace-nowrap">
{formatDate(item?.created_at)}&lrm;
</p>
</div>
</div>
</CustomLink>
);
};
export default ProductHorizontalCard;

View File

@@ -0,0 +1,34 @@
import { Skeleton } from "../ui/skeleton";
const ProductHorizontalCardSkeleton = () => {
return (
<div className="border p-2 rounded-md flex gap-2 sm:gap-4 w-full relative">
{/* Product image skeleton */}
<Skeleton className="w-[100px] sm:w-[219px] h-auto aspect-square sm:aspect-[219/190] rounded" />
{/* Like button skeleton */}
<Skeleton className="absolute h-8 w-8 ltr:right-2 rtl:left-2 top-2 rounded-full" />
<div className="flex flex-col gap-1 sm:gap-2 flex-1">
{/* Featured badge skeleton */}
<Skeleton className="h-6 w-24 rounded-md mb-1" />
{/* Price skeleton */}
<Skeleton className="h-5 sm:h-6 w-24 rounded" />
{/* Name skeleton */}
<Skeleton className="h-4 sm:h-5 w-3/4 rounded" />
{/* Location skeleton */}
<Skeleton className="h-3 sm:h-4 w-1/2 rounded" />
{/* Date skeleton */}
<div className="flex justify-end mt-auto">
<Skeleton className="h-3 sm:h-4 w-24 rounded" />
</div>
</div>
</div>
);
};
export default ProductHorizontalCardSkeleton;

View File

@@ -0,0 +1,46 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { t } from "@/utils";
import { Loader2 } from "lucide-react";
const ReusableAlertDialog = ({
open,
onCancel,
onConfirm,
title,
description,
cancelText = t("cancel"),
confirmText = t("confirm"),
confirmDisabled = false,
}) => {
return (
<AlertDialog open={open}>
<AlertDialogContent onInteractOutside={(e) => e.preventDefault()}>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
{description && (
<AlertDialogDescription asChild={typeof description !== "string"}>
{typeof description === "string" ? description : description}
</AlertDialogDescription>
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>{cancelText}</AlertDialogCancel>
<AlertDialogAction disabled={confirmDisabled} onClick={onConfirm}>
{confirmDisabled ? <Loader2 className="w-4 h-4 animate-spin" /> : confirmText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
export default ReusableAlertDialog;

View File

@@ -0,0 +1,107 @@
"use client";
import {
FacebookIcon,
FacebookShareButton,
TwitterShareButton,
WhatsappIcon,
WhatsappShareButton,
XIcon,
} from "react-share";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { IoShareSocialOutline } from "react-icons/io5";
import { CiLink } from "react-icons/ci";
import { toast } from "sonner";
import { t } from "@/utils/index";
import { useState } from "react";
import { useSearchParams } from "next/navigation";
import { getIsRtl } from "@/redux/reducer/languageSlice";
import { useSelector } from "react-redux";
const ShareDropdown = ({ url, title, headline, companyName, className }) => {
const [open, setOpen] = useState(false);
const searchParams = useSearchParams();
const langCode = searchParams.get("lang");
const isRTL = useSelector(getIsRtl);
const handleCopyUrl = async () => {
try {
await navigator.clipboard.writeText(url + "?share=true&lang=" + langCode);
toast.success(t("copyToClipboard"));
setOpen(false);
} catch (error) {
console.error("Error copying to clipboard:", error);
}
};
const handleShare = () => {
setOpen(false);
};
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<button className={className}>
<IoShareSocialOutline size={20} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align={isRTL ? "start" : "end"}>
<DropdownMenuItem>
<FacebookShareButton
className="w-full"
url={url}
hashtag={title}
onClick={handleShare}
>
<div className="flex items-center gap-2">
<FacebookIcon className="!size-6" round />
<span>{t("facebook")}</span>
</div>
</FacebookShareButton>
</DropdownMenuItem>
<DropdownMenuItem>
<TwitterShareButton
className="w-full"
url={url}
title={headline}
onClick={handleShare}
>
<div className="flex items-center gap-2">
<XIcon className="!size-6" round />
<span>X</span>
</div>
</TwitterShareButton>
</DropdownMenuItem>
<DropdownMenuItem>
<WhatsappShareButton
className="w-100"
url={url}
title={headline}
hashtag={companyName}
onClick={handleShare}
>
<div className="flex items-center gap-2">
<WhatsappIcon className="!size-6" round />
<span>{t("whatsapp")}</span>
</div>
</WhatsappShareButton>
</DropdownMenuItem>
<DropdownMenuItem>
<button
className="flex items-center gap-2 w-full"
onClick={handleCopyUrl}
>
<CiLink className="!size-6" />
<span>{t("copyLink")}</span>
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default ShareDropdown;

View File

@@ -0,0 +1,17 @@
import { useRef, useEffect } from "react";
const useAutoFocus = () => {
const inputRef = useRef(null);
useEffect(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 500);
}, []);
return inputRef;
};
export default useAutoFocus;

View File

@@ -0,0 +1,27 @@
"use client";
import { getCurrentLangCode } from "@/redux/reducer/languageSlice";
import { getDefaultLanguageCode } from "@/redux/reducer/settingSlice";
import { useRouter } from "next/navigation";
import { useSelector } from "react-redux";
export const useNavigate = () => {
const router = useRouter();
const currentLangCode = useSelector(getCurrentLangCode);
const defaultLangCode = useSelector(getDefaultLanguageCode);
const langCode = currentLangCode || defaultLangCode;
const navigate = (path, options = {}) => {
if (path.includes("?")) {
// Path already has query parameters, add lang parameter
const langParam = langCode ? `&lang=${langCode}` : "";
router.push(`${path}${langParam}`, options);
} else {
// Path has no query parameters, add lang parameter with ?
const langParam = langCode ? `?lang=${langCode}` : "";
router.push(`${path}${langParam}`, options);
}
};
return { navigate };
};