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,141 @@
"use client";
import { useEffect, useState } from "react";
import SellerLsitings from "./SellerLsitings";
import SellerDetailCard from "./SellerDetailCard";
import { getSellerApi } from "@/utils/api";
import { t } from "@/utils";
import SellerRating from "./SellerRating";
import SellerSkeleton from "./SellerSkeleton";
import NoData from "@/components/EmptyStates/NoData";
import Layout from "@/components/Layout/Layout";
import OpenInAppDrawer from "@/components/Common/OpenInAppDrawer";
import BreadCrumb from "@/components/BreadCrumb/BreadCrumb";
import { useSelector } from "react-redux";
import { CurrentLanguageData } from "@/redux/reducer/languageSlice";
const Seller = ({ id, searchParams }) => {
const CurrentLanguage = useSelector(CurrentLanguageData);
const [steps, setSteps] = useState(1);
const [IsNoUserFound, setIsNoUserFound] = useState(false);
const [seller, setSeller] = useState(null);
const [ratings, setRatings] = useState(null);
const [isSellerDataLoading, setIsSellerDataLoading] = useState(false);
const [isLoadMoreReview, setIsLoadMoreReview] = useState(false);
const [reviewHasMore, setReviewHasMore] = useState(false);
const [reviewCurrentPage, setReviewCurrentPage] = useState(1);
const [isOpenInApp, setIsOpenInApp] = useState(false);
const isShare = searchParams?.share == "true" ? true : false;
useEffect(() => {
if (window.innerWidth <= 768 && isShare) {
setIsOpenInApp(true);
}
}, []);
useEffect(() => {
getSeller(reviewCurrentPage);
}, []);
const getSeller = async (page) => {
if (page === 1) {
setIsSellerDataLoading(true);
}
try {
const res = await getSellerApi.getSeller({ id: Number(id), page });
if (res?.data.error && res?.data?.code === 103) {
setIsNoUserFound(true);
} else {
const sellerData = res?.data?.data?.ratings;
if (page === 1) {
setRatings(sellerData);
} else {
setRatings({
...ratings,
data: [...ratings?.data, ...sellerData?.data],
});
}
setSeller(res?.data?.data?.seller);
setReviewCurrentPage(res?.data?.data?.ratings?.current_page);
if (
res?.data?.data?.ratings?.current_page <
res?.data?.data?.ratings?.last_page
) {
setReviewHasMore(true);
}
}
} catch (error) {
console.log(error);
} finally {
setIsSellerDataLoading(false);
setIsLoadMoreReview(false);
}
};
const handleSteps = (step) => {
setSteps(step);
};
if (IsNoUserFound) {
return <NoData name={t("noSellerFound")} />;
}
return (
<Layout>
{isSellerDataLoading ? (
<SellerSkeleton steps={steps} />
) : (
<>
<BreadCrumb title2={seller?.name} />
<div className="container mx-auto mt-6">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4">
<div className="col-span-12 lg:col-span-4">
<SellerDetailCard seller={seller} ratings={ratings} />
</div>
<div className="flex flex-col gap-8 col-span-12 lg:col-span-8">
<div className="p-4 flex items-center gap-4 bg-muted border rounded-md w-full">
<button
onClick={() => handleSteps(1)}
className={`py-2 px-4 rounded-md ${
steps === 1 ? "bg-primary text-white" : ""
}`}
>
{t("liveAds")}
</button>
<button
onClick={() => handleSteps(2)}
className={`py-2 px-4 rounded-md ${
steps === 2 ? "bg-primary text-white" : ""
}`}
>
{t("reviews")}
</button>
</div>
{steps === 1 && <SellerLsitings id={id} />}
{steps === 2 && (
<SellerRating
ratingsData={ratings}
seller={seller}
isLoadMoreReview={isLoadMoreReview}
reviewHasMore={reviewHasMore}
reviewCurrentPage={reviewCurrentPage}
getSeller={getSeller}
/>
)}
</div>
</div>
</div>
</>
)}
<OpenInAppDrawer
isOpenInApp={isOpenInApp}
setIsOpenInApp={setIsOpenInApp}
/>
</Layout>
);
};
export default Seller;

