Skip to content

786-feat: Add merch page #882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

786-feat: Add merch page #882

wants to merge 13 commits into from

Conversation

YulikK
Copy link
Collaborator

@YulikK YulikK commented May 8, 2025

What type of PR is this? (select all that apply)

  • πŸ• Feature
  • πŸ› Bug Fix
  • 🚧 Breaking Change
  • πŸ§‘β€πŸ’» Code Refactor
  • πŸ“ Documentation Update

Description

Add merch page
Add merch card view
Add api for fetch and adapt merch catalog

Related Tickets & Documents

Screenshots, Recordings

image

Added/updated tests?

  • πŸ‘Œ Yes
  • πŸ™…β€β™‚οΈ No, because they aren't needed
  • πŸ™‹β€β™‚οΈ No, because I need help

[optional] Are there any post deployment tasks we need to perform?

[optional] What gif best describes this PR or how it makes you feel?

Alt text

Summary by CodeRabbit

  • New Features

    • Introduced a new "Merch" section with a dedicated page, integrated into navigation and breadcrumbs.
    • Added a merchandise catalog showcasing free design assets with preview images and download functionality.
    • Enabled downloading of single files or multiple assets bundled as ZIP archives.
    • Implemented new merch card and catalog layouts for a seamless browsing experience.
  • Style

    • Added styles for merch cards, catalog grid, and responsive layouts.
  • Chores

    • Updated dependencies to support ZIP file downloads.

@github-actions github-actions bot changed the title Feat/786 add merch cards 786-feat: Add merch page May 8, 2025
@YulikK YulikK self-assigned this May 8, 2025
@YulikK YulikK added this to RS Site May 8, 2025
@YulikK YulikK moved this to Review in RS Site May 8, 2025
@YulikK YulikK changed the base branch from feat/784-merch to main May 9, 2025 07:05
Copy link
Contributor

coderabbitai bot commented May 14, 2025

πŸ“ Walkthrough

"""

Walkthrough

A new "merch" page is introduced, accessible at /merch, featuring a hero section, breadcrumbs, and a merchandise catalog. Supporting backend API integration, data transformation, and download helpers are implemented. Type definitions, UI components, styles, and constants are added or updated to support the new feature.

Changes

File(s) Change Summary
dev-data/hero-page.data.ts, src/shared/constants.ts, src/widgets/breadcrumbs/constants.ts Added "merch" entries to hero page data, page/route constants, and breadcrumb mapping.
package.json Added jszip dependency.
src/app/merch/page.tsx, src/views/merch/merch.tsx Added route, metadata, and main view for the merch page.
src/core/api/app-api.ts, src/entities/merch/api/merch-api.ts Introduced MerchApi and integrated it into the main API class.
src/entities/merch/types.ts Defined merchandise-related TypeScript types.
src/entities/merch/helpers/download.ts Added helper for downloading files and ZIP archives.
src/entities/merch/helpers/transform-merch-catalog.ts Added utility to flatten and transform merch catalog data.
src/entities/merch/model/store.ts Implemented store for loading and transforming merch catalog data.
src/entities/merch/index.ts Centralized exports for merch entity types, component, and store.
src/entities/merch/ui/merch-card/merch-card.tsx, src/entities/merch/ui/merch-card/merch-card.module.scss Added MerchCard component and its styles for displaying merch items.
src/widgets/merch-catalog/index.ts, src/widgets/merch-catalog/ui/merch-catalog.tsx, src/widgets/merch-catalog/ui/merch-catalog.module.scss Added MerchCatalog widget, its index, and styles.
src/widgets/merch-catalog/ui/merch-list/merch-list.tsx, src/widgets/merch-catalog/ui/merch-list/merch-list.module.scss Added MerchList component and grid/list styles for displaying merch cards.
src/widgets/merch/ui/merch.tsx Removed external prop from a link in the Merch component.
src/core/styles/_constants.scss Introduced new SCSS opacity variable $opacity-40.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MerchPage
    participant MerchCatalog
    participant MerchStore
    participant Api
    participant MerchApi

    User->>MerchPage: Visit /merch
    MerchPage->>MerchCatalog: Render
    MerchCatalog->>MerchStore: loadMerchCatalog()
    MerchStore->>Api: api.merch.queryMerchCatalog()
    Api->>MerchApi: queryMerchCatalog()
    MerchApi->>Api: GET merch/filelist.json
    Api-->>MerchApi: MerchResponse
    MerchApi-->>Api: MerchResponse
    Api-->>MerchStore: MerchResponse
    MerchStore->>MerchStore: transformMerchCatalog()
    MerchStore-->>MerchCatalog: MerchProduct[]
    MerchCatalog->>MerchList: Render with products
    MerchList->>MerchCard: Render each product
    User->>MerchCard: Click download
    MerchCard->>downloadArchive: Download file(s)/ZIP
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement new merch page at /community/merch with items from merch-data (#786) βœ…

Suggested labels

feature

Suggested reviewers

  • andron13
  • dzmitry-varabei
  • natanchik
  • Quiddlee
  • SpaNb4
    """

