chore: transfer repo
This commit is contained in:
17
components/products/client/ProductCounter.tsx
Normal file
17
components/products/client/ProductCounter.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from "@/lib/hooks/useTranslation";
|
||||
|
||||
interface ProductCounterProps {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function ProductCounter({ count }: ProductCounterProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('products.counter.showing').replace('{count}', count.toString())}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
75
components/products/client/ProductGridWithTranslation.tsx
Normal file
75
components/products/client/ProductGridWithTranslation.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from "@/lib/hooks/useTranslation";
|
||||
import { Product } from "lib/shopify/types";
|
||||
import { useState } from "react";
|
||||
import { Heading } from "../../ui/Typography";
|
||||
import { FilterSidebar } from "../FilterSidebar";
|
||||
import { FilterButton } from "../ProductComponents/FilterButton";
|
||||
import { FilterTagList } from "../ProductComponents/FilterTagList";
|
||||
import { ProductsList } from "../ProductComponents/ProductsList";
|
||||
import { ProductCounter } from "../client/ProductCounter";
|
||||
import { SortDropdown } from "../client/SortDropdown";
|
||||
import { useProductFilters } from "../hooks/useProductFilters";
|
||||
|
||||
interface ProductGridWithTranslationProps {
|
||||
products: Product[];
|
||||
}
|
||||
|
||||
export function ProductGridWithTranslation({ products }: ProductGridWithTranslationProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||
const {
|
||||
activeFilters,
|
||||
sortOption,
|
||||
sortedProducts,
|
||||
handleApplyFilters,
|
||||
handleSortChange,
|
||||
removeFilter,
|
||||
clearAllFilters,
|
||||
formatFilterTag
|
||||
} = useProductFilters(products);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div>
|
||||
<Heading level={2} className="mb-8">{t('products.title')}</Heading>
|
||||
|
||||
{/* Filter and Sort Controls */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<FilterButton
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
filterText={t('products.filters.button')}
|
||||
/>
|
||||
<SortDropdown
|
||||
value={sortOption}
|
||||
onChange={handleSortChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filter tags and product count */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<FilterTagList
|
||||
filters={activeFilters}
|
||||
formatTag={formatFilterTag}
|
||||
onRemove={removeFilter}
|
||||
onClearAll={clearAllFilters}
|
||||
clearAllText={t('products.filters.clearAll')}
|
||||
/>
|
||||
<ProductCounter count={sortedProducts.length} />
|
||||
</div>
|
||||
|
||||
{/* Products grid */}
|
||||
<ProductsList products={sortedProducts} />
|
||||
</div>
|
||||
|
||||
{/* Filter Sidebar */}
|
||||
<FilterSidebar
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
onApplyFilters={handleApplyFilters}
|
||||
activeFilters={activeFilters}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
components/products/client/ProductsPageContent.tsx
Normal file
31
components/products/client/ProductsPageContent.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { Section } from "@/components/ui/Section";
|
||||
import { Heading, Text } from "@/components/ui/Typography";
|
||||
import { useTranslation } from "@/lib/hooks/useTranslation";
|
||||
import { Product } from "lib/shopify/types";
|
||||
import { ProductGridWithTranslation } from "./ProductGridWithTranslation";
|
||||
|
||||
interface ProductsPageContentProps {
|
||||
products: Product[];
|
||||
}
|
||||
|
||||
export function ProductsPageContent({ products }: ProductsPageContentProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// If no products, display a message
|
||||
if (!products || products.length === 0) {
|
||||
return (
|
||||
<Section>
|
||||
<Heading level={1} align="center" className="mb-4">{t('products.title')}</Heading>
|
||||
<Text className="text-center">{t('products.emptyMessage')}</Text>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Section spacing="xs">
|
||||
<ProductGridWithTranslation products={products} />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
117
components/products/client/SortDropdown.tsx
Normal file
117
components/products/client/SortDropdown.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from "@/lib/hooks/useTranslation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface SortDropdownProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function SortDropdown({ value, onChange, label = "Label" }: SortDropdownProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get the currently selected option's display text
|
||||
const getSelectedOptionText = () => {
|
||||
switch(value) {
|
||||
case 'newest': return t('products.sort.newest');
|
||||
case 'price-asc': return t('products.sort.priceAsc');
|
||||
case 'price-desc': return t('products.sort.priceDesc');
|
||||
case 'name-asc': return t('products.sort.nameAsc');
|
||||
case 'name-desc': return t('products.sort.nameDesc');
|
||||
default: return t('products.sort.title');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle option selection
|
||||
const handleSelect = (value: string) => {
|
||||
onChange(value);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
{/* Label */}
|
||||
<div className="text-sm text-gray-500 mb-1">{label}</div>
|
||||
|
||||
{/* Dropdown button */}
|
||||
<div
|
||||
className="flex items-center justify-between border border-gray-300 rounded px-4 py-2 bg-white cursor-pointer"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span className="text-gray-800">{getSelectedOptionText()}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 ml-2 transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{isOpen && (
|
||||
<div className="absolute z-10 mt-1 w-full bg-white border border-gray-300 rounded shadow-lg">
|
||||
<div
|
||||
className={`px-4 py-2 cursor-pointer hover:bg-primary/10 ${value === 'default' ? 'bg-primary/10 text-primary' : 'text-gray-800'}`}
|
||||
onClick={() => handleSelect('default')}
|
||||
>
|
||||
{t('products.sort.title')}
|
||||
</div>
|
||||
<div
|
||||
className={`px-4 py-2 cursor-pointer hover:bg-primary/10 ${value === 'newest' ? 'bg-primary/10 text-primary' : 'text-gray-800'}`}
|
||||
onClick={() => handleSelect('newest')}
|
||||
>
|
||||
{t('products.sort.newest')}
|
||||
</div>
|
||||
<div
|
||||
className={`px-4 py-2 cursor-pointer hover:bg-primary/10 ${value === 'price-asc' ? 'bg-primary/10 text-primary' : 'text-gray-800'}`}
|
||||
onClick={() => handleSelect('price-asc')}
|
||||
>
|
||||
{t('products.sort.priceAsc')}
|
||||
</div>
|
||||
<div
|
||||
className={`px-4 py-2 cursor-pointer hover:bg-primary/10 ${value === 'price-desc' ? 'bg-primary/10 text-primary' : 'text-gray-800'}`}
|
||||
onClick={() => handleSelect('price-desc')}
|
||||
>
|
||||
{t('products.sort.priceDesc')}
|
||||
</div>
|
||||
<div
|
||||
className={`px-4 py-2 cursor-pointer hover:bg-primary/10 ${value === 'name-asc' ? 'bg-primary/10 text-primary' : 'text-gray-800'}`}
|
||||
onClick={() => handleSelect('name-asc')}
|
||||
>
|
||||
{t('products.sort.nameAsc')}
|
||||
</div>
|
||||
<div
|
||||
className={`px-4 py-2 cursor-pointer hover:bg-primary/10 ${value === 'name-desc' ? 'bg-primary/10 text-primary' : 'text-gray-800'}`}
|
||||
onClick={() => handleSelect('name-desc')}
|
||||
>
|
||||
{t('products.sort.nameDesc')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user