View File

@@ -0,0 +1,114 @@
import { Badge } from "@/components/ui/badge";
import { MdOutlineMailOutline, MdVerifiedUser } from "react-icons/md";
import { IoMdStar } from "react-icons/io";
import { FiPhoneCall } from "react-icons/fi";
import { extractYear, t } from "@/utils";
import { usePathname } from "next/navigation";
import { useSelector } from "react-redux";
import { getCompanyName } from "@/redux/reducer/settingSlice";
import ShareDropdown from "@/components/Common/ShareDropdown";
import CustomLink from "@/components/Common/CustomLink";
import CustomImage from "@/components/Common/CustomImage";
import Link from "next/link";
const SellerDetailCard = ({ seller, ratings }) => {
const pathname = usePathname();
const memberSinceYear = seller?.created_at
? extractYear(seller.created_at)
: "";
const currentUrl = `${process.env.NEXT_PUBLIC_WEB_URL}${pathname}`;
const CompanyName = useSelector(getCompanyName);
const FbTitle = seller?.name + " | " + CompanyName;
return (
<div className="rounded-lg border overflow-hidden">
<div className="flex justify-between items-center p-4 bg-muted">
<h1 className="text-lg font-bold">{t("seller_info")}</h1>
<ShareDropdown
url={currentUrl}
title={FbTitle}
headline={FbTitle}
companyName={CompanyName}
className="rounded-md p-2 border bg-white"
/>
</div>
{(seller?.is_verified === 1 || memberSinceYear) && (
<div className="border-t p-4">
<div className="flex items-center">
{seller?.is_verified === 1 && (
<Badge className="p-1 bg-[#FA6E53] flex items-center gap-1 rounded-md text-white text-sm">
<MdVerifiedUser size={22} />
{t("verified")}
</Badge>
)}
{memberSinceYear && (
<div className="ltr:ml-auto rtl:mr-auto text-sm text-muted-foreground">
{t("memberSince")}: {memberSinceYear}
</div>
)}
</div>
</div>
)}
<div className="border-t flex flex-col justify-center items-center p-4 gap-4">
<CustomImage
src={seller?.profile}
alt="Seller Image"
width={120}
height={120}
className="aspect-square rounded-xl object-cover"
/>
<div className="text-center w-full">
<h3 className="text-xl font-bold">{seller?.name}</h3>
<div className="flex items-center justify-center gap-1 text-sm mt-1">
<IoMdStar />
<span>
{Number(seller?.average_rating).toFixed(2)} |{" "}
{ratings?.data?.length} {t("ratings")}
</span>
</div>
</div>
</div>
{seller?.show_personal_details === 1 &&
(seller?.email || seller?.mobile) && (
<div className="border-t p-4 flex flex-col gap-4">
{seller?.email && (
<div className="flex items-center gap-2">
<div className="p-3 bg-muted rounded-md border">
<MdOutlineMailOutline className="size-4" />
</div>
<CustomLink
href={`mailto:${seller?.email}`}
className="break-all"
>
{seller?.email}
</CustomLink>
</div>
)}
{seller?.mobile && (
<div className="flex items-center gap-2">
<div className="p-3 bg-muted rounded-md border">
<FiPhoneCall className="size-4" />
</div>
<Link
href={`tel:${seller?.mobile}`}
className="break-all"
>
{seller?.mobile}
</Link>
</div>
)}
</div>
)}
</div>
);
};
export default SellerDetailCard;

View File

