From e993b26cc846f0a4b47000b3b9d26b633cdc687f Mon Sep 17 00:00:00 2001 From: YulikK Date: Wed, 12 Mar 2025 12:55:16 +0100 Subject: [PATCH 01/13] feat: 786 - add merch page --- src/app/docs/components/search/search.tsx | 28 +++++++++++------------ src/app/merch/page.tsx | 13 +++++++++++ src/core/const/index.ts | 2 +- src/views/merch/merch.tsx | 10 ++++++++ src/widgets/breadcrumbs/constants.ts | 1 + src/widgets/merch/ui/merch.tsx | 6 +++-- 6 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 src/app/merch/page.tsx create mode 100644 src/views/merch/merch.tsx diff --git a/src/app/docs/components/search/search.tsx b/src/app/docs/components/search/search.tsx index 660cbd0b4..4794c20e7 100644 --- a/src/app/docs/components/search/search.tsx +++ b/src/app/docs/components/search/search.tsx @@ -79,20 +79,20 @@ export default function Search({ lang, resultsRef }: SearchProps) { />
{query - && createPortal( -
- {results.length > 0 - ? ( - results.map((result, index) => ) - ) - : ( -
- {translations[lang].search.noResults.replace('{{query}}', query)} -
- )} -
, - resultsRef.current!, - )} + && createPortal( +
+ {results.length > 0 + ? ( + results.map((result, index) => ) + ) + : ( +
+ {translations[lang].search.noResults.replace('{{query}}', query)} +
+ )} +
, + resultsRef.current!, + )}
); diff --git a/src/app/merch/page.tsx b/src/app/merch/page.tsx new file mode 100644 index 000000000..87651d22d --- /dev/null +++ b/src/app/merch/page.tsx @@ -0,0 +1,13 @@ +import { Metadata } from 'next'; + +import { Merch } from '@/views/merch/merch'; + +export async function generateMetadata(): Promise { + const title = 'Community · The Rolling Scopes School'; + + return { title }; +} + +export default function CommunityRoute() { + return ; +} diff --git a/src/core/const/index.ts b/src/core/const/index.ts index 7ddaa9858..2c33b6ffd 100644 --- a/src/core/const/index.ts +++ b/src/core/const/index.ts @@ -16,7 +16,6 @@ export const LINKS = { BECOME_MENTOR: 'https://app.rs.school/registry/mentor', BECOME_CONTRIBUTOR: 'https://docs.google.com/forms/d/e/1FAIpQLSdGKdEHK1CnZjgll9PpMU0xD1m0hm6xGoXc98H7woCDulyQkg/viewform', - MERCH: 'https://sloths.rs.school/', DONATE_OPEN_COLLECTIVE: 'https://opencollective.com/rsschool', DONATE_BOOSTY: 'https://boosty.to/rsschool', ANGULAR_MENTORING: 'https://github.com/rolling-scopes-school/tasks/tree/master/angular/mentoring', @@ -38,5 +37,6 @@ export const ROUTES = { MENTORSHIP: 'mentorship', DOCS_EN: 'docs/en', DOCS_RU: 'docs/ru', + MERCH: 'merch', NOT_FOUND: '*', } as const; diff --git a/src/views/merch/merch.tsx b/src/views/merch/merch.tsx new file mode 100644 index 000000000..93470576e --- /dev/null +++ b/src/views/merch/merch.tsx @@ -0,0 +1,10 @@ +import { Breadcrumbs } from '@/widgets/breadcrumbs'; + +export const Merch = () => { + return ( + <> + +

Merch page

+ + ); +}; diff --git a/src/widgets/breadcrumbs/constants.ts b/src/widgets/breadcrumbs/constants.ts index ca760eb8a..ec0c4f2a1 100644 --- a/src/widgets/breadcrumbs/constants.ts +++ b/src/widgets/breadcrumbs/constants.ts @@ -13,4 +13,5 @@ export const breadcrumbNameMap: BreadcrumbNameMap = { 'community': 'Community', 'aws-devops': 'AWS DevOps', 'mentorship': 'Mentorship', + 'merch': 'Merch', } as const; diff --git a/src/widgets/merch/ui/merch.tsx b/src/widgets/merch/ui/merch.tsx index 397cd15c1..d625f72f3 100644 --- a/src/widgets/merch/ui/merch.tsx +++ b/src/widgets/merch/ui/merch.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames/bind'; import Image from 'next/image'; -import { LINKS } from '@/core/const'; +import { ROUTES } from '@/core/const'; import rsSchoolMerchImage from '@/shared/assets/merch.webp'; import { LinkCustom } from '@/shared/ui/link-custom'; import { Paragraph } from '@/shared/ui/paragraph'; @@ -21,7 +21,9 @@ export const Merch = () => ( {merchData.title} {merchData.mainParagraph} {merchData.description} - {merchData.linkTitle} + + {merchData.linkTitle} + Date: Wed, 19 Mar 2025 14:21:30 +0100 Subject: [PATCH 02/13] feat: 786 - add fetch data and hero banner --- dev-data/hero-page.data.ts | 8 +++++ src/app/merch/page.tsx | 2 +- src/entities/merch/api/merch-api.ts | 24 ++++++++++++++ .../merch/helpers/adapt-merch-data.ts | 33 +++++++++++++++++++ src/entities/merch/types.ts | 26 +++++++++++++++ src/shared/constants.ts | 1 + src/views/merch/merch.tsx | 11 +++++-- 7 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 src/entities/merch/api/merch-api.ts create mode 100644 src/entities/merch/helpers/adapt-merch-data.ts create mode 100644 src/entities/merch/types.ts diff --git a/dev-data/hero-page.data.ts b/dev-data/hero-page.data.ts index b4fa0e4fd..868a33ed2 100644 --- a/dev-data/hero-page.data.ts +++ b/dev-data/hero-page.data.ts @@ -1,4 +1,5 @@ import coursesPageHeroImg from '@/shared/assets/mentor-with-his-students.webp'; +import welcome from '@/shared/assets/welcome.webp'; export const heroPageData = { school: { @@ -26,4 +27,11 @@ export const heroPageData = { subTitle: ['By teaching others, you learn yourself'], imageAltText: '', }, + merch: { + mainTitle: 'Merch', + widgetTitle: 'Free assets for your design', + subTitle: [''], + heroImageSrc: welcome, + imageAltText: 'A sloth mascot with arms raised under a welcome sign', + }, }; diff --git a/src/app/merch/page.tsx b/src/app/merch/page.tsx index 87651d22d..2d7764144 100644 --- a/src/app/merch/page.tsx +++ b/src/app/merch/page.tsx @@ -3,7 +3,7 @@ import { Metadata } from 'next'; import { Merch } from '@/views/merch/merch'; export async function generateMetadata(): Promise { - const title = 'Community · The Rolling Scopes School'; + const title = 'Merch · The Rolling Scopes School'; return { title }; } diff --git a/src/entities/merch/api/merch-api.ts b/src/entities/merch/api/merch-api.ts new file mode 100644 index 000000000..eb4ae9e0c --- /dev/null +++ b/src/entities/merch/api/merch-api.ts @@ -0,0 +1,24 @@ +import { adaptMerchData } from '../helpers/adapt-merch-data'; +import { ApiMerchResponse, MerchProduct } from '../types'; + +let cache: MerchProduct[] | null = null; + +export const getMerchData = async () => { + if (cache) { + return cache; + } + + try { + if (!process.env.MERCH_URL) { + throw new Error('MERCH_URL is not defined in the environment variables'); + } + const data = await fetch(process.env.MERCH_URL); + const merch = (await data.json()) as ApiMerchResponse; + + cache = adaptMerchData(merch); + + return cache; + } catch (e) { + throw new Error(`Something went wrong fetching merch data! (${e})`); + } +}; diff --git a/src/entities/merch/helpers/adapt-merch-data.ts b/src/entities/merch/helpers/adapt-merch-data.ts new file mode 100644 index 000000000..d25fe0c20 --- /dev/null +++ b/src/entities/merch/helpers/adapt-merch-data.ts @@ -0,0 +1,33 @@ +import { ApiMerchItem, ApiMerchItemAdapt, ApiMerchResponse, MerchProduct } from '../types'; + +export const adaptMerchData = (data: ApiMerchResponse): MerchProduct[] => { + const products: MerchProduct[] = []; + + const processCategory = (category: ApiMerchItemAdapt, parentTags: string[]) => { + for (const [key, value] of Object.entries(category)) { + if (isApiMerchItem(value)) { + products.push({ + name: key, + title: value.name, + preview: value.preview, + download: value.download, + tags: parentTags, + }); + } else { + processCategory(value, [...parentTags, key]); + } + } + }; + + for (const [categoryName, categoryData] of Object.entries(data)) { + processCategory(categoryData, [categoryName]); + } + + return products; +}; + +const isApiMerchItem = (item: unknown): item is ApiMerchItem => { + return Boolean( + item && typeof item === 'object' && 'name' in item && 'preview' in item && 'download' in item, + ); +}; diff --git a/src/entities/merch/types.ts b/src/entities/merch/types.ts new file mode 100644 index 000000000..6eca312eb --- /dev/null +++ b/src/entities/merch/types.ts @@ -0,0 +1,26 @@ +export type ApiMerchItem = { + name: string; + preview: string[]; + download: string[]; +}; + +type ApiMerchCategory = { + [key: string]: ApiMerchItem; +}; + +type ApiMerchData = { + [category: string]: ApiMerchCategory; +}; + +export type ApiMerchItemAdapt = ApiMerchItem | ApiMerchCategory | ApiMerchData; +export type ApiMerchResponse = { + [category: string]: ApiMerchData; +}; + +export type MerchProduct = { + name: string; + title: string; + preview: string[]; + download: string[]; + tags: string[]; +}; diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 43fd9239b..e1755053b 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -16,6 +16,7 @@ export const PAGE_NAMES = { COURSES: 'courses', COMMUNITY: 'community', MENTORSHIP: 'mentorship', + MERCH: 'merch', } as const; // ⚠️ These links are used to identify courses from the API diff --git a/src/views/merch/merch.tsx b/src/views/merch/merch.tsx index 93470576e..207f9d818 100644 --- a/src/views/merch/merch.tsx +++ b/src/views/merch/merch.tsx @@ -1,10 +1,17 @@ +import { getMerchData } from '@/entities/merch/api/merch-api'; +import { PAGE_NAMES } from '@/shared/constants'; import { Breadcrumbs } from '@/widgets/breadcrumbs'; +import { HeroPage } from '@/widgets/hero-page'; + +export const Merch = async () => { + const products = await getMerchData(); -export const Merch = () => { return ( <> + -

Merch page

+

Merch

+

{JSON.stringify(products)}

); }; From 9eafcbec53d2839f4417cba29cd065b9a0b4d3b9 Mon Sep 17 00:00:00 2001 From: YulikK Date: Thu, 27 Mar 2025 12:20:50 +0100 Subject: [PATCH 03/13] feat: 786 - add id and view --- package-lock.json | 15 +++++++- package.json | 3 +- src/app/docs/components/search/search.tsx | 28 +++++++------- .../merch/helpers/adapt-merch-data.ts | 3 ++ src/entities/merch/types.ts | 1 + src/views/merch/merch.tsx | 5 ++- .../ui/merch-item/merch-item.module.scss | 37 +++++++++++++++++++ src/views/merch/ui/merch-item/merch-item.tsx | 25 +++++++++++++ 8 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 src/views/merch/ui/merch-item/merch-item.module.scss create mode 100644 src/views/merch/ui/merch-item/merch-item.tsx diff --git a/package-lock.json b/package-lock.json index 53b8e6b12..35060be35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,8 @@ "remark-rehype": "^11.1.1", "remark-remove-comments": "^1.1.1", "remark-toc": "^9.0.0", - "swiper": "^11.2.4" + "swiper": "^11.2.4", + "uuid": "^11.1.0" }, "devDependencies": { "@eslint/js": "^9.21.0", @@ -13911,6 +13912,18 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/varint": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", diff --git a/package.json b/package.json index cbcc042c5..a593b2bdf 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "remark-rehype": "^11.1.1", "remark-remove-comments": "^1.1.1", "remark-toc": "^9.0.0", - "swiper": "^11.2.4" + "swiper": "^11.2.4", + "uuid": "^11.1.0" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/src/app/docs/components/search/search.tsx b/src/app/docs/components/search/search.tsx index 4794c20e7..660cbd0b4 100644 --- a/src/app/docs/components/search/search.tsx +++ b/src/app/docs/components/search/search.tsx @@ -79,20 +79,20 @@ export default function Search({ lang, resultsRef }: SearchProps) { />
{query - && createPortal( -
- {results.length > 0 - ? ( - results.map((result, index) => ) - ) - : ( -
- {translations[lang].search.noResults.replace('{{query}}', query)} -
- )} -
, - resultsRef.current!, - )} + && createPortal( +
+ {results.length > 0 + ? ( + results.map((result, index) => ) + ) + : ( +
+ {translations[lang].search.noResults.replace('{{query}}', query)} +
+ )} +
, + resultsRef.current!, + )}
); diff --git a/src/entities/merch/helpers/adapt-merch-data.ts b/src/entities/merch/helpers/adapt-merch-data.ts index d25fe0c20..7994041dd 100644 --- a/src/entities/merch/helpers/adapt-merch-data.ts +++ b/src/entities/merch/helpers/adapt-merch-data.ts @@ -1,3 +1,5 @@ +import { v4 as uuidv4 } from 'uuid'; + import { ApiMerchItem, ApiMerchItemAdapt, ApiMerchResponse, MerchProduct } from '../types'; export const adaptMerchData = (data: ApiMerchResponse): MerchProduct[] => { @@ -7,6 +9,7 @@ export const adaptMerchData = (data: ApiMerchResponse): MerchProduct[] => { for (const [key, value] of Object.entries(category)) { if (isApiMerchItem(value)) { products.push({ + id: uuidv4(), name: key, title: value.name, preview: value.preview, diff --git a/src/entities/merch/types.ts b/src/entities/merch/types.ts index 6eca312eb..6e900671e 100644 --- a/src/entities/merch/types.ts +++ b/src/entities/merch/types.ts @@ -18,6 +18,7 @@ export type ApiMerchResponse = { }; export type MerchProduct = { + id: string; name: string; title: string; preview: string[]; diff --git a/src/views/merch/merch.tsx b/src/views/merch/merch.tsx index 207f9d818..99526fefa 100644 --- a/src/views/merch/merch.tsx +++ b/src/views/merch/merch.tsx @@ -1,3 +1,4 @@ +import { MerchItem } from './ui/merch-item/merch-item'; import { getMerchData } from '@/entities/merch/api/merch-api'; import { PAGE_NAMES } from '@/shared/constants'; import { Breadcrumbs } from '@/widgets/breadcrumbs'; @@ -11,7 +12,9 @@ export const Merch = async () => {

Merch

-

{JSON.stringify(products)}

+ {products.map((product) => ( + + ))} ); }; diff --git a/src/views/merch/ui/merch-item/merch-item.module.scss b/src/views/merch/ui/merch-item/merch-item.module.scss new file mode 100644 index 000000000..c5a59e3bf --- /dev/null +++ b/src/views/merch/ui/merch-item/merch-item.module.scss @@ -0,0 +1,37 @@ +.merch-item { + overflow: hidden; + display: flex; + flex-direction: column; + + width: 100%; + max-width: 320px; + height: 100%; + margin: 0 auto; + border-radius: 5px; + + background-color: $color-gray-100; + box-shadow: 0 4px 12px 0 hsla(from $color-black h s l / $opacity-10); +} + +.preview-wrap { + display: flex; + height: 180px; +} + +.info-wrap { + display: flex; + flex: 1 1; + flex-direction: column; + justify-content: space-between; + + padding: 16px; +} + +.preview { + width: 100%; + height: 100%; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + + object-fit: cover; +} diff --git a/src/views/merch/ui/merch-item/merch-item.tsx b/src/views/merch/ui/merch-item/merch-item.tsx new file mode 100644 index 000000000..62fe4e6a6 --- /dev/null +++ b/src/views/merch/ui/merch-item/merch-item.tsx @@ -0,0 +1,25 @@ +import classNames from 'classnames/bind'; + +import { MerchProduct } from '@/entities/merch/types'; +import { LinkCustom } from '@/shared/ui/link-custom'; +import { Subtitle } from '@/shared/ui/subtitle'; + +import styles from './merch-item.module.scss'; + +export const cx = classNames.bind(styles); + +export const MerchItem = ({ title, preview, download }: MerchProduct) => { + return ( +
+
+ {title} +
+
+ {title} + + Download + +
+
+ ); +}; From 109650d0db125be560286fe5608adb78e1f98cc0 Mon Sep 17 00:00:00 2001 From: YulikK Date: Mon, 28 Apr 2025 22:02:29 +0200 Subject: [PATCH 04/13] feat: 786 - add merch catalog widget with item list and styles --- src/core/styles/_constants.scss | 1 + src/shared/assets/svg/download.svg | 20 +++++ src/views/merch/merch.tsx | 10 +-- .../ui/merch-item/merch-item.module.scss | 37 -------- src/widgets/merch-catalog/index.ts | 1 + .../ui/merch-catalog.module.scss | 4 + .../merch-catalog/ui/merch-catalog.tsx | 20 +++++ .../ui/merch-item/merch-item.module.scss | 86 +++++++++++++++++++ .../ui/merch-item/merch-item.tsx | 16 ++-- .../ui/merch-list/merch-list.module.scss | 9 ++ .../ui/merch-list/merch-list.tsx | 21 +++++ 11 files changed, 175 insertions(+), 50 deletions(-) create mode 100644 src/shared/assets/svg/download.svg delete mode 100644 src/views/merch/ui/merch-item/merch-item.module.scss create mode 100644 src/widgets/merch-catalog/index.ts create mode 100644 src/widgets/merch-catalog/ui/merch-catalog.module.scss create mode 100644 src/widgets/merch-catalog/ui/merch-catalog.tsx create mode 100644 src/widgets/merch-catalog/ui/merch-item/merch-item.module.scss rename src/{views/merch => widgets/merch-catalog}/ui/merch-item/merch-item.tsx (59%) create mode 100644 src/widgets/merch-catalog/ui/merch-list/merch-list.module.scss create mode 100644 src/widgets/merch-catalog/ui/merch-list/merch-list.tsx diff --git a/src/core/styles/_constants.scss b/src/core/styles/_constants.scss index 51c1694c8..0496288ad 100644 --- a/src/core/styles/_constants.scss +++ b/src/core/styles/_constants.scss @@ -82,6 +82,7 @@ $opacity-100: 1; $opacity-80: 0.8; $opacity-70: 0.7; $opacity-50: 0.5; +$opacity-40: 0.4; $opacity-30: 0.3; $opacity-20: 0.2; $opacity-10: 0.1; diff --git a/src/shared/assets/svg/download.svg b/src/shared/assets/svg/download.svg new file mode 100644 index 000000000..71618685e --- /dev/null +++ b/src/shared/assets/svg/download.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/views/merch/merch.tsx b/src/views/merch/merch.tsx index 99526fefa..f229a71c4 100644 --- a/src/views/merch/merch.tsx +++ b/src/views/merch/merch.tsx @@ -1,20 +1,14 @@ -import { MerchItem } from './ui/merch-item/merch-item'; -import { getMerchData } from '@/entities/merch/api/merch-api'; import { PAGE_NAMES } from '@/shared/constants'; import { Breadcrumbs } from '@/widgets/breadcrumbs'; import { HeroPage } from '@/widgets/hero-page'; +import { MerchCatalog } from '@/widgets/merch-catalog'; export const Merch = async () => { - const products = await getMerchData(); - return ( <> -

Merch

- {products.map((product) => ( - - ))} + ); }; diff --git a/src/views/merch/ui/merch-item/merch-item.module.scss b/src/views/merch/ui/merch-item/merch-item.module.scss deleted file mode 100644 index c5a59e3bf..000000000 --- a/src/views/merch/ui/merch-item/merch-item.module.scss +++ /dev/null @@ -1,37 +0,0 @@ -.merch-item { - overflow: hidden; - display: flex; - flex-direction: column; - - width: 100%; - max-width: 320px; - height: 100%; - margin: 0 auto; - border-radius: 5px; - - background-color: $color-gray-100; - box-shadow: 0 4px 12px 0 hsla(from $color-black h s l / $opacity-10); -} - -.preview-wrap { - display: flex; - height: 180px; -} - -.info-wrap { - display: flex; - flex: 1 1; - flex-direction: column; - justify-content: space-between; - - padding: 16px; -} - -.preview { - width: 100%; - height: 100%; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - - object-fit: cover; -} diff --git a/src/widgets/merch-catalog/index.ts b/src/widgets/merch-catalog/index.ts new file mode 100644 index 000000000..b7df6b3ee --- /dev/null +++ b/src/widgets/merch-catalog/index.ts @@ -0,0 +1 @@ +export { MerchCatalog } from './ui/merch-catalog'; diff --git a/src/widgets/merch-catalog/ui/merch-catalog.module.scss b/src/widgets/merch-catalog/ui/merch-catalog.module.scss new file mode 100644 index 000000000..718a5ee40 --- /dev/null +++ b/src/widgets/merch-catalog/ui/merch-catalog.module.scss @@ -0,0 +1,4 @@ +.merch-catalog { + display: flex; + width: 100%; +} diff --git a/src/widgets/merch-catalog/ui/merch-catalog.tsx b/src/widgets/merch-catalog/ui/merch-catalog.tsx new file mode 100644 index 000000000..f9ce11602 --- /dev/null +++ b/src/widgets/merch-catalog/ui/merch-catalog.tsx @@ -0,0 +1,20 @@ +import classNames from 'classnames/bind'; + +import { MerchList } from './merch-list/merch-list'; +import { getMerchData } from '@/entities/merch/api/merch-api'; + +import styles from './merch-catalog.module.scss'; + +const cx = classNames.bind(styles); + +export const MerchCatalog = async () => { + const products = await getMerchData(); + + return ( +
+
+ +
+
+ ); +}; diff --git a/src/widgets/merch-catalog/ui/merch-item/merch-item.module.scss b/src/widgets/merch-catalog/ui/merch-item/merch-item.module.scss new file mode 100644 index 000000000..1f86b5add --- /dev/null +++ b/src/widgets/merch-catalog/ui/merch-item/merch-item.module.scss @@ -0,0 +1,86 @@ +.merch-item { + overflow: hidden; + display: flex; + flex-direction: column; + + width: 100%; + max-width: 320px; + height: 100%; + margin: 0 auto; + border-radius: 5px; + + background-color: $color-white; + box-shadow: 0 4px 12px 0 hsla(from $color-black h s l / $opacity-10); + + transition: box-shadow 0.3s ease; + + &:hover { + box-shadow: + 0 1px 5px hsla(from $color-black h s l / $opacity-40), + 0 2px 8px -8px hsla(from $color-black h s l / $opacity-40); + } +} + +.preview-wrap { + position: relative; + display: flex; + height: 180px; + + &::after { + pointer-events: none; + content: ''; + + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + background: linear-gradient( + to bottom, + transparent 0%, + hsla(from $color-black h s l / $opacity-10) 100% + ); + } +} + +.info-wrap { + display: flex; + flex: 1 1; + gap: 10px; + align-items: center; + justify-content: space-between; + + padding: 16px; +} + +.preview { + width: 100%; + height: 100%; + object-fit: contain; +} + +.download { + position: absolute; + right: 10px; + bottom: 10px; + + padding: 10px; + border-radius: 100%; + + opacity: 0.8; + background-color: $color-yellow; + box-shadow: 0 4px 12px 0 hsla(from $color-black h s l / $opacity-10); + + &:hover { + opacity: 1; + } +} + +.download-img { + width: 20px; + height: 20px; + border-radius: 5px; + background-color: $color-yellow; +} diff --git a/src/views/merch/ui/merch-item/merch-item.tsx b/src/widgets/merch-catalog/ui/merch-item/merch-item.tsx similarity index 59% rename from src/views/merch/ui/merch-item/merch-item.tsx rename to src/widgets/merch-catalog/ui/merch-item/merch-item.tsx index 62fe4e6a6..f8641f3c0 100644 --- a/src/views/merch/ui/merch-item/merch-item.tsx +++ b/src/widgets/merch-catalog/ui/merch-item/merch-item.tsx @@ -1,8 +1,10 @@ import classNames from 'classnames/bind'; +import Image from 'next/image'; import { MerchProduct } from '@/entities/merch/types'; +import downloadImg from '@/shared/assets/svg/download.svg'; import { LinkCustom } from '@/shared/ui/link-custom'; -import { Subtitle } from '@/shared/ui/subtitle'; +import { Paragraph } from '@/shared/ui/paragraph'; import styles from './merch-item.module.scss'; @@ -13,12 +15,16 @@ export const MerchItem = ({ title, preview, download }: MerchProduct) => {
{title} + + download link +
- {title} - - Download - + {title}
); diff --git a/src/widgets/merch-catalog/ui/merch-list/merch-list.module.scss b/src/widgets/merch-catalog/ui/merch-list/merch-list.module.scss new file mode 100644 index 000000000..342e0e9be --- /dev/null +++ b/src/widgets/merch-catalog/ui/merch-list/merch-list.module.scss @@ -0,0 +1,9 @@ +.wrapper { + flex: 1 1; +} + +.list { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} diff --git a/src/widgets/merch-catalog/ui/merch-list/merch-list.tsx b/src/widgets/merch-catalog/ui/merch-list/merch-list.tsx new file mode 100644 index 000000000..03d127c3a --- /dev/null +++ b/src/widgets/merch-catalog/ui/merch-list/merch-list.tsx @@ -0,0 +1,21 @@ +import classNames from 'classnames/bind'; + +import { MerchItem } from '../merch-item/merch-item'; +import { MerchProduct } from '@/entities/merch/types'; + +import styles from './merch-list.module.scss'; + +const cx = classNames.bind(styles); + +export type MerchListProps = { + products: MerchProduct[]; +}; +export const MerchList = ({ products }: MerchListProps) => ( +
+
+ {products.map((product) => ( + + ))} +
+
+); From 0647f7b8eb1acd14ab2c08cf8ecb0875df4e54b2 Mon Sep 17 00:00:00 2001 From: YulikK Date: Wed, 30 Apr 2025 19:47:54 +0200 Subject: [PATCH 05/13] refactor: 786 - update merch button link to use relative path --- src/shared/__tests__/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/__tests__/constants.ts b/src/shared/__tests__/constants.ts index e78bd1518..797127358 100644 --- a/src/shared/__tests__/constants.ts +++ b/src/shared/__tests__/constants.ts @@ -211,7 +211,7 @@ export const MOCKED_MERCH_DATA = { subtitle: 'Are you an RS sloth fan and looking for RS merch?', paragraph: 'The wait is almost over', buttonText: 'Discover merch assets', - buttonLink: 'https://sloths.rs.school/', + buttonLink: '/merch', imageAltText: 'A collage of photos with branded T-shirts, cups, and stickers featuring the RSSchool logo', }; From ed4c77955a5944023f16ec68fa9616c740b1a6ee Mon Sep 17 00:00:00 2001 From: YulikK Date: Thu, 8 May 2025 18:02:30 +0200 Subject: [PATCH 06/13] refactor: 786 - restructure merch module and improve component reusability --- src/core/api/app-api.ts | 5 ++++ src/entities/merch/api/merch-api.ts | 28 +++++-------------- ...rch-data.ts => transform-merch-catalog.ts} | 4 +-- src/entities/merch/index.ts | 3 ++ src/entities/merch/model/store.ts | 18 ++++++++++++ src/entities/merch/types.ts | 2 +- .../ui/merch-card/merch-card.module.scss} | 2 +- .../merch/ui/merch-card/merch-card.tsx} | 6 ++-- .../merch-catalog/ui/merch-catalog.tsx | 6 ++-- .../ui/merch-list/merch-list.tsx | 4 +-- 10 files changed, 45 insertions(+), 33 deletions(-) rename src/entities/merch/helpers/{adapt-merch-data.ts => transform-merch-catalog.ts} (84%) create mode 100644 src/entities/merch/index.ts create mode 100644 src/entities/merch/model/store.ts rename src/{widgets/merch-catalog/ui/merch-item/merch-item.module.scss => entities/merch/ui/merch-card/merch-card.module.scss} (99%) rename src/{widgets/merch-catalog/ui/merch-item/merch-item.tsx => entities/merch/ui/merch-card/merch-card.tsx} (83%) diff --git a/src/core/api/app-api.ts b/src/core/api/app-api.ts index 93234daa9..aa44de401 100644 --- a/src/core/api/app-api.ts +++ b/src/core/api/app-api.ts @@ -1,3 +1,4 @@ +import { MerchApi } from '@/entities/merch/api/merch-api'; import { TrainerApi } from '@/entities/trainer/api/trainer-api'; import { ApiBaseClass } from '@/shared/api/api-base-class'; import { ApiServices } from '@/shared/types'; @@ -7,9 +8,13 @@ export class Api { public readonly trainer: TrainerApi; + public readonly merch: MerchApi; + constructor(private readonly baseURI: string) { this.services = { rest: new ApiBaseClass(this.baseURI) }; this.trainer = new TrainerApi(this.services); + + this.merch = new MerchApi(this.services); } } diff --git a/src/entities/merch/api/merch-api.ts b/src/entities/merch/api/merch-api.ts index eb4ae9e0c..5ada092d5 100644 --- a/src/entities/merch/api/merch-api.ts +++ b/src/entities/merch/api/merch-api.ts @@ -1,24 +1,10 @@ -import { adaptMerchData } from '../helpers/adapt-merch-data'; -import { ApiMerchResponse, MerchProduct } from '../types'; +import { MerchResponse } from '../types'; +import { ApiServices } from '@/shared/types'; -let cache: MerchProduct[] | null = null; +export class MerchApi { + constructor(private readonly services: ApiServices) {} -export const getMerchData = async () => { - if (cache) { - return cache; + public queryMerchCatalog() { + return this.services.rest.get(`merch/filelist.json`); } - - try { - if (!process.env.MERCH_URL) { - throw new Error('MERCH_URL is not defined in the environment variables'); - } - const data = await fetch(process.env.MERCH_URL); - const merch = (await data.json()) as ApiMerchResponse; - - cache = adaptMerchData(merch); - - return cache; - } catch (e) { - throw new Error(`Something went wrong fetching merch data! (${e})`); - } -}; +} diff --git a/src/entities/merch/helpers/adapt-merch-data.ts b/src/entities/merch/helpers/transform-merch-catalog.ts similarity index 84% rename from src/entities/merch/helpers/adapt-merch-data.ts rename to src/entities/merch/helpers/transform-merch-catalog.ts index 7994041dd..f7d9a6403 100644 --- a/src/entities/merch/helpers/adapt-merch-data.ts +++ b/src/entities/merch/helpers/transform-merch-catalog.ts @@ -1,8 +1,8 @@ import { v4 as uuidv4 } from 'uuid'; -import { ApiMerchItem, ApiMerchItemAdapt, ApiMerchResponse, MerchProduct } from '../types'; +import { ApiMerchItem, ApiMerchItemAdapt, MerchProduct, MerchResponse } from '../types'; -export const adaptMerchData = (data: ApiMerchResponse): MerchProduct[] => { +export const transformMerchCatalog = (data: MerchResponse): MerchProduct[] => { const products: MerchProduct[] = []; const processCategory = (category: ApiMerchItemAdapt, parentTags: string[]) => { diff --git a/src/entities/merch/index.ts b/src/entities/merch/index.ts new file mode 100644 index 000000000..2f60dd8e1 --- /dev/null +++ b/src/entities/merch/index.ts @@ -0,0 +1,3 @@ +export type { MerchProduct } from './types'; +export { MerchCard } from './ui/merch-card/merch-card'; +export { merchStore } from './model/store'; diff --git a/src/entities/merch/model/store.ts b/src/entities/merch/model/store.ts new file mode 100644 index 000000000..1b94a067c --- /dev/null +++ b/src/entities/merch/model/store.ts @@ -0,0 +1,18 @@ +import { transformMerchCatalog } from '../helpers/transform-merch-catalog'; +import { api } from '@/shared/api/api'; + +class MerchStore { + public loadMerchCatalog = async () => { + try { + const res = await api.merch.queryMerchCatalog(); + + if (res.isSuccess) { + return transformMerchCatalog(res.result); + } + } catch (e) { + console.error(e); + } + }; +} + +export const merchStore = new MerchStore(); diff --git a/src/entities/merch/types.ts b/src/entities/merch/types.ts index 6e900671e..7eed79a44 100644 --- a/src/entities/merch/types.ts +++ b/src/entities/merch/types.ts @@ -13,7 +13,7 @@ type ApiMerchData = { }; export type ApiMerchItemAdapt = ApiMerchItem | ApiMerchCategory | ApiMerchData; -export type ApiMerchResponse = { +export type MerchResponse = { [category: string]: ApiMerchData; }; diff --git a/src/widgets/merch-catalog/ui/merch-item/merch-item.module.scss b/src/entities/merch/ui/merch-card/merch-card.module.scss similarity index 99% rename from src/widgets/merch-catalog/ui/merch-item/merch-item.module.scss rename to src/entities/merch/ui/merch-card/merch-card.module.scss index 1f86b5add..71de6dc96 100644 --- a/src/widgets/merch-catalog/ui/merch-item/merch-item.module.scss +++ b/src/entities/merch/ui/merch-card/merch-card.module.scss @@ -1,4 +1,4 @@ -.merch-item { +.merch-card { overflow: hidden; display: flex; flex-direction: column; diff --git a/src/widgets/merch-catalog/ui/merch-item/merch-item.tsx b/src/entities/merch/ui/merch-card/merch-card.tsx similarity index 83% rename from src/widgets/merch-catalog/ui/merch-item/merch-item.tsx rename to src/entities/merch/ui/merch-card/merch-card.tsx index f8641f3c0..29ce92fb1 100644 --- a/src/widgets/merch-catalog/ui/merch-item/merch-item.tsx +++ b/src/entities/merch/ui/merch-card/merch-card.tsx @@ -6,13 +6,13 @@ import downloadImg from '@/shared/assets/svg/download.svg'; import { LinkCustom } from '@/shared/ui/link-custom'; import { Paragraph } from '@/shared/ui/paragraph'; -import styles from './merch-item.module.scss'; +import styles from './merch-card.module.scss'; export const cx = classNames.bind(styles); -export const MerchItem = ({ title, preview, download }: MerchProduct) => { +export const MerchCard = ({ title, preview, download }: MerchProduct) => { return ( -
+
{title} { - const products = await getMerchData(); + const products = await merchStore.loadMerchCatalog(); return (
- + {products && }
); diff --git a/src/widgets/merch-catalog/ui/merch-list/merch-list.tsx b/src/widgets/merch-catalog/ui/merch-list/merch-list.tsx index 03d127c3a..62a385c02 100644 --- a/src/widgets/merch-catalog/ui/merch-list/merch-list.tsx +++ b/src/widgets/merch-catalog/ui/merch-list/merch-list.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames/bind'; -import { MerchItem } from '../merch-item/merch-item'; +import { MerchCard } from '@/entities/merch'; import { MerchProduct } from '@/entities/merch/types'; import styles from './merch-list.module.scss'; @@ -14,7 +14,7 @@ export const MerchList = ({ products }: MerchListProps) => (
{products.map((product) => ( - + ))}
From ef8479a53c0ab7e7b70dcec9971bf67f5010b210 Mon Sep 17 00:00:00 2001 From: YulikK Date: Thu, 8 May 2025 18:52:32 +0200 Subject: [PATCH 07/13] refactor: 786 - remove hardcoded CDN URL and use base URL from env --- src/entities/merch/helpers/transform-merch-catalog.ts | 6 +++--- src/entities/merch/ui/merch-card/merch-card.tsx | 8 ++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/entities/merch/helpers/transform-merch-catalog.ts b/src/entities/merch/helpers/transform-merch-catalog.ts index f7d9a6403..168aaad21 100644 --- a/src/entities/merch/helpers/transform-merch-catalog.ts +++ b/src/entities/merch/helpers/transform-merch-catalog.ts @@ -4,7 +4,7 @@ import { ApiMerchItem, ApiMerchItemAdapt, MerchProduct, MerchResponse } from '.. export const transformMerchCatalog = (data: MerchResponse): MerchProduct[] => { const products: MerchProduct[] = []; - + const baseUrl = process.env.API_BASE_URL; const processCategory = (category: ApiMerchItemAdapt, parentTags: string[]) => { for (const [key, value] of Object.entries(category)) { if (isApiMerchItem(value)) { @@ -12,8 +12,8 @@ export const transformMerchCatalog = (data: MerchResponse): MerchProduct[] => { id: uuidv4(), name: key, title: value.name, - preview: value.preview, - download: value.download, + preview: value.preview.map((path) => `${baseUrl}/${path}`), + download: value.download.map((path) => `${baseUrl}/${path}`), tags: parentTags, }); } else { diff --git a/src/entities/merch/ui/merch-card/merch-card.tsx b/src/entities/merch/ui/merch-card/merch-card.tsx index 29ce92fb1..f1cd22932 100644 --- a/src/entities/merch/ui/merch-card/merch-card.tsx +++ b/src/entities/merch/ui/merch-card/merch-card.tsx @@ -14,12 +14,8 @@ export const MerchCard = ({ title, preview, download }: MerchProduct) => { return (
- {title} - + {title} + download link
From 098bc07cb5f58c80307f3d87590eb66fca8be547 Mon Sep 17 00:00:00 2001 From: YulikK Date: Fri, 9 May 2025 08:42:14 +0200 Subject: [PATCH 08/13] feat: 786 - add archive download functionality for merch items --- package-lock.json | 96 ++++++++++++++++++- package.json | 1 + src/entities/merch/helpers/download.ts | 45 +++++++++ .../ui/merch-card/merch-card.module.scss | 10 +- .../merch/ui/merch-card/merch-card.tsx | 25 ++++- 5 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 src/entities/merch/helpers/download.ts diff --git a/package-lock.json b/package-lock.json index 9c19e83d0..6c4888304 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "dayjs": "^1.11.13", "github-markdown-css": "^5.8.1", "http-status": "^2.1.0", + "jszip": "^3.10.1", "next": "15.3.1", "pagefind": "^1.3.0", "react": "19.1.0", @@ -6072,6 +6073,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -9042,6 +9049,12 @@ "node": ">=16.x" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", @@ -9102,7 +9115,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -10473,6 +10485,24 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/jwa": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", @@ -10537,6 +10567,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -13758,6 +13797,12 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -14054,6 +14099,33 @@ "react": ">=0.14.1" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -15234,6 +15306,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -15711,6 +15789,21 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -17309,7 +17402,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/uuid": { diff --git a/package.json b/package.json index 068b6c0b7..d69c9c481 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dayjs": "^1.11.13", "github-markdown-css": "^5.8.1", "http-status": "^2.1.0", + "jszip": "^3.10.1", "next": "15.3.1", "pagefind": "^1.3.0", "react": "19.1.0", diff --git a/src/entities/merch/helpers/download.ts b/src/entities/merch/helpers/download.ts new file mode 100644 index 000000000..7e5b622c4 --- /dev/null +++ b/src/entities/merch/helpers/download.ts @@ -0,0 +1,45 @@ +import JSZip from 'jszip'; + +function downloadFile(url: string, filename: string) { + const link = document.createElement('a'); + + link.href = url; + link.download = filename; + link.download = url.split('/').pop() || ''; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +function downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + + downloadFile(url, filename); +} + +async function createArchive(files: string[]): Promise { + const zip = new JSZip(); + + await Promise.all( + files.map(async (url) => { + const response = await fetch(url); + const blob = await response.blob(); + const filename = url.split('/').pop() || ''; + + zip.file(filename, blob); + }), + ); + + return zip.generateAsync({ type: 'blob' }); +} + +export async function downloadArchive(files: string[], filename: string) { + if (files.length === 1) { + downloadFile(files[0], filename); + return; + } + + const archiveBlob = await createArchive(files); + + downloadBlob(archiveBlob, `${filename}.zip`); +} diff --git a/src/entities/merch/ui/merch-card/merch-card.module.scss b/src/entities/merch/ui/merch-card/merch-card.module.scss index 71de6dc96..153e0dc02 100644 --- a/src/entities/merch/ui/merch-card/merch-card.module.scss +++ b/src/entities/merch/ui/merch-card/merch-card.module.scss @@ -62,11 +62,14 @@ } .download { + cursor: pointer; + position: absolute; right: 10px; bottom: 10px; - padding: 10px; + padding: 8px 10px; + border: none; border-radius: 100%; opacity: 0.8; @@ -76,6 +79,11 @@ &:hover { opacity: 1; } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } } .download-img { diff --git a/src/entities/merch/ui/merch-card/merch-card.tsx b/src/entities/merch/ui/merch-card/merch-card.tsx index f1cd22932..a2cf4800d 100644 --- a/src/entities/merch/ui/merch-card/merch-card.tsx +++ b/src/entities/merch/ui/merch-card/merch-card.tsx @@ -1,9 +1,11 @@ +'use client'; +import { useState } from 'react'; import classNames from 'classnames/bind'; import Image from 'next/image'; +import { downloadArchive } from '../../helpers/download'; import { MerchProduct } from '@/entities/merch/types'; import downloadImg from '@/shared/assets/svg/download.svg'; -import { LinkCustom } from '@/shared/ui/link-custom'; import { Paragraph } from '@/shared/ui/paragraph'; import styles from './merch-card.module.scss'; @@ -11,13 +13,30 @@ import styles from './merch-card.module.scss'; export const cx = classNames.bind(styles); export const MerchCard = ({ title, preview, download }: MerchProduct) => { + const [isLoading, setIsLoading] = useState(false); + + const handleDownload = async () => { + if (isLoading) { + return; + } + + try { + setIsLoading(true); + await downloadArchive(download, `${title}.zip`); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }; + return (
{title} - +
{title} From b647fbcda8eed1d872ee8c1782c718a994f1ea34 Mon Sep 17 00:00:00 2001 From: YulikK Date: Fri, 9 May 2025 08:46:23 +0200 Subject: [PATCH 09/13] refactor: 786 - replace ROUTES.MERCH with LINKS.MERCH for consistency --- src/widgets/merch/ui/merch.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/merch/ui/merch.tsx b/src/widgets/merch/ui/merch.tsx index 2f5e8b27b..0ed5e3e1a 100644 --- a/src/widgets/merch/ui/merch.tsx +++ b/src/widgets/merch/ui/merch.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames/bind'; import Image from 'next/image'; import rsSchoolMerchImage from '@/shared/assets/merch.webp'; -import { ROUTES } from '@/shared/constants'; +import { LINKS } from '@/shared/constants'; import { LinkCustom } from '@/shared/ui/link-custom'; import { Paragraph } from '@/shared/ui/paragraph'; import { SectionLabel } from '@/shared/ui/section-label'; @@ -21,7 +21,7 @@ export const Merch = () => ( {merchData.title} {merchData.mainParagraph} {merchData.description} - + {merchData.linkTitle}
From 3cacc8f19120df6ecce3d8ad2f857804162cc0f7 Mon Sep 17 00:00:00 2001 From: YulikK Date: Wed, 14 May 2025 10:23:37 +0200 Subject: [PATCH 10/13] refactor: 786 - change product ID from string to number and remove uuid dependency --- package-lock.json | 16 +--------------- package.json | 3 +-- .../merch/helpers/transform-merch-catalog.ts | 6 +++--- src/entities/merch/types.ts | 2 +- 4 files changed, 6 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c4888304..3469b0828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,8 +30,7 @@ "remark-rehype": "^11.1.2", "remark-remove-comments": "^1.1.1", "remark-toc": "^9.0.0", - "swiper": "^11.2.6", - "uuid": "^11.1.0" + "swiper": "^11.2.6" }, "devDependencies": { "@eslint/js": "^9.25.1", @@ -17404,19 +17403,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/varint": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", diff --git a/package.json b/package.json index d69c9c481..dc1d55f45 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,7 @@ "remark-rehype": "^11.1.2", "remark-remove-comments": "^1.1.1", "remark-toc": "^9.0.0", - "swiper": "^11.2.6", - "uuid": "^11.1.0" + "swiper": "^11.2.6" }, "devDependencies": { "@eslint/js": "^9.25.1", diff --git a/src/entities/merch/helpers/transform-merch-catalog.ts b/src/entities/merch/helpers/transform-merch-catalog.ts index 168aaad21..e83040371 100644 --- a/src/entities/merch/helpers/transform-merch-catalog.ts +++ b/src/entities/merch/helpers/transform-merch-catalog.ts @@ -1,15 +1,15 @@ -import { v4 as uuidv4 } from 'uuid'; - import { ApiMerchItem, ApiMerchItemAdapt, MerchProduct, MerchResponse } from '../types'; export const transformMerchCatalog = (data: MerchResponse): MerchProduct[] => { const products: MerchProduct[] = []; const baseUrl = process.env.API_BASE_URL; + let index = 0; const processCategory = (category: ApiMerchItemAdapt, parentTags: string[]) => { for (const [key, value] of Object.entries(category)) { if (isApiMerchItem(value)) { + index += 1; products.push({ - id: uuidv4(), + id: index, name: key, title: value.name, preview: value.preview.map((path) => `${baseUrl}/${path}`), diff --git a/src/entities/merch/types.ts b/src/entities/merch/types.ts index 7eed79a44..c9f4ea1a8 100644 --- a/src/entities/merch/types.ts +++ b/src/entities/merch/types.ts @@ -18,7 +18,7 @@ export type MerchResponse = { }; export type MerchProduct = { - id: string; + id: number; name: string; title: string; preview: string[]; From 451f808c92ea584bcb9af1923bbbaab9e712ba5b Mon Sep 17 00:00:00 2001 From: YulikK Date: Wed, 14 May 2025 10:26:23 +0200 Subject: [PATCH 11/13] fix: 786 - update merch button link in test constants --- src/shared/__tests__/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/__tests__/constants.ts b/src/shared/__tests__/constants.ts index 8ad88920a..32e59d072 100644 --- a/src/shared/__tests__/constants.ts +++ b/src/shared/__tests__/constants.ts @@ -211,7 +211,7 @@ export const MOCKED_MERCH_DATA = { subtitle: 'Are you an RS sloth fan and looking for RS merch?', paragraph: 'The wait is almost over', buttonText: 'Discover merch assets', - buttonLink: '/merch', + buttonLink: 'https://sloths.rs.school/', imageAltText: 'A collage of photos with branded T-shirts, cups, and stickers featuring the RSSchool logo', }; From eb3e9b0cca53b008bb771ac92f0c465200f59f1e Mon Sep 17 00:00:00 2001 From: YulikK Date: Wed, 14 May 2025 20:42:49 +0200 Subject: [PATCH 12/13] refactor: 786 - improve image handling and structure --- .../merch/ui/merch-card/merch-card.module.scss | 6 +++++- src/entities/merch/ui/merch-card/merch-card.tsx | 14 +++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/entities/merch/ui/merch-card/merch-card.module.scss b/src/entities/merch/ui/merch-card/merch-card.module.scss index 153e0dc02..81e98961e 100644 --- a/src/entities/merch/ui/merch-card/merch-card.module.scss +++ b/src/entities/merch/ui/merch-card/merch-card.module.scss @@ -56,9 +56,13 @@ } .preview { + object-fit: contain; +} + +.image-container { + position: relative; width: 100%; height: 100%; - object-fit: contain; } .download { diff --git a/src/entities/merch/ui/merch-card/merch-card.tsx b/src/entities/merch/ui/merch-card/merch-card.tsx index a2cf4800d..e4e81a7ea 100644 --- a/src/entities/merch/ui/merch-card/merch-card.tsx +++ b/src/entities/merch/ui/merch-card/merch-card.tsx @@ -32,12 +32,20 @@ export const MerchCard = ({ title, preview, download }: MerchProduct) => { return (
-
- {title} +
+
+ {title} +
-
+
{title}
From d6a751a5e5c23e26d279b5001b5d6aa9a11115fc Mon Sep 17 00:00:00 2001 From: YulikK Date: Tue, 3 Jun 2025 11:18:47 +0200 Subject: [PATCH 13/13] refactor: 786 - rename CommunityRoute to MerchRoute and streamline merch catalog loading --- src/app/merch/page.tsx | 2 +- src/entities/merch/model/store.ts | 12 +++++------- src/entities/merch/ui/merch-card/merch-card.tsx | 12 ++++-------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/app/merch/page.tsx b/src/app/merch/page.tsx index 2d7764144..5a0ca6561 100644 --- a/src/app/merch/page.tsx +++ b/src/app/merch/page.tsx @@ -8,6 +8,6 @@ export async function generateMetadata(): Promise { return { title }; } -export default function CommunityRoute() { +export default function MerchRoute() { return ; } diff --git a/src/entities/merch/model/store.ts b/src/entities/merch/model/store.ts index 1b94a067c..7162fb7f3 100644 --- a/src/entities/merch/model/store.ts +++ b/src/entities/merch/model/store.ts @@ -3,15 +3,13 @@ import { api } from '@/shared/api/api'; class MerchStore { public loadMerchCatalog = async () => { - try { - const res = await api.merch.queryMerchCatalog(); + const res = await api.merch.queryMerchCatalog(); - if (res.isSuccess) { - return transformMerchCatalog(res.result); - } - } catch (e) { - console.error(e); + if (res.isSuccess) { + return transformMerchCatalog(res.result); } + + throw new Error('Error while loading merch catalog.'); }; } diff --git a/src/entities/merch/ui/merch-card/merch-card.tsx b/src/entities/merch/ui/merch-card/merch-card.tsx index e4e81a7ea..fff49cca5 100644 --- a/src/entities/merch/ui/merch-card/merch-card.tsx +++ b/src/entities/merch/ui/merch-card/merch-card.tsx @@ -20,14 +20,10 @@ export const MerchCard = ({ title, preview, download }: MerchProduct) => { return; } - try { - setIsLoading(true); - await downloadArchive(download, `${title}.zip`); - } catch (error) { - console.error(error); - } finally { - setIsLoading(false); - } + setIsLoading(true); + await downloadArchive(download, `${title}.zip`); + + setIsLoading(false); }; return (