diff --git a/src/app/api/bannersApi.js b/src/app/api/bannersApi.js index 45a444b..7ab8187 100644 --- a/src/app/api/bannersApi.js +++ b/src/app/api/bannersApi.js @@ -8,7 +8,10 @@ export const mediaApi = baseApi.injectEndpoints({ getBanners: builder.query({ query: () => '/media/banners', }), + getStories: builder.query({ + query: () => '/media/stories', + }), }), }); -export const { useGetCarouselsQuery, useGetBannersQuery } = mediaApi; \ No newline at end of file +export const { useGetCarouselsQuery, useGetBannersQuery, useGetStoriesQuery } = mediaApi; \ No newline at end of file diff --git a/src/app/api/flashSalesApi.js b/src/app/api/flashSalesApi.js new file mode 100644 index 0000000..f4253e7 --- /dev/null +++ b/src/app/api/flashSalesApi.js @@ -0,0 +1,11 @@ +import { baseApi } from "./baseApi"; + +export const flashSalesApi = baseApi.injectEndpoints({ + endpoints: (builder) => ({ + getFlashSales: builder.query({ + query: () => "/flash-sales", + }), + }), +}); + +export const { useGetFlashSalesQuery } = flashSalesApi; diff --git a/src/components/Banner/Stories.module.scss b/src/components/Banner/Stories.module.scss new file mode 100644 index 0000000..906ed6f --- /dev/null +++ b/src/components/Banner/Stories.module.scss @@ -0,0 +1,109 @@ +.storiesContainer { + width: 100%; + max-width: 1366px; + margin: 0 auto 24px; + padding: 0 0.75rem; + box-sizing: border-box; + + @media screen and (max-width: 768px) { + padding: 0; + margin-bottom: 16px; + } +} + +.storiesWrapper { + display: flex; + gap: 14px; + padding: 10px 4px; + overflow-x: auto; + scroll-behavior: smooth; + scrollbar-width: none; + max-width: 1336px; + cursor: grab; + user-select: none; + &.dragging { + cursor: grabbing; + } + &::-webkit-scrollbar { + display: none; + } +} + +.storyButton { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + background: none; + border: none; + padding: 0; + cursor: pointer; + flex-shrink: 0; + + &:active .storyAvatar { + transform: scale(0.9); + } +} + +/* Gradient ring — conic-gradient, padding trick, temiz */ +.storyAvatar { + position: relative; + width: 68px; + height: 68px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + box-sizing: border-box; + transition: transform 0.15s ease; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + background: conic-gradient(#f44336, #e91e63, #ff9800, #f44336); + z-index: 0; + } + + img { + position: relative; + z-index: 1; + width: 60px; + height: 60px; + border-radius: 50%; + object-fit: cover; + background: #fff; + border: 2px solid #fff; + display: block; + box-sizing: border-box; + } + + &.viewed::before { + background: #d0d0d0; + } + &.viewed img { + opacity: 0.5; + } +} + +/* Badge/viewedIndicator tamamen kaldırıldı */ + +.storyLabel { + font-size: 11px; + color: #333; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 68px; + font-weight: 400; + transition: color 0.2s; + + .storyButton:hover & { + color: #e91e63; + } +} \ No newline at end of file diff --git a/src/components/Banner/hook/useDragScroll.js b/src/components/Banner/hook/useDragScroll.js new file mode 100644 index 0000000..d5df185 --- /dev/null +++ b/src/components/Banner/hook/useDragScroll.js @@ -0,0 +1,56 @@ +import { useRef, useEffect } from "react"; + +export function useDragScroll() { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + let isDown = false; + let startX = 0; + let scrollLeft = 0; + + const onMouseDown = (e) => { + isDown = true; + el.classList.add("dragging"); + startX = e.pageX - el.offsetLeft; + scrollLeft = el.scrollLeft; + delete el.dataset.dragged; + }; + + const onMouseLeave = () => { + isDown = false; + el.classList.remove("dragging"); + }; + + const onMouseUp = () => { + isDown = false; + el.classList.remove("dragging"); + setTimeout(() => delete el.dataset.dragged, 0); + }; + + const onMouseMove = (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - el.offsetLeft; + const walk = (x - startX) * 1.2; + if (Math.abs(walk) > 5) el.dataset.dragged = "true"; + el.scrollLeft = scrollLeft - walk; + }; + + el.addEventListener("mousedown", onMouseDown); + el.addEventListener("mouseleave", onMouseLeave); + el.addEventListener("mouseup", onMouseUp); + el.addEventListener("mousemove", onMouseMove); + + return () => { + el.removeEventListener("mousedown", onMouseDown); + el.removeEventListener("mouseleave", onMouseLeave); + el.removeEventListener("mouseup", onMouseUp); + el.removeEventListener("mousemove", onMouseMove); + }; + }, []); + + return ref; +} \ No newline at end of file diff --git a/src/components/Banner/index.jsx b/src/components/Banner/index.jsx index 803976b..71dc4ea 100644 --- a/src/components/Banner/index.jsx +++ b/src/components/Banner/index.jsx @@ -1,197 +1,152 @@ -import React, { useState, useEffect, useRef } from "react"; +import { useState, useEffect } from "react"; import { Swiper, SwiperSlide } from "swiper/react"; -import { - Autoplay, - Thumbs, - Pagination, - Navigation, - Mousewheel, - FreeMode, -} from "swiper/modules"; +import { Autoplay, Thumbs, Pagination, Navigation, Mousewheel, FreeMode } from "swiper/modules"; +import { Skeleton } from "antd"; + 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 storiesStyles from "./Stories.module.scss"; -import { Skeleton } from "antd"; +import { useGetCarouselsQuery, useGetStoriesQuery } from "../../app/api/bannersApi.js"; +import StoryViewer from "../StoryViewer/StoryViewer"; +import { useDragScroll } from "./hook/useDragScroll.js"; function Carousel() { - const { data, isLoading, isError } = useGetCarouselsQuery(); + const { data: carouselData, isLoading, isError } = useGetCarouselsQuery(); + const { data: storiesData, isLoading: isStoriesLoading, isError: isStoriesError } = useGetStoriesQuery(); + const [thumbsSwiper, setThumbsSwiper] = useState(null); const [activeIndex, setActiveIndex] = useState(0); const [isAnimating, setIsAnimating] = useState(true); - const thumbSliderRef = useRef(null); + const [selectedStoryIndex, setSelectedStoryIndex] = useState(null); + const [viewedStoryIds, setViewedStoryIds] = useState(new Set()); + + const storiesScrollRef = useDragScroll(); useEffect(() => { setIsAnimating(false); - setTimeout(() => setIsAnimating(true), 50); + const timer = setTimeout(() => setIsAnimating(true), 50); + return () => clearTimeout(timer); }, [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); + setActiveIndex(swiper.realIndex); + }; - if (thumbsSwiper?.slides) { - const slidesPerView = 4; - let targetIndex = newActiveIndex - Math.floor(slidesPerView / 2); - targetIndex = Math.max( - 0, - Math.min(targetIndex, thumbsSwiper.slides.length - slidesPerView) - ); + const handleImageClick = (link) => { + if (link) window.open(link, "_blank", "noopener,noreferrer"); + }; - thumbsSwiper.slideTo(targetIndex, 300); - updateScrollPosition(targetIndex); + const handleStoryViewed = (storyIndex) => { + const storyId = storiesData?.data[storyIndex]?.id; + if (storyId) { + setViewedStoryIds((prev) => new Set(prev).add(storyId)); } }; - // Handler for clicking on carousel images - const handleImageClick = (link) => { - if (link) { - window.open(link, '_blank', 'noopener,noreferrer'); - } + const handleStoryClick = (index) => { + if (storiesScrollRef.current?.dataset.dragged) return; + setSelectedStoryIndex(index); }; if (isLoading) { return (
- {/* Main slider skeleton */} -
- - -
- - {/* Thumbnail Slider skeleton */} -
- {[...Array(5)].map((_, index) => ( -
- - -
- ))} +
+
); } - if (isError || !data || !data.data || data.data.length === 0) { - return
No images available
; - } + if (isError || !carouselData?.data?.length) return null; return ( -
- {/* Main Slider */} - - {data.data.map((item) => ( - -
handleImageClick(item.link)} - style={{ cursor: item.link ? 'pointer' : 'default' }} - > - {item.title -
-
- ))} -
+ <> + {!isStoriesLoading && !isStoriesError && storiesData?.data?.length > 0 && ( +
+
+ {storiesData.data.map((story, index) => { + const isViewed = viewedStoryIds.has(story.id); + return ( + + + ); + })} +
+
+ )} - {/* Thumbnail Slider */} - - {data.data.map((item, index) => ( - -
- {item.title - {index === activeIndex && isAnimating && ( - <> -
-
- - )} -
-
- ))} -
-
+ {selectedStoryIndex !== null && ( + setSelectedStoryIndex(null)} + onStoryViewed={handleStoryViewed} + /> + )} + +
+ + {carouselData.data.map((item) => ( + +
handleImageClick(item.link)} + style={{ cursor: item.link ? "pointer" : "default" }} + > + {item.title +
+
+ ))} +
+ + + {carouselData.data.map((item, index) => ( + +
+ {`Thumb + {index === activeIndex && isAnimating && ( +
+
+
+
+ )} +
+
+ ))} +
+
+ ); } diff --git a/src/components/FlashSales/FlashSales.module.scss b/src/components/FlashSales/FlashSales.module.scss new file mode 100644 index 0000000..f913b5c --- /dev/null +++ b/src/components/FlashSales/FlashSales.module.scss @@ -0,0 +1,206 @@ +// ── Design tokens ────────────────────────────────────────────────────────── +$flash-red: #e53935; +$flash-dark-red: #c62828; +$flash-accent: #ff6b6b; +$flash-yellow: #FFD54F; +$white: #fff; + +// ── Section wrapper ──────────────────────────────────────────────────────── +.flashSales { + margin: 28px 0; + border-radius: 12px; +// overflow: hidden; + background: $white; + box-shadow: 0 4px 24px rgba(229, 57, 53, 0.15); + border: 1.5px solid rgba(229, 57, 53, 0.18); +} + +// ── Gradient header ──────────────────────────────────────────────────────── +.header { + padding: 14px 20px; + background: linear-gradient(120deg, $flash-red 0%, $flash-dark-red 100%); + gap: 12px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + + @media (max-width: 480px) { + padding: 10px 14px; + gap: 8px; + } +} + +// ── Left: label ──────────────────────────────────────────────────────────── +.flashLabel { + flex-shrink: 0; +} + +.zapWrapper { + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.18); + border-radius: 8px; + padding: 4px; +} + +.zapIcon { + color: $flash-yellow; + filter: drop-shadow(0 0 6px rgba(255, 213, 79, 0.8)); + animation: zapPulse 1s ease-in-out infinite alternate; + display: block; +} + +@keyframes zapPulse { + from { opacity: 0.75; transform: scale(1); } + to { opacity: 1; transform: scale(1.2); } +} + +.flashText { + font-size: 1.35rem; + font-weight: 900; + color: $white; + letter-spacing: 2.5px; + text-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); + white-space: nowrap; + + @media (max-width: 480px) { + font-size: 1.05rem; + letter-spacing: 1.5px; + } +} + +.saleTitle { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); + font-weight: 400; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 220px; + + @media (max-width: 480px) { + display: none; + } +} + +// ── Right: timer ─────────────────────────────────────────────────────────── +.timerWrapper { + flex-shrink: 0; +} + +.timerLabel { + font-size: 0.78rem; + color: rgba(255, 255, 255, 0.85); + white-space: nowrap; + font-weight: 500; + + @media (max-width: 600px) { + display: none; + } +} + +.timerBlock { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.28); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 7px; + padding: 5px 11px; + min-width: 44px; + backdrop-filter: blur(4px); + + @media (max-width: 480px) { + min-width: 36px; + padding: 4px 8px; + } +} + +.timerDigit { + font-size: 1.25rem; + font-weight: 800; + color: $white; + line-height: 1; + font-variant-numeric: tabular-nums; + letter-spacing: 1px; + + @media (max-width: 480px) { + font-size: 1rem; + } +} + +.timerUnit { + font-size: 0.58rem; + color: rgba(255, 255, 255, 0.65); + text-transform: uppercase; + letter-spacing: 0.5px; + line-height: 1; + margin-top: 2px; +} + +.timerSep { + color: $white; + font-size: 1.2rem; + font-weight: 800; + line-height: 1; + margin-bottom: 10px; // optical alignment with digit row + user-select: none; +} + +// ── Swiper container ─────────────────────────────────────────────────────── +.swiperWrapper { + padding: 16px 24px; + background: #fff8f8; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + + @media (max-width: 480px) { + padding: 12px 10px; + } + + // Navigation arrows + :global(.swiper-button-next), + :global(.swiper-button-prev) { + color: $flash-red !important; + background: $white; + border-radius: 50%; + width: 34px; + height: 34px; + box-shadow: 0 2px 10px rgba(229, 57, 53, 0.25); + transition: background 0.2s, color 0.2s; + + &::after { + font-size: 13px; + font-weight: 900; + } + + &:hover { + background: $flash-red !important; + color: $white !important; + } + } + + :global(.swiper-button-disabled) { + opacity: 0.3; + pointer-events: none; + } +} + +.swiper { + padding: 6px 2px 10px !important; +} + +.slide { + height: auto; + display: flex; + align-items: stretch; + // Make ProductCard fill the slide height + > * { + // height: 100%; + // min-height: 100%; + // max-height: 100%; + display: flex; + flex-direction: column; + } +} diff --git a/src/components/FlashSales/index.jsx b/src/components/FlashSales/index.jsx new file mode 100644 index 0000000..3f7289d --- /dev/null +++ b/src/components/FlashSales/index.jsx @@ -0,0 +1,148 @@ +import React, { useEffect, useState } from "react"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { Navigation } from "swiper/modules"; +import "swiper/css"; +import "swiper/css/navigation"; +import { Zap } from "lucide-react"; +import { Flex } from "antd"; +import { useGetFlashSalesQuery } from "../../app/api/flashSalesApi"; +import ProductCard from "../ProductCard"; +import styles from "./FlashSales.module.scss"; +import { useTranslation } from "react-i18next"; + +const parseTime = (timeStr) => { + if (!timeStr) return { hours: "00", minutes: "00", seconds: "00" }; + const parts = timeStr.split(":"); + return { + hours: parts[0] || "00", + minutes: parts[1] || "00", + seconds: parts[2] || "00", + }; +}; + +const FlashSales = () => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetFlashSalesQuery(); + const [timers, setTimers] = useState([]); + + const getTimeLeft = (end) => { + const endTime = new Date(end).getTime(); + const now = new Date().getTime(); + let diff = endTime - now; + if (diff <= 0) return "00:00:00"; + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + diff = diff % (1000 * 60 * 60 * 24); + const hours = String(Math.floor(diff / (1000 * 60 * 60))).padStart(2, "0"); + const minutes = String(Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))).padStart(2, "0"); + const seconds = String(Math.floor((diff % (1000 * 60)) / 1000)).padStart(2, "0"); + if (days > 0) { + return `${days}g ${hours}:${minutes}:${seconds}`; + } + return `${hours}:${minutes}:${seconds}`; + }; + + useEffect(() => { + if (!data?.data) return; + const updateTimers = () => { + setTimers(data.data.map((flashSale) => getTimeLeft(flashSale.ends_at))); + }; + updateTimers(); + const interval = setInterval(updateTimers, 1000); + return () => clearInterval(interval); + }, [data]); + + if (isLoading || isError || !data?.data?.length) return null; + + return ( +
+ {data.data.map((flashSale, idx) => { + // Timer parse + let days = 0, hours = "00", minutes = "00", seconds = "00"; + if (timers[idx]) { + const match = timers[idx].match(/(?:(\d+)g )?(\d{2}):(\d{2}):(\d{2})/); + if (match) { + days = match[1] ? Number(match[1]) : 0; + hours = match[2]; + minutes = match[3]; + seconds = match[4]; + } + } + + return ( +
+ {/* ── Header ── */} + + {/* Left: icon + title */} + + + + + {t("flashSales.flash_sale")} + {flashSale.title && ( + {flashSale.title} + )} + + + {/* Right: countdown timer */} + + {t("flashSales.ends_in")} + + {days > 0 && ( +
+ {days} + {t("flashSales.day")} +
+ )} +
+ {hours} + {t("flashSales.hour")} +
+ : +
+ {minutes} + {t("flashSales.minute")} +
+ : +
+ {seconds} + {t("flashSales.second")} +
+
+
+
+ + {/* ── Products Carousel ── */} +
+ + {flashSale.products.map((product) => ( + + + + ))} + +
+
+ ); + })} +
+ ); +}; + +export default FlashSales; diff --git a/src/components/Navbar/Navbar.module.scss b/src/components/Navbar/Navbar.module.scss index 326c85a..eb1cef4 100644 --- a/src/components/Navbar/Navbar.module.scss +++ b/src/components/Navbar/Navbar.module.scss @@ -8,7 +8,7 @@ position: sticky; top: 0; z-index: 99; - + .navbarUp { width: 100%; background-color: #fff; @@ -19,20 +19,29 @@ z-index: 100; } - .btn{ + .btn { display: flex; width: max-content; font-size: 16px; - border-radius: 4px; - border: #000000; - background-color: #000000; - padding: 6px 10px; font-weight: bold; color: #ffffff; - margin: 8px 14px 6px; - @media screen and (max-width: 500px) { - font-size: 14px; - margin: 8px 10px 6px; + background-color: #000000; + border: 1px solid #000000; // Border rengini belirtirken kalınlık da eklemelisin + border-radius: 4px; + padding: 6px 10px; + + cursor: pointer; + + // Mobil Görünüm (Ortak) + @media screen and (max-width: 500px) { + font-size: 14px; + margin: 8px 10px 6px; + } + + &__satyjy { + @media screen and (max-width: 500px) { + display: none; + } } } @@ -74,11 +83,11 @@ box-sizing: border-box; justify-content: center; flex-direction: column; - img{ + img { width: 300px; @media screen and (max-width: 500px) { - width: 100%; - } + width: 100%; + } } @media screen and (max-width: 500px) { width: 100%; @@ -87,7 +96,6 @@ svg { width: 100%; height: 100%; - } } .stick { @@ -99,9 +107,9 @@ } } -.navLinks { - width: 100%; -} + .navLinks { + width: 100%; + } .navLinks ul { list-style: none; display: flex; @@ -132,7 +140,6 @@ align-items: center; flex: 1; flex-direction: row-reverse; - svg { position: absolute; @@ -271,18 +278,23 @@ border: none; outline: none; - &::placeholder { color: #9ca3af; font-size: 0.75rem; } } - .langSelector { - display: flex; - align-items: center; - gap: 8px; - margin-left: auto; - @media screen and (max-width: 500px) { - display: none; - } - } \ No newline at end of file +.langSelector { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + @media screen and (max-width: 500px) { + display: none; + } +} + +.buttonsContainer { + display: flex; + gap: 8px; + margin: 8px 14px 6px; +} diff --git a/src/components/Navbar/index.jsx b/src/components/Navbar/index.jsx index 93e23c3..6e3907f 100644 --- a/src/components/Navbar/index.jsx +++ b/src/components/Navbar/index.jsx @@ -47,12 +47,10 @@ const Navbar = () => { className={styles.logoContainer} onClick={() => navigate("/")} > - +
-
+
{languages.map((lang) => ( ))}
-
- +
+
+ + +
- + { 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 @@ -55,6 +54,11 @@ const ProfileMenu = () => { return; } + if (item.action === "/panel") { + window.location.href = "/panel"; + return; + } + if (item.action) { setActiveModal(item.action); } @@ -62,7 +66,7 @@ const ProfileMenu = () => { const handleLanguageChange = async (langCode) => { await i18n.changeLanguage(langCode); - localStorage.setItem("preferredLanguage", langCode); + localStorage.setItem("preferredLanguage", langCode); setActiveModal(null); window.location.reload(); }; @@ -84,6 +88,7 @@ const ProfileMenu = () => { { icon: , text: t("profile.orders"), path: "/orders" }, { icon: , text: t("profile.favorites"), path: "/wishlist" }, { icon: , text: t("profile.language"), action: "language" }, + { icon: , text: t("profile.seller_panel"), action: "/panel" }, { icon: , text: t("profile.delivery"), @@ -102,7 +107,9 @@ const ProfileMenu = () => {
-
+993 {userData.phone_number}
+
+ +993 {userData.phone_number} +
+ )} + +
e.stopPropagation()} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + onTouchStart={handleTouchStart} + onTouchEnd={handleTouchEnd} + > + {/* Progress bars — driven by JS progress state */} +
+ {stories.map((_, idx) => ( +
+ {idx < currentIndex && ( +
+ )} + {idx === currentIndex && ( +
+ )} +
+ ))} +
+ + {/* Header */} +
+
+
+ {currentStory.title} +
+
+

{currentStory.title}

+ {currentStory.createdAt && ( +

{getTimeAgo(currentStory.createdAt)}

+ )} +
+
+ +
+ + {/* Image */} +
+ {currentStory.title} +
+ + {(currentStory.description || currentStory.caption) && ( +
+

+ {currentStory.description || currentStory.caption} +

+
+ )} +
+ + {!isLast && ( + + )} +
+ ); +}; + +export default StoryViewer; \ No newline at end of file diff --git a/src/components/StoryViewer/StoryViewer.module.scss b/src/components/StoryViewer/StoryViewer.module.scss new file mode 100644 index 0000000..11fdc48 --- /dev/null +++ b/src/components/StoryViewer/StoryViewer.module.scss @@ -0,0 +1,245 @@ +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.92); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.2s ease; + overflow: hidden; +} + +.container { + position: relative; + width: 100%; + max-width: 420px; + height: 90vh; + max-height: 780px; + display: flex; + flex-direction: column; + background: #111; + border-radius: 16px; + overflow: hidden; + touch-action: pan-y; + + @media (max-width: 480px) { + max-width: 100vw; + height: 100dvh; + max-height: none; + border-radius: 0; + } +} + +/* ── Outer nav ── */ +.outerNav { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 44px; + height: 44px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.18); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 10; + transition: background 0.15s, transform 0.15s; + backdrop-filter: blur(4px); + + &:hover { + background: rgba(255, 255, 255, 0.22); + transform: translateY(-50%) scale(1.08); + } + &:active { transform: translateY(-50%) scale(0.95); } + + @media (max-width: 600px) { display: none; } +} + +.outerNavLeft { + left: calc(50% - 210px - 60px); + @media (max-width: 700px) { left: 8px; } +} +.outerNavRight { + right: calc(50% - 210px - 60px); + @media (max-width: 700px) { right: 8px; } +} + +/* ── Progress bars — JS driven, no CSS animation ── */ +.progressBars { + position: absolute; + top: 0; + left: 0; + right: 0; + display: flex; + gap: 3px; + padding: 10px 10px 0; + z-index: 20; +} + +.track { + flex: 1; + height: 2px; + background: rgba(255, 255, 255, 0.25); + border-radius: 2px; + overflow: hidden; + position: relative; +} + +/* Single bar element — width set via inline style */ +.bar { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: #e53935; + border-radius: 2px; + /* No transition — rAF updates are fast enough */ +} + +/* ── Header ── */ +.header { + position: absolute; + top: 22px; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + z-index: 20; + background: linear-gradient(180deg, rgba(0,0,0,0.55) 0%, transparent 100%); +} + +.identity { + display: flex; + align-items: center; + gap: 9px; +} + +.avatar { + width: 36px; + height: 36px; + border-radius: 50%; + border: 1.5px solid rgba(255, 255, 255, 0.65); + overflow: hidden; + flex-shrink: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } +} + +.name { + color: #fff; + font-size: 13px; + font-weight: 600; + margin: 0; + line-height: 1.2; +} + +.time { + color: rgba(255, 255, 255, 0.55); + font-size: 11px; + margin: 0; + margin-top: 1px; +} + +.close { + background: none; + border: none; + color: rgba(255, 255, 255, 0.8); + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.15s; + flex-shrink: 0; + + &:hover { color: #fff; } +} + +/* ── Media ── */ +.media { + flex: 1; + position: relative; + background: #000; + overflow: hidden; +} + +.slideInFromRight { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + animation: slideInRight 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; +} + +.slideInFromLeft { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + animation: slideInLeft 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; +} + +/* Tap zones */ +.tap { + position: absolute; + top: 0; + bottom: 0; + width: 40%; + background: transparent; + border: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + z-index: 10; + + &:disabled { pointer-events: none; } +} +.tapLeft { left: 0; } +.tapRight { right: 0; } + +/* ── Footer ── */ +.footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 28px 16px 18px; + background: linear-gradient(0deg, rgba(0,0,0,0.72) 0%, transparent 100%); + z-index: 20; +} + +.caption { + color: rgba(255, 255, 255, 0.88); + font-size: 13px; + line-height: 1.5; + margin: 0; +} + +/* ── Keyframes ── */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideInRight { + from { opacity: 0; transform: translateX(5%) scale(0.98); } + to { opacity: 1; transform: translateX(0) scale(1); } +} + +@keyframes slideInLeft { + from { opacity: 0; transform: translateX(-5%) scale(0.98); } + to { opacity: 1; transform: translateX(0) scale(1); } +} \ No newline at end of file diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js index 30c02b7..bc5ff33 100644 --- a/src/i18n/locales/en.js +++ b/src/i18n/locales/en.js @@ -1,3 +1,4 @@ + export default { navbar: { category: "Categories", @@ -12,6 +13,14 @@ export default { ru: "Русский", en: "English", }, + }, + flashSales: { + flash_sale: "FLASH SALE", + ends_in: "Ends in:", + day: "day", + hour: "hr", + minute: "min", + second: "sec", }, cart: { basket: "Basket", @@ -129,6 +138,8 @@ export default { verify: "Verify", name: "Name", address: "Address", + seller_panel: "Seller Panel", + }, order: { orderDate: "Order Date", diff --git a/src/i18n/locales/ru.js b/src/i18n/locales/ru.js index 8abc871..6ab997a 100644 --- a/src/i18n/locales/ru.js +++ b/src/i18n/locales/ru.js @@ -1,3 +1,4 @@ + export default { navbar: { category: "Категории", @@ -12,6 +13,14 @@ export default { ru: "Русский", en: "English", }, + }, + flashSales: { + flash_sale: "ФЛЭШ-РАСПРОДАЖА", + ends_in: "До конца:", + day: "дн.", + hour: "ч.", + minute: "мин.", + second: "сек.", }, cart: { basket: "Корзина", @@ -126,6 +135,7 @@ export default { name: "Имя", address: "Address", lastname: "Фамилия", + seller_panel: "Панель продавца", }, order: { orderDate: "Дата заказа", diff --git a/src/i18n/locales/tm.js b/src/i18n/locales/tm.js index 3cc4f3c..bba079b 100644 --- a/src/i18n/locales/tm.js +++ b/src/i18n/locales/tm.js @@ -1,3 +1,4 @@ + export default { navbar: { category: "Kategoriýalar", @@ -12,6 +13,14 @@ export default { ru: "Русский", en: "English", }, + }, + flashSales: { + flash_sale: "GYSGA WAGTLYK ARZANLADYŞ", + ends_in: "Gutarýança:", + day: "gün", + hour: "sag", + minute: "min", + second: "sek", }, cart: { basket: "Sebet", @@ -129,6 +138,7 @@ export default { name: "Ady", address: "Salgy", lastname: "Familýaňyz", + seller_panel: "Satyjy paneli", }, order: { orderDate: "Sargyt senesi", diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx index c7c7e49..97c3fa1 100644 --- a/src/pages/home/index.jsx +++ b/src/pages/home/index.jsx @@ -3,6 +3,7 @@ import InfiniteScroll from "react-infinite-scroll-component"; import CategorySection from "../../components/CategorySection/index"; import Carousel from "../../components/Banner/index"; import CategoryCarousel from "../../components/CategoryCarousel/CategoryCarousel"; +import FlashSales from "../../components/FlashSales"; import styles from "./Home.module.scss"; import { useGetCollectionsQuery } from "../../app/api/collectionsApi"; import PageLoader from "../../components/Loader/pageLoader"; @@ -97,6 +98,7 @@ const Home = () => {
+