Tip

⚑️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boostβ€”your workflow just got faster.


πŸ“œ Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 451f808 and eb3e9b0.

πŸ“’ Files selected for processing (2)
  • src/entities/merch/ui/merch-card/merch-card.module.scss (1 hunks)
  • src/entities/merch/ui/merch-card/merch-card.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/entities/merch/ui/merch-card/merch-card.module.scss
  • src/entities/merch/ui/merch-card/merch-card.tsx
✨ Finishing Touches
  • πŸ“ Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share
πŸͺ§ Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (12)
dev-data/hero-page.data.ts (1)

30-36: Consider revising the subTitle array.

The merch section has been added with appropriate content, but contains a subTitle array with a single empty string. Consider either removing the empty string or adding actual content, similar to other sections.

-    subTitle: [''],
+    subTitle: [],
src/widgets/merch-catalog/ui/merch-list/merch-list.module.scss (1)

5-9: Grid layout is well-defined

The 3-column grid with equal fractions and consistent gap creates a clean, responsive layout for merchandise items.

Consider adding media queries to adjust the number of columns for smaller screens (e.g., mobile devices) to maintain readability and usability.

.list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
+
+  @media (max-width: 768px) {
+    grid-template-columns: repeat(2, 1fr);
+  }
+
+  @media (max-width: 480px) {
+    grid-template-columns: 1fr;
+  }
}
src/app/merch/page.tsx (1)

11-13: Function name doesn't match its purpose.

The function name CommunityRoute doesn't reflect that this is a merch page. Consider renaming it to MerchRoute for better clarity and consistency.