@@ -0,0 +1,200 @@
import { useEffect, useState } from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TbTransferVertical } from "react-icons/tb";
import { IoGrid } from "react-icons/io5";
import { allItemApi } from "@/utils/api";
import ProductHorizontalCardSkeleton from "@/components/Common/ProductHorizontalCardSkeleton";
import ProductCardSkeleton from "@/components/Common/ProductCardSkeleton";
import ProductCard from "@/components/Common/ProductCard";
import NoData from "@/components/EmptyStates/NoData";
import ProductHorizontalCard from "@/components/Common/ProductHorizontalCard";
import { useSearchParams } from "next/navigation";
import { MdViewStream } from "react-icons/md";
import { Button } from "@/components/ui/button";
import { CurrentLanguageData } from "@/redux/reducer/languageSlice";
import { useSelector } from "react-redux";
import { t } from "@/utils";
const SellerLsitings = ({ id }) => {
const searchParams = useSearchParams();
const view = searchParams.get("view") || "grid";
const sortBy = searchParams.get("sort") || "new-to-old";
const CurrentLanguage = useSelector(CurrentLanguageData);
const [isSellerItemsLoading, setIsSellerItemsLoading] = useState(false);
const [sellerItems, setSellerItems] = useState([]);
const [isSellerItemLoadMore, setIsSellerItemLoadMore] = useState(false);
const [CurrentPage, setCurrentPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
getSellerItems(1);
}, [sortBy, CurrentLanguage.id]);
const getSellerItems = async (page) => {
try {
if (page === 1) {
setIsSellerItemsLoading(true);
}
const res = await allItemApi.getItems({
user_id: id,
sort_by: sortBy,
page,
});
if (page > 1) {
// Append new data to existing sellerItems
setSellerItems((prevItems) => [...prevItems, ...res?.data?.data?.data]);
} else {
// Set new data if CurrentPage is 1 or initial load
setSellerItems(res?.data?.data?.data);
}
setCurrentPage(res?.data?.data?.current_page);
if (res?.data?.data.current_page === res?.data?.data.last_page) {
setHasMore(false); // Check if there's more data
}
} catch (error) {
console.log(error);
} finally {
setIsSellerItemsLoading(false);
setIsSellerItemLoadMore(false);
}
};
const handleLike = (id) => {
const updatedItems = sellerItems.map((item) => {
if (item.id === id) {
return { ...item, is_liked: !item.is_liked };
}
return item;
});
setSellerItems(updatedItems);
};
const handleProdLoadMore = () => {
setIsSellerItemLoadMore(true);
getSellerItems(CurrentPage + 1);
};
const toggleView = (newView) => {
const params = new URLSearchParams(searchParams);
params.set("view", newView);
window.history.pushState(null, "", `?${params.toString()}`);
};
const handleSortBy = (value) => {
const params = new URLSearchParams(searchParams);
params.set("sort", value);
window.history.pushState(null, "", `?${params.toString()}`);
};
return (
<>
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
<div className="flex items-center gap-1">
<TbTransferVertical />
<span className="whitespace-nowrap">{t("sortBy")}</span>
</div>
<Select value={sortBy} onValueChange={handleSortBy}>
<SelectTrigger>
<SelectValue placeholder={t("sortBy")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="new-to-old">
{t("newestToOldest")}
</SelectItem>
<SelectItem value="old-to-new">
{t("oldestToNewest")}
</SelectItem>
<SelectItem value="price-high-to-low">
{t("priceHighToLow")}
</SelectItem>
<SelectItem value="price-low-to-high">
{t("priceLowToHigh")}
</SelectItem>
<SelectItem value="popular_items">{t("popular")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => toggleView("grid")}
className={`flex items-center justify-center size-8 sm:size-10 text-muted-foreground transition-colors duration-300 cursor-pointer gap-2 rounded-full ${
view === "grid"
? "bg-primary text-white"
: "hover:bg-black/15 hover:text-black"
}`}
>
<IoGrid className="size-5 sm:size-6" />
</button>
<button
onClick={() => toggleView("list")}
className={`flex items-center justify-center size-8 sm:size-10 text-muted-foreground hover:text-black transition-colors duration-300 cursor-pointer gap-2 rounded-full ${
view === "list"
? "bg-primary text-white"
: "hover:text-black hover:bg-black/15"
}`}
>
<MdViewStream className="size-5 sm:size-6" />
</button>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
{isSellerItemsLoading ? (
Array.from({ length: 12 }).map((_, index) =>
view === "list" ? (
<div className="col-span-12" key={index}>
<ProductHorizontalCardSkeleton />
</div>
) : (
<div key={index} className="col-span-6 lg:col-span-4">
<ProductCardSkeleton />
</div>
)
)
) : sellerItems && sellerItems.length > 0 ? (
sellerItems?.map((item, index) =>
view === "list" ? (
<div className="col-span-12" key={index}>
<ProductHorizontalCard item={item} handleLike={handleLike} />
</div>
) : (
<div className="col-span-6 lg:col-span-4" key={index}>
<ProductCard item={item} handleLike={handleLike} />
</div>
)
)
) : (
<div className="col-span-12">
<NoData name={t("ads")} />
</div>
)}
</div>
{sellerItems && sellerItems.length > 0 && hasMore && (
<div className="text-center mt-6">
<Button
variant="outline"
className="text-sm sm:text-base text-primary w-[256px]"
disabled={isSellerItemsLoading || isSellerItemLoadMore}
onClick={handleProdLoadMore}
>
{isSellerItemLoadMore ? t("loading") : t("loadMore")}
</Button>
</div>
)}
</>
);
};
export default SellerLsitings;

