initial commit
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
.addressSelectContainer {
|
||||
:global(.ant-btn) {
|
||||
background-color: #fff;
|
||||
color: #000 !important;
|
||||
border-color: #9ca3af !important;
|
||||
width: 100%;
|
||||
border-radius: 4px !important;
|
||||
height: 37.6px !important;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding: 10px;
|
||||
box-shadow: none;
|
||||
&:active {
|
||||
background: #fff !important;
|
||||
}
|
||||
&:hover {
|
||||
background: #ffffff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.customClearIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.optionList {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.optionItem {
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
.selectedAddress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-select {
|
||||
&.ant-select-focused {
|
||||
.ant-select-selector {
|
||||
border-color: #2563eb !important;
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
height: 37.6px !important;
|
||||
padding: 10px !important;
|
||||
border-color: #9ca3af !important;
|
||||
border-radius: 4px !important;
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
height: 37.6px !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
height: 28px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.addressSelect {
|
||||
:global(.ant-select-clear) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 1 !important;
|
||||
background: none;
|
||||
color: #9ca3af !important;
|
||||
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-select-arrow) {
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
111
src/components/AddressSelect(CheckOut)/index.jsx
Normal file
111
src/components/AddressSelect(CheckOut)/index.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useState } from "react";
|
||||
import { Select, Modal, Button } from "antd";
|
||||
import { X } from "lucide-react";
|
||||
import styles from "./AddressSelect.module.scss";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const AddressSelect = ({
|
||||
selectedAddress,
|
||||
handleAddressSelect,
|
||||
handleClearAddress,
|
||||
deviceType,
|
||||
locations,
|
||||
}) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
const showModal = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleClear = (e) => {
|
||||
e.stopPropagation();
|
||||
handleClearAddress();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.addressSelectContainer}>
|
||||
{deviceType === "mobile" ? (
|
||||
<>
|
||||
<Button type="primary" onClick={showModal}>
|
||||
{selectedAddress ? (
|
||||
<div className={styles.selectedAddress}>
|
||||
{selectedAddress}
|
||||
<X className={styles.customClearIcon} onClick={handleClear} />
|
||||
</div>
|
||||
) : (
|
||||
"Sargyt Salgynyz Saýlaň"
|
||||
)}
|
||||
</Button>
|
||||
<Modal
|
||||
title="Salgynyz Saýlaň"
|
||||
open={isModalVisible}
|
||||
onCancel={handleCancel}
|
||||
closeIcon={<X />}
|
||||
footer={null}
|
||||
>
|
||||
<ul className={styles.optionList}>
|
||||
{Array.isArray(locations) && locations.length > 0 ? (
|
||||
locations.map((location) => (
|
||||
<li
|
||||
key={location.id}
|
||||
onClick={() => {
|
||||
handleAddressSelect(location.name);
|
||||
setIsModalVisible(false);
|
||||
}}
|
||||
className={styles.optionItem}
|
||||
>
|
||||
{location.name}
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li>Loading...</li>
|
||||
)}
|
||||
</ul>
|
||||
</Modal>
|
||||
</>
|
||||
) : (
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Salgynyz saýlaň"
|
||||
onChange={handleAddressSelect}
|
||||
value={selectedAddress}
|
||||
style={{ width: "100%" }}
|
||||
allowClear={{
|
||||
clearIcon: <X className={styles.customClearIcon} />,
|
||||
}}
|
||||
onClear={handleClearAddress}
|
||||
className={styles.addressSelect}
|
||||
showArrow={!selectedAddress}
|
||||
loading={!locations}
|
||||
dropdownRender={(menu) => (
|
||||
<div
|
||||
style={{
|
||||
maxHeight: "150px",
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
{menu}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{Array.isArray(locations) && locations.length > 0 ? (
|
||||
locations.map((location) => (
|
||||
<Option key={location.id} value={location.name}>
|
||||
{location.name}
|
||||
</Option>
|
||||
))
|
||||
) : (
|
||||
<Option disabled></Option>
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressSelect;
|
||||
109
src/components/Banner/Banner.module.scss
Normal file
109
src/components/Banner/Banner.module.scss
Normal file
@@ -0,0 +1,109 @@
|
||||
.carouselContainer {
|
||||
width: 100%;
|
||||
max-width: 1366px;
|
||||
padding: 0 0.75rem 0 0.75rem;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
box-sizing: border-box;
|
||||
aspect-ratio: 16/6;
|
||||
@media screen and (max-width: 1024px) {
|
||||
aspect-ratio: 16/7;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mainSlider {
|
||||
flex: 3.28;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: fill;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbSlider {
|
||||
flex: 0.72;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
scroll-behavior: smooth;
|
||||
@media screen and (max-width: 767px) {
|
||||
display: none;
|
||||
}
|
||||
:global(.swiper) {
|
||||
height: 100%;
|
||||
// max-height: 100%;
|
||||
}
|
||||
|
||||
:global(.swiper-wrapper) {
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:global(.swiper-slide) {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.thumbWrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.progressBarImg {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
background: rgba(57, 47, 47, 0.5);
|
||||
animation: progress 3s linear;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
background: rgba(207, 12, 12, 0.5);
|
||||
animation: progress 3s linear infinite;
|
||||
border-end-end-radius: 10px;
|
||||
border-end-start-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
198
src/components/Banner/index.jsx
Normal file
198
src/components/Banner/index.jsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import {
|
||||
Autoplay,
|
||||
Thumbs,
|
||||
Pagination,
|
||||
Navigation,
|
||||
Mousewheel,
|
||||
FreeMode,
|
||||
} from "swiper/modules";
|
||||
import "swiper/css";
|
||||
import "swiper/css/pagination";
|
||||
import "swiper/css/thumbs";
|
||||
import "swiper/css/navigation";
|
||||
|
||||
import styles from "./Banner.module.scss";
|
||||
import { useGetCarouselsQuery } from "../../app/api/bannersApi.js";
|
||||
|
||||
import { Skeleton } from "antd";
|
||||
|
||||
function Carousel() {
|
||||
const { data, isLoading, isError } = useGetCarouselsQuery();
|
||||
const [thumbsSwiper, setThumbsSwiper] = useState(null);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [isAnimating, setIsAnimating] = useState(true);
|
||||
const thumbSliderRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnimating(false);
|
||||
setTimeout(() => setIsAnimating(true), 50);
|
||||
}, [activeIndex]);
|
||||
|
||||
const updateScrollPosition = (targetIndex) => {
|
||||
if (!thumbSliderRef.current) return;
|
||||
|
||||
const container = thumbSliderRef.current.querySelector(".swiper-wrapper");
|
||||
const slideHeight = container.children[0]?.offsetHeight || 0;
|
||||
const spaceBetween = 15;
|
||||
const scrollPosition = targetIndex * (slideHeight + spaceBetween);
|
||||
|
||||
container.parentNode.scrollTop = scrollPosition;
|
||||
};
|
||||
|
||||
const handleSlideChange = (swiper) => {
|
||||
const newActiveIndex = swiper.realIndex;
|
||||
setActiveIndex(newActiveIndex);
|
||||
|
||||
if (thumbsSwiper?.slides) {
|
||||
const slidesPerView = 4;
|
||||
let targetIndex = newActiveIndex - Math.floor(slidesPerView / 2);
|
||||
targetIndex = Math.max(
|
||||
0,
|
||||
Math.min(targetIndex, thumbsSwiper.slides.length - slidesPerView)
|
||||
);
|
||||
|
||||
thumbsSwiper.slideTo(targetIndex, 300);
|
||||
updateScrollPosition(targetIndex);
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for clicking on carousel images
|
||||
const handleImageClick = (link) => {
|
||||
if (link) {
|
||||
window.open(link, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.carouselContainer}>
|
||||
{/* Main slider skeleton */}
|
||||
<div className={`${styles.mainSlider} skeleton-main-slider`}>
|
||||
<Skeleton.Image active={true} className="main-skeleton-image" />
|
||||
<style jsx>{`
|
||||
.skeleton-main-slider {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main-skeleton-image {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Slider skeleton */}
|
||||
<div className={`${styles.thumbSlider} skeleton-thumb-slider`}>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${styles.thumbWrapper} skeleton-thumb`}
|
||||
>
|
||||
<Skeleton.Image active={true} />
|
||||
<style jsx>{`
|
||||
.skeleton-thumb-slider {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
@media screen and (max-width:767px){
|
||||
display: none;}
|
||||
}
|
||||
.skeleton-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data || !data.data || data.data.length === 0) {
|
||||
return <div>No images available</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.carouselContainer}>
|
||||
{/* Main Slider */}
|
||||
<Swiper
|
||||
modules={[Thumbs, Pagination, Navigation, Autoplay]}
|
||||
thumbs={{ swiper: thumbsSwiper }}
|
||||
autoplay={{ delay: 3000, disableOnInteraction: false }}
|
||||
loop={true}
|
||||
pagination={{
|
||||
clickable: true,
|
||||
}}
|
||||
navigation={true}
|
||||
className={styles.mainSlider}
|
||||
onSlideChange={handleSlideChange}
|
||||
>
|
||||
{data.data.map((item) => (
|
||||
<SwiperSlide key={item.id}>
|
||||
<div
|
||||
className={styles.imageWrapper}
|
||||
onClick={() => handleImageClick(item.link)}
|
||||
style={{ cursor: item.link ? 'pointer' : 'default' }}
|
||||
>
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.title || `Carousel Image ${item.id}`}
|
||||
/>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
|
||||
{/* Thumbnail Slider */}
|
||||
<Swiper
|
||||
ref={thumbSliderRef}
|
||||
modules={[Thumbs, Autoplay, FreeMode, Mousewheel]}
|
||||
onSwiper={setThumbsSwiper}
|
||||
autoplay={{ delay: 3000 }}
|
||||
slidesPerView={4}
|
||||
spaceBetween={10}
|
||||
direction="vertical"
|
||||
watchSlidesProgress={true}
|
||||
slideToClickedSlide={true}
|
||||
cssMode={true}
|
||||
loop={false}
|
||||
allowTouchMove={true}
|
||||
className={styles.thumbSlider}
|
||||
>
|
||||
{data.data.map((item, index) => (
|
||||
<SwiperSlide key={item.id}>
|
||||
<div
|
||||
className={`${styles.thumbWrapper} ${
|
||||
index === activeIndex ? styles.active : ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={item.thumbnail}
|
||||
alt={item.title || `Thumbnail ${index + 1}`}
|
||||
/>
|
||||
{index === activeIndex && isAnimating && (
|
||||
<>
|
||||
<div className={styles.progressBarImg}></div>
|
||||
<div className={styles.progressBar}></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Carousel;
|
||||
65
src/components/BeSeller/SignupForm.module.scss
Normal file
65
src/components/BeSeller/SignupForm.module.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
// SignupForm.module.scss
|
||||
.formContainer {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
// padding: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
// margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
// margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form {
|
||||
background: #fff;
|
||||
padding: 1rem;
|
||||
// border-radius: 8px;
|
||||
// box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.formRow {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
// margin-bottom: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.formItem {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
:global {
|
||||
.ant-form-item-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ant-input,
|
||||
.ant-select-selector,
|
||||
.ant-input-password {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
font-size: 1rem;
|
||||
border-radius: 4px;
|
||||
background: #1890ff;
|
||||
|
||||
&:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
}
|
||||
137
src/components/BeSeller/index.jsx
Normal file
137
src/components/BeSeller/index.jsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, Select, Upload, Button, Checkbox } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import styles from './SignupForm.module.scss';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const SignupForm = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = (values) => {
|
||||
console.log('Form values:', values);
|
||||
};
|
||||
|
||||
const validatePhone = (_, value) => {
|
||||
if (!value || /^\+993\d{8}$/.test(value)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject('Please enter a valid phone number');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.formContainer}>
|
||||
<h1 className={styles.title}>Satyjy boluň!</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Satyjy bolmak üçin aşakdaky maglumatlary doldurmagyňyzy haýyş edýäris.
|
||||
Soň işgärlerimiz Siziň bilen habarlaşarlar.
|
||||
</p>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
className={styles.form}
|
||||
>
|
||||
<div className={styles.formRow}>
|
||||
<Form.Item
|
||||
label="Ady *"
|
||||
name="firstName"
|
||||
rules={[{ required: true, message: 'Adyňyzy giriziň' }]}
|
||||
className={styles.formItem}
|
||||
>
|
||||
<Input placeholder="Adyňyzy giriziň" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Familiýasy *"
|
||||
name="lastName"
|
||||
rules={[{ required: true, message: 'Familiýasy gerekli' }]}
|
||||
className={styles.formItem}
|
||||
>
|
||||
<Input placeholder="Familiýasy" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<Form.Item
|
||||
label="Email salgy *"
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: 'Email salgy gerekli' },
|
||||
{ type: 'email', message: 'Email salgy dogry däl' }
|
||||
]}
|
||||
className={styles.formItem}
|
||||
>
|
||||
<Input placeholder="Email salgy" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Açar sözi *"
|
||||
name="password"
|
||||
rules={[{ required: true, message: 'Açar sözi gerekli' }]}
|
||||
className={styles.formItem}
|
||||
>
|
||||
<Input.Password placeholder="Açar sözi" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<Form.Item
|
||||
label="Telefon belgiňizi giriziň *"
|
||||
name="phone"
|
||||
rules={[
|
||||
{ required: true, message: 'Telefon belgi gerekli' },
|
||||
{ validator: validatePhone }
|
||||
]}
|
||||
className={styles.formItem}
|
||||
>
|
||||
<Input addonBefore="+993" placeholder="Telefon belgi" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Welaýat *"
|
||||
name="region"
|
||||
rules={[{ required: true, message: 'Welaýat saýlaň' }]}
|
||||
className={styles.formItem}
|
||||
>
|
||||
<Select placeholder="Aşgabat">
|
||||
<Option value="ashgabat">Aşgabat</Option>
|
||||
<Option value="ahal">Ahal</Option>
|
||||
<Option value="mary">Mary</Option>
|
||||
<Option value="lebap">Lebap</Option>
|
||||
<Option value="dashoguz">Daşoguz</Option>
|
||||
<Option value="balkan">Balkan</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Patent *"
|
||||
name="patent"
|
||||
rules={[{ required: true, message: 'Patent faýly gerekli' }]}
|
||||
>
|
||||
<Upload>
|
||||
<Button icon={<UploadOutlined />}>Choose File</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="agreement"
|
||||
valuePropName="checked"
|
||||
rules={[{ required: true, message: 'Şertnamany kabul ediň' }]}
|
||||
>
|
||||
<Checkbox>SATYJY ŞERTNAMASY</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" className={styles.submitButton}>
|
||||
Tassykla
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignupForm;
|
||||
51
src/components/BrandsSidebar/BrandsSidebar.module.scss
Normal file
51
src/components/BrandsSidebar/BrandsSidebar.module.scss
Normal file
@@ -0,0 +1,51 @@
|
||||
.sidebarContainer {
|
||||
@media (max-width: 1023px) {
|
||||
// display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.mobileNavButton {
|
||||
background-color: #888888;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
padding: 11px;
|
||||
font-weight: 600;
|
||||
color: #ffffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarDrawer {
|
||||
:global(.ant-drawer-body) {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.searchInput {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.brandsList {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
|
||||
.brandItem {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
82
src/components/BrandsSidebar/index.jsx
Normal file
82
src/components/BrandsSidebar/index.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from "react";
|
||||
import { Drawer, Input, Checkbox } from "antd";
|
||||
import styles from "./BrandsSidebar.module.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import brand from "../../assets/icons/brand.svg";
|
||||
|
||||
const Sidebar = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const { t, i18n } = useTranslation();
|
||||
const brands = [
|
||||
"Abat",
|
||||
"Altın",
|
||||
"Arçalyk",
|
||||
"Aýaz baba",
|
||||
"Balşeker",
|
||||
"Bars",
|
||||
"Belet Film",
|
||||
"Beýlekiler / Другие",
|
||||
"Bingo",
|
||||
"Bold",
|
||||
"Carte Noire",
|
||||
"Çaykur",
|
||||
"Dabara",
|
||||
"Datmeni",
|
||||
"Elin",
|
||||
"Emin Et",
|
||||
"Enemeli",
|
||||
"Ermak",
|
||||
"Eyfel",
|
||||
"Familia",
|
||||
"Farmasi",
|
||||
"Ferrero Rocher",
|
||||
"Granum",
|
||||
];
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setSearchTerm(e.target.value.toLowerCase());
|
||||
};
|
||||
|
||||
const filteredBrands = brands.filter((brand) =>
|
||||
brand.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.sidebarContainer}>
|
||||
<button onClick={handleToggle} className={styles.mobileNavButton}>
|
||||
<img src={brand} alt="" />
|
||||
{t("navbar.brands")}
|
||||
</button>
|
||||
|
||||
<Drawer
|
||||
title={t("navbar.brands")}
|
||||
placement="right"
|
||||
onClose={handleToggle}
|
||||
open={isOpen}
|
||||
className={styles.sidebarDrawer}
|
||||
width={320}
|
||||
>
|
||||
<Input
|
||||
placeholder={t("common.search")}
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
<div className={styles.brandsList}>
|
||||
{filteredBrands.map((brand, index) => (
|
||||
<div key={index} className={styles.brandItem}>
|
||||
<Checkbox>{brand}</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
234
src/components/CategoryDropdown/DropdownMenu.module.scss
Normal file
234
src/components/CategoryDropdown/DropdownMenu.module.scss
Normal file
@@ -0,0 +1,234 @@
|
||||
.dropdownContainer {
|
||||
@media screen and (max-width: 1023px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navButton {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
border: none;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-left: 0.875rem;
|
||||
padding-right: 0.875rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563;
|
||||
background-color: transparent;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdownPanel {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
margin-top: 8px;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
box-sizing: border-box;
|
||||
width: 1366px;
|
||||
padding: 0 1.375rem;
|
||||
}
|
||||
|
||||
.categoriesList {
|
||||
flex: 1;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #ebe7eb;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
// &::-webkit-scrollbar {
|
||||
// width: 6px;
|
||||
// }
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
}
|
||||
.title {
|
||||
&:hover {
|
||||
color: #888888;
|
||||
}
|
||||
&:active {
|
||||
color: #888888;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.categoryItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border: 1px solid #3615371a;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
color: #888888;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contentPanel {
|
||||
flex: 3;
|
||||
padding: 16px;
|
||||
max-height: 400px;
|
||||
overflow-y: hidden;
|
||||
|
||||
// &::-webkit-scrollbar {
|
||||
// width: 6px;
|
||||
// }
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
// border-radius: 3px;
|
||||
}
|
||||
.title {
|
||||
cursor: pointer;
|
||||
color: #361517;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
&:hover {
|
||||
color: #888888;
|
||||
}
|
||||
}
|
||||
}
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 2;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
color: #361517;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #888888;
|
||||
}
|
||||
}
|
||||
|
||||
.subcategoryList {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.subcategoryItem {
|
||||
font-size: 14px;
|
||||
color: #361517;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #888888;
|
||||
}
|
||||
}
|
||||
|
||||
.subCategoriesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nestedCategoryContainer:last-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.nestedCategoryContainer {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nestedCategoryItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.categoryLabel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.expandButton,
|
||||
.navigateButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.nestedChildren {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.noSubcategories {
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
204
src/components/CategoryDropdown/index.jsx
Normal file
204
src/components/CategoryDropdown/index.jsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styles from "./DropdownMenu.module.scss";
|
||||
import { useGetCategoriesQuery } from "../../app/api/categories";
|
||||
import { CategoryIcon } from "../Icons";
|
||||
import { ChevronRight, ChevronDown } from "lucide-react"; // Assuming you have access to lucide-react or similar
|
||||
|
||||
const NestedCategory = ({
|
||||
category,
|
||||
level = 0,
|
||||
handleCategorySelect,
|
||||
closeDropdown,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const hasChildren = category.children && category.children.length > 0;
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (hasChildren) {
|
||||
setIsExpanded(!isExpanded);
|
||||
} else {
|
||||
handleCategorySelect(category);
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDirectNavigation = (e) => {
|
||||
e.stopPropagation();
|
||||
handleCategorySelect(category);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.nestedCategoryContainer}
|
||||
style={{ paddingLeft: `${level * 16}px` }}
|
||||
>
|
||||
<div className={styles.nestedCategoryItem} onClick={handleClick}>
|
||||
<div className={styles.categoryLabel}>
|
||||
<span className={styles.title}>{category.name}</span>
|
||||
</div>
|
||||
|
||||
{hasChildren && (
|
||||
<button
|
||||
className={styles.expandButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={16} />
|
||||
) : (
|
||||
<ChevronRight size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{hasChildren && (
|
||||
<button
|
||||
className={styles.navigateButton}
|
||||
onClick={handleDirectNavigation}
|
||||
title="Go to category"
|
||||
>
|
||||
→
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasChildren && isExpanded && (
|
||||
<div className={styles.nestedChildren}>
|
||||
{category.children.map((child) => (
|
||||
<NestedCategory
|
||||
key={child.id}
|
||||
category={child}
|
||||
level={level + 1}
|
||||
handleCategorySelect={handleCategorySelect}
|
||||
closeDropdown={closeDropdown}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const dropdownRef = useRef(null);
|
||||
const {
|
||||
data: categoriesData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useGetCategoriesQuery("tree");
|
||||
|
||||
const categories = categoriesData?.data || [];
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [activeMainCategory, setActiveMainCategory] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (categories.length > 0) {
|
||||
const defaultCategory =
|
||||
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
|
||||
setActiveMainCategory(defaultCategory);
|
||||
}
|
||||
}, [categories]);
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (categories.length > 0) {
|
||||
const defaultCategory =
|
||||
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
|
||||
setActiveMainCategory(defaultCategory);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCategorySelect = (category) => {
|
||||
navigate(`/category/${category.id}`, { state: { category } });
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error loading categories</div>;
|
||||
|
||||
return (
|
||||
<div className={styles.dropdownContainer} ref={dropdownRef}>
|
||||
<button onClick={handleToggle} className={styles.navButton}>
|
||||
<CategoryIcon />
|
||||
{t("navbar.category")}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className={styles.dropdownWrapper}>
|
||||
<div className={styles.dropdownPanel} onMouseLeave={handleMouseLeave}>
|
||||
<div className={styles.categoriesList}>
|
||||
{categories.map((category) => (
|
||||
<div
|
||||
key={category.id}
|
||||
className={`${styles.categoryItem} ${
|
||||
activeMainCategory?.id === category.id ? styles.active : ""
|
||||
}`}
|
||||
onMouseEnter={() => setActiveMainCategory(category)}
|
||||
onClick={() => handleCategorySelect(category)}
|
||||
>
|
||||
<span className={styles.title}>{category.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeMainCategory && (
|
||||
<div className={styles.contentPanel}>
|
||||
<h2
|
||||
onClick={() => handleCategorySelect(activeMainCategory)}
|
||||
className={styles.title}
|
||||
>
|
||||
{activeMainCategory.name}
|
||||
</h2>
|
||||
|
||||
<div className={styles.subCategoriesContainer}>
|
||||
{activeMainCategory.children &&
|
||||
activeMainCategory.children.length > 0 ? (
|
||||
activeMainCategory.children.map((subcategory) => (
|
||||
<NestedCategory
|
||||
key={subcategory.id}
|
||||
category={subcategory}
|
||||
handleCategorySelect={handleCategorySelect}
|
||||
closeDropdown={() => setIsOpen(false)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.noSubcategories}>
|
||||
{/* No subcategories available */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenu;
|
||||
55
src/components/CategorySection/CategorySection.module.scss
Normal file
55
src/components/CategorySection/CategorySection.module.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.categorySection {
|
||||
margin: 20px 0;
|
||||
max-width: 1366px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
color: #000;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
margin-top: 3px;
|
||||
color: #888888;
|
||||
}
|
||||
&:hover {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
@media screen and (max-width: 1023px) {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.productList {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
@media screen and (max-width: 1230px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
}
|
||||
@media screen and (max-width: 1023px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
}
|
||||
@media screen and (max-width: 774px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
@media screen and (max-width: 519px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
@media screen and (max-width: 510px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
@media screen and (max-width: 425px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
101
src/components/CategorySection/index.jsx
Normal file
101
src/components/CategorySection/index.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ProductCard from "../ProductCard/index";
|
||||
import SkeletonProductCard from "../../components/Skeletons/homePage";
|
||||
import styles from "./CategorySection.module.scss";
|
||||
import { IoIosArrowRoundForward } from "react-icons/io";
|
||||
import { useGetCollectionProductsQuery } from "../../app/api/collectionsApi";
|
||||
|
||||
const CategorySection = ({
|
||||
collection,
|
||||
selectedBrand,
|
||||
preventEmptyRender = false,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
data: allProducts,
|
||||
error,
|
||||
isLoading,
|
||||
} = useGetCollectionProductsQuery(collection.id);
|
||||
|
||||
// State to track if this section should be displayed
|
||||
const [shouldRender, setShouldRender] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Only update visibility when loading is complete and we have data
|
||||
if (!isLoading && allProducts) {
|
||||
const hasProducts = allProducts.data && allProducts.data.length > 0;
|
||||
setShouldRender(hasProducts);
|
||||
}
|
||||
}, [isLoading, allProducts]);
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(`/collections/${collection.id}`);
|
||||
};
|
||||
|
||||
// Render skeleton cards while loading
|
||||
const renderSkeletons = () => {
|
||||
return Array(4)
|
||||
.fill(null)
|
||||
.map((_, index) => <SkeletonProductCard key={`skeleton-${index}`} />);
|
||||
};
|
||||
|
||||
// Render actual products when data is loaded
|
||||
const renderProducts = () => {
|
||||
if (!allProducts || !allProducts.data || allProducts.data.length === 0)
|
||||
return null;
|
||||
|
||||
const filteredProducts = selectedBrand
|
||||
? allProducts.data.filter(
|
||||
(product) => product.brand && product.brand.name === selectedBrand
|
||||
)
|
||||
: allProducts.data;
|
||||
|
||||
return filteredProducts.length > 0
|
||||
? filteredProducts
|
||||
.slice(0, 4)
|
||||
.map((product) => <ProductCard key={product.id} product={product} />)
|
||||
: null;
|
||||
};
|
||||
|
||||
// If we're in a state where we shouldn't render, return null immediately
|
||||
if (preventEmptyRender && !isLoading && !shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If still loading and we want to prevent flash, return minimal placeholder
|
||||
if (preventEmptyRender && isLoading) {
|
||||
return (
|
||||
<section className={`${styles.categorySection} ${styles.loadingSection}`}>
|
||||
<h2 className={styles.title}>
|
||||
{collection.name} <IoIosArrowRoundForward />
|
||||
</h2>
|
||||
<div className={styles.productList}>{renderSkeletons()}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// If there's data and we decide not to show the section, return null
|
||||
if (
|
||||
!isLoading &&
|
||||
(!allProducts || !allProducts.data || allProducts.data.length === 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.categorySection}>
|
||||
<h2 className={styles.title} onClick={handleClick}>
|
||||
{collection.name} <IoIosArrowRoundForward />
|
||||
</h2>
|
||||
<div className={styles.productList}>
|
||||
{isLoading ? renderSkeletons() : renderProducts()}
|
||||
</div>
|
||||
{error && (
|
||||
<div className={styles.errorMessage}>Failed to load products</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategorySection;
|
||||
92
src/components/CategorySideBar/Sidebar.module.scss
Normal file
92
src/components/CategorySideBar/Sidebar.module.scss
Normal file
@@ -0,0 +1,92 @@
|
||||
.sidebarContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobileNavButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarDrawer {
|
||||
:global(.ant-drawer-header) {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
:global(.ant-drawer-body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mobileMenuContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.nestedCategoryContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nestedCategoryItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.categoryActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.expandButton,
|
||||
.navigateButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.nestedSubcategories {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
145
src/components/CategorySideBar/index.jsx
Normal file
145
src/components/CategorySideBar/index.jsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Drawer } from "antd";
|
||||
import styles from "./Sidebar.module.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetCategoriesQuery } from "../../app/api/categories";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ChevronRight, ChevronDown } from "lucide-react"; // Assuming you have access to lucide-react or similar
|
||||
|
||||
const NestedCategoryMobile = ({ category, handleCategoryClick, level = 0 }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const hasChildren = category.children && category.children.length > 0;
|
||||
|
||||
const handleNestedClick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (hasChildren) {
|
||||
setIsExpanded(!isExpanded);
|
||||
} else {
|
||||
handleCategoryClick(category);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigateClick = (e) => {
|
||||
e.stopPropagation();
|
||||
handleCategoryClick(category);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.nestedCategoryContainer}>
|
||||
<div
|
||||
className={styles.nestedCategoryItem}
|
||||
style={{ paddingLeft: `${level * 16 + 16}px` }}
|
||||
onClick={handleNestedClick}
|
||||
>
|
||||
<span className={styles.title}>{category.name}</span>
|
||||
|
||||
<div className={styles.categoryActions}>
|
||||
{hasChildren && (
|
||||
<button
|
||||
className={styles.expandButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={16} />
|
||||
) : (
|
||||
<ChevronRight size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{hasChildren && (
|
||||
<button
|
||||
className={styles.navigateButton}
|
||||
onClick={handleNavigateClick}
|
||||
>
|
||||
→
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasChildren && isExpanded && (
|
||||
<div className={styles.nestedSubcategories}>
|
||||
{category.children.map((child) => (
|
||||
<NestedCategoryMobile
|
||||
key={child.id}
|
||||
category={child}
|
||||
handleCategoryClick={handleCategoryClick}
|
||||
level={level + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Sidebar = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: categoriesData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useGetCategoriesQuery("tree");
|
||||
const navigate = useNavigate();
|
||||
const categories = categoriesData?.data || [];
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleCategoryClick = (category) => {
|
||||
navigate(`/category/${category.id}`, { state: { category } });
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error loading categories</div>;
|
||||
|
||||
return (
|
||||
<div className={styles.sidebarContainer}>
|
||||
<button onClick={handleToggle} className={styles.mobileNavButton}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fillRule="evenodd"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="2"
|
||||
clipRule="evenodd"
|
||||
viewBox="0 0 32 32"
|
||||
id="category"
|
||||
>
|
||||
<path
|
||||
fill="#4b5563"
|
||||
d="M30 20c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 17h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 27v-7Zm-15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 17H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 27v-7Zm13 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm-15 0v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm2-15c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 2H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 12V5Zm15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 2h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 12V5ZM13 5v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm15 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Z"
|
||||
></path>
|
||||
</svg>
|
||||
{t("navbar.category")}
|
||||
</button>
|
||||
|
||||
<Drawer
|
||||
title={t("navbar.category")}
|
||||
placement="left"
|
||||
onClose={handleToggle}
|
||||
open={isOpen}
|
||||
className={styles.sidebarDrawer}
|
||||
width={300}
|
||||
>
|
||||
<div className={styles.mobileMenuContent}>
|
||||
{categories.map((category) => (
|
||||
<NestedCategoryMobile
|
||||
key={category.id}
|
||||
category={category}
|
||||
handleCategoryClick={handleCategoryClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
324
src/components/Checkout/Checkout.module.scss
Normal file
324
src/components/Checkout/Checkout.module.scss
Normal file
@@ -0,0 +1,324 @@
|
||||
.checkoutContainer {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
h2 {
|
||||
margin: 0;
|
||||
@media screen and (max-width: 768px) {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formSection {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.paymentOptions {
|
||||
margin-bottom: 30px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
input[type="radio"] {
|
||||
margin-top: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
.optionTitle {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.optionDesc {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.customRadio {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
background-color: #d1d5db;
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .customRadio {
|
||||
background-color: #888888;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.customCheckbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #d1d5db;
|
||||
position: relative;
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
display: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: #888888;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + .customCheckbox {
|
||||
background-color: #d1d5db;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + .customCheckbox .checkIcon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 600;
|
||||
color: #888888;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
span:nth-of-type(2) {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.balance {
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #586bca;
|
||||
font-weight: 700;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.deliveryForm {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.formGroup {
|
||||
margin-bottom: 15px;
|
||||
flex: 1;
|
||||
|
||||
label {
|
||||
display: table-cell;
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
transform: translateY(8px);
|
||||
background-color: #fff;
|
||||
width: min-content;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(156 163 175);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #888888;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formRow {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
@media screen and (max-width: 640px) {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.deliveryInfo {
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
border-top: 0.5px solid rgb(156 163 175);
|
||||
padding-top: 10px;
|
||||
|
||||
li {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.deliveryOptions {
|
||||
margin-top: 20px;
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.deliveryOptionRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.deliveryOption {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
margin-right: 12px;
|
||||
.customRadio {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
background-color: #d1d5db;
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .customRadio {
|
||||
background-color: #888888;
|
||||
}
|
||||
}
|
||||
|
||||
.deliveryOption input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.deliveryOption .optionTitle {
|
||||
color: #888888;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.deliveryOption .optionCost {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.deliveryTimeOptions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.timeOptionRow,
|
||||
.timeOptionRowDay,
|
||||
.timeOptionRowBtn {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.timeOptionRowDay {
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.timeOptionRowBtn {
|
||||
@media screen and (max-width: 640px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.timeOption {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
label {
|
||||
font-size: 16px;
|
||||
color: #888888;
|
||||
}
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
&.selected {
|
||||
border-bottom: 2px solid #888888;
|
||||
}
|
||||
}
|
||||
|
||||
.hourOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
font-size: 12px;
|
||||
padding: 6px 16px;
|
||||
border-radius: 0.5rem;
|
||||
border-width: 1px;
|
||||
border-color: #d1d5db;
|
||||
gap: 5px;
|
||||
max-width: 125px;
|
||||
// box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
span {
|
||||
border-radius: 9999px;
|
||||
border-width: 1px;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid #9ca3af;
|
||||
}
|
||||
&.selected {
|
||||
background-color: #888888;
|
||||
color: #fff;
|
||||
border-color: #888888;
|
||||
span {
|
||||
border-color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeOption input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
327
src/components/Checkout/index.jsx
Normal file
327
src/components/Checkout/index.jsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import styles from "./Checkout.module.scss";
|
||||
import { X } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
usePlaceOrderMutation,
|
||||
useGetOrderTimesQuery,
|
||||
useGetOrderPaymentsQuery,
|
||||
} from "../../app/api/orderApi";
|
||||
import { useGetLocationsQuery } from "../../app/api/locationApi";
|
||||
|
||||
const useDeviceType = () => {
|
||||
const [deviceType, setDeviceType] = useState("desktop");
|
||||
|
||||
useEffect(() => {
|
||||
const userAgent = navigator.userAgent;
|
||||
if (/Mobi|Android/i.test(userAgent)) {
|
||||
setDeviceType("mobile");
|
||||
} else {
|
||||
setDeviceType("desktop");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return deviceType;
|
||||
};
|
||||
|
||||
const Checkout = ({ cartItems, onBackToCart, onPlaceOrder }) => {
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState({
|
||||
customer_name: "",
|
||||
customer_phone: "",
|
||||
customer_address: "",
|
||||
deliveryAddress: "null",
|
||||
payment_type_id: "",
|
||||
notes: "",
|
||||
region: "",
|
||||
});
|
||||
|
||||
const [selectedAddress, setSelectedAddress] = useState(null);
|
||||
const [placeOrder, { isLoading: isPlacingOrder }] = usePlaceOrderMutation();
|
||||
const { data: orderTimes = {} } = useGetOrderTimesQuery();
|
||||
const { data: orderPayments = [] } = useGetOrderPaymentsQuery();
|
||||
const { data: locationsData } = useGetLocationsQuery();
|
||||
const deviceType = useDeviceType();
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
if (name === "customer_phone") {
|
||||
// Always keep the +993 prefix
|
||||
const prefix = "+993 ";
|
||||
|
||||
// If user is trying to delete the prefix, prevent it
|
||||
if (value.length < prefix.length) {
|
||||
return; // Don't update state, keep the current value
|
||||
}
|
||||
|
||||
// Extract only the digits after the prefix
|
||||
const inputWithoutPrefix = value.substring(prefix.length).replace(/\D/g, "");
|
||||
|
||||
// Limit to 8 digits max (Turkmenistan mobile number format)
|
||||
const limitedDigits = inputWithoutPrefix.substring(0, 8);
|
||||
|
||||
// Format with space after first 2 digits
|
||||
let formattedPhone = prefix;
|
||||
if (limitedDigits.length > 0) {
|
||||
formattedPhone += limitedDigits.substring(0, 2);
|
||||
|
||||
if (limitedDigits.length > 2) {
|
||||
formattedPhone += " " + limitedDigits.substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: formattedPhone,
|
||||
}));
|
||||
} else {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddressSelect = (value) => {
|
||||
setSelectedAddress(value);
|
||||
const selectedLocation = locationsData?.data?.find(
|
||||
(location) => location.name === value
|
||||
);
|
||||
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
address: value,
|
||||
region: selectedLocation ? selectedLocation.region : "",
|
||||
}));
|
||||
};
|
||||
|
||||
// Initialize phone with prefix
|
||||
useEffect(() => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
customer_phone: "+993 "
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const formatPhoneNumber = (phoneNumber) => {
|
||||
// Remove the +993 prefix and any spaces
|
||||
return phoneNumber.replace(/^\+993\s*/, "").replace(/\s+/g, "");
|
||||
};
|
||||
|
||||
const handleClearAddress = () => {
|
||||
setSelectedAddress(null);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
address: "",
|
||||
}));
|
||||
};
|
||||
|
||||
const handleFocus = (event) => {
|
||||
event.target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
};
|
||||
|
||||
const getOrderData = () => {
|
||||
// Validation checks
|
||||
if (
|
||||
!formData.customer_name ||
|
||||
!formData.customer_phone ||
|
||||
!formData.customer_address ||
|
||||
!formData.payment_type_id
|
||||
) {
|
||||
console.error("Missing required fields");
|
||||
alert("Please fill in all required fields");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set default values for delivery
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
const defaultTimeSlot = {
|
||||
date: currentDate,
|
||||
hour: "12:00-14:00" // Default time slot
|
||||
};
|
||||
|
||||
// Prepare data in the format expected by the API
|
||||
return {
|
||||
customer_name: formData.customer_name,
|
||||
customer_phone: formatPhoneNumber(formData.customer_phone),
|
||||
customer_address: formData.customer_address,
|
||||
shipping_method: "standard", // Default to standard shipping
|
||||
payment_type_id: formData.payment_type_id,
|
||||
delivery_time: defaultTimeSlot.hour,
|
||||
delivery_at: defaultTimeSlot.date,
|
||||
region: formData.region || "",
|
||||
notes: formData.notes || ""
|
||||
};
|
||||
};
|
||||
|
||||
// Make handlePlaceOrder available to the parent through a ref or expose it
|
||||
useEffect(() => {
|
||||
if (onPlaceOrder) {
|
||||
onPlaceOrder.current = async () => {
|
||||
const orderDetails = getOrderData();
|
||||
if (!orderDetails) return false;
|
||||
|
||||
try {
|
||||
const response = await placeOrder(orderDetails);
|
||||
|
||||
if (response.data && !response.error) {
|
||||
console.log("Order placed successfully:", response.data);
|
||||
window.location.href = "/orders";
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.error || "Unknown error occurred");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to place order:", error);
|
||||
|
||||
if (
|
||||
error.data &&
|
||||
typeof error.data === "string" &&
|
||||
error.data.includes("<!doctype html>")
|
||||
) {
|
||||
console.error(
|
||||
"Server returned HTML instead of a proper API response"
|
||||
);
|
||||
alert(
|
||||
"There was a problem with the server. Please try again later or contact support."
|
||||
);
|
||||
} else {
|
||||
alert(
|
||||
"Failed to place order. Please check your information and try again."
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [formData, placeOrder, onPlaceOrder]);
|
||||
|
||||
return (
|
||||
<div className={styles.checkoutContainer}>
|
||||
<h2>{t("cart.basket")} (2)</h2>
|
||||
<div className={styles.formSection}>
|
||||
<div className={styles.paymentOptions}>
|
||||
<h3>{t("checkout.paymentMethod")}:</h3>
|
||||
{orderPayments.map((payment) => (
|
||||
<div className={styles.option} key={payment.id}>
|
||||
<input
|
||||
type="radio"
|
||||
id={`payment${payment.id}`}
|
||||
name="payment_type_id"
|
||||
value={payment.id}
|
||||
checked={formData.payment_type_id === String(payment.id)}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`payment${payment.id}`}
|
||||
className={styles.customRadio}
|
||||
></label>
|
||||
<div
|
||||
className={styles.text}
|
||||
onClick={() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
payment_type_id: String(payment.id),
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<span className={styles.optionTitle}>{payment.name}</span>
|
||||
<span className={styles.optionDesc}>
|
||||
{payment.name === "Nagt"
|
||||
? t("checkout.payment_in_cash_upon_delivery_of_the_order")
|
||||
: t("checkout.payment_by_card")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.deliveryForm}>
|
||||
<h3>{t("checkout.address")}:</h3>
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t("checkout.fullName")}*</label>
|
||||
<input
|
||||
type="text"
|
||||
name="customer_name"
|
||||
value={formData.customer_name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t("checkout.telephone")}*</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="customer_phone"
|
||||
value={formData.customer_phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder="+993 61 097651"
|
||||
required
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t("checkout.moreAboutYourAddress")}*</label>
|
||||
<input
|
||||
type="text"
|
||||
name="customer_address"
|
||||
value={formData.customer_address}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t("checkout.note")}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="notes"
|
||||
value={formData.notes}
|
||||
onChange={handleInputChange}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.deliveryInfo}>
|
||||
<ul>
|
||||
<li>
|
||||
{t(
|
||||
"checkout.Delivery_is_carried_out_in_the_cities_of_Ashgabat_Buzmein_and_Anau"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
"checkout.The_minimum_order_amount_must_be_at_least_50_manat_for_orders_over_150_manat_delivery_is_free"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
"checkout.After_you_place_an_order_on_the_website_the_operator_will_call_you_to_confirm_the_order_for_regular_customers_confirmation_is_carried_out_automatically_at_their_request"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
"checkout.Payment_is_made_after_you_check_and_accept_the_order_The_amount_of_your_payment_is_indicated_on_the_delivery_persons_payment_document_Payment_is_made_in_cash_and_by_card_in_national_currency_Accepted_and_paid_goods_are_not_subject_to_return"
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkout;
|
||||
52
src/components/FilterSideBar/FilterSideBar.module.scss
Normal file
52
src/components/FilterSideBar/FilterSideBar.module.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
.sidebarContainer {
|
||||
@media (max-width: 1023px) {
|
||||
// display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.mobileNavButton {
|
||||
background-color: #888888;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
padding: 11px;
|
||||
font-weight: 600;
|
||||
color: #ffffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarDrawer {
|
||||
:global(.ant-drawer-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.radioGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.radioItem {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
|
||||
:global {
|
||||
.ant-radio-checked .ant-radio-inner {
|
||||
border-color: #888888;
|
||||
background-color: #888888;
|
||||
|
||||
&::after {
|
||||
background-color: #ffffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
94
src/components/FilterSideBar/index.jsx
Normal file
94
src/components/FilterSideBar/index.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Drawer, Radio } from "antd";
|
||||
import styles from "./FilterSideBar.module.scss";
|
||||
import arrow from "../../assets/icons/topBottom.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const FilterSidebar = ({ onPriceSortChange, currentPriceSort = "none" }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState(currentPriceSort);
|
||||
|
||||
// Map sort values to display options
|
||||
const sortOptionsMap = {
|
||||
none: t("category.notSelected") || "Hiç hili",
|
||||
lowToHigh: t("category.lowestPrice") || "Arzandan gymmada",
|
||||
highToLow: t("category.highestPrice") || "Gymmatdan arzana",
|
||||
};
|
||||
|
||||
// Map display options back to sort values
|
||||
const displayToSortMap = {
|
||||
[t("category.notSelected") || "Hiç hili"]: "none",
|
||||
[t("category.lowestPrice") || "Arzandan gymmada"]: "lowToHigh",
|
||||
[t("category.highestPrice") || "Gymmatdan arzana"]: "highToLow",
|
||||
};
|
||||
|
||||
// Filter options for display
|
||||
const filterOptions = Object.values(sortOptionsMap);
|
||||
|
||||
// Update local state when prop changes
|
||||
// Change the useEffect to properly set the radio button value
|
||||
useEffect(() => {
|
||||
// Map the currentPriceSort prop to the display value
|
||||
setSelectedOption(sortOptionsMap[currentPriceSort] || sortOptionsMap.none);
|
||||
}, [currentPriceSort]);
|
||||
|
||||
// Then make sure the handleOptionChange correctly gets the value
|
||||
const handleOptionChange = (e) => {
|
||||
const displayValue = e.target.value;
|
||||
const sortValue = displayToSortMap[displayValue] || "none";
|
||||
setSelectedOption(displayValue);
|
||||
|
||||
if (onPriceSortChange) {
|
||||
onPriceSortChange(sortValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterToggle = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.sidebarContainer}>
|
||||
<button onClick={handleFilterToggle} className={styles.mobileNavButton}>
|
||||
<img src={arrow} alt="" />
|
||||
{t("category.order") || "Suzguc"}
|
||||
</button>
|
||||
|
||||
<Drawer
|
||||
title={t("category.order") || "Tertip"}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={isOpen}
|
||||
className={styles.sidebarDrawer}
|
||||
width={320}
|
||||
>
|
||||
<Radio.Group
|
||||
onChange={(e) => {
|
||||
console.log("Radio changed:", e.target.value);
|
||||
handleOptionChange(e);
|
||||
}}
|
||||
value={selectedOption}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
{filterOptions.map((option, index) => (
|
||||
<Radio key={index} value={option} className={styles.radioItem} style={{
|
||||
'--radio-dot-color': '#888888',
|
||||
'--radio-checked-color': '#888888'
|
||||
}}>
|
||||
{option}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterSidebar;
|
||||
94
src/components/Footer/Footer.module.scss
Normal file
94
src/components/Footer/Footer.module.scss
Normal file
@@ -0,0 +1,94 @@
|
||||
/* Footer.module.scss */
|
||||
|
||||
.footer {
|
||||
background-color: #f9f9f9;
|
||||
padding: 40px 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #4b5563;
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 2rem;
|
||||
margin-top: auto;
|
||||
@media screen and (max-width: 1023px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1366px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 0.75rem 0 0.75rem;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
@media screen and (min-width: 1024px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
width: 10rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.marketSection,
|
||||
.contactSection,
|
||||
.appsSection {
|
||||
flex: 1;
|
||||
margin: 10px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
a{
|
||||
color: #4b5563;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.appLinks {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.appLogo {
|
||||
width: 120px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.appLogo:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1.5rem;
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
}
|
||||
101
src/components/Footer/FooterBar.module.scss
Normal file
101
src/components/Footer/FooterBar.module.scss
Normal file
@@ -0,0 +1,101 @@
|
||||
.navbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
border-top: 1px solid #e5e7eb;
|
||||
background-color: #fff;
|
||||
z-index: 9;
|
||||
@media screen and (min-width: 1024px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
// padding: 0 1rem;
|
||||
@media screen and (max-width: 425px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navItems {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0 0 0;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&:hover .icon {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-bottom: 3px solid #888888;
|
||||
.label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #888888;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #000000;
|
||||
transition: color 0.2s ease;
|
||||
svg {
|
||||
@media screen and (max-width: 425px) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
@media screen and (max-width: 375px) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background-color: #888888;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
@media screen and (max-width: 425px) {
|
||||
font-size: 11px;
|
||||
}
|
||||
@media screen and (max-width: 375px) {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
55
src/components/Footer/FooterMobile.jsx
Normal file
55
src/components/Footer/FooterMobile.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { Home, ShoppingBag, ShoppingCart, Heart, User } from "lucide-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import styles from "./FooterBar.module.scss";
|
||||
import { useGetCartQuery } from "../../app/api/cartApi"; // Sepet API
|
||||
import { useGetFavoritesQuery } from "../../app/api/favoritesApi"; // Favori API
|
||||
import { useTranslation } from "react-i18next";
|
||||
const FooterBar = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { data: cartData } = useGetCartQuery();
|
||||
const { data: favoriteData } = useGetFavoritesQuery();
|
||||
|
||||
|
||||
|
||||
const cartCount =
|
||||
cartData?.data?.reduce((total, item) => {
|
||||
return total + (parseInt(item.product_quantity, 10) || 0);
|
||||
}, 0) || 0;
|
||||
|
||||
const favoriteCount = favoriteData?.length || 0;
|
||||
|
||||
const navItems = [
|
||||
{ id: 1, icon: <Home />, label: t("navbar.home"), count: 0, path: "/" },
|
||||
{ id: 2, icon: <ShoppingBag />, label: t("navbar.brands"), count: 0, path: "/brands" },
|
||||
{ id: 3, icon: <ShoppingCart />, label: t("navbar.cart"), count: cartCount, path: "/cart" },
|
||||
{ id: 4, icon: <Heart />, label: t("wishtList.likedProducts"), count: favoriteCount, path: "/wishlist" },
|
||||
{ id: 5, icon: <User />, label: t("profile.profile"), count: 0, path: "/profile" },
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className={styles.navbar}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.navItems}>
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={item.path}
|
||||
className={`${styles.navItem} ${location.pathname === item.path ? styles.active : ""}`}
|
||||
>
|
||||
<div className={styles.iconWrapper}>
|
||||
<div className={styles.icon}>{item.icon}</div>
|
||||
{item.count > 0 && <div className={styles.badge}>{item.count}</div>}
|
||||
</div>
|
||||
<span className={styles.label}>{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterBar;
|
||||
107
src/components/Footer/index.jsx
Normal file
107
src/components/Footer/index.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import styles from "./Footer.module.scss";
|
||||
import playstore from "../../assets/playstore.png";
|
||||
import appstore from "../../assets/appstore.png";
|
||||
import apk from "../../assets/apk.png";
|
||||
|
||||
import FooterBar from "./FooterMobile";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LogoWithText } from "../Icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
const Footer = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.logo} onClick={() => navigate("/")}>
|
||||
<LogoWithText />
|
||||
</div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div className={styles.marketSection}>
|
||||
<h3> {t("footer.market")}</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/about-us">{t("footer.about")}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/delivery-and-payment">
|
||||
{t("footer.delivery_and_payment_procedure")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to={"/contactus"}>{t("footer.contact")}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to={"/privacy-policy"}>
|
||||
{t("footer.TermsofUseandPrivacyPolicy")}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={styles.contactSection}>
|
||||
<h3>{t("footer.contactUs")}</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="tel:+99360122213">
|
||||
{t("checkout.telephone")}: +993 60 12-22-13
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://imo.im"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Imo: +993 65 95-00-91
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="mailto:mm.marketplace.tm@gmail.com">
|
||||
E-mail: mm.marketplace.tm@gmail.com
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.instagram.com/mm.com.tm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Instagram: mm.com.tm
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={styles.appsSection}>
|
||||
<h3>{t("footer.mobile_applications")}</h3>
|
||||
<div className={styles.appLinks}>
|
||||
<div style={{ display: "flex", gap: "10px" }}>
|
||||
<img
|
||||
src={playstore}
|
||||
alt="Google Play"
|
||||
className={styles.appLogo}
|
||||
/>
|
||||
<img
|
||||
src={appstore}
|
||||
alt="App Store"
|
||||
className={styles.appLogo}
|
||||
/>
|
||||
</div>
|
||||
<img src={apk} alt="Download APK" className={styles.appLogo} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.bottom}>
|
||||
<p> © 2019-2025 mm.com.tm {t("footer.copyright")}</p>
|
||||
</div>
|
||||
</footer>
|
||||
<FooterBar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
294
src/components/Icons/index.jsx
Normal file
294
src/components/Icons/index.jsx
Normal file
@@ -0,0 +1,294 @@
|
||||
// src/icons.jsx
|
||||
|
||||
import React from "react";
|
||||
|
||||
export const LogoWithText = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
viewBox="0 0 339.55 127.63"
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
{`.cls-1 {
|
||||
fill: #d82622;
|
||||
stroke-width: 0px;
|
||||
}`}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_10" data-name="Layer 10">
|
||||
<g>
|
||||
<g>
|
||||
<polygon
|
||||
class="cls-1"
|
||||
points="122.42 0 122.42 32.65 97.93 32.65 97.93 32.65 85.69 44.89 73.45 32.65 48.97 32.65 48.97 0 0 0 0 48.97 32.65 48.97 32.65 73.45 73.45 73.45 73.45 57.13 85.69 69.36 97.93 57.13 97.93 73.45 138.74 73.45 138.74 48.97 171.39 48.97 171.39 0 122.42 0"
|
||||
/>
|
||||
<polygon
|
||||
class="cls-1"
|
||||
points="97.93 81.61 97.93 81.62 85.69 93.86 73.45 81.61 32.65 81.61 32.65 122.42 73.45 122.42 73.45 106.1 85.69 118.33 97.93 106.1 97.93 122.42 138.74 122.42 138.74 81.61 97.93 81.61"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m207.37,103.8v18.53h-1.85v-15.73l-6.49,10.86h-.26l-6.49-10.86v15.73h-1.85v-18.53h2.3l6.17,10.33,6.17-10.33h2.3Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m222.73,109.09h1.75v13.24h-1.75v-2.59c-1.22,1.94-3,2.91-5.35,2.91-1.89,0-3.5-.67-4.83-2.01-1.33-1.34-2-2.98-2-4.92s.67-3.58,2-4.92c1.33-1.34,2.94-2.01,4.83-2.01,2.35,0,4.13.97,5.35,2.91v-2.6Zm-8.92,10.34c1.01,1.02,2.24,1.52,3.71,1.52s2.7-.51,3.71-1.52c1.01-1.01,1.51-2.25,1.51-3.72s-.5-2.7-1.51-3.72c-1.01-1.01-2.24-1.52-3.71-1.52s-2.7.51-3.71,1.52c-1.01,1.02-1.51,2.26-1.51,3.72s.5,2.71,1.51,3.72Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m229.93,111.29c.81-1.61,2.21-2.41,4.18-2.41v1.69c-1.2,0-2.2.35-2.99,1.06-.79.71-1.19,1.82-1.19,3.34v7.36h-1.75v-13.24h1.75v2.2Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m247.19,122.33h-2.33l-6.62-6.38v6.38h-1.75v-18.53h1.75v11.15l6.3-5.85h2.38l-6.83,6.35,7.1,6.88Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m254.37,108.77c1.96,0,3.54.69,4.75,2.08s1.81,3.02,1.81,4.91c0,.19-.02.47-.05.82h-11.62c.18,1.34.75,2.41,1.71,3.19.96.79,2.15,1.18,3.56,1.18,1.01,0,1.88-.21,2.61-.62.73-.41,1.28-.96,1.65-1.63l1.54.9c-.58.94-1.38,1.68-2.38,2.22-1.01.55-2.15.82-3.44.82-2.08,0-3.78-.65-5.08-1.96-1.31-1.31-1.96-2.97-1.96-4.98s.64-3.63,1.93-4.95c1.29-1.32,2.95-1.99,4.98-1.99Zm0,1.69c-1.38,0-2.53.41-3.46,1.23s-1.48,1.88-1.66,3.19h9.88c-.18-1.39-.71-2.48-1.62-3.26-.9-.78-1.95-1.17-3.15-1.17Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m270.34,110.79h-3.81v8.02c0,.72.14,1.24.42,1.55.28.31.71.47,1.27.48.56,0,1.27,0,2.12-.04v1.54c-1.85.28-3.24.15-4.17-.4-.93-.55-1.39-1.59-1.39-3.12v-8.02h-2.78v-1.69h2.78v-3.18l1.75-.53v3.71h3.81v1.69Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m280.34,108.77c1.89,0,3.5.67,4.83,2.01,1.33,1.34,2,2.98,2,4.92s-.67,3.58-2,4.92c-1.33,1.34-2.94,2.01-4.83,2.01-2.35,0-4.13-.97-5.35-2.91v7.89h-1.75v-18.53h1.75v2.6c1.22-1.94,3-2.91,5.35-2.91Zm-3.84,10.66c1.01,1.02,2.24,1.52,3.71,1.52s2.7-.51,3.71-1.52c1.01-1.01,1.51-2.25,1.51-3.72s-.5-2.7-1.51-3.72c-1.01-1.01-2.24-1.52-3.71-1.52s-2.7.51-3.71,1.52c-1.01,1.02-1.51,2.26-1.51,3.72s.5,2.71,1.51,3.72Z"
|
||||
/>
|
||||
<path class="cls-1" d="m290.09,122.33v-19.33h1.75v19.33h-1.75Z" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m306.93,109.09h1.75v13.24h-1.75v-2.59c-1.22,1.94-3,2.91-5.35,2.91-1.89,0-3.5-.67-4.83-2.01-1.33-1.34-2-2.98-2-4.92s.67-3.58,2-4.92c1.33-1.34,2.94-2.01,4.83-2.01,2.35,0,4.13.97,5.35,2.91v-2.6Zm-8.92,10.34c1.01,1.02,2.24,1.52,3.71,1.52s2.7-.51,3.71-1.52c1.01-1.01,1.51-2.25,1.51-3.72s-.5-2.7-1.51-3.72c-1.01-1.01-2.24-1.52-3.71-1.52s-2.7.51-3.71,1.52c-1.01,1.02-1.51,2.26-1.51,3.72s.5,2.71,1.51,3.72Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m318.55,122.65c-1.99,0-3.65-.67-4.98-2s-1.99-2.98-1.99-4.94.66-3.6,1.99-4.94c1.32-1.33,2.98-2,4.98-2,1.31,0,2.48.31,3.52.94,1.04.63,1.8,1.47,2.28,2.53l-1.43.82c-.35-.79-.92-1.43-1.71-1.89-.79-.47-1.67-.7-2.66-.7-1.46,0-2.7.51-3.71,1.52-1.01,1.02-1.51,2.26-1.51,3.72s.5,2.71,1.51,3.72c1.01,1.02,2.24,1.52,3.71,1.52.99,0,1.87-.23,2.65-.7.78-.47,1.39-1.1,1.83-1.89l1.46.85c-.55,1.06-1.35,1.9-2.41,2.52-1.06.62-2.23.93-3.52.93Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m332.98,108.77c1.96,0,3.54.69,4.75,2.08s1.81,3.02,1.81,4.91c0,.19-.02.47-.05.82h-11.62c.18,1.34.75,2.41,1.71,3.19.96.79,2.15,1.18,3.56,1.18,1.01,0,1.88-.21,2.61-.62.73-.41,1.28-.96,1.65-1.63l1.54.9c-.58.94-1.38,1.68-2.38,2.22-1.01.55-2.15.82-3.44.82-2.08,0-3.78-.65-5.08-1.96-1.31-1.31-1.96-2.97-1.96-4.98s.64-3.63,1.93-4.95c1.29-1.32,2.95-1.99,4.98-1.99Zm0,1.69c-1.38,0-2.53.41-3.46,1.23s-1.48,1.88-1.66,3.19h9.88c-.18-1.39-.71-2.48-1.62-3.26-.9-.78-1.95-1.17-3.15-1.17Z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m208.43,97.83h-4.24v-10.77l-4.79,7.86h-.48l-4.79-7.86v10.77h-4.24v-18.53h4.24l5.03,8.23,5.03-8.23h4.24v18.53Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m230.14,97.83h-4.24v-10.77l-4.79,7.86h-.48l-4.79-7.86v10.77h-4.24v-18.53h4.24l5.03,8.23,5.03-8.23h4.24v18.53Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
export const Logo = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 171.39 122.42"
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
>
|
||||
<defs>
|
||||
<style>{`.cls1 { fill: #d82622; }`}</style>
|
||||
</defs>
|
||||
<g id="Layer_10" data-name="Layer 10">
|
||||
<g>
|
||||
<polygon
|
||||
className="cls1"
|
||||
points="122.42 0 122.42 32.65 97.93 32.65 97.93 32.65 85.69 44.89 73.45 32.65 48.97 32.65 48.97 0 0 0 0 48.97 32.65 48.97 32.65 73.45 73.45 73.45 73.45 57.13 85.69 69.36 97.93 57.13 97.93 73.45 138.74 73.45 138.74 48.97 171.39 48.97 171.39 0 122.42 0"
|
||||
/>
|
||||
<polygon
|
||||
className="cls1"
|
||||
points="97.93 81.61 97.93 81.62 85.69 93.86 73.45 81.61 32.65 81.61 32.65 122.42 73.45 122.42 73.45 106.1 85.69 118.33 97.93 106.1 97.93 122.42 138.74 122.42 138.74 81.61 97.93 81.61"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CartIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 19 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-v-bf54e7ab=""
|
||||
stroke="#4b5563"
|
||||
>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M16.5 15.355C17.3284 15.355 18 14.6834 18 13.855C18 13.0266 17.3284 12.355 16.5 12.355C15.6716 12.355 15 13.0266 15 13.855C15 14.6834 15.6716 15.355 16.5 15.355Z"
|
||||
data-v-bf54e7ab=""
|
||||
></path>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M5.625 15.355C6.45342 15.355 7.12499 14.6834 7.12499 13.855C7.12499 13.0266 6.45342 12.355 5.625 12.355C4.79657 12.355 4.125 13.0266 4.125 13.855C4.125 14.6834 4.79657 15.355 5.625 15.355Z"
|
||||
data-v-bf54e7ab=""
|
||||
></path>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M17.6249 10.48H5.91411L6.16423 10.0742C6.27148 9.9002 6.30298 9.68983 6.25161 9.49183L6.00748 8.55208L16.8761 7.98733C17.2882 7.96633 17.6249 7.61121 17.6249 7.19871V2.22997C17.6249 1.81748 17.2874 1.47998 16.875 1.47998H4.16961L4.02299 0.916354C3.98122 0.755602 3.88727 0.613263 3.75587 0.511665C3.62448 0.410066 3.46308 0.354957 3.29699 0.35498H0.749998C0.551086 0.35498 0.360321 0.433998 0.219669 0.57465C0.0790174 0.715302 0 0.906066 0 1.10498C0 1.30389 0.0790174 1.49465 0.219669 1.63531C0.360321 1.77596 0.551086 1.85498 0.749998 1.85498H2.71724L4.71974 9.55933L3.93224 10.8362C3.86219 10.9498 3.82376 11.0801 3.82093 11.2135C3.81809 11.3469 3.85096 11.4787 3.91611 11.5952C3.98101 11.7118 4.07589 11.809 4.19094 11.8766C4.30599 11.9443 4.43702 11.9799 4.57049 11.9799H17.6249C17.8239 11.9799 18.0146 11.9009 18.1553 11.7603C18.2959 11.6196 18.3749 11.4289 18.3749 11.2299C18.3749 11.031 18.2959 10.8403 18.1553 10.6996C18.0146 10.559 17.8239 10.48 17.6249 10.48Z"
|
||||
data-v-bf54e7ab=""
|
||||
></path>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M16.5 15.355C17.3284 15.355 18 14.6834 18 13.855C18 13.0266 17.3284 12.355 16.5 12.355C15.6716 12.355 15 13.0266 15 13.855C15 14.6834 15.6716 15.355 16.5 15.355Z"
|
||||
data-v-bf54e7ab=""
|
||||
></path>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M5.625 15.355C6.45342 15.355 7.12499 14.6834 7.12499 13.855C7.12499 13.0266 6.45342 12.355 5.625 12.355C4.79657 12.355 4.125 13.0266 4.125 13.855C4.125 14.6834 4.79657 15.355 5.625 15.355Z"
|
||||
data-v-bf54e7ab=""
|
||||
></path>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M17.6249 10.48H5.91411L6.16423 10.0742C6.27148 9.9002 6.30298 9.68983 6.25161 9.49183L6.00748 8.55208L16.8761 7.98733C17.2882 7.96633 17.6249 7.61121 17.6249 7.19871V2.22997C17.6249 1.81748 17.2874 1.47998 16.875 1.47998H4.16961L4.02299 0.916354C3.98122 0.755602 3.88727 0.613263 3.75587 0.511665C3.62448 0.410066 3.46308 0.354957 3.29699 0.35498H0.749998C0.551086 0.35498 0.360321 0.433998 0.219669 0.57465C0.0790174 0.715302 0 0.906066 0 1.10498C0 1.30389 0.0790174 1.49465 0.219669 1.63531C0.360321 1.77596 0.551086 1.85498 0.749998 1.85498H2.71724L4.71974 9.55933L3.93224 10.8362C3.86219 10.9498 3.82376 11.0801 3.82093 11.2135C3.81809 11.3469 3.85096 11.4787 3.91611 11.5952C3.98101 11.7118 4.07589 11.809 4.19094 11.8766C4.30599 11.9443 4.43702 11.9799 4.57049 11.9799H17.6249C17.8239 11.9799 18.0146 11.9009 18.1553 11.7603C18.2959 11.6196 18.3749 11.4289 18.3749 11.2299C18.3749 11.031 18.2959 10.8403 18.1553 10.6996C18.0146 10.559 17.8239 10.48 17.6249 10.48Z"
|
||||
data-v-bf54e7ab=""
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const WishlistIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 212.6 212.6"
|
||||
data-v-aebc1ac8=""
|
||||
>
|
||||
<path
|
||||
stroke="#4b5563"
|
||||
d="M 106.3 193.05 C 103.528 193.049 100.848 192.043 98.76 190.22 C 90.88 183.33 83.29 176.85 76.59 171.14 L 76.59 171.14 C 56.94 154.4 39.98 139.94 28.17 125.7 C 14.98 109.78 8.83 94.7 8.83 78.19 C 8.83 62.19 14.33 47.38 24.3 36.56 C 29.252 31.159 35.282 26.856 41.999 23.928 C 48.716 21 55.973 19.512 63.3 19.56 C 74.404 19.512 85.201 23.241 93.91 30.13 C 98.638 33.824 102.811 38.179 106.3 43.06 C 109.789 38.179 113.962 33.824 118.69 30.13 C 127.405 23.231 138.214 19.498 149.33 19.55 C 156.657 19.502 163.914 20.99 170.631 23.918 C 177.348 26.846 183.378 31.149 188.33 36.55 C 198.33 47.37 203.8 62.15 203.8 78.18 C 203.8 94.67 197.65 109.77 184.46 125.69 C 172.65 139.93 155.69 154.39 136.04 171.13 C 129.33 176.85 121.72 183.33 113.83 190.24 C 111.741 192.052 109.066 193.05 106.3 193.05 Z M 63.27 31 C 57.522 30.955 51.828 32.115 46.555 34.406 C 41.283 36.696 36.549 40.067 32.66 44.3 C 24.63 53 20.21 65 20.21 78.16 C 20.21 92.03 25.37 104.43 36.93 118.38 C 48.1 131.86 64.72 146.02 83.93 162.38 L 83.93 162.38 C 90.66 168.11 98.29 174.61 106.22 181.55 C 114.22 174.55 121.84 168.09 128.58 162.35 C 147.82 145.95 164.44 131.79 175.58 118.35 C 187.14 104.4 192.29 92 192.29 78.13 C 192.38 65 188 53 179.94 44.27 C 176.048 40.043 171.313 36.678 166.041 34.392 C 160.769 32.106 155.076 30.951 149.33 31 C 140.748 30.972 132.406 33.86 125.68 39.19 C 120.39 43.426 115.919 48.594 112.49 54.44 C 111.636 55.877 110.308 56.973 108.734 57.538 C 107.161 58.104 105.439 58.104 103.866 57.538 C 102.292 56.973 100.964 55.877 100.11 54.44 C 96.684 48.584 92.213 43.405 86.92 39.16 C 80.19 33.841 71.848 30.963 63.27 31 Z"
|
||||
data-v-aebc1ac8=""
|
||||
></path>
|
||||
<path
|
||||
stroke="#4b5563"
|
||||
opacity={0}
|
||||
d="M 106.3 193.05 C 103.528 193.049 100.848 192.043 98.76 190.22 C 90.88 183.33 83.29 176.85 76.59 171.14 L 76.59 171.14 C 56.94 154.4 39.98 139.94 28.17 125.7 C 14.98 109.78 8.83 94.7 8.83 78.19 C 8.83 62.19 14.33 47.38 24.3 36.56 C 29.252 31.159 35.282 26.856 41.999 23.928 C 48.716 21 55.973 19.512 63.3 19.56 C 74.404 19.512 85.201 23.241 93.91 30.13 C 98.638 33.824 102.811 38.179 106.3 43.06 C 109.789 38.179 113.962 33.824 118.69 30.13 C 127.405 23.231 138.214 19.498 149.33 19.55 C 156.657 19.502 163.914 20.99 170.631 23.918 C 177.348 26.846 183.378 31.149 188.33 36.55 C 198.33 47.37 203.8 62.15 203.8 78.18 C 203.8 94.67 197.65 109.77 184.46 125.69 C 172.65 139.93 155.69 154.39 136.04 171.13 C 129.33 176.85 121.72 183.33 113.83 190.24 C 111.741 192.052 109.066 193.05 106.3 193.05 Z"
|
||||
data-v-aebc1ac8=""
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const BrandIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
data-v-4940fd39=""
|
||||
width={16}
|
||||
height={16}
|
||||
>
|
||||
<path
|
||||
d="M 49.421 54.07 C 50.531 54.07 51.471 53.62 52.761 52.87 L 89.211 31.86 C 91.211 30.7 92.241 29.68 92.241 28.03 C 92.241 26.43 91.221 25.4 89.211 24.25 L 52.761 3.2 C 51.471 2.44 50.531 2 49.421 2 C 48.311 2 47.371 2.45 46.081 3.2 L 9.631 24.25 C 7.631 25.41 6.601 26.43 6.601 28.03 C 6.601 29.68 7.621 30.7 9.631 31.86 L 46.081 52.87 C 47.371 53.63 48.311 54.07 49.421 54.07 Z M 49.421 76.59 C 50.491 76.59 51.331 76.1 52.621 75.39 L 90.761 53.27 C 91.921 52.6 92.361 51.76 92.361 51 C 92.361 50.11 91.911 49.44 91.511 49.18 L 50.791 72.59 C 50.261 72.9 49.901 73.08 49.411 73.08 C 48.961 73.08 48.561 72.9 48.031 72.59 L 7.311 49.18 C 6.911 49.45 6.461 50.11 6.461 51 C 6.461 51.76 6.951 52.65 8.061 53.27 L 46.251 75.39 C 47.511 76.1 48.351 76.59 49.421 76.59 Z M 49.421 98.22 C 50.491 98.22 51.331 97.73 52.621 97.02 L 90.761 74.9 C 91.871 74.28 92.361 73.39 92.361 72.63 C 92.361 71.74 91.911 71.07 91.511 70.81 L 50.791 94.22 C 50.261 94.49 49.901 94.71 49.411 94.71 C 48.961 94.71 48.561 94.49 48.031 94.22 L 7.311 70.81 C 6.911 71.08 6.461 71.74 6.461 72.63 C 6.461 73.39 6.951 74.28 8.061 74.9 L 46.251 97.02 C 47.511 97.73 48.351 98.22 49.421 98.22 Z"
|
||||
data-v-4940fd39=""
|
||||
></path>
|
||||
<path
|
||||
opacity={0}
|
||||
d="M 49.421 54.07 C 50.531 54.07 51.471 53.62 52.761 52.87 L 89.211 31.86 C 91.211 30.7 92.241 29.68 92.241 28.03 C 92.241 26.43 91.221 25.4 89.211 24.25 L 52.761 3.2 C 51.471 2.44 50.531 2 49.421 2 C 48.311 2 47.371 2.45 46.081 3.2 L 9.631 24.25 C 7.631 25.41 6.601 26.43 6.601 28.03 C 6.601 29.68 7.621 30.7 9.631 31.86 L 46.081 52.87 C 47.371 53.63 48.311 54.07 49.421 54.07 Z M 49.421 76.59 C 50.491 76.59 51.331 76.1 52.621 75.39 L 90.761 53.27 C 91.921 52.6 92.361 51.76 92.361 51 C 92.361 50.11 91.911 49.44 91.511 49.18 L 50.791 72.59 C 50.261 72.9 49.901 73.08 49.411 73.08 C 48.961 73.08 48.561 72.9 48.031 72.59 L 7.311 49.18 C 6.911 49.45 6.461 50.11 6.461 51 C 6.461 51.76 6.951 52.65 8.061 53.27 L 46.251 75.39 C 47.511 76.1 48.351 76.59 49.421 76.59 Z M 49.421 98.22 C 50.491 98.22 51.331 97.73 52.621 97.02 L 90.761 74.9 C 91.871 74.28 92.361 73.39 92.361 72.63 C 92.361 71.74 91.911 71.07 91.511 70.81 L 50.791 94.22 C 50.261 94.49 49.901 94.71 49.411 94.71 C 48.961 94.71 48.561 94.49 48.031 94.22 L 7.311 70.81 C 6.911 71.08 6.461 71.74 6.461 72.63 C 6.461 73.39 6.951 74.28 8.061 74.9 L 46.251 97.02 C 47.511 97.73 48.351 98.22 49.421 98.22 Z"
|
||||
data-v-4940fd39=""
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
export const OrderIcon = () => (
|
||||
<svg
|
||||
data-name="Layer 1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 28.35 28.35"
|
||||
data-v-5c1608dd=""
|
||||
>
|
||||
<path
|
||||
d="M24.4,16a2.37,2.37,0,0,0-.94-.59V14.22h0v-2.7h0V9a2.27,2.27,0,0,0-.29-1.11L21.72,5.33a2.27,2.27,0,0,0-2-1.15H11A2.27,2.27,0,0,0,9,5.33L7.52,7.91A2.38,2.38,0,0,0,7.23,9v2.5h0v2h0v.81H4.68a1.46,1.46,0,0,0-1.45,1.46v6.86a1.45,1.45,0,0,0,1.45,1.45H7.2a1.46,1.46,0,0,0,1.28-.78h1.68a2.69,2.69,0,0,1,.57.06l3.55.71a4.09,4.09,0,0,0,.85.09,4.24,4.24,0,0,0,.94-.11l4.64-1,3.65-3.5a2.43,2.43,0,0,0,0-3.49Zm-7.17-1.14-.06,0-3.89-.63a4.34,4.34,0,0,0-2.88.55l-1.75.8V11.52a1,1,0,0,1,1-1H21a1,1,0,0,1,1,1v2h0v1.87a2.45,2.45,0,0,0-.94.5L19,17.68c0-.11,0-.23,0-.36A2.64,2.64,0,0,0,17.23,14.89ZM20.49,6,21.93,8.6A.78.78,0,0,1,22,9v.29a2.51,2.51,0,0,0-1-.23h-5V5.59h3.7A.86.86,0,0,1,20.49,6ZM8.76,8.6,10.2,6A.86.86,0,0,1,11,5.59h3.69V9.08h-5a2.51,2.51,0,0,0-1,.23V9A.78.78,0,0,1,8.76,8.6Zm-1.53,14s0,0,0,0H4.68a0,0,0,0,1,0,0V15.78a0,0,0,0,1,0,0H7.2s0,0,0,0v6.86ZM23.39,18.5,20,21.73l-4.26,1a2.85,2.85,0,0,1-1.2,0L11,22a4.8,4.8,0,0,0-.85-.08H8.65V17.14L11,16.06l.08,0a2.87,2.87,0,0,1,2-.38l3.75.6a1.21,1.21,0,0,1,.76,1.08c0,.44,0,1.18-1.77,1.18H14.54a.7.7,0,0,0-.7.71.7.7,0,0,0,.7.7h4L22,17A1,1,0,0,1,23.4,17a1,1,0,0,1,.3.74A1,1,0,0,1,23.39,18.5Z"
|
||||
data-v-5c1608dd=""
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CategoryIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fillRule="evenodd"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="2"
|
||||
clipRule="evenodd"
|
||||
viewBox="0 0 32 32"
|
||||
id="category"
|
||||
width={20}
|
||||
height={20}
|
||||
>
|
||||
<path
|
||||
fill="#4b5563"
|
||||
d="M30 20c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 17h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 27v-7Zm-15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 17H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 27v-7Zm13 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm-15 0v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm2-15c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 2H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 12V5Zm15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 2h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 12V5ZM13 5v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm15 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LoginIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 212 212">
|
||||
<path
|
||||
id="path"
|
||||
d="M 78.76 21.76 L 45.92 21.76 C 39.359 21.765 33.062 24.376 28.421 29.014 C 23.781 33.653 21.168 39.949 21.16 46.51 L 21.16 68.36 C 21.227 70.082 21.959 71.714 23.201 72.909 C 24.443 74.104 26.101 74.772 27.825 74.772 C 29.549 74.772 31.207 74.104 32.449 72.909 C 33.691 71.714 34.423 70.082 34.49 68.36 L 34.49 46.51 C 34.493 43.481 35.699 40.575 37.841 38.434 C 39.984 36.294 42.891 35.09 45.92 35.09 L 78.76 35.09 C 80.482 35.023 82.114 34.291 83.309 33.049 C 84.504 31.807 85.172 30.149 85.172 28.425 C 85.172 26.701 84.504 25.043 83.309 23.801 C 82.114 22.559 80.482 21.827 78.76 21.76 Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
id="path_1"
|
||||
d="M 78.76 177.59 L 45.92 177.59 C 42.891 177.587 39.983 176.381 37.841 174.239 C 35.699 172.097 34.493 169.189 34.49 166.16 L 34.49 144.31 C 34.423 142.588 33.691 140.956 32.449 139.761 C 31.207 138.566 29.549 137.898 27.825 137.898 C 26.101 137.898 24.443 138.566 23.201 139.761 C 21.959 140.956 21.227 142.588 21.16 144.31 L 21.16 166.16 C 21.168 172.721 23.781 179.017 28.421 183.656 C 33.062 188.294 39.359 190.905 45.92 190.91 L 78.76 190.91 C 80.526 190.91 82.221 190.208 83.469 188.959 C 84.718 187.711 85.42 186.016 85.42 184.25 C 85.42 182.484 84.718 180.789 83.469 179.541 C 82.221 178.292 80.526 177.59 78.76 177.59 Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
id="path_2"
|
||||
d="M 21.16 106.3 C 21.155 107.474 21.46 108.629 22.044 109.648 C 22.628 110.666 23.47 111.513 24.486 112.101 C 25.502 112.69 26.656 113 27.83 113 L 60.24 113 L 50.43 122.81 C 49.429 123.803 48.769 125.089 48.545 126.481 C 48.321 127.874 48.545 129.301 49.183 130.559 C 49.822 131.816 50.844 132.838 52.1 133.478 C 53.357 134.118 54.784 134.343 56.177 134.121 C 57.569 133.898 58.856 133.24 59.85 132.24 L 81 111 C 81.828 110.174 82.425 109.144 82.728 108.015 C 83.031 106.885 83.031 105.695 82.728 104.565 C 82.425 103.436 81.828 102.406 81 101.58 L 59.85 80.4 C 58.599 79.158 56.904 78.462 55.141 78.465 C 53.378 78.469 51.686 79.172 50.44 80.42 C 49.194 81.667 48.492 83.36 48.49 85.123 C 48.489 86.886 49.187 88.58 50.43 89.83 L 60.24 99.64 L 27.83 99.64 C 26.063 99.64 24.367 100.342 23.117 101.59 C 21.867 102.838 21.163 104.533 21.16 106.3 Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
id="path_3"
|
||||
d="M 173.13 24.34 L 130.67 12.89 C 125.763 11.565 120.564 11.784 115.785 13.516 C 111.006 15.249 106.875 18.412 103.957 22.574 C 101.038 26.735 99.471 31.697 99.47 36.78 L 99.47 175.78 C 99.475 182.331 102.077 188.621 106.702 193.261 C 111.328 197.901 117.609 200.524 124.16 200.55 C 126.359 200.546 128.547 200.254 130.67 199.68 L 173.13 188.23 C 178.38 186.807 183.019 183.694 186.327 179.376 C 189.635 175.058 191.432 169.77 191.44 164.33 L 191.44 48.24 C 191.434 42.8 189.637 37.51 186.329 33.192 C 183.02 28.874 178.381 25.762 173.13 24.34 Z M 178.13 164.34 C 178.117 166.845 177.284 169.277 175.758 171.264 C 174.232 173.25 172.097 174.682 169.68 175.34 L 127.2 186.85 C 124.937 187.461 122.54 187.362 120.336 186.565 C 118.131 185.769 116.224 184.313 114.875 182.396 C 113.525 180.48 112.797 178.194 112.79 175.85 L 112.79 36.85 C 112.793 33.826 113.994 30.923 116.13 28.781 C 118.266 26.64 121.166 25.431 124.19 25.42 C 125.203 25.421 126.212 25.555 127.19 25.82 L 169.66 37.2 C 172.077 37.858 174.212 39.29 175.738 41.276 C 177.264 43.263 178.097 45.695 178.11 48.2 Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
id="path_4"
|
||||
d="M 135.41 88.1 C 133.644 88.1 131.949 88.802 130.701 90.051 C 129.452 91.299 128.75 92.994 128.75 94.76 L 128.75 117.84 C 128.817 119.562 129.549 121.194 130.791 122.389 C 132.033 123.584 133.691 124.252 135.415 124.252 C 137.139 124.252 138.797 123.584 140.039 122.389 C 141.281 121.194 142.013 119.562 142.08 117.84 L 142.08 94.76 C 142.077 92.993 141.373 91.298 140.123 90.05 C 138.873 88.802 137.177 88.1 135.41 88.1 Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const RegisterIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 212 212">
|
||||
<path
|
||||
id="path"
|
||||
d="M 94.43 101.71 C 104.598 101.712 114.471 98.26 122.422 91.922 C 130.373 85.584 135.939 76.729 138.203 66.816 C 140.467 56.903 139.297 46.51 134.887 37.348 C 130.476 28.187 123.081 20.79 113.921 16.377 C 104.76 11.965 94.367 10.793 84.454 13.055 C 74.541 15.317 65.684 20.881 59.344 28.83 C 53.005 36.78 49.55 46.652 49.55 56.82 C 49.563 68.715 54.299 80.132 62.709 88.544 C 71.119 96.956 82.535 101.694 94.43 101.71 Z M 94.43 25.26 C 101.579 25.258 108.52 27.684 114.111 32.14 C 119.701 36.596 123.615 42.821 125.207 49.79 C 126.799 56.759 125.978 64.067 122.877 70.508 C 119.777 76.95 114.578 82.15 108.137 85.253 C 101.697 88.355 94.39 89.179 87.42 87.589 C 80.45 85.999 74.224 82.087 69.766 76.498 C 65.309 70.91 62.88 63.969 62.88 56.82 C 62.891 48.458 66.22 40.433 72.132 34.519 C 78.044 28.605 86.068 25.273 94.43 25.26 Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
id="path_1"
|
||||
d="M 94.43 118.88 C 42.93 118.88 17.86 139.88 7.51 152.41 C 2.684 158.273 0.046 165.636 0.05 173.23 L 0.05 181.32 C 0.055 186.448 2.097 191.37 5.723 194.997 C 9.35 198.623 14.272 200.665 19.4 200.67 L 169.47 200.67 C 174.598 200.665 179.52 198.623 183.147 194.997 C 186.773 191.37 188.815 186.448 188.82 181.32 L 188.82 173.23 C 188.824 165.636 186.186 158.273 181.36 152.41 C 171 139.89 145.94 118.88 94.43 118.88 Z M 175.49 181.32 C 175.49 182.911 174.857 184.438 173.733 185.563 C 172.608 186.687 171.081 187.32 169.49 187.32 L 19.4 187.32 C 17.809 187.32 16.282 186.687 15.157 185.563 C 14.033 184.438 13.4 182.911 13.4 181.32 L 13.4 173.23 C 13.392 168.738 14.948 164.381 17.8 160.91 C 26.67 150.19 48.41 132.21 94.45 132.21 C 140.49 132.21 162.24 150.21 171.11 160.91 C 173.957 164.383 175.513 168.739 175.51 173.23 Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
id="path_2"
|
||||
d="M 205.88 48 L 194.16 48 L 194.16 36.27 C 194.216 34.827 193.802 33.405 192.981 32.218 C 192.16 31.03 190.975 30.141 189.605 29.684 C 188.236 29.228 186.754 29.228 185.385 29.684 C 184.015 30.141 182.83 31.03 182.009 32.218 C 181.188 33.405 180.774 34.827 180.83 36.27 L 180.83 48 L 169.11 48 C 167.388 48.067 165.756 48.799 164.561 50.041 C 163.366 51.283 162.698 52.941 162.698 54.665 C 162.698 56.389 163.366 58.047 164.561 59.289 C 165.756 60.531 167.388 61.263 169.11 61.33 L 180.83 61.33 L 180.83 73 C 180.774 74.443 181.188 75.865 182.009 77.052 C 182.83 78.24 184.015 79.129 185.385 79.586 C 186.754 80.042 188.236 80.042 189.605 79.586 C 190.975 79.129 192.16 78.24 192.981 77.052 C 193.802 75.865 194.216 74.443 194.16 73 L 194.16 61.32 L 205.88 61.32 C 207.323 61.376 208.745 60.962 209.932 60.141 C 211.12 59.32 212.009 58.135 212.466 56.765 C 212.922 55.396 212.922 53.914 212.466 52.545 C 212.009 51.175 211.12 49.99 209.932 49.169 C 208.745 48.348 207.323 47.934 205.88 47.99 Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IncreaseIcon = () => (
|
||||
<svg viewBox="0 0 9 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.64389 4.03427C7.42494 4.81532 7.42494 6.08165 6.64389 6.8627L3.44324 10.0634C2.18331 11.3233 0.0290222 10.431 0.0290226 8.64914V2.24783C0.0290226 0.466021 2.18331 -0.426312 3.44324 0.833617L6.64389 4.03427Z"
|
||||
fill="white"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
export const DecreaseIcon = () => (
|
||||
<svg viewBox="0 0 9 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.41422 6.86246C0.633166 6.08141 0.633165 4.81508 1.41421 4.03403L4.61487 0.833374C5.8748 -0.426555 8.02908 0.465776 8.02908 2.24759V8.6489C8.02908 10.4307 5.8748 11.323 4.61487 10.0631L1.41422 6.86246Z"
|
||||
fill="white"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
20
src/components/Layout/index.jsx
Normal file
20
src/components/Layout/index.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Navbar from "../Navbar/index";
|
||||
import NavbarDown from "../Navbar/NavbarDown.jsx";
|
||||
import Footer from "../Footer/index";
|
||||
|
||||
const Layout = () => {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<NavbarDown/>
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
53
src/components/Loader/PageLoader.module.scss
Normal file
53
src/components/Loader/PageLoader.module.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
.loaderContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 15%;
|
||||
margin-bottom: 15%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: max-content;
|
||||
@media screen and (max-width:768px) {
|
||||
|
||||
margin-top: 50%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dotWaveLoader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: #888888;
|
||||
display: inline-block;
|
||||
animation: wave 1.3s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
23
src/components/Loader/index.jsx
Normal file
23
src/components/Loader/index.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import styles from "./PageLoader.module.scss";
|
||||
|
||||
export const Loader = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
return () => setLoading(false);
|
||||
}, []);
|
||||
|
||||
return loading ? (
|
||||
<div className={styles.loaderContainer}>
|
||||
<div className={styles.dotWaveLoader}>
|
||||
<div className={styles.dot}></div>
|
||||
<div className={styles.dot}></div>
|
||||
<div className={styles.dot}></div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
14
src/components/Loader/pageLoader.jsx
Normal file
14
src/components/Loader/pageLoader.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React, { useEffect } from "react";
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
|
||||
const PageLoader = () => {
|
||||
useEffect(() => {
|
||||
NProgress.start();
|
||||
return () => NProgress.done();
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default PageLoader;
|
||||
201
src/components/LogIn/LoginModal.module.scss
Normal file
201
src/components/LogIn/LoginModal.module.scss
Normal file
@@ -0,0 +1,201 @@
|
||||
.navButton {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
border: none;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-left: 0.875rem;
|
||||
padding-right: 0.875rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #f3f4f6 !important;
|
||||
color: #4b5563 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.modalWrapper {
|
||||
:global {
|
||||
.ant-modal-content {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
// top: 100px;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
border-bottom: none;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 0 15px 15px;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
display: none;
|
||||
}
|
||||
.ant-modal-close-x{
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabWrapper {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
color: #1890ff;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputGroup {
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
display: table-cell;
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
transform: translateY(11px);
|
||||
background-color: #fff;
|
||||
width: min-content;
|
||||
z-index: 99;
|
||||
position: relative;
|
||||
left: 15px;
|
||||
}
|
||||
input{
|
||||
border-color: #9ca3af !important;
|
||||
height: 44px ;
|
||||
@media screen and (max-width: 768px) {
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: #888888;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin: 16px 0;
|
||||
svg{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #aaaaaa;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
color: #000;
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 45%;
|
||||
height: 1px;
|
||||
background: #e8e8e8;
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 42%;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.socialLogin {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 1px solid #6b7280;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgotPassword{
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
p{
|
||||
margin: 0;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
253
src/components/LogIn/index.jsx
Normal file
253
src/components/LogIn/index.jsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Modal, Input, Button, message } from "antd";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import IMask from "imask";
|
||||
import styles from "./LoginModal.module.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useLoginMutation,
|
||||
useVerifyTokenMutation,
|
||||
} from "../../app/api/authApi";
|
||||
import { LoginIcon } from "../Icons";
|
||||
import { useAuth } from "../../context/authContext";
|
||||
|
||||
const LoginModal = ({ isVisible: propIsVisible, onClose: propOnClose }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [internalIsVisible, setInternalIsVisible] = useState(false);
|
||||
const { login: authLogin } = useAuth();
|
||||
|
||||
const isControlled = propIsVisible !== undefined;
|
||||
const isVisible = isControlled ? propIsVisible : internalIsVisible;
|
||||
|
||||
const [phone, setPhone] = useState("+993");
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [isVerificationModalVisible, setIsVerificationModalVisible] =
|
||||
useState(false);
|
||||
const [verificationCode, setVerificationCode] = useState("");
|
||||
const [formattedPhone, setFormattedPhone] = useState("");
|
||||
|
||||
// API hooks
|
||||
const [login, { isLoading: isLoginLoading }] = useLoginMutation();
|
||||
const [verifyToken, { isLoading: isVerifyLoading }] =
|
||||
useVerifyTokenMutation();
|
||||
|
||||
const phoneInputRef = useRef(null);
|
||||
const maskRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (phoneInputRef.current) {
|
||||
const inputElement = phoneInputRef.current.input;
|
||||
|
||||
if (inputElement) {
|
||||
const maskOptions = {
|
||||
mask: "+{993} 00 000000",
|
||||
lazy: false,
|
||||
placeholderChar: "_",
|
||||
};
|
||||
|
||||
maskRef.current = IMask(inputElement, maskOptions);
|
||||
maskRef.current.value = phone;
|
||||
|
||||
maskRef.current.on("accept", () => {
|
||||
setPhone(maskRef.current.value);
|
||||
|
||||
// Process phone number for API (extract digits only)
|
||||
const digits = maskRef.current.value.replace(/\D/g, "").substring(3); // Remove non-digits and country code
|
||||
setFormattedPhone(digits);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (maskRef.current) {
|
||||
maskRef.current.destroy();
|
||||
maskRef.current = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [phone]);
|
||||
|
||||
const showModal = () => {
|
||||
if (!isControlled) {
|
||||
setInternalIsVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (hasChanges) {
|
||||
Modal.confirm({
|
||||
title: t("common.Are_you_sure_you_want_to_close_the_modal"),
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
okText: t("common.yes"),
|
||||
cancelText: t("common.no"),
|
||||
onOk() {
|
||||
closeModal();
|
||||
resetForm();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
closeModal();
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setPhone("+993 ");
|
||||
setHasChanges(false);
|
||||
setVerificationCode("");
|
||||
};
|
||||
|
||||
const handleInputChange = (type, value) => {
|
||||
setHasChanges(true);
|
||||
switch (type) {
|
||||
case "phone":
|
||||
if (maskRef.current) {
|
||||
maskRef.current.value = value;
|
||||
}
|
||||
setPhone(value);
|
||||
break;
|
||||
case "verification":
|
||||
setVerificationCode(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
if (isControlled) {
|
||||
propOnClose?.();
|
||||
} else {
|
||||
setInternalIsVisible(false);
|
||||
}
|
||||
setIsVerificationModalVisible(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (formattedPhone.length < 8) {
|
||||
return message.error(t("profile.enter_valid_phone"));
|
||||
}
|
||||
const phoneInt = parseInt(formattedPhone, 10);
|
||||
const response = await login({ phone_number: phoneInt }).unwrap();
|
||||
if (response) {
|
||||
message.success(t("profile.verification_code_sent"));
|
||||
setIsVerificationModalVisible(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Authentication error:", err);
|
||||
message.error(err.data?.message || t("common.something_went_wrong"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyCode = async () => {
|
||||
try {
|
||||
if (!verificationCode || verificationCode.length < 5) {
|
||||
return message.error(t("profile.enter_valid_code"));
|
||||
}
|
||||
const phoneInt = parseInt(formattedPhone, 10);
|
||||
const codeInt = parseInt(verificationCode, 10);
|
||||
|
||||
const response = await verifyToken({
|
||||
phone_number: phoneInt,
|
||||
code: codeInt,
|
||||
}).unwrap();
|
||||
|
||||
if (response && response.data) {
|
||||
const token = response.data;
|
||||
|
||||
authLogin(token);
|
||||
|
||||
message.success(t("profile.login_successful"));
|
||||
closeModal();
|
||||
resetForm();
|
||||
} else {
|
||||
message.error(t("errors.verification_failed"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Verification error:", err);
|
||||
console.log("Full error object:", JSON.stringify(err, null, 2));
|
||||
message.error(
|
||||
err.data?.message || err.error || t("errors.something_went_wrong")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isControlled && (
|
||||
<Button onClick={showModal} className={styles.navButton}>
|
||||
<LoginIcon />
|
||||
{t("profile.login")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Main Login Modal */}
|
||||
<Modal
|
||||
title={t("profile.login")}
|
||||
open={isVisible && !isVerificationModalVisible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
className={styles.modalWrapper}
|
||||
closeIcon={<span>×</span>}
|
||||
>
|
||||
<div className={styles.inputGroup}>
|
||||
<label>{t("profile.telephone")}</label>
|
||||
<Input
|
||||
ref={phoneInputRef}
|
||||
value={phone}
|
||||
onChange={(e) => handleInputChange("phone", e.target.value)}
|
||||
className={styles.phoneInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.submitButton}
|
||||
onClick={handleSubmit}
|
||||
disabled={isLoginLoading}
|
||||
>
|
||||
<LoginIcon />
|
||||
{isLoginLoading ? t("common.processing") : t("profile.login")}
|
||||
</button>
|
||||
</Modal>
|
||||
|
||||
{/* Verification Code Modal */}
|
||||
<Modal
|
||||
title={t("profile.verification")}
|
||||
open={isVerificationModalVisible}
|
||||
onCancel={() => setIsVerificationModalVisible(false)}
|
||||
footer={null}
|
||||
className={styles.modalWrapper}
|
||||
closeIcon={<span>×</span>}
|
||||
>
|
||||
<div className={styles.verificationContent}>
|
||||
<p>
|
||||
{t("profile.verification_code_message")}
|
||||
<strong>{phone}</strong>
|
||||
</p>
|
||||
|
||||
<div className={styles.inputGroup}>
|
||||
<label>{t("profile.verification_code")}</label>
|
||||
<Input
|
||||
value={verificationCode}
|
||||
onChange={(e) =>
|
||||
handleInputChange("verification", e.target.value)
|
||||
}
|
||||
placeholder="00000"
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.submitButton}
|
||||
onClick={handleVerifyCode}
|
||||
disabled={isVerifyLoading}
|
||||
>
|
||||
{isVerifyLoading ? t("common.verifying") : t("profile.verify")}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginModal;
|
||||
61
src/components/MyProfileModal/ProfileModal.module.scss
Normal file
61
src/components/MyProfileModal/ProfileModal.module.scss
Normal file
@@ -0,0 +1,61 @@
|
||||
.profileModal {
|
||||
:global(.ant-modal-content) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:global(.ant-modal-header) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
:global(.ant-modal-title) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 16px;
|
||||
|
||||
:global(.ant-form-item-label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.ant-input-affix-wrapper) {
|
||||
border-radius: 6px;
|
||||
height: 40px;
|
||||
:hover{
|
||||
border-color: #888888 !important;
|
||||
}
|
||||
&:focus-within{
|
||||
outline: #888888;
|
||||
border-color: #888888 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 0;
|
||||
|
||||
button {
|
||||
min-width: 100px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
90
src/components/MyProfileModal/index.jsx
Normal file
90
src/components/MyProfileModal/index.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { Modal, Form, Input, Button, message } from "antd"
|
||||
import { UserOutlined, PhoneOutlined, HomeOutlined } from "@ant-design/icons"
|
||||
import styles from "./ProfileModal.module.scss"
|
||||
import { useGetProfileQuery, useUpdateProfileMutation } from "../../app/api/myProfileApi"
|
||||
import { useTranslation } from "react-i18next";
|
||||
const ProfileModal = ({ visible, onClose }) => {
|
||||
const [form] = Form.useForm()
|
||||
const { data: profileData, isLoading, refetch } = useGetProfileQuery()
|
||||
const [updateProfile, { isLoading: isUpdating }] = useUpdateProfileMutation()
|
||||
const { t, i18n } = useTranslation();
|
||||
useEffect(() => {
|
||||
if (profileData?.data) {
|
||||
// Map the API response fields to form fields
|
||||
form.setFieldsValue({
|
||||
name: profileData.data.first_name,
|
||||
last_name: profileData.data.last_name,
|
||||
phone_number: profileData.data.phone_number,
|
||||
address: profileData.data.address || "",
|
||||
})
|
||||
}
|
||||
}, [profileData, form])
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
try {
|
||||
// Send the form values directly to the API
|
||||
await updateProfile(values).unwrap()
|
||||
message.success("Profile updated successfully")
|
||||
refetch()
|
||||
onClose()
|
||||
} catch (error) {
|
||||
message.error("Failed to update profile")
|
||||
console.error("Update profile error:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title={t("profile.profile")} open={visible} onCancel={onClose} footer={null} className={styles.profileModal}>
|
||||
{isLoading ? (
|
||||
<div className={styles.loading}>Loading profile data...</div>
|
||||
) : (
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit} className={styles.form}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t("profile.name")}
|
||||
rules={[{ required: true, message: "Please enter your first name" }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="First Name" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="last_name"
|
||||
label={t("profile.lastname")}
|
||||
rules={[{ required: true, message: "Please enter your last name" }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="Last Name" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="phone_number"
|
||||
label={t("profile.telephone")}
|
||||
rules={[
|
||||
{ required: true, message: "Please enter your phone number" },
|
||||
{ pattern: /^\d+$/, message: "Phone number must contain only digits" },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<PhoneOutlined />} placeholder="Phone Number" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="address" label="Address">
|
||||
<Input prefix={<HomeOutlined />} placeholder="Address" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className={styles.buttons}>
|
||||
<Button type="default" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={isUpdating}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileModal
|
||||
258
src/components/Navbar/Navbar.module.scss
Normal file
258
src/components/Navbar/Navbar.module.scss
Normal file
@@ -0,0 +1,258 @@
|
||||
.navbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
background-color: #fff;
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
|
||||
.navbarUp {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
margin-bottom: 1px;
|
||||
border-bottom: 3px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.btn{
|
||||
display: flex;
|
||||
width: max-content;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: #000000;
|
||||
background-color: #000000;
|
||||
padding: 6px 10px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.navbarDown {
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
max-width: 1366px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 1.375rem;
|
||||
padding-right: 1.375rem;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
@media screen and (min-width: 1024px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 10px 22px 0px;
|
||||
height: 60px;
|
||||
gap: 10px;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
@media screen and (max-width: 426px) {
|
||||
height: 40px;
|
||||
justify-content: flex-start;
|
||||
padding: 10px 15px 6px;
|
||||
}
|
||||
}
|
||||
.logoContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
padding: 8px 14px 6px;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
@media screen and (max-width: 426px) {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
}
|
||||
.stick {
|
||||
width: 0.5px !important;
|
||||
background-color: #4b5563;
|
||||
height: auto;
|
||||
@media screen and (max-width: 1023px) {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.navLinks ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0 auto 0 auto;
|
||||
box-sizing: border-box;
|
||||
gap: 5px;
|
||||
|
||||
li,
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
}
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin: 0;
|
||||
svg {
|
||||
fill: #4b5563;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.searchWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transform: translateX(-35%);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 16px;
|
||||
background-color: #e5e7eb;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navButton {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
border: none;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-left: 0.875rem;
|
||||
padding-right: 0.875rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563;
|
||||
font-weight: 600;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
.cartSection {
|
||||
.cartLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
|
||||
.cartIcon {
|
||||
margin-right: 5px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mobile
|
||||
.navbarContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 11px 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: sticky;
|
||||
@media screen and (min-width: 1024px) {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (max-width: 426px) {
|
||||
padding: 9px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navbarContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
padding: 0 1.375rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.categories,
|
||||
.location {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
.searchIcon {
|
||||
min-width: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.searchInputWrapper {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 1.375rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 16px;
|
||||
background-color: #e5e7eb;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
307
src/components/Navbar/NavbarDown.jsx
Normal file
307
src/components/Navbar/NavbarDown.jsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { CartIcon, WishlistIcon, BrandIcon, OrderIcon } from "../Icons";
|
||||
import styles from "./Navbar.module.scss";
|
||||
import { UserOutlined, LogoutOutlined, HomeOutlined } from "@ant-design/icons";
|
||||
import { FaGlobe } from "react-icons/fa6";
|
||||
import { Input, Badge, Menu, Dropdown } from "antd";
|
||||
const { Search } = Input;
|
||||
import DropdownMenu from "../CategoryDropdown/index";
|
||||
import LoginModal from "../LogIn/index";
|
||||
import SignUpModal from "../SignUp/index";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { CiSearch } from "react-icons/ci";
|
||||
import tm from "../../assets/tm.png";
|
||||
import ru from "../../assets/ru.png";
|
||||
import en from "../../assets/en.png";
|
||||
import { CiLocationOn } from "react-icons/ci";
|
||||
import Sidebar from "../CategorySideBar";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchProductQuery } from "../../app/api/searchApi";
|
||||
import { useGetCartQuery } from "../../app/api/cartApi";
|
||||
import { useGetOrdersQuery } from "../../app/api/orderApi";
|
||||
import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
|
||||
import { useAuth } from "../../context/authContext";
|
||||
import ProfileModal from "../../components/MyProfileModal/index";
|
||||
const NavbarDown = () => {
|
||||
const [isSearchVisible, setSearchVisible] = useState(false);
|
||||
const { t, i18n } = useTranslation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const { data: searchData, refetch } = useSearchProductQuery(searchQuery, {
|
||||
skip: !searchQuery,
|
||||
});
|
||||
const { data: cartData } = useGetCartQuery(undefined, {
|
||||
refetchOnMountOrArgChange: false,
|
||||
});
|
||||
const { isAuthenticated, logout } = useAuth();
|
||||
const cartItemCount =
|
||||
cartData?.data?.reduce((total, item) => {
|
||||
return total + (parseInt(item.product_quantity, 10) || 0);
|
||||
}, 0) || 0;
|
||||
|
||||
const { data: ordersData } = useGetOrdersQuery();
|
||||
const ordersItemCount = ordersData?.length || 0;
|
||||
|
||||
const { data: favoritesData } = useGetFavoritesQuery();
|
||||
const favoritesItemCount = favoritesData?.length || 0;
|
||||
const [profileModalVisible, setProfileModalVisible] = useState(false);
|
||||
const handleSearch = () => {
|
||||
if (searchQuery.trim()) {
|
||||
refetch();
|
||||
navigate(`/search?keyword=${encodeURIComponent(searchQuery)}`, {
|
||||
state: {
|
||||
searchData,
|
||||
searchQuery,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSearch = () => {
|
||||
setSearchVisible(!isSearchVisible);
|
||||
};
|
||||
|
||||
const changeLanguage = (langCode) => {
|
||||
i18n.changeLanguage(langCode);
|
||||
localStorage.setItem("preferredLanguage", langCode);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {}, [isAuthenticated]);
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: "tk",
|
||||
label: (
|
||||
<div onClick={() => changeLanguage("tk")}>
|
||||
<img
|
||||
src={tm}
|
||||
alt={t("navbar.languages.tm")}
|
||||
style={{ width: "20px", marginRight: "10px" }}
|
||||
/>
|
||||
{t("navbar.languages.tm")}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "ru",
|
||||
label: (
|
||||
<div onClick={() => changeLanguage("ru")}>
|
||||
<img
|
||||
src={ru}
|
||||
alt={t("navbar.languages.ru")}
|
||||
style={{ width: "20px", marginRight: "10px" }}
|
||||
/>
|
||||
{t("navbar.languages.ru")}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "en",
|
||||
label: (
|
||||
<div onClick={() => changeLanguage("en")}>
|
||||
<img
|
||||
src={en}
|
||||
alt={t("navbar.languages.en")}
|
||||
style={{ width: "20px", marginRight: "10px" }}
|
||||
/>
|
||||
{t("navbar.languages.en")}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
const profileItems = [
|
||||
{
|
||||
key: "profile",
|
||||
label: (
|
||||
<div onClick={() => setProfileModalVisible(true)}>
|
||||
<UserOutlined style={{ marginRight: "10px" }} />
|
||||
{t("profile.my_profile")}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// key: "address",
|
||||
// label: (
|
||||
// <Link to="/addresses">
|
||||
// <HomeOutlined style={{ marginRight: "10px" }} />
|
||||
// {t("profile.my_address")}
|
||||
// </Link>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
key: "logout",
|
||||
label: (
|
||||
<div onClick={handleLogout}>
|
||||
<LogoutOutlined style={{ marginRight: "10px" }} />
|
||||
{t("profile.logout")}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<header className={styles.navbar}>
|
||||
<div className={styles.navbarDown} style={{ position: "sticky" }}>
|
||||
<nav className={styles.navLinks}>
|
||||
<ul>
|
||||
<li>
|
||||
<DropdownMenu />
|
||||
</li>
|
||||
<div className={styles.stick}></div>
|
||||
<li>
|
||||
{" "}
|
||||
<Link to={"/brands"}>
|
||||
<button className={styles.navButton}>
|
||||
<BrandIcon />
|
||||
{t("navbar.brands")}
|
||||
</button>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.searchWrapper}>
|
||||
<CiSearch />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Haryt ady boyunca gozle..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Dropdown
|
||||
menu={{ items }}
|
||||
placement="bottomLeft"
|
||||
trigger={["click"]}
|
||||
>
|
||||
<span
|
||||
className={styles.navButton}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<FaGlobe />
|
||||
</span>
|
||||
</Dropdown>
|
||||
</li>
|
||||
<div className={styles.stick}></div>
|
||||
|
||||
{!isAuthenticated ? (
|
||||
<>
|
||||
<li>
|
||||
<LoginModal />
|
||||
</li>
|
||||
<div className={styles.stick}></div>
|
||||
{/* <li>
|
||||
<SignUpModal />
|
||||
</li> */}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
<Dropdown
|
||||
menu={{ items: profileItems }}
|
||||
placement="bottomLeft"
|
||||
trigger={["click"]}
|
||||
>
|
||||
<span
|
||||
className={styles.navButton}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<UserOutlined /> {t("profile.my_profile")}
|
||||
</span>
|
||||
</Dropdown>
|
||||
</li>
|
||||
<div className={styles.stick}></div>
|
||||
</>
|
||||
)}
|
||||
<li>
|
||||
<Link to={"/orders"}>
|
||||
<Badge
|
||||
style={{ marginRight: "4px" }}
|
||||
count={ordersItemCount}
|
||||
offset={[10, 0]}
|
||||
>
|
||||
<button className={styles.navButton}>
|
||||
<OrderIcon />
|
||||
</button>
|
||||
</Badge>
|
||||
</Link>
|
||||
</li>
|
||||
<div className={styles.stick}></div>
|
||||
<li>
|
||||
<Link to={"/wishlist"}>
|
||||
<Badge
|
||||
style={{ marginRight: "4px" }}
|
||||
count={favoritesItemCount}
|
||||
offset={[10, 0]}
|
||||
>
|
||||
<button className={styles.navButton}>
|
||||
<WishlistIcon />
|
||||
</button>
|
||||
</Badge>
|
||||
</Link>
|
||||
</li>
|
||||
<div className={styles.stick}></div>
|
||||
<li>
|
||||
<Link to={"/cart"}>
|
||||
<Badge count={cartItemCount} offset={[10, 0]}>
|
||||
<button className={styles.navButton}>
|
||||
<CartIcon />
|
||||
</button>
|
||||
</Badge>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{/* Mobile */}
|
||||
<div className={styles.navbarContainer}>
|
||||
<div className={styles.navbarContent}>
|
||||
<div className={styles.categories}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className={styles.stick}></div>
|
||||
<div className={styles.location}>
|
||||
<CiLocationOn /> Aşgabat
|
||||
</div>
|
||||
<div className={styles.stick}></div>
|
||||
<div className={styles.searchIcon} onClick={toggleSearch}>
|
||||
<CiSearch />
|
||||
</div>
|
||||
</div>
|
||||
{isSearchVisible && (
|
||||
<div className={styles.searchInputWrapper}>
|
||||
<input
|
||||
className={styles.searchInput}
|
||||
type="text"
|
||||
placeholder="Haryt ady boyunca gozle..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isAuthenticated && (
|
||||
<ProfileModal
|
||||
visible={profileModalVisible}
|
||||
onClose={() => setProfileModalVisible(false)}
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavbarDown;
|
||||
54
src/components/Navbar/index.jsx
Normal file
54
src/components/Navbar/index.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import styles from "./Navbar.module.scss";
|
||||
import { Modal } from "antd";
|
||||
import SignupForm from "../BeSeller/index";
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LogoWithText } from "../Icons";
|
||||
import Logo from "../../assets/logo2.png"
|
||||
const Navbar = () => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const showModal = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<header className={styles.navbar}>
|
||||
<div className={styles.navbarUp}>
|
||||
<div
|
||||
style={{ maxWidth: "1366px", display: "flex", margin: "0 auto", alignItems: "center"}}
|
||||
>
|
||||
<div className={styles.logo}>
|
||||
<div
|
||||
className={styles.logoContainer}
|
||||
onClick={() => navigate("/")}
|
||||
>
|
||||
{/* <LogoWithText /> */}
|
||||
<img style={{width: "200px"}} src={Logo} alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", padding: "8px 14px 6px" }}>
|
||||
<button className={styles.btn} onClick={showModal}>
|
||||
Satyjy bol
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<Modal
|
||||
open={isModalVisible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
width={900}
|
||||
>
|
||||
<SignupForm />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
242
src/components/ProductCard/ProductCard.module.scss
Normal file
242
src/components/ProductCard/ProductCard.module.scss
Normal file
@@ -0,0 +1,242 @@
|
||||
// ProductListing.module.scss
|
||||
|
||||
.productCard {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
@media screen and (max-width: 426px) {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.discountBadge {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
background: #ff0000;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
@media screen and (max-width: 426px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
svg {
|
||||
fill: #facc15;
|
||||
color: #facc15;
|
||||
}
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
// aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #eee;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.productImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.productInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.productName {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
@media screen and (max-width: 426px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.productDescription {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
|
||||
@media screen and (max-width: 1023px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.priceContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
margin: 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.currentPrice {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
@media screen and (max-width: 426px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.oldPrice {
|
||||
font-size: 12px;
|
||||
color: #d32824;
|
||||
text-decoration: line-through;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
gap: 0.5rem;
|
||||
svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.favoriteButton {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
// margin-right: 0.5rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.375rem;
|
||||
background-color: rgb(255 255 255);
|
||||
border: 1px solid rgb(237 228 255);
|
||||
svg {
|
||||
fill: #888888;
|
||||
}
|
||||
}
|
||||
|
||||
.addToCartButton {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
border-width: 1px;
|
||||
width: 100%;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
transition-duration: 150ms;
|
||||
background-color: #d32824;
|
||||
border: none;
|
||||
svg {
|
||||
fill: #fff;
|
||||
@media screen and (max-width: 426px) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e86064;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.productGrid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.quantityControls {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2.5rem;
|
||||
background-color: #d32824;
|
||||
border-radius: 5px;
|
||||
min-width: 160px;
|
||||
|
||||
span {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quantityBtn {
|
||||
// width: 100%;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@media screen and (max-width: 720px) {
|
||||
width: auto;
|
||||
}
|
||||
svg {
|
||||
fill: #fff;
|
||||
@media screen and (max-width: 720px) {
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #e86064;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 720px) {
|
||||
width: 100%;
|
||||
min-width: 80px;
|
||||
gap: 0.5rem;
|
||||
min-height: 33px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
.outOfStock {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modalButton {
|
||||
// Style for modal buttons
|
||||
padding: 6px 15px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
.carouselContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.productImage {
|
||||
width: 99%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
// transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Fixed frame container to prevent layout shifts */
|
||||
.fixedFrameContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1; /* Square container */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Style for product card (non-detail view) images */
|
||||
.cardImage {
|
||||
width: auto;
|
||||
height: 300px; /* Increased height for card view */
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Style for images inside detail view */
|
||||
.detailImage {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
margin: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Specific styles for the detail carousel */
|
||||
.detailCarousel {
|
||||
.carouselContainer {
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.fixedFrameContainer {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// .transitioning img {
|
||||
// opacity: 0.8;
|
||||
// transition: opacity 0.3s ease-in-out;
|
||||
// }
|
||||
|
||||
.arrowButton {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, background-color 0.2s ease;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fixed positions for arrows - they won't move with image size changes */
|
||||
.leftArrow {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.rightArrow {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
/* Make sure the carousel wrapper has a defined position */
|
||||
.carouselWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.carouselContainer:hover .arrowButton {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.indicators {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(200, 200, 200, 0.7);
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnailContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 5px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e0e0e0;
|
||||
opacity: 0.7;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.activeThumbnail {
|
||||
opacity: 1;
|
||||
border: 2px solid #ff6b00;
|
||||
}
|
||||
|
||||
/* Responsive styling */
|
||||
@media (max-width: 768px) {
|
||||
.thumbnailContainer {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.detailCarousel {
|
||||
.carouselContainer {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.fixedFrameContainer {
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.cardImage {
|
||||
height: 180px; /* Adjusted height for mobile view */
|
||||
}
|
||||
}
|
||||
218
src/components/ProductCard/imageCarousel/index.jsx
Normal file
218
src/components/ProductCard/imageCarousel/index.jsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import styles from "./ImageCarousel.module.scss";
|
||||
|
||||
const ImageCarousel = ({
|
||||
images,
|
||||
altText,
|
||||
showThumbnails = false,
|
||||
isDetailView = false, // Prop to differentiate between card and detail view
|
||||
}) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const touchStartX = useRef(0);
|
||||
const touchEndX = useRef(0);
|
||||
const carouselRef = useRef(null);
|
||||
|
||||
// Check if there are multiple images
|
||||
const hasMultipleImages = Array.isArray(images) && images.length > 1;
|
||||
|
||||
// Get current image URL
|
||||
const currentImage =
|
||||
hasMultipleImages && images[currentIndex]
|
||||
? images[currentIndex].images_400x400
|
||||
: images[0]?.images_400x400 || "";
|
||||
|
||||
// Auto-slide functionality - every 9 seconds
|
||||
useEffect(() => {
|
||||
if (!hasMultipleImages) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
||||
}, 9000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [hasMultipleImages, images]);
|
||||
|
||||
// Navigate to previous image
|
||||
const handlePrev = (e) => {
|
||||
if (e) e.stopPropagation();
|
||||
if (!hasMultipleImages) return;
|
||||
setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
|
||||
};
|
||||
|
||||
// Navigate to next image
|
||||
const handleNext = (e) => {
|
||||
if (e) e.stopPropagation();
|
||||
if (!hasMultipleImages) return;
|
||||
setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
||||
};
|
||||
|
||||
// Handle thumbnail click
|
||||
const handleThumbnailClick = (index, e) => {
|
||||
if (e) e.stopPropagation();
|
||||
setCurrentIndex(index);
|
||||
};
|
||||
|
||||
// Touch event handlers
|
||||
const handleTouchStart = (e) => {
|
||||
touchStartX.current = e.touches[0].clientX;
|
||||
};
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
touchEndX.current = e.touches[0].clientX;
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!hasMultipleImages) return;
|
||||
|
||||
const touchDiff = touchStartX.current - touchEndX.current;
|
||||
|
||||
// Swipe threshold - only respond to intentional swipes
|
||||
if (Math.abs(touchDiff) > 50) {
|
||||
if (touchDiff > 0) {
|
||||
// Swipe left -> Next image
|
||||
handleNext();
|
||||
} else {
|
||||
// Swipe right -> Previous image
|
||||
handlePrev();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Apply transition effect using CSS
|
||||
useEffect(() => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.classList.add(styles.transitioning);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.classList.remove(styles.transitioning);
|
||||
}
|
||||
}, 900); // Match this timing with CSS transition duration
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [currentIndex]);
|
||||
|
||||
// If there's only one image, just show it - applying different classes based on view
|
||||
if (!hasMultipleImages) {
|
||||
return (
|
||||
<div className={isDetailView ? styles.fixedFrameContainer : undefined}>
|
||||
<img
|
||||
src={currentImage || "/placeholder.svg"}
|
||||
alt={altText || "Ürün resmi"}
|
||||
className={`${styles.productImage} ${
|
||||
isDetailView ? styles.detailImage : styles.cardImage
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.carouselWrapper} ${
|
||||
isDetailView ? styles.detailCarousel : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={styles.carouselContainer}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className={`${styles.imageWrapper} ${
|
||||
isDetailView ? styles.fixedFrameContainer : ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={currentImage || "/placeholder.svg"}
|
||||
alt={altText || "Ürün resmi"}
|
||||
className={`${styles.productImage} ${
|
||||
isDetailView ? styles.detailImage : styles.cardImage
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Navigation arrows */}
|
||||
<button
|
||||
onClick={handlePrev}
|
||||
className={`${styles.arrowButton} ${styles.leftArrow}`}
|
||||
aria-label="Önceki resim"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className={`${styles.arrowButton} ${styles.rightArrow}`}
|
||||
aria-label="Sonraki resim"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Indicators (dots) */}
|
||||
<div className={styles.indicators}>
|
||||
{images.map((_, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`${styles.indicator} ${
|
||||
currentIndex === idx ? styles.active : ""
|
||||
}`}
|
||||
onClick={(e) => handleThumbnailClick(idx, e)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thumbnails - only show if showThumbnails is true */}
|
||||
{showThumbnails && (
|
||||
<div className={styles.thumbnailContainer}>
|
||||
{images.map((image, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`${styles.thumbnail} ${
|
||||
currentIndex === idx ? styles.activeThumbnail : ""
|
||||
}`}
|
||||
onClick={(e) => handleThumbnailClick(idx, e)}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
image.thumbnail || image.images_400x400 || "/placeholder.svg"
|
||||
}
|
||||
alt={`${altText || "Ürün"} thumbnail ${idx + 1}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageCarousel;
|
||||
318
src/components/ProductCard/index.jsx
Normal file
318
src/components/ProductCard/index.jsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import styles from "./ProductCard.module.scss";
|
||||
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
|
||||
import { FaShoppingCart } from "react-icons/fa";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { debounce } from "lodash";
|
||||
import {
|
||||
useAddFavoriteMutation,
|
||||
useRemoveFavoriteMutation,
|
||||
} from "../../app/api/favoritesApi";
|
||||
import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
|
||||
import {
|
||||
useAddToCartMutation,
|
||||
useUpdateCartItemMutation,
|
||||
useRemoveFromCartMutation,
|
||||
useGetCartQuery,
|
||||
} from "../../app/api/cartApi";
|
||||
import { Modal } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DecreaseIcon, IncreaseIcon } from "../Icons";
|
||||
import ImageCarousel from "./imageCarousel/index";
|
||||
|
||||
// Helper function to strip HTML tags and truncate text
|
||||
const truncateDescription = (htmlString, maxLength = 80) => {
|
||||
// Create a temporary div to parse HTML
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = htmlString;
|
||||
|
||||
// Get text content without HTML tags
|
||||
const textContent = tempDiv.textContent || tempDiv.innerText || "";
|
||||
|
||||
// Truncate the text
|
||||
const truncatedText =
|
||||
textContent.length > maxLength
|
||||
? textContent.substring(0, maxLength).trim() + "..."
|
||||
: textContent;
|
||||
|
||||
return truncatedText;
|
||||
};
|
||||
|
||||
const ProductCard = ({
|
||||
product,
|
||||
showAddToCart = true,
|
||||
showFavoriteButton = true,
|
||||
onAddToCart,
|
||||
onToggleFavorite,
|
||||
isFavorite = false,
|
||||
descriptionMaxLength = 85, // New prop to control description length
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
||||
const [addFavorite] = useAddFavoriteMutation();
|
||||
const [removeFavorite] = useRemoveFavoriteMutation();
|
||||
const { data: favoriteProducts = [], refetch } = useGetFavoritesQuery();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [localIsFavorite, setLocalIsFavorite] = useState(
|
||||
favoriteProducts.some((fav) => fav.product?.id === product.id)
|
||||
);
|
||||
// Process description
|
||||
const truncatedDesc = truncateDescription(
|
||||
product.description,
|
||||
descriptionMaxLength
|
||||
);
|
||||
|
||||
const { data: cartData } = useGetCartQuery(undefined, {
|
||||
selectFromResult: (result) => ({
|
||||
data: result.data,
|
||||
}),
|
||||
});
|
||||
|
||||
const [addToCart] = useAddToCartMutation();
|
||||
const [updateCartItem] = useUpdateCartItemMutation();
|
||||
const [removeFromCart] = useRemoveFromCartMutation();
|
||||
|
||||
const cartItem = cartData?.data?.find(
|
||||
(item) => item.product?.id === product.id || item.product_id === product.id
|
||||
);
|
||||
const quantity = cartItem?.quantity || cartItem?.product_quantity || 0;
|
||||
const [localQuantity, setLocalQuantity] = useState(0);
|
||||
const [pendingQuantity, setPendingQuantity] = useState(localQuantity);
|
||||
|
||||
useEffect(() => {
|
||||
if (cartItem) {
|
||||
setLocalQuantity(cartItem.quantity || cartItem.product_quantity || 0);
|
||||
setPendingQuantity(cartItem.quantity || cartItem.product_quantity || 0);
|
||||
} else {
|
||||
setLocalQuantity(0);
|
||||
setPendingQuantity(0);
|
||||
}
|
||||
}, [cartData, cartItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(favoriteProducts)) {
|
||||
const isFav = favoriteProducts.some(
|
||||
(fav) => fav.product?.id === product.id
|
||||
);
|
||||
setLocalIsFavorite(isFav);
|
||||
}
|
||||
}, [favoriteProducts, product.id]);
|
||||
|
||||
const handleAddToCart = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (product.stock <= 0) {
|
||||
setStockErrorModalVisible(true);
|
||||
return;
|
||||
}
|
||||
setLocalQuantity((prev) => prev + 1);
|
||||
setPendingQuantity((prev) => prev + 1);
|
||||
try {
|
||||
await addToCart({ productId: product.id, quantity: 1 }).unwrap();
|
||||
} catch (error) {
|
||||
console.error("Failed to add to cart:", error);
|
||||
setLocalQuantity((prev) => prev - 1);
|
||||
setPendingQuantity((prev) => prev - 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateCart = async () => {
|
||||
if (pendingQuantity !== quantity && pendingQuantity > 0) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await updateCartItem({
|
||||
productId: product.id,
|
||||
quantity: pendingQuantity,
|
||||
}).unwrap();
|
||||
} catch (error) {
|
||||
console.error("Failed to update cart item:", error);
|
||||
setLocalQuantity(quantity);
|
||||
setPendingQuantity(quantity);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedUpdate = debounce(updateCart, 300);
|
||||
|
||||
if (pendingQuantity !== quantity) {
|
||||
debouncedUpdate();
|
||||
}
|
||||
|
||||
return () => debouncedUpdate.cancel();
|
||||
}, [pendingQuantity, quantity, product.id, updateCartItem]);
|
||||
|
||||
const handleQuantityIncrease = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (isLoading) return;
|
||||
|
||||
if (localQuantity >= product.stock) {
|
||||
setStockErrorModalVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalQuantity((prev) => prev + 1);
|
||||
setPendingQuantity((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const handleQuantityDecrease = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (isLoading) return;
|
||||
|
||||
if (pendingQuantity <= 1) {
|
||||
setPendingQuantity(0);
|
||||
setLocalQuantity(0);
|
||||
setIsLoading(true);
|
||||
removeFromCart({ productId: product.id })
|
||||
.unwrap()
|
||||
.catch(() => {
|
||||
setLocalQuantity(1);
|
||||
setPendingQuantity(1);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLocalQuantity((prev) => prev - 1);
|
||||
setPendingQuantity((prev) => prev - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleFavorite = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (isLoading) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (localIsFavorite) {
|
||||
const result = await removeFavorite(product.id).unwrap();
|
||||
if (result === "Removed" || result?.status === "success") {
|
||||
setLocalIsFavorite(false);
|
||||
}
|
||||
} else {
|
||||
const result = await addFavorite(product.id).unwrap();
|
||||
if (result === "Added" || result?.status === "success") {
|
||||
setLocalIsFavorite(true);
|
||||
}
|
||||
}
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle favorite:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCardClick = () => {
|
||||
navigate(`/product/${product.id}`);
|
||||
};
|
||||
|
||||
const { name, price_amount, old_price_amount, media = [], reviews } = product;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.productCard} onClick={handleCardClick}>
|
||||
<div className={styles.imageContainer}>
|
||||
{product.discount && (
|
||||
<span className={styles.discountBadge}>-{product.discount}%</span>
|
||||
)}
|
||||
{product.stock === 0 && (
|
||||
<span className={`${styles.discountBadge} ${styles.outOfStock}`}>
|
||||
{t("common.out_of_stock")}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<ImageCarousel images={media} altText={name} />
|
||||
</div>
|
||||
<div className={styles.productInfo}>
|
||||
<h3 className={styles.productName}>{name}</h3>
|
||||
|
||||
{/* Simple truncated description */}
|
||||
<p className={styles.productDescription}>{truncatedDesc}</p>
|
||||
|
||||
<div className={styles.priceContainer}>
|
||||
<div>
|
||||
<span className={styles.currentPrice}>{price_amount} m.</span>
|
||||
{old_price_amount && (
|
||||
<span className={styles.oldPrice}>{old_price_amount} m.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{showFavoriteButton && (
|
||||
<button
|
||||
className={styles.favoriteButton}
|
||||
onClick={handleToggleFavorite}
|
||||
>
|
||||
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
||||
</button>
|
||||
)}
|
||||
{showAddToCart && (
|
||||
<>
|
||||
{localQuantity > 0 ? (
|
||||
<div className={styles.quantityControls}>
|
||||
<button
|
||||
onClick={handleQuantityDecrease}
|
||||
className={styles.quantityBtn}
|
||||
>
|
||||
<DecreaseIcon />
|
||||
</button>
|
||||
<span>{localQuantity}</span>
|
||||
<button
|
||||
onClick={handleQuantityIncrease}
|
||||
className={styles.quantityBtn}
|
||||
>
|
||||
<IncreaseIcon />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className={styles.addToCartButton}
|
||||
onClick={handleAddToCart}
|
||||
>
|
||||
<FaShoppingCart />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stock Error Modal */}
|
||||
<Modal
|
||||
title={t("common.warning")}
|
||||
open={stockErrorModalVisible}
|
||||
onOk={() => setStockErrorModalVisible(false)}
|
||||
onCancel={() => setStockErrorModalVisible(false)}
|
||||
okText={t("common.ok")}
|
||||
footer={[
|
||||
<button
|
||||
key="ok"
|
||||
onClick={() => setStockErrorModalVisible(false)}
|
||||
className={styles.modalButton}
|
||||
>
|
||||
{t("common.ok")}
|
||||
</button>,
|
||||
]}
|
||||
>
|
||||
<p>
|
||||
{t("common.not_enough_stock", {
|
||||
available: product.stock,
|
||||
requested: localQuantity + 1,
|
||||
})}
|
||||
</p>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCard;
|
||||
207
src/components/Profile/ProfileMenu.module.scss
Normal file
207
src/components/Profile/ProfileMenu.module.scss
Normal file
@@ -0,0 +1,207 @@
|
||||
.profileMenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background-color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:focus-visible {
|
||||
background-color: #f5f5f5;
|
||||
box-shadow: inset 0 0 0 2px #d9d9d9;
|
||||
}
|
||||
&:active {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f8f8;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
&:last-child{
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #000;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 16px;
|
||||
color: #888888;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.langModal {
|
||||
:global(.ant-modal-content) {
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
top: 50%;
|
||||
transform: translateY(50%);
|
||||
width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
:global(.ant-modal-header) {
|
||||
margin: 0;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
:global(.ant-modal-body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.languageList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.languageItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.langContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.langFlag {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.activeDot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.userProfileHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.userIconContainer {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.userIcon {
|
||||
color: #999;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.phoneNumber {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.editProfileLink {
|
||||
color: #888888;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.balanceSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.balanceLabel {
|
||||
font-size: 14px;
|
||||
color: #888888;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.balanceAmount {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.infoButton {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
302
src/components/Profile/index.jsx
Normal file
302
src/components/Profile/index.jsx
Normal file
@@ -0,0 +1,302 @@
|
||||
import React, { useState } from "react";
|
||||
import { Modal } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
User,
|
||||
LogIn,
|
||||
Wallet,
|
||||
Heart,
|
||||
Languages,
|
||||
List,
|
||||
Mail,
|
||||
Info,
|
||||
Edit,
|
||||
MapPin,
|
||||
LogOut,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styles from "./ProfileMenu.module.scss";
|
||||
import LoginModal from "../LogIn";
|
||||
import SignUpModal from "../SignUp";
|
||||
import ProfileModal from "..//MyProfileModal/index";
|
||||
import tm from "../../assets/tm.png";
|
||||
import ru from "../../assets/ru.png";
|
||||
import en from "../../assets/en.png";
|
||||
import { useAuth } from "../../context/authContext";
|
||||
import { useGetProfileQuery } from "../../app/api/myProfileApi";
|
||||
|
||||
const ProfileMenu = () => {
|
||||
const [activeModal, setActiveModal] = useState(null);
|
||||
const { t, i18n } = useTranslation();
|
||||
const { isAuthenticated, logout } = useAuth();
|
||||
|
||||
// Fetch profile data from API
|
||||
const { data: profileData, isLoading } = useGetProfileQuery(undefined, {
|
||||
skip: !isAuthenticated, // Skip the API call if not authenticated
|
||||
});
|
||||
|
||||
// Extract user data from API response
|
||||
const userData = profileData?.data || {
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
phone_number: "",
|
||||
address: "",
|
||||
};
|
||||
|
||||
const languages = [
|
||||
{ code: "tk", img: tm, name: "Türkmen" },
|
||||
{ code: "ru", img: ru, name: "Русский" },
|
||||
{ code: "en", img: en, name: "English" },
|
||||
];
|
||||
|
||||
const handleMenuClick = (item) => {
|
||||
if (item.action === "logout") {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.action) {
|
||||
setActiveModal(item.action);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = async (langCode) => {
|
||||
await i18n.changeLanguage(langCode);
|
||||
localStorage.setItem("preferredLanguage", langCode);
|
||||
setActiveModal(null);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// Handle edit profile click
|
||||
const handleEditProfile = () => {
|
||||
setActiveModal("editProfile");
|
||||
};
|
||||
|
||||
// Close any modal
|
||||
const handleCloseModal = () => {
|
||||
setActiveModal(null);
|
||||
};
|
||||
|
||||
// Render authenticated profile view
|
||||
const renderAuthenticatedProfile = () => {
|
||||
const menuItems = [
|
||||
// { icon: <MapPin />, text: t("profile.my_address"), path: "/addresses" },
|
||||
{ icon: <Wallet />, text: t("profile.orders"), path: "/orders" },
|
||||
{ icon: <Heart />, text: t("profile.favorites"), path: "/wishlist" },
|
||||
{ icon: <Languages />, text: t("profile.language"), action: "language" },
|
||||
{
|
||||
icon: <List />,
|
||||
text: t("profile.delivery"),
|
||||
path: "/delivery-and-payment",
|
||||
},
|
||||
{ icon: <Mail />, text: t("profile.contact"), path: "/contactus" },
|
||||
{ icon: <Info />, text: t("profile.about"), path: "/about-us" },
|
||||
{ icon: <LogOut />, text: t("profile.logout"), action: "logout" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.profileMenuContainer}>
|
||||
{/* User profile header */}
|
||||
<div className={styles.userProfileHeader}>
|
||||
<div className={styles.userIconContainer}>
|
||||
<User className={styles.userIcon} />
|
||||
</div>
|
||||
<div className={styles.userInfo}>
|
||||
<div className={styles.phoneNumber}>+993 {userData.phone_number}</div>
|
||||
<button
|
||||
onClick={handleEditProfile}
|
||||
className={styles.editProfileLink}
|
||||
>
|
||||
<Edit size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "16px" }}>
|
||||
{/* First name section */}
|
||||
<div className={styles.balanceSection}>
|
||||
<div className={styles.balanceLabel}>{t("profile.name")}</div>
|
||||
<div className={styles.balanceAmount}>
|
||||
{userData.first_name || "Registered User"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last name section */}
|
||||
<div className={styles.balanceSection}>
|
||||
<div className={styles.balanceLabel}>{t("profile.lastname")}</div>
|
||||
<div className={styles.balanceAmount}>
|
||||
{userData.last_name || "Registered User"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address section - Add this new section */}
|
||||
<div className={styles.balanceSection}>
|
||||
<div className={styles.balanceLabel}>{t("profile.address")}</div>
|
||||
<div className={styles.balanceAmount}>
|
||||
{userData.address || "Ashgabat"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Menu items */}
|
||||
<div className={styles.profileMenu}>
|
||||
{menuItems.map((item, index) => renderMenuItem(item, index))}
|
||||
</div>
|
||||
|
||||
{/* Language Modal */}
|
||||
<Modal
|
||||
title={t("profile.selectLanguage")}
|
||||
open={activeModal === "language"}
|
||||
onCancel={handleCloseModal}
|
||||
footer={null}
|
||||
className={styles.langModal}
|
||||
>
|
||||
<div className={styles.languageList}>
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
className={`${styles.languageItem} ${
|
||||
i18n.language === lang.code ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => handleLanguageChange(lang.code)}
|
||||
>
|
||||
<div className={styles.langContent}>
|
||||
<img
|
||||
src={lang.img}
|
||||
alt={lang.name}
|
||||
className={styles.langFlag}
|
||||
/>
|
||||
<span>{lang.name}</span>
|
||||
</div>
|
||||
{i18n.language === lang.code && (
|
||||
<span className={styles.activeDot} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Edit Profile Modal */}
|
||||
<ProfileModal
|
||||
visible={activeModal === "editProfile"}
|
||||
onClose={handleCloseModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render unauthenticated menu
|
||||
const renderUnauthenticatedMenu = () => {
|
||||
const menuItems = [
|
||||
// { icon: <User />, text: t("profile.registration"), action: "signUp" },
|
||||
{ icon: <LogIn />, text: t("profile.login"), action: "login" },
|
||||
{ icon: <Wallet />, text: t("profile.orders"), path: "/orders" },
|
||||
{ icon: <Heart />, text: t("profile.favorites"), path: "/wishlist" },
|
||||
{ icon: <Languages />, text: t("profile.language"), action: "language" },
|
||||
{
|
||||
icon: <List />,
|
||||
text: t("profile.delivery"),
|
||||
path: "/delivery-and-payment",
|
||||
},
|
||||
{ icon: <Mail />, text: t("profile.contact"), path: "/contactus" },
|
||||
{ icon: <Info />, text: t("profile.about"), path: "/about-us" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.profileMenuContainer}>
|
||||
<div className={styles.profileMenu}>
|
||||
{menuItems.map((item, index) => renderMenuItem(item, index))}
|
||||
</div>
|
||||
|
||||
{/* Login/Signup Modals */}
|
||||
<LoginModal
|
||||
isVisible={activeModal === "login"}
|
||||
onClose={handleCloseModal}
|
||||
/>
|
||||
<SignUpModal
|
||||
isVisible={activeModal === "signUp"}
|
||||
onClose={handleCloseModal}
|
||||
/>
|
||||
|
||||
{/* Language Modal */}
|
||||
<Modal
|
||||
title={t("profile.selectLanguage")}
|
||||
open={activeModal === "language"}
|
||||
onCancel={handleCloseModal}
|
||||
footer={null}
|
||||
className={styles.langModal}
|
||||
>
|
||||
<div className={styles.languageList}>
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
className={`${styles.languageItem} ${
|
||||
i18n.language === lang.code ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => handleLanguageChange(lang.code)}
|
||||
>
|
||||
<div className={styles.langContent}>
|
||||
<img
|
||||
src={lang.img}
|
||||
alt={lang.name}
|
||||
className={styles.langFlag}
|
||||
/>
|
||||
<span>{lang.name}</span>
|
||||
</div>
|
||||
{i18n.language === lang.code && (
|
||||
<span className={styles.activeDot} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenuItem = (item, index) => {
|
||||
const content = (
|
||||
<>
|
||||
<span className={styles.icon}>{item.icon}</span>
|
||||
<span className={styles.text}>{item.text}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
if (item.path) {
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
to={item.path}
|
||||
className={styles.menuItem}
|
||||
title={item.text}
|
||||
aria-label={item.text}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className={styles.menuItem}
|
||||
onClick={() => handleMenuClick(item)}
|
||||
title={item.text}
|
||||
aria-label={item.text}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// Show loading state while fetching profile data
|
||||
if (isAuthenticated && isLoading) {
|
||||
return <div className={styles.loading}>Loading profile...</div>;
|
||||
}
|
||||
|
||||
// Render different views based on authentication state
|
||||
return isAuthenticated
|
||||
? renderAuthenticatedProfile()
|
||||
: renderUnauthenticatedMenu();
|
||||
};
|
||||
|
||||
export default ProfileMenu;
|
||||
268
src/components/Review/index.jsx
Normal file
268
src/components/Review/index.jsx
Normal file
@@ -0,0 +1,268 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Star } from "lucide-react";
|
||||
import { Modal, Button } from "antd";
|
||||
import styles from "./review.module.scss";
|
||||
import {
|
||||
useSubmitReviewMutation,
|
||||
useGetReviewsByProductQuery,
|
||||
} from "../../app/api/reviewApi";
|
||||
import { useAuth } from "../../context/authContext";
|
||||
import LoginModal from "../LogIn/index";
|
||||
import SignUpModal from "../SignUp/index";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StarRating = ({ rating, onRatingChange, interactive = false }) => {
|
||||
const [hoverRating, setHoverRating] = useState(0);
|
||||
|
||||
return (
|
||||
<div className={styles.starRating}>
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star
|
||||
key={star}
|
||||
className={`${styles.star} ${
|
||||
(interactive ? hoverRating || rating : rating) >= star
|
||||
? styles.starFilled
|
||||
: styles.starEmpty
|
||||
}`}
|
||||
onClick={() => interactive && onRatingChange(star)}
|
||||
onMouseEnter={() => interactive && setHoverRating(star)}
|
||||
onMouseLeave={() => interactive && setHoverRating(star)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ReviewSection = ({
|
||||
productId,
|
||||
existingReviews = [],
|
||||
reviewStats = { count: 0, rating: "0.00" },
|
||||
}) => {
|
||||
// Get authentication status from auth context
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { t, i18n } = useTranslation();
|
||||
// States for modals
|
||||
const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
|
||||
const [isLoginModalVisible, setIsLoginModalVisible] = useState(false);
|
||||
const [isSignUpModalVisible, setIsSignUpModalVisible] = useState(false);
|
||||
|
||||
// Always call the hook, but skip the query if we already have reviews
|
||||
const { data: apiReviews, isLoading: isLoadingReviews } =
|
||||
useGetReviewsByProductQuery(productId, {
|
||||
skip: existingReviews.length > 0,
|
||||
});
|
||||
|
||||
// Use the API hooks from reviewApi.js
|
||||
const [submitReview, { isLoading: isSubmitting }] = useSubmitReviewMutation();
|
||||
|
||||
const [reviews, setReviews] = useState(
|
||||
existingReviews.length > 0
|
||||
? existingReviews.map((review) => ({
|
||||
id: review.id,
|
||||
rating: Number.parseInt(review.rating),
|
||||
text: review.title,
|
||||
date: review.created_at || new Date().toISOString(),
|
||||
source: review.source,
|
||||
}))
|
||||
: []
|
||||
);
|
||||
|
||||
const [newReview, setNewReview] = useState({
|
||||
rating: 0,
|
||||
title: "",
|
||||
source: "site",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (apiReviews && existingReviews.length === 0) {
|
||||
setReviews(apiReviews);
|
||||
}
|
||||
}, [apiReviews, existingReviews]);
|
||||
|
||||
// Calculate average rating, ensuring we display "0" if there are no reviews
|
||||
const averageRating =
|
||||
reviews.length > 0
|
||||
? (
|
||||
reviews.reduce((sum, review) => sum + review.rating, 0) /
|
||||
reviews.length
|
||||
).toFixed(2)
|
||||
: "0";
|
||||
|
||||
const handleSubmitReview = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!isAuthenticated) {
|
||||
// Show warning modal
|
||||
setIsWarningModalVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newReview.rating || !newReview.title?.trim()) return;
|
||||
|
||||
try {
|
||||
await submitReview({
|
||||
productId,
|
||||
rating: newReview.rating,
|
||||
title: newReview.title,
|
||||
source: newReview.source,
|
||||
}).unwrap();
|
||||
|
||||
const review = {
|
||||
id: Date.now(),
|
||||
rating: newReview.rating,
|
||||
text: newReview.title,
|
||||
date: new Date().toISOString(),
|
||||
source: newReview.source,
|
||||
};
|
||||
|
||||
setReviews((currentReviews) =>
|
||||
Array.isArray(currentReviews) ? [review, ...currentReviews] : [review]
|
||||
);
|
||||
setNewReview({
|
||||
rating: 0,
|
||||
title: "",
|
||||
source: "site",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to submit review:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Modal handlers
|
||||
const handleLoginClick = () => {
|
||||
setIsWarningModalVisible(false);
|
||||
setIsLoginModalVisible(true);
|
||||
};
|
||||
|
||||
const handleSignUpClick = () => {
|
||||
setIsWarningModalVisible(false);
|
||||
setIsSignUpModalVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{/* Warning Modal */}
|
||||
<Modal
|
||||
open={isWarningModalVisible}
|
||||
onCancel={() => setIsWarningModalVisible(false)}
|
||||
footer={null}
|
||||
closeIcon={<span>×</span>}
|
||||
>
|
||||
<div style={{ textAlign: 'center', padding: '10px 0'}}>
|
||||
<p style={{ marginBottom: '20px', gap: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
|
||||
{t("common.Register_or_use_your_existing_account_to_post_a_comment")}
|
||||
</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '16px' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleLoginClick}
|
||||
style={{ minWidth: '100px' }}
|
||||
>
|
||||
{t("profile.login")}
|
||||
</Button>
|
||||
{/* <Button
|
||||
onClick={handleSignUpClick}
|
||||
style={{ minWidth: '100px' }}
|
||||
type="primary"
|
||||
>
|
||||
{t("profile.registration")}
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Login Modal */}
|
||||
<LoginModal
|
||||
isVisible={isLoginModalVisible}
|
||||
onClose={() => setIsLoginModalVisible(false)}
|
||||
/>
|
||||
|
||||
{/* SignUp Modal */}
|
||||
<SignUpModal
|
||||
isVisible={isSignUpModalVisible}
|
||||
onClose={() => setIsSignUpModalVisible(false)}
|
||||
/>
|
||||
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.title}>{t("common.comment")}({reviews.length || 0})</h2>
|
||||
<div className={styles.ratingStar}>
|
||||
<span>{averageRating}</span>
|
||||
<div>
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star
|
||||
key={star}
|
||||
className={
|
||||
Number.parseFloat(averageRating) >= star
|
||||
? styles.starFilled
|
||||
: styles.starEmpty
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
<form className={styles.form} onSubmit={handleSubmitReview}>
|
||||
<h3 className={styles.formTitle}>{t("common.Writecomment")}</h3>
|
||||
|
||||
<div className={styles.formRating}>
|
||||
<StarRating
|
||||
rating={newReview.rating}
|
||||
onRatingChange={(rating) =>
|
||||
setNewReview((prev) => ({ ...prev, rating }))
|
||||
}
|
||||
interactive
|
||||
/>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
className={styles.formTextarea}
|
||||
value={newReview.title}
|
||||
onChange={(e) =>
|
||||
setNewReview((prev) => ({ ...prev, title: e.target.value }))
|
||||
}
|
||||
placeholder={t("common.comment")}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.formSubmit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Ugratmak..." : t("common.Writecomment")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className={styles.reviews}>
|
||||
{isLoadingReviews ? (
|
||||
<div>Loading...</div>
|
||||
) : reviews.length > 0 ? (
|
||||
<div className={styles.reviewsList}>
|
||||
{reviews.map((review) => (
|
||||
<div key={review.id} className={styles.reviewsItem}>
|
||||
<StarRating rating={review.rating} />
|
||||
<p className={styles.reviewsText}>{review.text}</p>
|
||||
<span className={styles.reviewsDate}>
|
||||
{new Date(review.date).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.reviewsEmpty}>
|
||||
{t("common.noComment")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReviewSection;
|
||||
149
src/components/Review/review.module.scss
Normal file
149
src/components/Review/review.module.scss
Normal file
@@ -0,0 +1,149 @@
|
||||
.root {
|
||||
margin: 15px 0 15px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.ratingStar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
svg {
|
||||
color: #facc15;
|
||||
fill: #facc15;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
@media screen and (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 45%;
|
||||
background-color: #fff;
|
||||
padding: 15px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 5px;
|
||||
@media screen and (max-width: 768px) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&Title {
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&Rating {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&Textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
resize: vertical;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #888888;
|
||||
}
|
||||
}
|
||||
|
||||
&Submit {
|
||||
background-color: #888888;
|
||||
color: white;
|
||||
padding: 8px 32px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.starRating {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.star {
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&Filled {
|
||||
color: #facc15;
|
||||
fill: #facc15;
|
||||
}
|
||||
|
||||
&Empty {
|
||||
color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.reviews {
|
||||
background-color: #fff;
|
||||
width: 45%;
|
||||
padding: 15px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 5px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
@media screen and (max-width: 768px) {
|
||||
width: auto;
|
||||
}
|
||||
&List {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
&Item {
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&Text {
|
||||
margin: 8px 0;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
&Date {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&Empty {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
padding: 32px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
src/components/ScrollToTop/index.jsx
Normal file
14
src/components/ScrollToTop/index.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
function ScrollToTop() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ScrollToTop;
|
||||
194
src/components/SignUp/SignUpModal.module.scss
Normal file
194
src/components/SignUp/SignUpModal.module.scss
Normal file
@@ -0,0 +1,194 @@
|
||||
.navButton {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
border: none;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-left: 0.875rem;
|
||||
padding-right: 0.875rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #f3f4f6 !important;
|
||||
color: #4b5563 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.modalWrapper {
|
||||
:global {
|
||||
.ant-modal-content {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
// top: 100px;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
border-bottom: none;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 0 15px 15px;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
display: none;
|
||||
}
|
||||
.ant-modal-close-x{
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabWrapper {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
color: #1890ff;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputGroup {
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
display: table-cell;
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
transform: translateY(11px);
|
||||
background-color: #fff;
|
||||
width: min-content;
|
||||
z-index: 99;
|
||||
position: relative;
|
||||
left: 15px;
|
||||
}
|
||||
input{
|
||||
border-color: #9ca3af !important;
|
||||
height: 44px ;
|
||||
@media screen and (max-width: 768px) {
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: #888888;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin: 16px 0;
|
||||
svg{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
&:hover {
|
||||
background: #aaaaaa;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
color: #000;
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 45%;
|
||||
height: 1px;
|
||||
background: #e8e8e8;
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 42%;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.socialLogin {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 1px solid #6b7280;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
357
src/components/SignUp/index.jsx
Normal file
357
src/components/SignUp/index.jsx
Normal file
@@ -0,0 +1,357 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Modal, Input, Button, message } from "antd";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import IMask from "imask";
|
||||
import styles from "./SignUpModal.module.scss";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import { FaApple } from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useRegisterMutation,
|
||||
useVerifyTokenMutation,
|
||||
} from "../../app/api/authApi";
|
||||
import { RegisterIcon } from "../Icons";
|
||||
import { useAuth } from "../../context/authContext";
|
||||
|
||||
const SignUpModal = ({ isVisible: propIsVisible, onClose: propOnClose }) => {
|
||||
const [internalIsVisible, setInternalIsVisible] = useState(false);
|
||||
const { t, i18n } = useTranslation();
|
||||
const isControlled = propIsVisible !== undefined;
|
||||
const isVisible = isControlled ? propIsVisible : internalIsVisible;
|
||||
const [activeTab, setActiveTab] = useState("phone");
|
||||
const { login: authLogin } = useAuth();
|
||||
const [phone, setPhone] = useState("+993");
|
||||
const [email, setEmail] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [address, setAddress] = useState("");
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const phoneInputRef = useRef(null);
|
||||
const maskRef = useRef(null);
|
||||
|
||||
// Verification code related states
|
||||
const [showVerification, setShowVerification] = useState(false);
|
||||
const [verificationCode, setVerificationCode] = useState("");
|
||||
|
||||
// API mutations
|
||||
const [register, { isLoading: isRegistering }] = useRegisterMutation();
|
||||
const [verifyToken, { isLoading: isVerifying }] = useVerifyTokenMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === "phone" && phoneInputRef.current) {
|
||||
const inputElement = phoneInputRef.current.input;
|
||||
|
||||
if (inputElement) {
|
||||
const maskOptions = {
|
||||
mask: "+{993} 00 000000",
|
||||
lazy: false,
|
||||
placeholderChar: "_",
|
||||
};
|
||||
|
||||
maskRef.current = IMask(inputElement, maskOptions);
|
||||
maskRef.current.value = phone;
|
||||
|
||||
maskRef.current.on("accept", () => {
|
||||
setPhone(maskRef.current.value);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (maskRef.current) {
|
||||
maskRef.current.destroy();
|
||||
maskRef.current = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [activeTab, phone]);
|
||||
|
||||
const showModal = () => {
|
||||
if (!isControlled) {
|
||||
setInternalIsVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (hasChanges) {
|
||||
Modal.confirm({
|
||||
title: t("common.Are_you_sure_you_want_to_close_the_modal"),
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
okText: t("common.yes"),
|
||||
cancelText: t("common.no"),
|
||||
onOk() {
|
||||
closeModal();
|
||||
resetForm();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
closeModal();
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setPhone("+993");
|
||||
setEmail("");
|
||||
setAddress("");
|
||||
setName("");
|
||||
setHasChanges(false);
|
||||
setShowVerification(false);
|
||||
setVerificationCode("");
|
||||
};
|
||||
|
||||
const handleInputChange = (type, value) => {
|
||||
setHasChanges(true);
|
||||
switch (type) {
|
||||
case "phone":
|
||||
setPhone(value);
|
||||
break;
|
||||
case "email":
|
||||
setEmail(value);
|
||||
break;
|
||||
case "name":
|
||||
setName(value);
|
||||
break;
|
||||
case "address":
|
||||
setAddress(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
if (isControlled) {
|
||||
propOnClose?.();
|
||||
} else {
|
||||
setInternalIsVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to clean the phone number - remove country code and spaces
|
||||
const getCleanPhoneNumber = () => {
|
||||
// Remove the country code (+993) and any spaces/non-digit characters
|
||||
return phone.replace(/^\+993\s?/, "").replace(/\s+/g, "");
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const userData = {
|
||||
name: name,
|
||||
address: address,
|
||||
};
|
||||
console.log(userData);
|
||||
|
||||
if (activeTab === "phone") {
|
||||
// Get phone number without the country code
|
||||
const cleanPhone = getCleanPhoneNumber();
|
||||
userData.phone_number = parseInt(cleanPhone, 10);
|
||||
} else {
|
||||
userData.email = email;
|
||||
}
|
||||
|
||||
// Call register API
|
||||
const response = await register(userData).unwrap();
|
||||
|
||||
// If successful, show verification modal
|
||||
if (response) {
|
||||
message.success(t("profile.verification_code_sent"));
|
||||
setShowVerification(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
message.error(error?.data?.message || t("common.something_went_wrong"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyCode = async () => {
|
||||
try {
|
||||
// Get phone number without the country code
|
||||
const cleanPhone = getCleanPhoneNumber();
|
||||
|
||||
// Ensure verificationCode is sent as an integer
|
||||
const verificationCodeInt = parseInt(verificationCode, 10);
|
||||
|
||||
// Call verify API
|
||||
const response = await verifyToken({
|
||||
phone_number:
|
||||
activeTab === "phone" ? parseInt(cleanPhone, 10) : undefined,
|
||||
email: activeTab === "email" ? email : undefined,
|
||||
code: verificationCodeInt, // Use the integer version of the code
|
||||
}).unwrap();
|
||||
|
||||
if (response?.token || response?.data) {
|
||||
const token = response?.token || response?.data;
|
||||
|
||||
authLogin(token);
|
||||
|
||||
message.success(t("profile.registration_successful"));
|
||||
closeModal();
|
||||
resetForm();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Verification error:", error);
|
||||
message.error(
|
||||
error?.data?.message || t("common.invalid_verification_code")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = (event) => {
|
||||
event.target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
};
|
||||
|
||||
// Render verification modal
|
||||
const renderVerificationModal = () => {
|
||||
return (
|
||||
<Modal
|
||||
title={t("profile.verification_code")}
|
||||
open={showVerification}
|
||||
onCancel={() => setShowVerification(false)}
|
||||
footer={null}
|
||||
className={styles.modalWrapper}
|
||||
closeIcon={<span>×</span>}
|
||||
>
|
||||
<div className={styles.verificationWrapper}>
|
||||
<p>
|
||||
{activeTab === "phone"
|
||||
? t("profile.verification_code_sent_to_phone", { phone })
|
||||
: t("profile.verification_code_sent_to_email", { email })}
|
||||
</p>
|
||||
|
||||
<div className={styles.inputGroup}>
|
||||
<label>{t("profile.verification_code")}</label>
|
||||
<Input
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
placeholder="00000"
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.submitButton}
|
||||
onClick={handleVerifyCode}
|
||||
disabled={isVerifying || verificationCode.length < 5}
|
||||
>
|
||||
{isVerifying ? t("common.verifying") : t("common.verify")}
|
||||
</button>
|
||||
|
||||
<div className={styles.resendCode}>
|
||||
<Button type="link" onClick={handleSubmit}>
|
||||
{t("profile.resend_code")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isControlled && (
|
||||
<Button onClick={showModal} className={styles.navButton}>
|
||||
<RegisterIcon />
|
||||
{t("profile.registration")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={t("profile.registration")}
|
||||
open={isVisible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
className={styles.modalWrapper}
|
||||
closeIcon={<span>×</span>}
|
||||
>
|
||||
<div className={styles.tabWrapper}>
|
||||
<div
|
||||
className={`${styles.tab} ${
|
||||
activeTab === "phone" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("phone")}
|
||||
>
|
||||
{t("profile.telephone")}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.tab} ${
|
||||
activeTab === "email" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("email")}
|
||||
>
|
||||
{t("profile.email")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputGroup}>
|
||||
<label>
|
||||
{activeTab === "phone"
|
||||
? t("profile.telephone")
|
||||
: t("profile.email")}
|
||||
</label>
|
||||
{activeTab === "phone" ? (
|
||||
<Input
|
||||
ref={phoneInputRef}
|
||||
value={phone}
|
||||
onChange={(e) => handleInputChange("phone", e.target.value)}
|
||||
className={styles.phoneInput}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.inputGroup}>
|
||||
<label>{t("profile.name")}</label>
|
||||
<Input
|
||||
onFocus={handleFocus}
|
||||
value={name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputGroup}>
|
||||
<label>{t("profile.address")}</label>
|
||||
<Input
|
||||
onFocus={handleFocus}
|
||||
value={address}
|
||||
onChange={(e) => handleInputChange("address", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.submitButton}
|
||||
onClick={handleSubmit}
|
||||
disabled={
|
||||
isRegistering ||
|
||||
(activeTab === "phone" ? !phone || phone.length < 12 : !email) ||
|
||||
!name
|
||||
}
|
||||
>
|
||||
<RegisterIcon />
|
||||
{isRegistering ? t("common.processing") : t("profile.registration")}
|
||||
</button>
|
||||
|
||||
<div className={styles.divider}>{t("common.or")}</div>
|
||||
|
||||
<div className={styles.socialLogin}>
|
||||
<button>
|
||||
<FcGoogle />
|
||||
</button>
|
||||
<button>
|
||||
<FaApple />
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Verification Code Modal */}
|
||||
{renderVerificationModal()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignUpModal;
|
||||
26
src/components/Skeletons/homePage.jsx
Normal file
26
src/components/Skeletons/homePage.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { Skeleton, Card } from "antd";
|
||||
import styles from "./skeleton.module.scss";
|
||||
|
||||
const SkeletonProductCard = () => {
|
||||
return (
|
||||
<div className={styles.skeletonCard}>
|
||||
<Card
|
||||
bordered={false}
|
||||
cover={
|
||||
<Skeleton.Image className={styles.skeletonImage} active={true} />
|
||||
}
|
||||
>
|
||||
<Skeleton active paragraph={{ rows: 2 }} title={{ width: "70%" }} />
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="default"
|
||||
shape="round"
|
||||
className={styles.priceButton}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonProductCard;
|
||||
23
src/components/Skeletons/skeleton.module.scss
Normal file
23
src/components/Skeletons/skeleton.module.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
.skeletonCard {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
:global {
|
||||
.ant-card {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.skeletonImage {
|
||||
width: 100% !important;
|
||||
height: 250px !important;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.priceButton {
|
||||
margin-top: 12px;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user