Skip to content

Commit

Permalink
EPMGCIP-180: Decrease quantity of warnings to 0 (#36)
Browse files Browse the repository at this point in the history
* EPMGCIP-180: Add configuration to ESLint to extend "react/recommended" plugin

* EPMGCIP-180: Resolve ESLint warnings in "i18n.ts" file

* EPMGCIP-180: Resolve ESLint warnings in "getExhibit" function

* EPMGCIP-180: Resolve ESLint warnings in "sendContactForm" function

* EPMGCIP-180: Resolve ESLint warnings related to "IExhibit" interface

* EPMGCIP-180: Split "Exhibit" page into separate subcomponents, update usages

* EPMGCIP-180: Suppress complexity issue on "ExhibitDetails" due to simple structure

* EPMGCIP-180: Ignore blank lines and comments in "max-lines" ESLint rules

* EPMGCIP-180: Resolve ESLint warnings related to "ImageGallery" component

* EPMGCIP-180: Decrease max warnings level in Git hooks to 0 allowed
Dzmitry-Yaniuk authored Dec 27, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 77c60aa commit 7b29fc9
Showing 11 changed files with 224 additions and 159 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:jest/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"ignorePatterns": ["node_modules", "src/__generated__", "build", "coverage"],
@@ -75,7 +76,7 @@
}
],
"max-depth": ["warn", 2],
"max-lines": ["warn", 200],
"max-lines": ["warn", { "max": 200, "skipBlankLines": true, "skipComments": true }],
"max-nested-callbacks": ["warn", 3],
"max-params": ["warn", 3],
"max-statements-per-line": [
@@ -110,6 +111,7 @@
"no-warning-comments": "warn",
"one-var-declaration-per-line": ["warn", "always"],
"react/display-name": "warn",
"react/react-in-jsx-scope": "off",
"react/sort-comp": "warn",
"sort-keys": "warn",
"unused-imports/no-unused-imports": "error"
2 changes: 1 addition & 1 deletion lint-staged.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const config = {
'*.{js,jsx,ts,tsx}': ['eslint --fix --max-warnings 20', 'prettier --write'],
'*.{js,jsx,ts,tsx}': ['eslint --fix --max-warnings 0', 'prettier --write'],
'*.{json,scss,css}': ['prettier --write'],
};

4 changes: 2 additions & 2 deletions src/actions/send-contact-form/sendContactForm.ts
Original file line number Diff line number Diff line change
@@ -31,8 +31,6 @@ export async function sendContactForm({ email, message, name, subject }: SendEma

await transporter.sendMail({
from: name,
subject,
to: process.env.CONTACTS_EMAIL,
html: `
<header>
<p><strong>Name:</strong> ${name}</p>
@@ -44,6 +42,8 @@ export async function sendContactForm({ email, message, name, subject }: SendEma
<p style="white-space: pre-wrap;">${message}</p>
</main>
`,
subject,
to: process.env.CONTACTS_EMAIL,
});

return { success: true };
3 changes: 1 addition & 2 deletions src/app/[locale]/exhibit/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Exhibit from '@/components/pages/Exhibit/Exhibit';

import { getExhibit } from '../../../../lib/exhibit';
import { getExhibit } from '@/lib/exhibit';

interface Props {
params: {
96 changes: 52 additions & 44 deletions src/components/organisms/ImageGallery/ImageGallery.tsx
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ interface Props {
isLinkImage: boolean;
}

function ImageGallery({ images, displayArrows, isLinkImage }: Props) {
export default function ImageGallery({ images, displayArrows, isLinkImage }: Props) {
const zoomRef = useRef<ZoomRef>(null);
const {
id: galleryId,
@@ -117,8 +117,8 @@ function ImageGallery({ images, displayArrows, isLinkImage }: Props) {
) => {
const { innerWidth, innerHeight } = window;
const aspectRatio = image.width / image.height;
let wrapperX = 0;
let wrapperY = 0;
let wrapperX: number;
let wrapperY: number;

if (image.height > window.innerHeight && image.width < window.innerWidth) {
wrapperY = Math.min(innerHeight - padding * 2, image.height);
@@ -131,19 +131,57 @@ function ImageGallery({ images, displayArrows, isLinkImage }: Props) {
return { wrapperX, wrapperY };
};

function calculateTranslationOffsets(
wrapperX: number,
wrapperY: number,
translateX: number,
translateY: number,
zoom: number,
) {
const calculateTranslationOffsets = ({
wrapperX,
wrapperY,
translateX,
translateY,
zoom,
}: {
wrapperX: number;
wrapperY: number;
translateX: number;
translateY: number;
zoom: number;
}) => {
const wrapperOffset = 100;
const translateOffset = 50;

const yetAnotherReactLightboxShift = 1 - 1 / zoom;
const offsetX = ((wrapperX / 100) * (translateX - 50)) / yetAnotherReactLightboxShift;
const offsetY = ((wrapperY / 100) * (translateY - 50)) / yetAnotherReactLightboxShift;
const offsetX =
((wrapperX / wrapperOffset) * (translateX - translateOffset)) / yetAnotherReactLightboxShift;
const offsetY =
((wrapperY / wrapperOffset) * (translateY - translateOffset)) / yetAnotherReactLightboxShift;

return { offsetX, offsetY };
}
};

const onEnterLightbox = async (): Promise<void> => {
if (!isOpeningWithZoom) {
const imageUrl = images.find((image) => image.id === galleryId)?.url || '';

if (!imageUrl) {
console.error('Image URL not found.');

return;
}

const currentImage = await loadImage(imageUrl);
const { wrapperX, wrapperY } = determineWrapperDimensions(currentImage, carouselPadding);
const { offsetX, offsetY } = calculateTranslationOffsets({
translateX: zoomOffsetX,
translateY: zoomOffsetY,
wrapperX,
wrapperY,
zoom: zoomValue,
});

zoomRef.current?.changeZoom(zoomValue, true, offsetX, offsetY);
}

setZoom(0, 0, 0);
setIsOpeningWithZoom(false);
};

return (
<>
@@ -180,39 +218,9 @@ function ImageGallery({ images, displayArrows, isLinkImage }: Props) {
carousel={{
padding: carouselPadding,
}}
on={{
entered: async () => {
if (isOpeningWithZoom) {
const imageUrl = images.find((image) => image.id === galleryId)?.url || '';
if (!imageUrl) {
console.error('Image URL not found.');

return;
}

const currentImage = await loadImage(imageUrl);
const { wrapperX, wrapperY } = determineWrapperDimensions(
currentImage,
carouselPadding,
);
const { offsetX, offsetY } = calculateTranslationOffsets(
wrapperX,
wrapperY,
zoomOffsetX,
zoomOffsetY,
zoomValue,
);

zoomRef.current?.changeZoom(zoomValue, true, offsetX, offsetY);
}
setZoom(0, 0, 0);
setIsOpeningWithZoom(false);
},
}}
on={{ entered: onEnterLightbox }}
className={clsx(isOpeningWithZoom && 'image-gallery-zooming')}
/>
</>
);
}

export default ImageGallery;
141 changes: 41 additions & 100 deletions src/components/pages/Exhibit/Exhibit.tsx
Original file line number Diff line number Diff line change
@@ -2,19 +2,17 @@

import { useEffect, useState } from 'react';

import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { useLocale, useTranslations } from 'next-intl';
import { useLocale } from 'next-intl';
import { useShallow } from 'zustand/react/shallow';

import IExhibit from '@/interfaces/IExhibit';
import { LocaleCodeCamelcase, localesCamelcase } from '@/locales';
import { LocaleCodeCamelcase } from '@/locales';
import capitalizeFirstLetter from '@/utils/capitalizeFirstLetter';

import styles from './Exhibit.module.scss';
import { ExhibitDetails } from './ExhibitDetails';
import { ExhibitNotFoundMessage } from './ExhibitNotFoundMessage';
import triggerGtagVisit from '../../../gtag/visit';
import useImageGalleryStore from '../../../stores/useImageGalleryStore';
import ImageGallery from '../../organisms/ImageGallery/ImageGallery';
import Player from '../../organisms/Player/Player';

interface Props {
exhibit: IExhibit;
@@ -35,114 +33,57 @@ export default function ExhibitPage({ exhibit, slug }: Props) {
setZoom: state.setZoom,
})),
);
const t = useTranslations();
const locale = capitalizeFirstLetter(useLocale()) as LocaleCodeCamelcase;
const [currentLocale, setCurrentLocale] = useState(locale);

const images =
exhibit?.imagesCollection?.items?.map((i) => ({
id: i?.sys.id || '',
url: i?.url || '',
})) || [];

const title = exhibit?.[`name${currentLocale}`];
const author = exhibit?.[`author${currentLocale}`];
const audioFile = exhibit?.[`audioFile${currentLocale}`]?.url;
const description = exhibit?.[`description${currentLocale}`]?.json;

useEffect(() => {
if (exhibit?.sys.id && slug && currentLocale) {
triggerGtagVisit(exhibit.sys.id, currentLocale, slug);
}
}, [exhibit?.sys.id, slug, currentLocale]);

useEffect(() => {
useEffect((): void => {
const initGalleryZoomWithSelectedImage = (href: string): void => {
const params = new URLSearchParams(new URL(href).search);
const idString = params.get('imageId') || '';
const xString = params.get('x');
const yString = params.get('y');
const zoomString = params.get('zoom');
const x = xString ? parseInt(xString, 10) : 0;
const y = yString ? parseInt(yString, 10) : 0;
const zoom = zoomString ? parseInt(zoomString, 10) : 0;

setIsOpeningGalleryWithZoom(true);
setGalleryId(idString);
setGalleryZoom(zoom, x, y);
setIsOpenGallery(true);
};

const clickEventListener = (e: MouseEvent): void => {
const { href } = e?.target as HTMLAnchorElement;

if (!href.includes('imageId')) {
return;
}

e.preventDefault();

initGalleryZoomWithSelectedImage(href);
};

document
.getElementById('exhibit-description')
?.querySelectorAll('a')
.forEach((link) => {
link.addEventListener('click', (e) => {
const { href } = e?.target as HTMLAnchorElement;
if (!href.includes('imageId')) {
return;
}

e.preventDefault();

const params = new URLSearchParams(new URL(href).search);
const idString = params.get('imageId') || '';
const xString = params.get('x');
const yString = params.get('y');
const zoomString = params.get('zoom');
const x = xString ? parseInt(xString, 10) : 0;
const y = yString ? parseInt(yString, 10) : 0;
const zoom = zoomString ? parseInt(zoomString, 10) : 0;

setIsOpeningGalleryWithZoom(true);
setGalleryId(idString);
setGalleryZoom(zoom, x, y);
setIsOpenGallery(true);
});
link.addEventListener('click', clickEventListener);
});
});

const handleLinkOnClick = (lang: LocaleCodeCamelcase) => {
setCurrentLocale(lang);
};

const links = localesCamelcase
.filter((lang) => exhibit?.[`name${lang}`])
.map((lang) => (
<span key={lang}>
<button className={styles.link} onClick={() => handleLinkOnClick(lang)}>
{t(`exhibitNotFound${lang}`)}
</button>{' '}
</span>
));

return (
<>
{!title && (
<div className={styles.error}>
{t('exhibitNotFoundForLanguage')} {links}
</div>
)}

{title && (
<article className={styles.exhibit} data-testid="exhibit">
<h2 className={styles.title}>{title}</h2>

{author && <div className={styles.author}>{author}</div>}

{images.length > 0 && (
<div className={styles.gallery}>
<ImageGallery images={images} isLinkImage={false} displayArrows={false} />
</div>
)}

{audioFile && (
<div className={styles.audioPlayer}>
<Player url={audioFile || ''} />
</div>
)}

{exhibit?.yearOfCreation && (
<div className={styles.description}>
<div className={styles.descriptionName}>{t('date')}</div>
<div>{exhibit?.yearOfCreation}</div>
</div>
)}

{description && (
<div className={styles.description}>
<div className={styles.descriptionName}>{t('description')}</div>
<div id="exhibit-description" className={styles.descriptionContent}>
{documentToReactComponents(description)}
</div>
</div>
)}
</article>
)}
</>
);
const title = exhibit[`name${currentLocale}`];

if (!title) {
return <ExhibitNotFoundMessage exhibit={exhibit} setLocale={setCurrentLocale} />;
}

return <ExhibitDetails exhibit={exhibit} selectedLocale={locale} />;
}
69 changes: 69 additions & 0 deletions src/components/pages/Exhibit/ExhibitDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';

import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { useTranslations } from 'next-intl';

import ImageGallery from '@/components/organisms/ImageGallery/ImageGallery';
import Player from '@/components/organisms/Player/Player';
import IExhibit from '@/interfaces/IExhibit';
import { LocaleCodeCamelcase } from '@/locales';

import styles from './Exhibit.module.scss';

interface Props {
exhibit: IExhibit;
selectedLocale: LocaleCodeCamelcase;
}

// eslint-disable-next-line complexity
export const ExhibitDetails: React.FC<Props> = (props) => {
const t = useTranslations();

const title = props.exhibit[`name${props.selectedLocale}`];
const author = props.exhibit[`author${props.selectedLocale}`];
const audioFile = props.exhibit[`audioFile${props.selectedLocale}`]?.url;
const description = props.exhibit[`description${props.selectedLocale}`]?.json;
const yearOfCreation = props.exhibit.yearOfCreation;

const images =
props.exhibit.imagesCollection?.items?.map((i) => ({
id: i?.sys.id || '',
url: i?.url || '',
})) ?? [];

return (
<article className={styles.exhibit} data-testid="exhibit">
<h2 className={styles.title}>{title}</h2>

{author && <div className={styles.author}>{author}</div>}

{images.length > 0 && (
<div className={styles.gallery}>
<ImageGallery images={images} isLinkImage={false} displayArrows={false} />
</div>
)}

{audioFile && (
<div className={styles.audioPlayer}>
<Player url={audioFile || ''} />
</div>
)}

{yearOfCreation && (
<div className={styles.description}>
<div className={styles.descriptionName}>{t('date')}</div>
<div>{yearOfCreation}</div>
</div>
)}

{description && (
<div className={styles.description}>
<div className={styles.descriptionName}>{t('description')}</div>
<div id="exhibit-description" className={styles.descriptionContent}>
{documentToReactComponents(description)}
</div>
</div>
)}
</article>
);
};
41 changes: 41 additions & 0 deletions src/components/pages/Exhibit/ExhibitNotFoundMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';

import { useTranslations } from 'next-intl';

import IExhibit from '@/interfaces/IExhibit';
import { LocaleCodeCamelcase, localesCamelcase } from '@/locales';

import styles from './Exhibit.module.scss';

interface Props {
exhibit: IExhibit;
setLocale: (locale: LocaleCodeCamelcase) => void;
}

export const ExhibitNotFoundMessage: React.FC<Props> = (props) => {
const t = useTranslations();

const handleLinkOnClick =
(lang: LocaleCodeCamelcase) =>
(e: React.SyntheticEvent<HTMLButtonElement>): void => {
e.preventDefault();

props.setLocale(lang);
};

const links = localesCamelcase
.filter((lang) => props.exhibit?.[`name${lang}`])
.map((lang) => (
<span key={lang}>
<button className={styles.link} onClick={handleLinkOnClick(lang)}>
{t(`exhibitNotFound${lang}`)}
</button>{' '}
</span>
));

return (
<div className={styles.error}>
{t('exhibitNotFoundForLanguage')} {links}
</div>
);
};
6 changes: 3 additions & 3 deletions src/i18n/request.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { getRequestConfig } from 'next-intl/server';

import { locales } from '@/locales';
import { LocaleCode, locales } from '@/locales';

import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
let locale = (await requestLocale) as LocaleCode;

if (!locale || !locales.includes(locale as any)) {
if (!locale || !locales.includes(locale)) {
locale = routing.defaultLocale;
}

10 changes: 6 additions & 4 deletions src/interfaces/IExhibit.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Document } from '@contentful/rich-text-types';

export default interface IExhibit {
sys: {
id: string;
@@ -12,16 +14,16 @@ export default interface IExhibit {
authorKa?: string | null;
yearOfCreation?: string | null;
descriptionEn?: {
json: any;
json: undefined | Document;
} | null;
descriptionRu?: {
json: any;
json: undefined | Document;
} | null;
descriptionUz?: {
json: any;
json: undefined | Document;
} | null;
descriptionKa?: {
json: any;
json: undefined | Document;
} | null;
imagesCollection?: {
items: Array<{
7 changes: 5 additions & 2 deletions src/lib/exhibit.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import IExhibit from '@/interfaces/IExhibit';
import { IPreviewExhibit } from '@/interfaces/IPreviewExhibit';

import { getClient } from './ApolloClient';
import { GetExhibitDocument, GetTopLatestExhibitsDocument } from '../__generated__/graphql';

export async function getExhibit(slug: string) {
export async function getExhibit(slug: string): Promise<IExhibit | undefined> {
try {
const { data } = await getClient().query({
query: GetExhibitDocument,
variables: { slug },
});

return data.exhibitCollection?.items[0];
return data.exhibitCollection?.items[0] as IExhibit;
} catch (error) {
console.error('Failed to fetch exhibit', error);
}

return undefined;
}

export async function getTopLatestExhibits(limit: number): Promise<IPreviewExhibit[]> {

0 comments on commit 7b29fc9

Please sign in to comment.