View File

@@ -0,0 +1,39 @@
import { t } from '@/utils';
import RatingsSummary from '../Reviews/RatingsSummary';
import SellerReviewCard from "@/components/PagesComponent/Reviews/SellerReviewCard";
import { Button } from '@/components/ui/button';
import NoData from '@/components/EmptyStates/NoData';
const SellerRating = ({ ratingsData, seller, isLoadMoreReview, reviewHasMore, reviewCurrentPage, getSeller }) => {
return (
ratingsData?.data?.length > 0 ?
<>
<RatingsSummary averageRating={seller?.average_rating} reviews={ratingsData?.data} />
<div className='flex flex-col gap-4 bg-muted p-4 rounded-lg'>
{ratingsData?.data?.map((rating) => (
<SellerReviewCard key={rating.id} rating={rating} />
))}
{
ratingsData?.data?.length > 0 && reviewHasMore && (
<div className="text-center">
<Button
variant="outline"
className="text-sm sm:text-base text-primary w-[256px]"
disabled={isLoadMoreReview}
onClick={() => getSeller(reviewCurrentPage + 1)}
>
{isLoadMoreReview ? t("loading") : t("loadMore")}
</Button>
</div>
)
}
</div>
</>
:
<NoData name={t('reviews')} />
);
};
export default SellerRating;

View File

@@ -0,0 +1,75 @@
import { Skeleton } from '@/components/ui/skeleton';
const SellerSkeleton = ({ steps }) => {
return (
<div className="container mx-auto mt-6">
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
{/* Left Side - Seller Detail Card Skeleton */}
<div className="col-span-12 md:col-span-4">
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="flex flex-col items-center gap-4">
<Skeleton className="h-32 w-32 rounded-full" />
<Skeleton className="h-6 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<div className="flex gap-2">
{[1, 2, 3, 4, 5].map(i => (
<Skeleton key={i} className="h-5 w-5 rounded-full" />
))}
</div>
</div>
</div>
</div>
{/* Right Side - Content Skeleton */}
<div className="flex flex-col gap-4 col-span-12 md:col-span-8">
{/* Tabs Skeleton */}
<div className="p-4 flex items-center gap-4 bg-muted rounded-md w-full">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-24" />
</div>
{/* Content Area Skeleton */}
{steps === 1 ? (
// Listings Skeleton
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map(i => (
<div key={i} className="border rounded-lg p-4">
<Skeleton className="h-40 w-full mb-4" />
<Skeleton className="h-4 w-3/4 mb-2" />
<Skeleton className="h-4 w-1/2" />
</div>
))}
</div>
) : (
// Ratings Skeleton
<div className="flex flex-col gap-6">
<div className="flex gap-6 items-center">
<div className="flex flex-col gap-3 items-center">
<Skeleton className="h-16 w-16" />
<div className="flex gap-2">
{[1, 2, 3, 4, 5].map(i => (
<Skeleton key={i} className="h-5 w-5" />
))}
</div>
<Skeleton className="h-4 w-20" />
</div>
<div className="flex-1">
{[5, 4, 3, 2, 1].map(i => (
<div key={i} className="flex items-center space-x-3 mb-3">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-2 flex-1" />
<Skeleton className="h-4 w-8" />
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default SellerSkeleton;