diff --git a/public/img/png/promo-slider-1.png b/public/img/png/promo-slider-1.png new file mode 100644 index 00000000..866278d0 Binary files /dev/null and b/public/img/png/promo-slider-1.png differ diff --git a/public/img/png/promo-slider-2.png b/public/img/png/promo-slider-2.png new file mode 100644 index 00000000..a62f4c63 Binary files /dev/null and b/public/img/png/promo-slider-2.png differ diff --git a/public/img/png/promo-slider-3.png b/public/img/png/promo-slider-3.png new file mode 100644 index 00000000..a62b0aaa Binary files /dev/null and b/public/img/png/promo-slider-3.png differ diff --git a/src/entities/PromocodeSlider/model/PromoCodeSliderModel.ts b/src/entities/PromocodeSlider/model/PromoCodeSliderModel.ts new file mode 100644 index 00000000..ad4cd4a9 --- /dev/null +++ b/src/entities/PromocodeSlider/model/PromoCodeSliderModel.ts @@ -0,0 +1,49 @@ +import Swiper from 'swiper'; +import 'swiper/css'; +import 'swiper/css/autoplay'; +import 'swiper/css/bundle'; +import 'swiper/css/pagination'; +import { Autoplay, Pagination } from 'swiper/modules'; + +import PromoCodeSliderView from '../view/PromoCodeSliderView.ts'; + +const SLIDER_DELAY = 10000; +const SLIDER_PER_VIEW = 1; + +class PromoCodeSliderModel { + private slider: Swiper | null = null; + + private view: PromoCodeSliderView; + + constructor() { + this.view = new PromoCodeSliderView(); + this.init(); + } + + private init(): void { + this.initSlider(); + } + + private initSlider(): void { + this.slider = new Swiper(this.view.getSlider(), { + autoplay: { + delay: SLIDER_DELAY, + }, + direction: 'horizontal', + loop: true, + modules: [Autoplay, Pagination], + pagination: { + clickable: true, + el: this.view.getPaginationWrapper(), + }, + slidesPerView: SLIDER_PER_VIEW, + }); + this.slider.autoplay.start(); + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } +} + +export default PromoCodeSliderModel; diff --git a/src/entities/PromocodeSlider/view/PromoCodeSliderView.ts b/src/entities/PromocodeSlider/view/PromoCodeSliderView.ts new file mode 100644 index 00000000..baefe6a2 --- /dev/null +++ b/src/entities/PromocodeSlider/view/PromoCodeSliderView.ts @@ -0,0 +1,218 @@ +import type { User } from '@/shared/types/user'; + +import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts'; +import InputModel from '@/shared/Input/model/InputModel.ts'; +import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; +import { AUTOCOMPLETE_OPTION } from '@/shared/constants/common.ts'; +import { INPUT_TYPE } from '@/shared/constants/forms.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; +import PROMO_SLIDER_CONTENT from '@/shared/constants/promo.ts'; +import SVG_DETAILS from '@/shared/constants/svg.ts'; +import calcUserBirthDayRange from '@/shared/utils/calcUserBirthDayRange.ts'; +import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import createSVGUse from '@/shared/utils/createSVGUse.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; + +import styles from './promoCodeSliderView.module.scss'; + +class PromoCodeSliderView { + private paginationWrapper: HTMLDivElement; + + private slider: HTMLDivElement; + + private view: HTMLDivElement; + + constructor() { + this.paginationWrapper = this.createPaginationWrapper(); + this.slider = this.createSlider(); + this.view = this.createHTML(); + } + + private createDateSpan(index: number, currentUser?: User): HTMLSpanElement { + const date = createBaseElement({ + cssClasses: [styles.sliderDate], + tag: 'span', + }); + + const start = createBaseElement({ + cssClasses: [styles.sliderDateStart], + innerContent: currentUser + ? calcUserBirthDayRange(currentUser.birthDate).start + : PROMO_SLIDER_CONTENT[index].en.date.start ?? '', + tag: 'span', + }); + + const end = createBaseElement({ + cssClasses: [styles.sliderDateEnd], + innerContent: currentUser + ? calcUserBirthDayRange(currentUser.birthDate).end + : PROMO_SLIDER_CONTENT[index].en.date.end ?? '', + tag: 'span', + }); + + date.append(start, end); + return date; + } + + private createHTML(): HTMLDivElement { + this.view = createBaseElement({ + cssClasses: [styles.wrapper], + tag: 'div', + }); + + this.view.append(this.slider); + return this.view; + } + + private createPaginationWrapper(): HTMLDivElement { + this.paginationWrapper = createBaseElement({ + cssClasses: [styles.paginationWrapper], + tag: 'div', + }); + + return this.paginationWrapper; + } + + private createPromoCodeSpan(code: string): HTMLSpanElement { + const promoCode = createBaseElement({ + cssClasses: [styles.sliderPromoCode], + tag: 'span', + }); + + const currentPromoCode = new InputModel({ + autocomplete: AUTOCOMPLETE_OPTION.ON, + id: '', + placeholder: '', + type: INPUT_TYPE.TEXT, + value: code, + }); + + currentPromoCode.getHTML().classList.add(styles.currentPromoCode); + + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.COPY)); + + svg.addEventListener('click', () => { + window.navigator.clipboard + .writeText(currentPromoCode.getValue()) + .then(() => + serverMessageModel.showServerMessage( + SERVER_MESSAGE_KEYS.SUCCESSFUL_COPY_PROMO_CODE_TO_CLIPBOARD, + MESSAGE_STATUS.SUCCESS, + ), + ) + .catch(() => serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.BAD_REQUEST, MESSAGE_STATUS.ERROR)); + }); + + promoCode.append(svg, currentPromoCode.getHTML()); + return promoCode; + } + + private createSlider(): HTMLDivElement { + this.slider = createBaseElement({ + cssClasses: ['swiper', styles.slider], + tag: 'div', + }); + + this.slider.append(this.createSliderWrapper(), this.paginationWrapper); + + return this.slider; + } + + private async createSliderSlideContent(index: number): Promise { + const slide = createBaseElement({ + cssClasses: [styles.sliderContent], + tag: 'div', + }); + + const { description, img, title } = this.createSliderSlideInfo(index); + + slide.append( + title, + description, + this.createPromoCodeSpan(PROMO_SLIDER_CONTENT[index][getStore().getState().currentLanguage].promoCode), + img, + ); + + observeStore(selectCurrentLanguage, () => { + title.textContent = PROMO_SLIDER_CONTENT[index][getStore().getState().currentLanguage].title; + description.textContent = PROMO_SLIDER_CONTENT[index][getStore().getState().currentLanguage].description; + }); + + if (PROMO_SLIDER_CONTENT[index].en.date.start === null) { + const currentUser = await getCustomerModel().getCurrentUser(); + if (currentUser) { + slide.append(this.createDateSpan(index, currentUser)); + } + } else { + slide.append(this.createDateSpan(index)); + } + + return slide; + } + + private createSliderSlideInfo(index: number): { + description: HTMLParagraphElement; + img: HTMLImageElement; + title: HTMLHeadingElement; + } { + const img = createBaseElement({ + attributes: { + src: PROMO_SLIDER_CONTENT[index][getStore().getState().currentLanguage].img, + }, + cssClasses: [styles.sliderImage], + tag: 'img', + }); + + const title = createBaseElement({ + cssClasses: [styles.sliderTitle], + innerContent: PROMO_SLIDER_CONTENT[index][getStore().getState().currentLanguage].title, + tag: 'h3', + }); + + const description = createBaseElement({ + cssClasses: [styles.sliderDescription], + innerContent: PROMO_SLIDER_CONTENT[index][getStore().getState().currentLanguage].description, + tag: 'p', + }); + + return { description, img, title }; + } + + private createSliderWrapper(): HTMLDivElement { + const sliderWrapper = createBaseElement({ + cssClasses: ['swiper-wrapper', styles.sliderWrapper], + tag: 'div', + }); + + PROMO_SLIDER_CONTENT.forEach((_, index) => { + const slideWrapper = createBaseElement({ + cssClasses: ['swiper-slide', styles.sliderSlide], + tag: 'div', + }); + this.createSliderSlideContent(index) + .then((slide) => slideWrapper.append(slide)) + .catch(showErrorMessage); + + sliderWrapper.append(slideWrapper); + }); + + return sliderWrapper; + } + + public getHTML(): HTMLDivElement { + return this.view; + } + + public getPaginationWrapper(): HTMLDivElement { + return this.paginationWrapper; + } + + public getSlider(): HTMLDivElement { + return this.slider; + } +} + +export default PromoCodeSliderView; diff --git a/src/entities/PromocodeSlider/view/promoCodeSliderView.module.scss b/src/entities/PromocodeSlider/view/promoCodeSliderView.module.scss new file mode 100644 index 00000000..edea3b26 --- /dev/null +++ b/src/entities/PromocodeSlider/view/promoCodeSliderView.module.scss @@ -0,0 +1,111 @@ +.slider { + border: calc(var(--two) * 1.5) solid var(--steam-green-1100); + border-radius: var(--large-br); + background-color: var(--steam-green-1000); +} + +.sliderContent { + position: relative; + padding: var(--medium-offset); +} + +.sliderTitle { + margin-bottom: var(--medium-offset); + max-width: 50%; + font: var(--extra-black-font); + letter-spacing: var(--one); + color: var(--steam-green-800); + + @media (max-width: 600px) { + max-width: 100%; + } +} + +.sliderImage { + position: absolute; + right: var(--medium-offset); + top: 50%; + width: 20%; + height: auto; + transform: translateY(-50%); + + @media (max-width: 950px) { + width: 30%; + } + + @media (max-width: 600px) { + display: none; + } +} + +.sliderDescription { + margin-bottom: var(--small-offset); + max-width: 50%; + font: var(--bold-font); + letter-spacing: var(--one); + color: var(--noble-gray-800); + + @media (max-width: 600px) { + max-width: 100%; + } +} + +.sliderPromoCode { + display: flex; + align-items: center; + margin-bottom: var(--small-offset); + max-width: max-content; + gap: var(--tiny-offset); + + svg { + width: 1.5rem; + height: 1.5rem; + fill: transparent; + stroke: var(--steam-green-400); + transition: stroke 0.2s; + cursor: copy; + + @media (hover: hover) { + &:hover { + stroke: var(--steam-green-800); + } + } + } +} + +.currentPromoCode { + font: var(--medium-font); + letter-spacing: var(--one); + color: var(--steam-green-400); + background-color: transparent; + pointer-events: none; +} + +.paginationWrapper { + z-index: 2; + display: flex; + margin: 0 auto; + margin-bottom: var(--tiny-offset); + max-width: max-content; + gap: var(--tiny-offset); + + > * { + border-radius: 50%; + width: 1rem; + height: 1rem; + background-color: var(--steam-green-800); + cursor: pointer; + } +} + +.sliderDate { + display: flex; + gap: var(--tiny-offset); +} + +.sliderDateStart, +.sliderDateEnd { + font: var(--regular-font); + letter-spacing: var(--one); + color: var(--noble-gray-800); +} diff --git a/src/pages/MainPage/model/MainPageModel.ts b/src/pages/MainPage/model/MainPageModel.ts index 303a338d..979067ed 100644 --- a/src/pages/MainPage/model/MainPageModel.ts +++ b/src/pages/MainPage/model/MainPageModel.ts @@ -1,5 +1,6 @@ import type { Page } from '@/shared/types/page.ts'; +import PromoCodeSliderModel from '@/entities/PromocodeSlider/model/PromoCodeSliderModel.ts'; import PostWidgetModel from '@/pages/Blog/PostWidget/model/PostWidgetModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { setCurrentPage } from '@/shared/Store/actions.ts'; @@ -12,6 +13,8 @@ class MainPageModel implements Page { private parent: HTMLDivElement; + private promoCodeSlider = new PromoCodeSliderModel(); + private view: MainPageView; constructor(parent: HTMLDivElement) { @@ -22,7 +25,7 @@ class MainPageModel implements Page { } private init(): void { - this.getHTML().append(this.blogWidget.getHTML()); + this.getHTML().append(this.promoCodeSlider.getHTML(), this.blogWidget.getHTML()); getStore().dispatch(setCurrentPage(PAGE_ID.MAIN_PAGE)); } diff --git a/src/shared/constants/messages.ts b/src/shared/constants/messages.ts index 0cedca64..ef13f7e1 100644 --- a/src/shared/constants/messages.ts +++ b/src/shared/constants/messages.ts @@ -35,6 +35,7 @@ export const SERVER_MESSAGE: Record> SUCCESSFUL_ADD_PRODUCT_TO_CART: 'Product has been added successfully to your cart', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'Product has been added successfully to your wishlist', SUCCESSFUL_CLEAR_CART: 'Cart has been cleared successfully', + SUCCESSFUL_COPY_PROMO_CODE_TO_CLIPBOARD: 'Promo code has been copied to clipboard', SUCCESSFUL_COPY_TO_CLIPBOARD: 'SKU has been copied to clipboard', SUCCESSFUL_DELETE_PRODUCT_FROM_CART: 'Product has been deleted successfully from your cart', SUCCESSFUL_DELETE_PRODUCT_FROM_WISHLIST: 'Product has been deleted successfully from your wishlist', @@ -63,6 +64,7 @@ export const SERVER_MESSAGE: Record> SUCCESSFUL_ADD_PRODUCT_TO_CART: 'Товар успешно добавлен в корзину', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'Товар успешно добавлен в избранное', SUCCESSFUL_CLEAR_CART: 'Корзина была успешно очищена', + SUCCESSFUL_COPY_PROMO_CODE_TO_CLIPBOARD: 'Промо-код скопирован в буфер обмена', SUCCESSFUL_COPY_TO_CLIPBOARD: 'SKU успешно скопирован в буфер обмена', SUCCESSFUL_DELETE_PRODUCT_FROM_CART: 'Товар успешно удален из корзины', SUCCESSFUL_DELETE_PRODUCT_FROM_WISHLIST: 'Товар успешно удален из избранного', @@ -93,6 +95,7 @@ export const SERVER_MESSAGE_KEYS: Record = { SUCCESSFUL_ADD_PRODUCT_TO_CART: 'SUCCESSFUL_ADD_PRODUCT_TO_CART', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST', SUCCESSFUL_CLEAR_CART: 'SUCCESSFUL_CLEAR_CART', + SUCCESSFUL_COPY_PROMO_CODE_TO_CLIPBOARD: 'SUCCESSFUL_COPY_PROMO_CODE_TO_CLIPBOARD', SUCCESSFUL_COPY_TO_CLIPBOARD: 'SUCCESSFUL_COPY_TO_CLIPBOARD', SUCCESSFUL_DELETE_PRODUCT_FROM_CART: 'SUCCESSFUL_DELETE_PRODUCT_FROM_CART', SUCCESSFUL_DELETE_PRODUCT_FROM_WISHLIST: 'SUCCESSFUL_DELETE_PRODUCT_FROM_WISHLIST', diff --git a/src/shared/constants/promo.ts b/src/shared/constants/promo.ts new file mode 100644 index 00000000..4bc9c3e0 --- /dev/null +++ b/src/shared/constants/promo.ts @@ -0,0 +1,70 @@ +const PROMO_SLIDER_CONTENT = [ + { + en: { + date: { + end: '2024-06-30', + start: '2024-06-01', + }, + description: 'Everything for garden and vegetable garden with 10% discount', + img: './img/png/promo-slider-1.png', + promoCode: 'SUMMER-SALE-10', + title: 'Summer sale!', + }, + ru: { + date: { + end: '2024-06-30', + start: '2024-06-01', + }, + description: 'Все для сада и огорода со скидкой в 10%', + img: './img/png/promo-slider-1.png', + promoCode: 'SUMMER-SALE-10', + title: 'Летняя распродажа!', + }, + }, + { + en: { + date: { + end: null, + start: null, + }, + description: 'For your birthday discount 10% from total purchase amount', + img: './img/png/promo-slider-2.png', + promoCode: 'HAPPY-BIRTHDAY-10', + title: 'Happy birthday!', + }, + ru: { + date: { + end: null, + start: null, + }, + description: 'В честь вашего дня рождения скидка 10% от общей суммы покупок', + img: './img/png/promo-slider-2.png', + promoCode: 'HAPPY-BIRTHDAY-10', + title: 'С днем рождения!', + }, + }, + { + en: { + date: { + end: '2024-06-15', + start: '2024-06-01', + }, + description: 'For all products from the category Succulents', + img: './img/png/promo-slider-3.png', + promoCode: 'SUCCULENT-SALE-15', + title: 'Succulents are for everyone!', + }, + ru: { + date: { + end: '2024-06-15', + start: '2024-06-01', + }, + description: 'На все товары из категории Суккуленты', + img: './img/png/promo-slider-3.png', + promoCode: 'SUCCULENT-SALE-15', + title: 'Каждому по суккуленту!', + }, + }, +]; + +export default PROMO_SLIDER_CONTENT; diff --git a/src/shared/utils/calcUserBirthDayRange.ts b/src/shared/utils/calcUserBirthDayRange.ts new file mode 100644 index 00000000..20d2ac27 --- /dev/null +++ b/src/shared/utils/calcUserBirthDayRange.ts @@ -0,0 +1,33 @@ +const getDaysInMonth = (date: Date): number => new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); + +const calcUserBirthDayRange = (birthDay: string): { end: string; start: string } => { + const birthDate = new Date(birthDay); + + const start = new Date(birthDate.getFullYear(), birthDate.getMonth(), birthDate.getDate() - 3); + const end = new Date(birthDate.getFullYear(), birthDate.getMonth(), birthDate.getDate() + 4); + + if (start.getDate() < 1) { + start.setMonth(start.getMonth() - 1); + } + + if (start.getMonth() < 0) { + start.setFullYear(start.getFullYear() - 1); + start.setMonth(12); + } + + if (end.getDate() > getDaysInMonth(end)) { + end.setMonth(end.getMonth() + 1); + } + + if (end.getMonth() > 11) { + end.setFullYear(end.getFullYear() + 1); + end.setMonth(1); + } + + const endDate = end.toISOString().split('T')[0]; + const startDate = start.toISOString().split('T')[0]; + + return { end: endDate, start: startDate }; +}; + +export default calcUserBirthDayRange;