-export default function CommunityRoute() {
+export default function MerchRoute() {
  return <Merch />;
}
src/entities/merch/api/merch-api.ts (2)

7-9: Add explicit return type for better type safety.

The queryMerchCatalog method would benefit from an explicit return type definition.

-  public queryMerchCatalog() {
+  public queryMerchCatalog(): Promise<{ isSuccess: boolean; result: MerchResponse }> {
    return this.services.rest.get<MerchResponse>(`merch/filelist.json`);
  }

8-8: Consider moving hardcoded URL to constants.

The endpoint URL merch/filelist.json is hardcoded. For better maintainability, consider extracting it to a constants file.

src/entities/merch/model/store.ts (1)

1-2: Missing import for MerchProduct type.

The return type suggestion requires importing the MerchProduct type.

 import { transformMerchCatalog } from '../helpers/transform-merch-catalog';
+import { MerchProduct } from '../types';
 import { api } from '@/shared/api/api';
src/entities/merch/ui/merch-card/merch-card.module.scss (3)

24-46: Consider adding z-index to overlay

The overlay uses absolute positioning but doesn't specify a z-index, which could cause layering issues.

  &::after {
    pointer-events: none;
    content: '';

    position: absolute;
    top: 0;
    left: 0;

    width: 100%;
    height: 100%;
+   z-index: 1;

    background: linear-gradient(
      to bottom,
      transparent 0%,
      hsla(from $color-black h s l / $opacity-10) 100%
    );
  }

64-87: Add z-index to download button

Ensure the download button appears above the gradient overlay by adding z-index.

  cursor: pointer;

  position: absolute;
  right: 10px;
  bottom: 10px;
+ z-index: 2;

  padding: 8px 10px;

89-94: Remove unnecessary background-color

The background-color is redundant for an image element that will display its own content.

.download-img {
  width: 20px;
  height: 20px;
  border-radius: 5px;
- background-color: $color-yellow;
}
src/entities/merch/ui/merch-card/merch-card.tsx (1)

36-36: Consider handling multiple preview images.

The component accepts an array of preview images but only uses the first one.

- <img className={cx('preview')} src={preview[0]} alt={title} />
+ {preview.length > 0 ? (
+   <img className={cx('preview')} src={preview[0]} alt={title} />
+ ) : (
+   <div className={cx('preview-placeholder')}>No image available</div>
+ )}
src/entities/merch/types.ts (2)

15-15: Consider adding type discriminator for union type.

ApiMerchItemAdapt combines three different types without a discriminator property, which could lead to runtime type checking issues.

- export type ApiMerchItemAdapt = ApiMerchItem | ApiMerchCategory | ApiMerchData;
+ export type ApiMerchItemAdapt = 
+   | (ApiMerchItem & { type: 'item' })
+   | (ApiMerchCategory & { type: 'category' })
+   | (ApiMerchData & { type: 'data' });

1-28: Add JSDoc comments to improve type documentation.

The types lack documentation explaining their purpose and relationships.

+/**
+ * Represents a single merchandise item in the API response
+ */
export type ApiMerchItem = {
  name: string;
  preview: string[];
  download: string[];
};

+/**
+ * Represents a category of merchandise items in the API response
+ */
type ApiMerchCategory = {
  [key: string]: ApiMerchItem;
};
πŸ“œ Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between c513d17 and 451f808.

β›” Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • src/shared/assets/svg/download.svg is excluded by !**/*.svg
πŸ“’ Files selected for processing (22)
  • dev-data/hero-page.data.ts (2 hunks)
  • package.json (1 hunks)
  • src/app/merch/page.tsx (1 hunks)
  • src/core/api/app-api.ts (2 hunks)
  • src/core/styles/_constants.scss (1 hunks)
  • src/entities/merch/api/merch-api.ts (1 hunks)
  • src/entities/merch/helpers/download.ts (1 hunks)
  • src/entities/merch/helpers/transform-merch-catalog.ts (1 hunks)
  • src/entities/merch/index.ts (1 hunks)
  • src/entities/merch/model/store.ts (1 hunks)
  • src/entities/merch/types.ts (1 hunks)
  • src/entities/merch/ui/merch-card/merch-card.module.scss (1 hunks)
  • src/entities/merch/ui/merch-card/merch-card.tsx (1 hunks)
  • src/shared/constants.ts (2 hunks)
  • src/views/merch/merch.tsx (1 hunks)
  • src/widgets/breadcrumbs/constants.ts (1 hunks)
  • src/widgets/merch-catalog/index.ts (1 hunks)
  • src/widgets/merch-catalog/ui/merch-catalog.module.scss (1 hunks)
  • src/widgets/merch-catalog/ui/merch-catalog.tsx (1 hunks)
  • src/widgets/merch-catalog/ui/merch-list/merch-list.module.scss (1 hunks)
  • src/widgets/merch-catalog/ui/merch-list/merch-list.tsx (1 hunks)
  • src/widgets/merch/ui/merch.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/widgets/merch/ui/merch.tsx (3)
src/shared/ui/link-custom/link-custom.tsx (1)
  • LinkCustom (45-92)
src/shared/constants.ts (1)
  • LINKS (79-87)
dev-data/merch.data.ts (1)
  • merchData (1-7)
src/core/api/app-api.ts (3)
src/entities/merch/api/merch-api.ts (1)
  • MerchApi (4-10)
src/shared/api/api-base-class.ts (1)
  • ApiBaseClass (14-127)
src/entities/trainer/api/trainer-api.ts (1)
  • TrainerApi (6-19)
src/views/merch/merch.tsx (2)
src/shared/constants.ts (1)
  • PAGE_NAMES (25-31)
src/widgets/merch-catalog/ui/merch-catalog.tsx (1)
  • MerchCatalog (10-20)
src/entities/merch/api/merch-api.ts (1)
src/entities/merch/types.ts (1)
  • MerchResponse (16-18)
src/widgets/merch-catalog/ui/merch-list/merch-list.tsx (2)
src/entities/merch/ui/merch-card/merch-card.tsx (2)
  • cx (13-13)
  • MerchCard (15-46)
src/entities/merch/types.ts (1)
  • MerchProduct (20-27)
src/entities/merch/helpers/transform-merch-catalog.ts (1)
src/entities/merch/types.ts (4)
  • MerchResponse (16-18)
  • MerchProduct (20-27)
  • ApiMerchItemAdapt (15-15)
  • ApiMerchItem (1-5)
πŸ”‡ Additional comments (24)
src/widgets/merch-catalog/ui/merch-catalog.module.scss (1)

1-4: Simple, effective styling for the container component.

The class provides basic flex layout which is appropriate for a container element. The styling is minimal but sufficient for this purpose.

dev-data/hero-page.data.ts (1)

2-2: Image import looks good.

The welcome image import is properly added and will be used in the merch section.

package.json (1)

34-34: JSZip dependency added correctly.

The jszip library is properly added with an appropriate version constraint for ZIP archive functionality in the merch feature.

src/core/styles/_constants.scss (1)

85-85: Opacity constant added in proper order.

The new opacity value is correctly positioned in numerical sequence with the existing opacity constants.

src/widgets/breadcrumbs/constants.ts (1)

16-16: Addition of merch breadcrumb entry looks good.

The new breadcrumb entry for the merch page is properly added and aligns with the PR objectives of creating a dedicated merchandise page.

src/widgets/merch/ui/merch.tsx (1)

24-26: Consider marking the merch link as external.

The link points to an external domain (https://sloths.rs.school/) but is no longer marked as external. This could affect user expectations as they might not realize they're leaving the current site.

-<LinkCustom href={LINKS.MERCH} variant="primary">
+<LinkCustom href={LINKS.MERCH} variant="primary" external>
  {merchData.linkTitle}
</LinkCustom>
src/widgets/merch-catalog/index.ts (1)

1-1: Export of MerchCatalog component looks good.

Clean barrel file export that provides a convenient import path for the MerchCatalog component.

src/shared/constants.ts (2)

30-30: Addition of MERCH page name is appropriate.

The new constant follows the established pattern and supports the new merch page functionality.


105-105: Addition of MERCH route is correct.

The route constant is properly added and will enable proper routing to the new merch page.

src/entities/merch/index.ts (1)

1-3: Well-structured barrel exports

The exports are cleanly organized, following the barrel pattern to provide a centralized access point for merch-related entities.

src/core/api/app-api.ts (3)

1-1: Import added correctly

The MerchApi import is properly added using the project's path alias pattern.


11-12: Property declaration follows established pattern

The merch API property is correctly declared as public readonly, matching the pattern used for other API clients.


17-18: API initialization is consistent

The MerchApi instance is properly initialized with services dependency injection, consistent with other API clients.

src/views/merch/merch.tsx (2)

1-5: Imports are well-organized

The imports follow the project's organization pattern, separating constants, widgets, and other dependencies.


6-14: Component structure is clean and follows async pattern

The async component is properly structured with clear hierarchy. The page composition with HeroPage, Breadcrumbs, and MerchCatalog follows the application's consistent layout pattern.

src/widgets/merch-catalog/ui/merch-list/merch-list.module.scss (1)

1-3: Wrapper flex styling is appropriate

The flex property allows the wrapper to grow and shrink as needed, which is good for responsive layouts.

src/app/merch/page.tsx (1)

5-9: Clean metadata implementation.

Good use of Next.js metadata API to set the page title appropriately.

src/widgets/merch-catalog/ui/merch-list/merch-list.tsx (3)

1-8: Clean imports and proper style setup

The imports are well-organized, and the CSS modules setup with classnames/bind follows best practices.


10-12: Well-typed props interface

Good TypeScript practice with explicit typing for the component props.


13-21: Clean component implementation

The component follows React best practices with proper key usage and spreading props to child components.

src/entities/merch/ui/merch-card/merch-card.module.scss (1)

1-22: Well-structured card component styles

Good use of flexbox, transitions, and hover effects for the card container.

src/entities/merch/helpers/transform-merch-catalog.ts (2)

7-23: Well-implemented recursive category processing

The recursive approach for flattening nested categories is clean and efficient.

However, consider adding validation for empty preview or download arrays:

  products.push({
    id: index,
    name: key,
    title: value.name,
-   preview: value.preview.map((path) => `${baseUrl}/${path}`),
-   download: value.download.map((path) => `${baseUrl}/${path}`),
+   preview: value.preview.length ? value.preview.map((path) => `${baseUrl}/${path}`) : [],
+   download: value.download.length ? value.download.map((path) => `${baseUrl}/${path}`) : [],
    tags: parentTags,
  });

32-36: Robust type guard implementation

The type guard properly checks for all required properties of a merchandise item.

src/entities/merch/types.ts (1)

4-5:

❌ Incorrect review comment

Clarify download type usage.

The download property is defined as string[] but used as a single value in the MerchCard component.


🏁 Script executed:

#!/bin/bash
# Check how the download property is used across the codebase
rg -A 2 "downloadArchive.+download" --glob "*.{ts,tsx}"

Length of output: 624


I’d like to inspect the downloadArchive helper signature to confirm what it expects. Let’s locate and view its definition:


🏁 Script executed:

#!/bin/bash
# Find and display the downloadArchive function definition
rg -n "downloadArchive" -C3 --glob "*.{ts,tsx}"

Length of output: 1713


No changes needed for download type usage

The download prop is correctly typed as string[] in MerchProduct and is passed directly into downloadArchive(files: string[], filename: string), which already handles single-file arrays internally. You can ignore the original clarification request.

Likely an incorrect or invalid review comment.

Comment on lines +10 to +20
export const MerchCatalog = async () => {
const products = await merchStore.loadMerchCatalog();

return (
<section className={cx('container')}>
<div className={cx('content', 'merch-catalog')}>
{products && <MerchList products={products} />}
</div>
</section>
);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Add loading and error states for better UX.

The component lacks loading indicators and error handling that would improve user experience.

 export const MerchCatalog = async () => {
+  // In a client component you'd typically use useState for these states
   const products = await merchStore.loadMerchCatalog();

   return (
     <section className={cx('container')}>
       <div className={cx('content', 'merch-catalog')}>
-        {products && <MerchList products={products} />}
+        {!products && products === undefined ? (
+          <div className={cx('loading')}>Loading merchandise...</div>
+        ) : products && products.length > 0 ? (
+          <MerchList products={products} />
+        ) : (
+          <div className={cx('empty-state')}>No merchandise available at this time.</div>
+        )}
       </div>
     </section>
   );
 };

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +5 to +15
public loadMerchCatalog = async () => {
try {
const res = await api.merch.queryMerchCatalog();

if (res.isSuccess) {
return transformMerchCatalog(res.result);
}
} catch (e) {
console.error(e);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Improve error handling and add explicit return type.

The current implementation lacks proper error handling and type annotations. Also consider adding caching for repeated calls.

-  public loadMerchCatalog = async () => {
+  private cachedProducts = null;
+
+  public loadMerchCatalog = async (): Promise<Array<MerchProduct> | null> => {
+    if (this.cachedProducts) {
+      return this.cachedProducts;
+    }
+    
     try {
       const res = await api.merch.queryMerchCatalog();

       if (res.isSuccess) {
-        return transformMerchCatalog(res.result);
+        this.cachedProducts = transformMerchCatalog(res.result);
+        return this.cachedProducts;
       }
+      
+      return null;
     } catch (e) {
       console.error(e);
+      return null;
     }
   };
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public loadMerchCatalog = async () => {
try {
const res = await api.merch.queryMerchCatalog();
if (res.isSuccess) {
return transformMerchCatalog(res.result);
}
} catch (e) {
console.error(e);
}
};
private cachedProducts = null;
public loadMerchCatalog = async (): Promise<Array<MerchProduct> | null> => {
if (this.cachedProducts) {
return this.cachedProducts;
}
try {
const res = await api.merch.queryMerchCatalog();
if (res.isSuccess) {
this.cachedProducts = transformMerchCatalog(res.result);
return this.cachedProducts;
}
return null;
} catch (e) {
console.error(e);
return null;
}
};

Comment on lines +20 to +34
async function createArchive(files: string[]): Promise<Blob> {
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' });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Add error handling for fetch operations

The fetch operations could fail if URLs are invalid or network issues occur. Consider adding error handling.

  await Promise.all(
    files.map(async (url) => {
-     const response = await fetch(url);
-     const blob = await response.blob();
+     try {
+       const response = await fetch(url);
+       if (!response.ok) {
+         throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
+       }
+       const blob = await response.blob();
+       const filename = url.split('/').pop() || '';
+       zip.file(filename, blob);
+     } catch (error) {
+       console.error(`Error processing file ${url}:`, error);
+       // Optionally: throw error or add a placeholder file indicating the error
+     }
-     const filename = url.split('/').pop() || '';
-
-     zip.file(filename, blob);
    }),
  );
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function createArchive(files: string[]): Promise<Blob> {
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' });
}
async function createArchive(files: string[]): Promise<Blob> {
const zip = new JSZip();
await Promise.all(
files.map(async (url) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
}
const blob = await response.blob();
const filename = url.split('/').pop() || '';
zip.file(filename, blob);
} catch (error) {
console.error(`Error processing file ${url}:`, error);
// Optionally: throw error or add a placeholder file indicating the error
}
}),
);
return zip.generateAsync({ type: 'blob' });
}

Comment on lines +3 to +12
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix duplicate download attribute assignment

There's a duplicate assignment to the download attribute which overwrites the provided filename.

  link.href = url;
  link.download = filename;
- link.download = url.split('/').pop() || '';

Alternatively, if you want to use the filename from the URL as a fallback:

  link.href = url;
- link.download = filename;
- link.download = url.split('/').pop() || '';
+ link.download = filename || url.split('/').pop() || '';
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 downloadFile(url: string, filename: string) {
const link = document.createElement('a');
link.href = url;
link.download = filename || url.split('/').pop() || '';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}

Comment on lines +3 to +6
export const transformMerchCatalog = (data: MerchResponse): MerchProduct[] => {
const products: MerchProduct[] = [];
const baseUrl = process.env.API_BASE_URL;
let index = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Add fallback for API base URL

If process.env.API_BASE_URL is undefined, URLs will be invalid. Add a fallback value.

export const transformMerchCatalog = (data: MerchResponse): MerchProduct[] => {
  const products: MerchProduct[] = [];
- const baseUrl = process.env.API_BASE_URL;
+ const baseUrl = process.env.API_BASE_URL || '';
+ if (!baseUrl) {
+   console.warn('API_BASE_URL is not defined. URLs will be relative.');
+ }
  let index = 0;
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const transformMerchCatalog = (data: MerchResponse): MerchProduct[] => {
const products: MerchProduct[] = [];
const baseUrl = process.env.API_BASE_URL;
let index = 0;
export const transformMerchCatalog = (data: MerchResponse): MerchProduct[] => {
const products: MerchProduct[] = [];
const baseUrl = process.env.API_BASE_URL || '';
if (!baseUrl) {
console.warn('API_BASE_URL is not defined. URLs will be relative.');
}
let index = 0;

Comment on lines +37 to +39
<button onClick={handleDownload} className={cx('download')} disabled={isLoading}>
<Image src={downloadImg} alt="download link" className={cx('download-img')} />
</button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Improve accessibility for the download button.

The button lacks accessible text and loading state indication.

<button 
  onClick={handleDownload} 
  className={cx('download')} 
  disabled={isLoading}
+  aria-label="Download merch files"
>
  <Image src={downloadImg} alt="download link" className={cx('download-img')} />
+  {isLoading && <span className="sr-only">Downloading...</span>}
</button>

Comment on lines +26 to +28
} catch (error) {
console.error(error);
} finally {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Add user feedback for download errors.

The error is logged to console but there's no feedback to the user when the download fails.

try {
  setIsLoading(true);
  await downloadArchive(download, `${title}.zip`);
} catch (error) {
  console.error(error);
+  // Consider adding user-visible error notification here
+  // Example: toast.error('Failed to download. Please try again.');
} finally {
  setIsLoading(false);
}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
console.error(error);
} finally {
} catch (error) {
console.error(error);
// Consider adding user-visible error notification here
// Example: toast.error('Failed to download. Please try again.');
} finally {


try {
setIsLoading(true);
await downloadArchive(download, `${title}.zip`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Verification agent

🧩 Analysis chain

Verify download array handling.

The code assumes download contains at least one URL, but doesn't handle empty arrays.


🏁 Script executed:

#!/bin/bash
# Check if there's validation for empty download arrays
rg -A 3 -B 3 "download.+\.length" --glob "*.{ts,tsx,js,jsx}"

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Locate the definition and usages of downloadArchive to inspect empty-array handling
rg "downloadArchive" -n --glob "*.{ts,tsx,js,jsx}"

Length of output: 381


🏁 Script executed:

#!/bin/bash
# Show the downloadArchive function implementation to check for empty-array handling
rg -n -A 20 -B 5 "export async function downloadArchive" src/entities/merch/helpers/download.ts

Length of output: 456


Add empty-array guard before downloading

The downloadArchive helper only handles one or many filesβ€”passing an empty download array will fall through to createArchive and produce an empty (or malformed) ZIP. Please add a check for zero-length before invoking:

β€’ In src/entities/merch/ui/merch-card/merch-card.tsx:25, wrap the call:

if (download.length === 0) {
  // e.g. show β€œNo files to download” feedback
  return;
}
await downloadArchive(download, `${title}.zip`);

β€’ Optionally, in src/entities/merch/helpers/download.ts (at the top of downloadArchive), add:

if (files.length === 0) {
  throw new Error('No files to download');
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add new merch page
1 participant