diff --git a/frontend-next-migration/package.json b/frontend-next-migration/package.json index 2fb40b27b..c38c6e772 100644 --- a/frontend-next-migration/package.json +++ b/frontend-next-migration/package.json @@ -9,6 +9,7 @@ "dev": "next dev -p 5173", "build": "next build --no-lint", "start": "next start", + "start-dev": "next start -p 5173", "test": "jest", "test:ci": "jest --ci --config=jest.ci-config.ts", "test:ci-retry-failed": "jest --ci --onlyFailures --config=jest.ci-config.ts", diff --git a/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/[category]/_getPage.ts b/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/[category]/_getPage.ts deleted file mode 100644 index 129c0989a..000000000 --- a/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/[category]/_getPage.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { createPage } from '@/app/_helpers'; -import { PictureGalleryPageProps } from '@/preparedPages/PictureGalleryPages'; -import { getServerTranslation } from '@/shared/i18n'; -import { AppExternalLinks } from '@/shared/appLinks/appExternalLinks'; -import { getRouteGalleryCategoryPage } from '@/shared/appLinks/RoutePaths'; -import { baseUrl, defaultOpenGraph } from '@/shared/seoConstants'; - -const toAbsolute = (src?: string | null) => - !src ? null : /^https?:\/\//i.test(src) ? src : `${baseUrl}${src}`; - -export async function _getPage(lng: string, category: string) { - const { t } = await getServerTranslation(lng, 'picture-galleries'); - - const name = t(`categories.${category}.name`, category); - const desc = t(`categories.${category}.description`, t('head-description')); - - // Routes & SEO - const relPath = getRouteGalleryCategoryPage(encodeURIComponent(category)); - const path = `/${lng}${relPath}`; - const title = `${name} — ${t('picture-galleries')}`; - const keywords = `${t('head-keywords')}, ${name}`; - - // OG image: default only (no category-specific image exists) - const defaultOg = defaultOpenGraph.images?.[0]?.url ?? null; - const ogUrl = toAbsolute(defaultOg); - const ogImages = ogUrl - ? [{ url: ogUrl, alt: `${name} — ${t('picture-galleries')}` }] - : (defaultOpenGraph.images ?? []); - - return createPage({ - buildPage: () => ({ - title, - infoText: t('info-text'), - socialsText: t('socials-text'), - socialMediaLinks: [ - AppExternalLinks.igPost1, - AppExternalLinks.igPost2, - AppExternalLinks.fbPost1, - ], - videoLink: AppExternalLinks.previewVideoYoutube, - }), - buildSeo: () => ({ - title, - description: desc, - keywords, - openGraph: { - ...defaultOpenGraph, - type: 'website', - title, - description: desc, - url: path, - images: ogImages, - }, - alternates: { canonical: path }, - }), - }); -} diff --git a/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/[category]/page.tsx b/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/[category]/page.tsx deleted file mode 100644 index e95bf93ce..000000000 --- a/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/[category]/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { PictureGalleryPage } from '@/preparedPages/PictureGalleryPages'; -import { withMetadataGenerator, withPageData } from '@/app/_helpers'; -import { _getPage } from './_getPage'; - -export const generateMetadata = withMetadataGenerator(_getPage); -export default withPageData(PictureGalleryPage, _getPage); diff --git a/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/_getPage.ts b/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/_getPage.ts index b084dd8a6..9c4b00410 100644 --- a/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/_getPage.ts +++ b/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/_getPage.ts @@ -1,5 +1,4 @@ import { createPage } from '@/app/_helpers'; -import { PictureGalleryPageProps } from '@/preparedPages/PictureGalleryPages'; import { getServerTranslation } from '@/shared/i18n'; import { AppExternalLinks } from '@/shared/appLinks/appExternalLinks'; import { getRouteGalleryPage } from '@/shared/appLinks/RoutePaths'; @@ -7,7 +6,7 @@ import { defaultOpenGraph } from '@/shared/seoConstants'; export async function _getPage(lng: string) { const { t } = await getServerTranslation(lng, 'picture-galleries'); - return createPage({ + return createPage({ buildPage: () => ({ title: t('picture-galleries'), infoText: t('info-text'), diff --git a/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/layout.tsx b/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/layout.tsx deleted file mode 100644 index 96370dcf3..000000000 --- a/frontend-next-migration/src/app/[lng]/(helper)/picture-galleries/layout.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client'; -import { ReactNode } from 'react'; -import { LayoutWithSidebars } from '@/preparedPages/Layouts'; -import { GalleryNavMenuAsDropdown } from '@/features/NavigateGalleries'; -import useSizes from '@/shared/lib/hooks/useSizes'; -import { useClientTranslation } from '@/shared/i18n'; -import { PageTitle } from '@/shared/ui/PageTitle'; -import { cls } from '@/preparedPages/PictureGalleryPages'; - -export default function PictureGalleryLayout({ children }: { children: ReactNode }) { - const { isMobileSize, isTabletSize } = useSizes(); - const isTouchDevice = isMobileSize || isTabletSize; - const { t } = useClientTranslation('picture-galleries'); - - return ( - - ), - hideOnMobile: true, - }} - > - {isTouchDevice && ( - <> - - - - )} - {children} - - ); -} diff --git a/frontend-next-migration/src/app/_styles/index.scss b/frontend-next-migration/src/app/_styles/index.scss index 50da966d4..deffdcd0d 100644 --- a/frontend-next-migration/src/app/_styles/index.scss +++ b/frontend-next-migration/src/app/_styles/index.scss @@ -18,6 +18,9 @@ overflow-x: hidden; } } +html { + scroll-behavior: smooth; +} body { font: 16px/24px; diff --git a/frontend-next-migration/src/app/_styles/variables/global.scss b/frontend-next-migration/src/app/_styles/variables/global.scss index 66b396a2a..1a152ab0f 100644 --- a/frontend-next-migration/src/app/_styles/variables/global.scss +++ b/frontend-next-migration/src/app/_styles/variables/global.scss @@ -167,6 +167,7 @@ --content-primary: #FFA100; --base-card-background: #1E3544; --drop-shadows: #121212; + --white-text: #faf9f6; // border-radius diff --git a/frontend-next-migration/src/entities/Gallery/api/filterAndTransformImages.test.ts b/frontend-next-migration/src/entities/Gallery/api/filterAndTransformImages.test.ts deleted file mode 100644 index f4a89c2ed..000000000 --- a/frontend-next-migration/src/entities/Gallery/api/filterAndTransformImages.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { filterAndTransformImages } from '../api/filterAndTransformImages'; - -describe('filterAndTransformImages', () => { - const mockPhotoObjects = [ - { - id: '56789', - category: { - id: '1', - translations: [ - { - id: 'category-56789', - language: 'en', - name: 'Category 1', - languages_code: 'en-US', - category_id: '1', - }, - ], - }, - versions: { - preview: { - id: '56789', - image: 'https://upload.wikimedia.org/wikipedia/commons/e/e0/PlaceholderLC.png', - width: 350, - height: 350, - altText: 'preview picture 1', - }, - full: { - id: '56789', - image: 'https://upload.wikimedia.org/wikipedia/commons/e/e0/PlaceholderLC.png', - width: 200, - height: 200, - altText: 'full picture 1', - }, - }, - }, - { - id: '12345', - category: { - id: '1', - translations: [ - { - id: 'category-12345', - language: 'en', - name: 'Category 1', - languages_code: 'en-US', - category_id: '1', - }, - ], - }, - versions: { - preview: { - id: '12345', - image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTOwRConBYl2t6L8QMOAQqa5FDmPB_bg7EnGA&s', - width: 350, - height: 350, - altText: 'preview picture 2', - }, - full: { - id: '12345', - image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTOwRConBYl2t6L8QMOAQqa5FDmPB_bg7EnGA&s', - width: 200, - height: 200, - altText: 'full picture 2', - }, - }, - }, - ]; - - it('should transform and return full version objects correctly', () => { - const result = filterAndTransformImages(mockPhotoObjects, 'full'); - expect(result).toEqual([ - { - id: '56789', - image: 'https://upload.wikimedia.org/wikipedia/commons/e/e0/PlaceholderLC.png', - width: 200, - height: 200, - altText: 'full picture 1', - }, - { - id: '12345', - image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTOwRConBYl2t6L8QMOAQqa5FDmPB_bg7EnGA&s', - width: 200, - height: 200, - altText: 'full picture 2', - }, - ]); - }); - - it('should transform and return preview objects correctly', () => { - const result = filterAndTransformImages(mockPhotoObjects, 'preview'); - expect(result).toEqual([ - { - id: '56789', - image: 'https://upload.wikimedia.org/wikipedia/commons/e/e0/PlaceholderLC.png', - width: 350, - height: 350, - altText: 'preview picture 1', - }, - { - id: '12345', - image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTOwRConBYl2t6L8QMOAQqa5FDmPB_bg7EnGA&s', - width: 350, - height: 350, - altText: 'preview picture 2', - }, - ]); - }); - - it('should return an empty array for an empty input array', () => { - const result = filterAndTransformImages([], 'full'); - expect(result).toEqual([]); - }); - - it('should return an empty array for an invalid version type', () => { - const result = filterAndTransformImages(mockPhotoObjects, 'thumbnail'); - expect(result).toEqual([]); - }); -}); diff --git a/frontend-next-migration/src/entities/Gallery/api/filterAndTransformImages.ts b/frontend-next-migration/src/entities/Gallery/api/filterAndTransformImages.ts deleted file mode 100644 index 3c82e60e5..000000000 --- a/frontend-next-migration/src/entities/Gallery/api/filterAndTransformImages.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { PhotoObject, PhotoVersion } from '../types/gallery'; - -/** - * Filters and transforms photo objects into an array of photo versions based on the specified version type. - * - * This function processes a list of photo objects and extracts either the "full" or "preview" versions, - * transforming them into a standardized array of photo version objects. If an error occurs, it logs the error - * and returns an empty array. - * - * @param {PhotoObject[]} photoObjects - Array of photo objects to filter and transform. - * Each photo object contains metadata and version details. - * @param {string} version - The version type to extract, either `"full"` or `"preview"`. - * - * @returns {PhotoVersion[]} An array of photo versions filtered and transformed based on the specified type. - * If an error occurs, the function logs it to the console and returns an empty array. - */ - -export const filterAndTransformImages = ( - photoObjects: PhotoObject[], - version: string, -): PhotoVersion[] => { - try { - // Only allow the two supported versions - if (version !== 'full' && version !== 'preview') { - return []; - } - - // Filter out entries with missing versions or missing selected version - const valid = photoObjects.filter((po) => { - const v = po.versions; - return !!(v && v[version]); - }); - - const images: PhotoVersion[] = valid.map((po) => { - const v = po.versions![version]!; // safe due to filtering above - return { - id: v.id, - image: v.image, - width: v.width, - height: v.height, - altText: v.altText || '', - }; - }); - - return images; - } catch (error) { - console.error('Error transforming and filtering images: ', error); - const images: PhotoVersion[] = []; - return images; - } -}; diff --git a/frontend-next-migration/src/entities/Gallery/api/galleryApi.ts b/frontend-next-migration/src/entities/Gallery/api/galleryApi.ts index 848eddb40..c6945d28c 100644 --- a/frontend-next-migration/src/entities/Gallery/api/galleryApi.ts +++ b/frontend-next-migration/src/entities/Gallery/api/galleryApi.ts @@ -1,76 +1,46 @@ import { directusApi } from '@/shared/api'; import { envHelper } from '@/shared/const/envHelper'; import { createDirectus, rest, readItems } from '@directus/sdk'; +import { DirectusPhotoObjectV2 } from '../types/gallery'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; const directusBaseUrl = envHelper.directusHost; const client = createDirectus(directusBaseUrl).with(rest()); /** * API service for fetching gallery data from a Directus backend. - * - * This service utilizes Directus SDK and Redux Toolkit Query to define endpoints for fetching - * `photo_object` and `photo_version` data, including their associated translations and relationships. - * - * @module galleryApi - * - * Has two endpoints for data fetching. - * - * @endpoint getPhotoObjects - * Endpoint to fetch photo objects from the Directus `photo_object` collection. - * Retrieves detailed information about photo objects, including: - * - Associated categories with translations. - * - Preview and full versions of the photos with translations. - * - * @returns {object} Response containing an array of photo objects. - * - * @endpoint getPhotoVersions - * Endpoint to fetch photo versions from the Directus `photo_version` collection. - * Retrieves detailed information about photo versions, including: - * - Image metadata such as dimensions (width, height). - * - Associated translations for each photo version. - * - * @returns {object} Response containing an array of photo versions. + * Defined an endpoint for fetching photo objects from the Directus `photo_object_v2` collection. + * @returns DirectusPhotoObjectV2[] - An array of photo objects with their associated data. */ - const galleryApi = directusApi.injectEndpoints({ endpoints: (builder) => ({ - getPhotoObjects: builder.query({ - queryFn: async (_arg: void) => { - const photoObjects = await client.request( - readItems('photo_object', { - fields: [ - '*', - 'category.*', - 'preview.*', - 'full.*', - 'category.translations.*', - 'full.translations.*', - 'preview.translations.*', - 'translations.*', - ], - deep: { - category: { translations: true }, - preview: { translations: true }, - full: { translations: true }, - translations: true, + getPhotoObjectsV2: builder.query({ + queryFn: async ( + _arg: void, + ): Promise<{ data: DirectusPhotoObjectV2[] } | { error: FetchBaseQueryError }> => { + try { + const photoObjects = await client.request( + readItems('photo_object_v2', { + fields: [ + '*', + 'category.*', + 'translations.*', + 'category.translations.*', + ], + }), + ); + return { data: photoObjects as DirectusPhotoObjectV2[] }; + } catch (error: any) { + return { + error: { + status: error.status || 500, + data: { message: error.message || 'Data fetch failed' } as any, }, - }), - ); - return { data: photoObjects }; - }, - }), - getPhotoVersions: builder.query({ - queryFn: async (_arg: void) => { - const photoVersions = await client.request( - readItems('photo_version', { - fields: ['id', 'image', 'width', 'height', 'translations.*'], - deep: { translations: true }, - }), - ); - return { data: photoVersions }; + }; + } }, }), }), }); -export const { useGetPhotoObjectsQuery, useGetPhotoVersionsQuery } = galleryApi; +export const { useGetPhotoObjectsV2Query } = galleryApi; diff --git a/frontend-next-migration/src/entities/Gallery/api/galleryCategoriesApi.ts b/frontend-next-migration/src/entities/Gallery/api/galleryCategoriesApi.ts index b454505de..b1331e4f5 100644 --- a/frontend-next-migration/src/entities/Gallery/api/galleryCategoriesApi.ts +++ b/frontend-next-migration/src/entities/Gallery/api/galleryCategoriesApi.ts @@ -1,6 +1,7 @@ import { directusApi } from '@/shared/api'; import { envHelper } from '@/shared/const/envHelper'; import { createDirectus, rest, readItems } from '@directus/sdk'; +import { Category } from '../types/gallery'; const directusBaseUrl = envHelper.directusHost; const client = createDirectus(directusBaseUrl).with(rest()); @@ -30,7 +31,7 @@ const galleryCategoryApi = directusApi.injectEndpoints({ deep: { translations: true }, }), ); - return { data: categories }; + return { data: categories as Category[] }; }, }), }), diff --git a/frontend-next-migration/src/entities/Gallery/api/mappers.ts b/frontend-next-migration/src/entities/Gallery/api/mappers.ts new file mode 100644 index 000000000..c2e1c7d1b --- /dev/null +++ b/frontend-next-migration/src/entities/Gallery/api/mappers.ts @@ -0,0 +1,141 @@ +import { envHelper } from '@/shared/const/envHelper'; +import { DirectusPhotoObjectV2, PhotoObject, PhotoObjectLink } from '../types/gallery'; +import { getPhotoObjectTexts, getTranslation } from './translations'; + +/** + * Maps an array of DirectusPhotoObjectV2 to an array of PhotoObject. + * Maps the fields and translations, assigns urls to images, sanitizes links, + * and generates an url-safe anchor id from the author's name. + * @param directusPhotoObject - The array of DirectusPhotoObjectV2 to map. + * @param lng - The language code for translations. + * @returns An array of PhotoObject. + */ +export const mapDirectusToPhotoObjectV2 = ( + directusPhotoObject: DirectusPhotoObjectV2[], + lng: string, +): PhotoObject[] => { + return directusPhotoObject.map((item) => { + const { + id, + category, + translations, + author, + website, + date_created, + github, + linkedin, + instagram, + facebook, + image, + image_2, + image_3, + animation, + } = item; + + const mappedCategory = { + id: category?.id || '', + name: getTranslation(category?.translations || [], lng, 'name', ''), + }; + + const { title, description } = getPhotoObjectTexts(translations ? translations : [], lng); + const mappedLinks = [ + { name: 'website', url: sanitizeLink(website) }, + { name: 'facebook', url: sanitizeLink(facebook) }, + { name: 'linkedin', url: sanitizeLink(linkedin) }, + { name: 'instagram', url: sanitizeLink(instagram) }, + { name: 'github', url: sanitizeLink(github) }, + ].filter((link) => link.url !== undefined) as PhotoObjectLink[]; + + const mappedFrames = mapFrames(image, image_2, image_3); + const mappedAnimation = mapAnimation(animation); + + // url-safe anchor id from author's name + const anchorId = mapAnchorId(author); + + return { + id, + category: mappedCategory, + title, + description, + anchorId, + author: author || undefined, + links: mappedLinks, + frames: mappedFrames, + animation: mappedAnimation, + date_created, + }; + }); +}; + +const mapFrames = ( + image: string | null, + image_2: string | null, + image_3: string | null, +): string[][] => { + const frames: string[][] = []; + const directusBaseUrl = envHelper.directusHost; + + // for managing file sizes/quality to lower bandwidth usage/load times + const imageWidth = 800; + const imageQuality = 80; + + // insert image url and image id into frames, id can be used for fetching original image for example + if (image) { + frames.push([ + `${directusBaseUrl}/assets/${image}?format=auto&width=${imageWidth}&quality=${imageQuality}`, + image, + ]); + } + if (image_2) { + frames.push([ + `${directusBaseUrl}/assets/${image_2}?format=auto&width=${imageWidth}&quality=${imageQuality}`, + image_2, + ]); + } + if (image_3) { + frames.push([ + `${directusBaseUrl}/assets/${image_3}?format=auto&width=${imageWidth}&quality=${imageQuality}`, + image_3, + ]); + } + + return frames; +}; + +const mapAnimation = (animation: string | null): string[] | undefined => { + if (!animation) return undefined; + + const directusBaseUrl = envHelper.directusHost; + const imageWidth = 800; + const imageQuality = 80; + + return [ + `${directusBaseUrl}/assets/${animation}?format=auto&width=${imageWidth}&quality=${imageQuality}`, + animation, + ]; +}; + +const mapAnchorId = (author: string | null): string => { + const anchorId = `${author || ''}` + .toLowerCase() + .normalize('NFD') // åäöé etc -> aaoe + diacritics separately + .replace(/[\u0300-\u036f]/g, '') // remove ¨´~ etc after normalization + .replace(/[^a-z0-9]+/g, '-') // replace non-alphanumeric characters with hyphens + .replace(/^-+|-+$/g, ''); // remove leading/trailing hyphens + return anchorId; +}; + +const sanitizeLink = (link: string | null): string | undefined => { + if (!link) return undefined; + // trim whitespace, check http/https prefix, and validate URL + const rawLink = link.trim(); + if (!rawLink) return undefined; + const normalizedLink = /^https?:\/\//i.test(rawLink) ? rawLink : `https://${rawLink}`; + try { + const url = new URL(normalizedLink); + return url.href; + } catch (error) { + console.warn(`Invalid URL: ${link}, error: ${error}`); + return undefined; + } +}; diff --git a/frontend-next-migration/src/entities/Gallery/api/translations.ts b/frontend-next-migration/src/entities/Gallery/api/translations.ts index b607ea678..8bd877a57 100644 --- a/frontend-next-migration/src/entities/Gallery/api/translations.ts +++ b/frontend-next-migration/src/entities/Gallery/api/translations.ts @@ -1,14 +1,10 @@ -import { - PhotoVersionTranslations, - CategoryTranslations, - PhotoObjectTranslations, -} from '../types/gallery'; +import { PhotoObjectV2Translations } from '../types/gallery'; export const getLanguageCode = (language: string): string => { return language === 'en' ? 'en-US' : language === 'fi' ? 'fi-FI' : 'default'; }; -const getTranslation = ( +export const getTranslation = ( translations: T[], languageCode: string, key: keyof T, @@ -18,33 +14,18 @@ const getTranslation = ( return translation && key in translation ? (translation[key] as string) : defaultValue; }; -export const getCategoryTranslation = ( - translations: CategoryTranslations[], - languageCode: string, -) => { - return getTranslation(translations, languageCode, 'name', ''); -}; - -export const getPhotoVersionTranslation = ( - translations: PhotoVersionTranslations[], - languageCode: string, -) => { - return getTranslation(translations, languageCode, 'altText', ''); -}; - export const getPhotoObjectTexts = ( - translations: PhotoObjectTranslations[] = [], + translations: PhotoObjectV2Translations[] = [], languageCode: string, ) => { if (!translations || translations.length === 0) { - return { title: '', author: '', description: '' }; + return { title: '', description: '' }; } const tr = translations.find((t) => t.languages_code === languageCode) ?? translations[0]; return { title: tr.title ?? '', - author: tr.author ?? '', description: tr.description ?? '', }; }; diff --git a/frontend-next-migration/src/entities/Gallery/api/useGetDirectusGalleryImages.test.ts b/frontend-next-migration/src/entities/Gallery/api/useGetDirectusGalleryImages.test.ts index ae6931f6a..35418d165 100644 --- a/frontend-next-migration/src/entities/Gallery/api/useGetDirectusGalleryImages.test.ts +++ b/frontend-next-migration/src/entities/Gallery/api/useGetDirectusGalleryImages.test.ts @@ -1,12 +1,11 @@ import { renderHook } from '@testing-library/react'; -import { useGetPhotoObjectsQuery, useGetPhotoVersionsQuery } from '../api/galleryApi'; +import { useGetPhotoObjectsV2Query } from '../api/galleryApi'; import { useGetGalleryCategoriesQuery } from '../api/galleryCategoriesApi'; -import { getPhotoVersionTranslation, getCategoryTranslation } from '../api/translations'; import { useGetDirectusGalleryImages } from '../api/useGetDirectusGalleryImages'; +import { getPhotoObjectTexts, getTranslation } from '../api/translations'; jest.mock('../api/galleryApi', () => ({ - useGetPhotoObjectsQuery: jest.fn(), - useGetPhotoVersionsQuery: jest.fn(), + useGetPhotoObjectsV2Query: jest.fn(), })); jest.mock('../api/galleryCategoriesApi', () => ({ @@ -14,8 +13,7 @@ jest.mock('../api/galleryCategoriesApi', () => ({ })); jest.mock('../api/translations', () => ({ - getPhotoVersionTranslation: jest.fn(), - getCategoryTranslation: jest.fn(), + getTranslation: jest.fn(), getPhotoObjectTexts: jest.fn(() => ({ title: '', author: '', description: '' })), })); @@ -33,12 +31,7 @@ describe('useGetDirectusGalleryImages', () => { }); it('returns empty arrays when no data is available', () => { - (useGetPhotoObjectsQuery as jest.Mock).mockReturnValue({ - data: null, - isLoading: false, - error: null, - }); - (useGetPhotoVersionsQuery as jest.Mock).mockReturnValue({ + (useGetPhotoObjectsV2Query as jest.Mock).mockReturnValue({ data: null, isLoading: false, error: null, @@ -52,7 +45,6 @@ describe('useGetDirectusGalleryImages', () => { const { result } = renderHook(() => useGetDirectusGalleryImages(mockLanguage)); expect(result.current.photoObjects).toEqual([]); - expect(result.current.photoVersions).toEqual([]); expect(result.current.categories).toEqual([]); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBeNull(); @@ -60,98 +52,98 @@ describe('useGetDirectusGalleryImages', () => { it('transforms data correctly', () => { const mockCategories = [ - { id: '1', translations: [{ language: 'en', name: 'Category 1' }] }, - ]; - const mockPhotoVersions = [ { id: '1', - image: 'image1.jpg', - width: 100, - height: 100, - translations: [{ language: 'en', altText: 'Alt 1' }], + translations: [ + { id: '1', languages_code: 'en', category_id: '1', name: 'Category 1' }, + ], }, ]; const mockPhotoObjects = [ { id: '1', - category: { id: '1', translations: [{ language: 'en', name: 'Category 1' }] }, - preview: { - id: '2', - image: 'preview1.jpg', - width: 50, - height: 50, - translations: [], + category: { + id: '1', + translations: [ + { id: '1', languages_code: 'en', category_id: '1', name: 'Category 1' }, + ], }, - full: { id: '3', image: 'full1.jpg', width: 100, height: 100, translations: [] }, + translations: [ + { + id: '1', + languages_code: 'en', + photo_object_id: '1', + title: 'Title 1', + description: 'Description 1', + }, + ], + author: 'Author 1', + website: 'https://example.com', + github: null, + linkedin: null, + instagram: null, + facebook: null, + image: 'image1id', + image_2: 'image2id', + image_3: null, + animation: null, + date_created: '2024-01-01T00:00:00Z', }, ]; - (useGetPhotoObjectsQuery as jest.Mock).mockReturnValue({ + (useGetPhotoObjectsV2Query as jest.Mock).mockReturnValue({ data: mockPhotoObjects, isLoading: false, error: null, }); - (useGetPhotoVersionsQuery as jest.Mock).mockReturnValue({ - data: mockPhotoVersions, - isLoading: false, - error: null, - }); (useGetGalleryCategoriesQuery as jest.Mock).mockReturnValue({ data: mockCategories, isLoading: false, error: null, }); - (getPhotoVersionTranslation as jest.Mock).mockImplementation( - (translations) => translations[0]?.altText || '', - ); - (getCategoryTranslation as jest.Mock).mockImplementation( + (getTranslation as jest.Mock).mockImplementation( (translations) => translations[0]?.name || '', ); + (getPhotoObjectTexts as jest.Mock).mockImplementation((translations) => ({ + title: translations[0]?.title || '', + description: translations[0]?.description || '', + })); const { result } = renderHook(() => useGetDirectusGalleryImages(mockLanguage)); - expect(result.current.categories).toEqual([ - { id: '1', translations: [{ language: 'en', name: 'Category 1' }] }, - ]); - expect(result.current.photoVersions).toEqual([ - { id: '1', image: 'image1.jpg', width: 100, height: 100, altText: 'Alt 1' }, - ]); + expect(result.current.categories).toEqual([{ id: '1', name: 'Category 1' }]); expect(result.current.photoObjects).toEqual([ { id: '1', - category: { id: '1', translations: [{ language: 'en', name: 'Category 1' }] }, - versions: { - preview: { - id: '2', - image: 'https://strapi.altzone.fi/assets/preview1.jpg', - width: 50, - height: 50, - altText: '', - }, - full: { - id: '3', - image: 'https://strapi.altzone.fi/assets/full1.jpg', - width: 100, - height: 100, - altText: '', - }, - }, + category: { id: '1', name: 'Category 1' }, + title: 'Title 1', + description: 'Description 1', + author: 'Author 1', + anchorId: 'author-1', + links: [{ name: 'website', url: 'https://example.com/' }], + frames: [ + [ + `https://strapi.altzone.fi/assets/image1id?format=auto&width=800&quality=80`, + 'image1id', + ], + [ + `https://strapi.altzone.fi/assets/image2id?format=auto&width=800&quality=80`, + 'image2id', + ], + ], + animation: undefined, + date_created: '2024-01-01T00:00:00Z', }, ]); }); it('handles loading states', () => { - (useGetPhotoObjectsQuery as jest.Mock).mockReturnValue({ + (useGetPhotoObjectsV2Query as jest.Mock).mockReturnValue({ data: null, isLoading: true, error: null, }); - (useGetPhotoVersionsQuery as jest.Mock).mockReturnValue({ - data: null, - isLoading: false, - error: null, - }); (useGetGalleryCategoriesQuery as jest.Mock).mockReturnValue({ data: null, isLoading: false, @@ -165,16 +157,11 @@ describe('useGetDirectusGalleryImages', () => { it('handles errors', () => { const mockError = new Error('Test Error'); - (useGetPhotoObjectsQuery as jest.Mock).mockReturnValue({ + (useGetPhotoObjectsV2Query as jest.Mock).mockReturnValue({ data: null, isLoading: false, error: mockError, }); - (useGetPhotoVersionsQuery as jest.Mock).mockReturnValue({ - data: null, - isLoading: false, - error: null, - }); (useGetGalleryCategoriesQuery as jest.Mock).mockReturnValue({ data: null, isLoading: false, diff --git a/frontend-next-migration/src/entities/Gallery/api/useGetDirectusGalleryImages.ts b/frontend-next-migration/src/entities/Gallery/api/useGetDirectusGalleryImages.ts index 338aa16f1..1816e48de 100644 --- a/frontend-next-migration/src/entities/Gallery/api/useGetDirectusGalleryImages.ts +++ b/frontend-next-migration/src/entities/Gallery/api/useGetDirectusGalleryImages.ts @@ -1,113 +1,34 @@ -import { envHelper } from '@/shared/const/envHelper'; +import { useGetPhotoObjectsV2Query } from './galleryApi'; +import { useGetGalleryCategoriesQuery } from './galleryCategoriesApi'; +import { mapDirectusToPhotoObjectV2 } from './mappers'; +import { PhotoObject, PhotoCategory } from '../types/gallery'; import { useMemo } from 'react'; -import { Category, PhotoObject, PhotoVersion } from '../types/gallery'; -import { getPhotoVersionTranslation, getPhotoObjectTexts } from '../api/translations'; -import { useGetGalleryCategoriesQuery } from '../api/galleryCategoriesApi'; -import { useGetPhotoObjectsQuery, useGetPhotoVersionsQuery } from '../api/galleryApi'; - -/** - * Hook to fetch and process gallery images from Directus. - * - * This function combines data from three Directus queries: photo objects, photo versions, - * and gallery categories. It processes the raw data to generate structured objects - * representing the gallery's images, versions, and categories, and also manages - * loading and error states. - * - * @returns {object} An object containing the following properties: - * @property {PhotoVersion[]} photoVersions - Array of processed photo versions, including dimensions and alt text. - * @property {Category[]} categories - Array of processed gallery categories, including id and name. - * @property {PhotoObject[]} photoObjects - Array of processed photo objects, including category and version information. - * @property {Error | null} error - Any error encountered while fetching data, or `null` if there was no error. - * @property {boolean} isLoading - `true` if any of the data queries are still loading, `false` otherwise. - * - * @throws {Error} Logs any error encountered during data transformation. - * - * @example - * const { photoObjects, error, isLoading } = useGetDirectusGalleryImages(); - * - * if (isLoading) { - * console.log('Loading data...'); - * } - * if (error) { - * console.error('Error:', error); - * } - * console.log('Categories:', categories); - * console.log('Photo Objects:', photoObjects); - */ +import { getTranslation } from './translations'; export const useGetDirectusGalleryImages = (lng: string) => { - const { data: poData, error: poError, isLoading: poIsLoading } = useGetPhotoObjectsQuery(); - const { data: pvData, error: pvError, isLoading: pvIsLoading } = useGetPhotoVersionsQuery(); + const { data: poData, error: poError, isLoading: poIsLoading } = useGetPhotoObjectsV2Query(); const { data: cData, error: cError, isLoading: cIsLoading } = useGetGalleryCategoriesQuery(); - const directusBaseUrl = envHelper.directusHost; - - const isLoading = poIsLoading || pvIsLoading || cIsLoading; - const error = poError || pvError || cError; + const isLoading = poIsLoading || cIsLoading; + const error = poError || cError; - const categories: Category[] = useMemo(() => { + const categories: PhotoCategory[] = useMemo(() => { if (!cData) return []; return cData.map((item) => ({ id: item.id, - translations: item.translations, + name: getTranslation(item.translations || [], lng, 'name', ''), })); - }, [cData]); - - const photoVersions: PhotoVersion[] = useMemo(() => { - if (!pvData) return []; - return pvData.map((item) => ({ - id: item.id, - image: item.image, - width: item.width, - height: item.height, - altText: getPhotoVersionTranslation(item.translations || [], lng), - })); - }, [pvData, lng]); + }, [cData, lng]); const photoObjects: PhotoObject[] = useMemo(() => { if (!poData) return []; - return poData.map((item) => { - const texts = getPhotoObjectTexts(item.translations || [], lng); - - return { - id: item.id, - title: texts.title || item.title, - author: texts.author || item.author, - description: texts.description || item.description, - category: item.category - ? { - id: item.category.id, - translations: item.category.translations, - } - : undefined, - versions: - item.preview && item.full - ? { - preview: { - id: item.preview.id, - image: `${directusBaseUrl}/assets/${item.preview.image}`, - width: item.preview.width, - height: item.preview.height, - altText: getPhotoVersionTranslation( - item.preview.translations || [], - lng, - ), - }, - full: { - id: item.full.id, - image: `${directusBaseUrl}/assets/${item.full.image}`, - width: item.full.width, - height: item.full.height, - altText: getPhotoVersionTranslation( - item.full.translations || [], - lng, - ), - }, - } - : undefined, - }; - }); - }, [poData, directusBaseUrl, lng]); - - return { photoVersions, categories, photoObjects, error, isLoading }; + return mapDirectusToPhotoObjectV2(poData, lng); + }, [poData, lng]); + + return { + photoObjects, + categories, + error, + isLoading, + }; }; diff --git a/frontend-next-migration/src/entities/Gallery/index.ts b/frontend-next-migration/src/entities/Gallery/index.ts index 7937d93f7..88aabe5d2 100644 --- a/frontend-next-migration/src/entities/Gallery/index.ts +++ b/frontend-next-migration/src/entities/Gallery/index.ts @@ -3,27 +3,15 @@ export type { IGalleryPicture, ImageData, PhotoObject, - PhotoVersion, Category, + PhotoCategory, CategoryTranslations, - PhotoVersionTranslations, } from './types/gallery'; export type { ParentDirectory } from './model/galleryApi'; export { useGalleryCategories } from './model/useGalleryCategories'; -export { mockImagesFull, mockImagesPreview } from './model/mockImages'; - -export type { GalleryCategoriesWithModalSliderProps } from './ui/GalleryCategoriesWithModalSlider'; export { GalleryCategoriesWithModalSlider } from './ui/GalleryCategoriesWithModalSlider'; -export { ImageWall } from './ui/ImageWall/ImageWall'; - export { useGetStrapiGalleryImages } from './api/useGetStrapiGalleryImages'; export { useGetDirectusGalleryImages } from './api/useGetDirectusGalleryImages'; -export { filterAndTransformImages } from './api/filterAndTransformImages'; -export { useGetPhotoObjectsQuery, useGetPhotoVersionsQuery } from './api/galleryApi'; export { useGetGalleryCategoriesQuery } from './api/galleryCategoriesApi'; -export { - getCategoryTranslation, - getPhotoVersionTranslation, - getLanguageCode, -} from './api/translations'; +export { getLanguageCode } from './api/translations'; diff --git a/frontend-next-migration/src/entities/Gallery/model/mockImages.test.ts b/frontend-next-migration/src/entities/Gallery/model/mockImages.test.ts deleted file mode 100644 index c1f44267e..000000000 --- a/frontend-next-migration/src/entities/Gallery/model/mockImages.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { generateMockImages, baseImage1, baseImage2 } from '../model/mockImages'; - -describe('Base Mock Images', () => { - it('should have correct properties for baseImage1', () => { - expect(baseImage1).toHaveProperty('id', '56789'); - expect(baseImage1.category).toEqual({ - id: '56789', - translations: [ - { - id: 'category-56789', - language: 'en', - name: 'All', - languages_code: 'en-US', - category_id: '56789', - }, - ], - }); - expect(baseImage1.versions.preview).toHaveProperty('altText', 'preview picture'); - expect(baseImage1.versions.full).toHaveProperty('altText', 'full picture'); - }); - - it('should have correct properties for baseImage2', () => { - expect(baseImage2).toHaveProperty('id', '12345'); - expect(baseImage2.category).toEqual({ - id: '12345', - translations: [ - { - id: 'category-12345', - language: 'en', - name: 'All', - languages_code: 'en-US', - category_id: '12345', - }, - ], - }); - expect(baseImage2.versions.preview).toHaveProperty('altText', 'preview picture'); - expect(baseImage2.versions.full).toHaveProperty('altText', 'full picture'); - }); -}); - -describe('generateMockImages', () => { - it('should generate the correct number of mock images', () => { - const result = generateMockImages(baseImage1, baseImage2, 6); - expect(result).toHaveLength(6); - }); - - it('should generate unique IDs for each mock image', () => { - const result = generateMockImages(baseImage1, baseImage2, 4); - const ids = result.map((img) => img.id); - const uniqueIds = new Set(ids); - expect(uniqueIds.size).toBe(4); - }); - - it('should alternate between baseImage1 and baseImage2', () => { - const result = generateMockImages(baseImage1, baseImage2, 4); - expect(result[0].id.startsWith(baseImage1.id)).toBe(true); - expect(result[1].id.startsWith(baseImage2.id)).toBe(true); - expect(result[2].id.startsWith(baseImage1.id)).toBe(true); - expect(result[3].id.startsWith(baseImage2.id)).toBe(true); - }); -}); diff --git a/frontend-next-migration/src/entities/Gallery/model/mockImages.ts b/frontend-next-migration/src/entities/Gallery/model/mockImages.ts deleted file mode 100644 index f136820f3..000000000 --- a/frontend-next-migration/src/entities/Gallery/model/mockImages.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { PhotoObject } from '@/entities/Gallery/types/gallery'; - -/** - * Base mock photo objects for Storybook usage. - * - * Represents a photo object with predefined category, versions, and localized alt texts. - * There are two different templates for generating mock photo objects. - */ - -export const baseImage1 = { - id: '56789', - category: { - id: '56789', - translations: [ - { - id: 'category-56789', - language: 'en', - name: 'All', - languages_code: 'en-US', - category_id: '56789', - }, - ], - }, - versions: { - preview: { - id: '56789', - image: 'https://upload.wikimedia.org/wikipedia/commons/e/e0/PlaceholderLC.png', - width: 275, - height: 275, - altText: 'preview picture', - }, - full: { - id: '56789', - image: 'https://upload.wikimedia.org/wikipedia/commons/e/e0/PlaceholderLC.png', - width: 275, - height: 275, - altText: 'full picture', - }, - }, -}; - -export const baseImage2 = { - id: '12345', - category: { - id: '12345', - translations: [ - { - id: 'category-12345', - language: 'en', - name: 'All', - languages_code: 'en-US', - category_id: '12345', - }, - ], - }, - versions: { - preview: { - id: '12345', - image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTOwRConBYl2t6L8QMOAQqa5FDmPB_bg7EnGA&s', - width: 275, - height: 275, - altText: 'preview picture', - }, - full: { - id: '12345', - image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTOwRConBYl2t6L8QMOAQqa5FDmPB_bg7EnGA&s', - width: 275, - height: 275, - altText: 'full picture', - }, - }, -}; - -/** - * Generates an array of mock photo objects by duplicating and modifying two base images. - * - * The generated array alternates between `baseImage1` and `baseImage2` with unique IDs - * for each photo object and its respective versions. The number of mock objects generated is fixed at 12. - * - * @param {PhotoObject} baseImage1 - The first base image template to duplicate and modify. - * @param {PhotoObject} baseImage2 - The second base image template to duplicate and modify. - * @returns {PhotoObject[]} An array of 12 mock photo objects, alternating between the two base templates. - */ - -export const generateMockImages = ( - baseImage1: PhotoObject, - baseImage2: PhotoObject, - length: number, -): PhotoObject[] => { - const mockImages = Array.from({ length: length }, (_, i) => { - const baseImage = i % 2 === 0 ? baseImage1 : baseImage2; - return { - ...baseImage, - id: `${baseImage.id}-${i + 1}`, - versions: { - preview: { - ...baseImage.versions!.preview, - id: `${baseImage.versions!.preview.id}-${i + 1}`, - }, - full: { - ...baseImage.versions!.full, - id: `${baseImage.versions!.full.id}-${i + 1}`, - }, - }, - }; - }); - return mockImages; -}; - -export const mockImagesFull = generateMockImages(baseImage1, baseImage2, 12); -export const mockImagesPreview = generateMockImages(baseImage1, baseImage2, 4); diff --git a/frontend-next-migration/src/entities/Gallery/types/gallery.d.ts b/frontend-next-migration/src/entities/Gallery/types/gallery.d.ts index a759bcfb4..b3c6c50fa 100644 --- a/frontend-next-migration/src/entities/Gallery/types/gallery.d.ts +++ b/frontend-next-migration/src/entities/Gallery/types/gallery.d.ts @@ -22,45 +22,55 @@ export interface CategoryTranslations { name: string; } -export interface PhotoVersionTranslations { +export interface Category { id: string; - languages_code: string; - photo_version_id: string; - altText: string; + translations: CategoryTranslations[]; } -export interface PhotoObjectTranslations { +export interface PhotoObjectV2Translations { id: string; languages_code: string; photo_object_id: string; - title?: string; - author?: string; - description?: string; + title?: string | null; + description?: string | null; } -export interface Category { +export interface DirectusPhotoObjectV2 { id: string; - translations: CategoryTranslations[]; + category: Category | null; + translations: PhotoObjectV2Translations[] | null; + author: string | null; + website: string | null; + github: string | null; + linkedin: string | null; + instagram: string | null; + facebook: string | null; + image: string | null; + image_2: string | null; + image_3: string | null; + animation: string | null; + date_created: string; } -export interface PhotoVersion { +export interface PhotoCategory { id: string; - image: string; - width: number; - height: number; - altText: string; + name?: string; +} + +export interface PhotoObjectLink { + name: 'github' | 'linkedin' | 'instagram' | 'facebook' | 'website'; + url: string; } export interface PhotoObject { - title?: string; author?: string; + anchorId?: string; + id: string; + date_created: string; + category: PhotoCategory; + title?: string; description?: string; + links: PhotoObjectLink[]; frames?: string[][]; - id?: string; - category?: Category; - translations?: PhotoObjectTranslations[]; - versions?: { - preview: PhotoVersion; - full: PhotoVersion; - }; + animation?: string[]; } diff --git a/frontend-next-migration/src/entities/Gallery/ui/ImageWall/ImageWall.module.scss b/frontend-next-migration/src/entities/Gallery/ui/ImageWall/ImageWall.module.scss deleted file mode 100644 index ce3bb8e0f..000000000 --- a/frontend-next-migration/src/entities/Gallery/ui/ImageWall/ImageWall.module.scss +++ /dev/null @@ -1,20 +0,0 @@ -.Item { - cursor: pointer; - width: 100%; - height: auto; - position: relative; - border-radius: var(--border-radius-custom); - background-color: var(--transparent-bg-dark); - overflow: hidden; - pointer-events: all; -} - -.Image { - width: 100%; - height: auto; - object-fit: cover; - &:hover, - &:active { - transform: scale(1.1); - } -} diff --git a/frontend-next-migration/src/entities/Gallery/ui/ImageWall/ImageWall.stories.tsx b/frontend-next-migration/src/entities/Gallery/ui/ImageWall/ImageWall.stories.tsx deleted file mode 100644 index 91b0725d9..000000000 --- a/frontend-next-migration/src/entities/Gallery/ui/ImageWall/ImageWall.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { StoryObj } from '@storybook/nextjs'; -import { ImageWall } from './ImageWall'; -import { mockImagesFull, mockImagesPreview } from '../../model/mockImages'; - -const meta = { - title: 'entities/Gallery/ImageWall', - component: ImageWall, -}; - -export default meta; - -type Story = StoryObj; - -export const Full: Story = { - args: { - images: mockImagesFull, - version: 'full', - }, -}; - -export const Preview: Story = { - args: { - images: mockImagesPreview, - version: 'preview', - }, -}; diff --git a/frontend-next-migration/src/entities/Gallery/ui/ImageWall/ImageWall.tsx b/frontend-next-migration/src/entities/Gallery/ui/ImageWall/ImageWall.tsx deleted file mode 100644 index d7f8a9625..000000000 --- a/frontend-next-migration/src/entities/Gallery/ui/ImageWall/ImageWall.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; -import cls from './ImageWall.module.scss'; -import Image from 'next/image'; -import { PhotoObject, PhotoVersion } from '../../types/gallery'; -import { filterAndTransformImages } from '../../api/filterAndTransformImages'; -import Fancybox from '@/shared/ui/Fancybox/Fancybox'; -import { Border } from '../Border/Border'; -import { MasonryWrapper } from '@/shared/ui/MasonryWrapper'; -import { AppLink } from '@/shared/ui/AppLink/AppLink'; -import { useEffect, useState } from 'react'; - -export type ImageWallProps = { - images: PhotoObject[]; - version: string; -}; - -export const ImageWall = ({ images, version }: ImageWallProps) => { - const [filteredImages, setFilteredImages] = useState([]); - - useEffect(() => { - const filteredImages = filterAndTransformImages(images, version); - if (filteredImages) setFilteredImages(filteredImages); - }, [images]); - - return ( - - - {filteredImages.map((image, index) => ( - -
- - {image.altText} - -
-
- ))} -
-
- ); -}; diff --git a/frontend-next-migration/src/features/NavigateGalleries/index.ts b/frontend-next-migration/src/features/NavigateGalleries/index.ts deleted file mode 100644 index 0976d5395..000000000 --- a/frontend-next-migration/src/features/NavigateGalleries/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as GalleryNavMenuAsDropdown } from './ui/GalleryNavMenuAsDropdown'; -export { default as GalleryNavMenuAsSidebar } from './ui/GalleryNavMenuAsSidebar'; diff --git a/frontend-next-migration/src/features/NavigateGalleries/ui/GalleryNavMenuAsDropdown.tsx b/frontend-next-migration/src/features/NavigateGalleries/ui/GalleryNavMenuAsDropdown.tsx deleted file mode 100644 index 3dd702763..000000000 --- a/frontend-next-migration/src/features/NavigateGalleries/ui/GalleryNavMenuAsDropdown.tsx +++ /dev/null @@ -1,115 +0,0 @@ -'use client'; -import { useParams, useRouter } from 'next/navigation'; -import { - NavMenuWithDropdowns, - NavMenuWithDropdownsProps, - DropDownElementASTextOrLink, -} from '@/shared/ui/NavMenuWithDropdownsV2'; -import { getRouteGalleryCategoryPage } from '@/shared/appLinks/RoutePaths'; -import { - getLanguageCode, - useGetDirectusGalleryImages, - getCategoryTranslation, -} from '@/entities/Gallery'; -import { useEffect, useState } from 'react'; -import useSizes from '@/shared/lib/hooks/useSizes'; -import { useClientTranslation } from '@/shared/i18n'; - -interface GalleryNavMenuProps { - openByDefault?: boolean; - className?: string; -} - -const GalleryNavMenuAsDropdown = (props: GalleryNavMenuProps) => { - const { openByDefault = false, className } = props; - const { isMobileSize, isTabletSize } = useSizes(); - const isTouchDevice = isMobileSize || isTabletSize; - const params = useParams(); - const router = useRouter(); - const lng = params.lng as string; - const currentCategory = params.category as string; - const language = getLanguageCode(lng); - const { categories } = useGetDirectusGalleryImages(language); - const allCategory = lng === 'en' ? 'all' : 'kaikki'; - const [selectedCategory, setSelectedCategory] = useState(currentCategory || allCategory); - const { t } = useClientTranslation('picture-galleries'); - - useEffect(() => { - if (currentCategory) setSelectedCategory(currentCategory); - }, [currentCategory]); - - useEffect(() => { - if (categories) { - const matchingCategory = categories.find((cat) => - cat.translations.some((t) => t.name === currentCategory), - ); - - if (matchingCategory) { - const translatedName = getCategoryTranslation( - matchingCategory.translations, - language, - ); - - if (translatedName && translatedName !== currentCategory) { - const newPath = getRouteGalleryCategoryPage(translatedName); - router.replace(newPath); - setSelectedCategory(translatedName); - } else { - setSelectedCategory(currentCategory); - } - } else { - setSelectedCategory(allCategory); - } - } - }, [categories, lng, currentCategory]); - - const dropdownItems: DropDownElementASTextOrLink[] = [ - { - link: { - isExternal: false, - path: getRouteGalleryCategoryPage(allCategory), - }, - elementText: allCategory.charAt(0).toUpperCase() + allCategory.slice(1), - active: selectedCategory === allCategory, - }, - ...categories.map((category) => { - const translatedName = getCategoryTranslation(category.translations, language); - return { - link: { - isExternal: false, - path: getRouteGalleryCategoryPage(translatedName), - }, - elementText: - translatedName.charAt(0).toUpperCase() + - translatedName.slice(1).replace('-', ' '), - active: translatedName === selectedCategory, - }; - }), - ]; - - const navMenuWithDropdownsMobileProps: NavMenuWithDropdownsProps = { - title: t('category-menu-title'), - openByDefault: openByDefault, - dropdownItems: dropdownItems, - }; - - const navMenuWithDropdownsDesktopProps: NavMenuWithDropdownsProps = { - title: t('category-menu-title'), - openByDefault: openByDefault, - staticDropdown: true, - dropdownItems: dropdownItems, - }; - - return ( -
- - -
- ); -}; - -export default GalleryNavMenuAsDropdown; diff --git a/frontend-next-migration/src/features/NavigateGalleries/ui/GalleryNavMenuAsSidebar.module.scss b/frontend-next-migration/src/features/NavigateGalleries/ui/GalleryNavMenuAsSidebar.module.scss deleted file mode 100644 index b53e2ec6f..000000000 --- a/frontend-next-migration/src/features/NavigateGalleries/ui/GalleryNavMenuAsSidebar.module.scss +++ /dev/null @@ -1,47 +0,0 @@ -.Arrow { - display: inline-block; - position: absolute; - top: 45%; - color: var(--secondary-color); - font: var(--font-dm-l); - right: .3em; - cursor: pointer; -} - -.Box { - width: 100%; - position: relative; - background: black; - border-radius: 10px; - transition: width 0.3s ease-in-out, opacity 0.3s ease-out; - - &.Hidden { - width: 0; - } -} - -.Category { - padding-left: 1em; - width: fit-content; - cursor: pointer; - - &:hover { - color: var(--secondary-color) - } -} - -.Text { - padding: 1em 0.5em; - font: var(--font-dm-l); - overflow: hidden; - transition: opacity 0.3s ease-in-out; - - h2 { - margin-bottom: 10px; - text-align: center; - } - - &.Hidden { - opacity: 0; - } -} diff --git a/frontend-next-migration/src/features/NavigateGalleries/ui/GalleryNavMenuAsSidebar.tsx b/frontend-next-migration/src/features/NavigateGalleries/ui/GalleryNavMenuAsSidebar.tsx deleted file mode 100644 index 17f27190e..000000000 --- a/frontend-next-migration/src/features/NavigateGalleries/ui/GalleryNavMenuAsSidebar.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useParams, useRouter } from 'next/navigation'; -import { getRouteGalleryCategoryPage } from '@/shared/appLinks/RoutePaths'; -import { - getLanguageCode, - useGetDirectusGalleryImages, - getCategoryTranslation, - Category, -} from '@/entities/Gallery'; -import { useEffect, useState } from 'react'; -import { useClientTranslation } from '@/shared/i18n'; -import cls from './GalleryNavMenuAsSidebar.module.scss'; -import { classNames, Mods } from '@/shared/lib/classNames/classNames'; - -export interface SidebarProps { - sidebarVisible: boolean; - setSidebarVisible: (visible: boolean) => void; -} - -const GalleryNavMenuAsSidebar = (props: SidebarProps) => { - const params = useParams(); - const router = useRouter(); - const lng = params.lng as string; - const currentCategory = params.category as string; - const language = getLanguageCode(lng); - const { categories } = useGetDirectusGalleryImages(language); - const allCategory = lng === 'en' ? 'all' : 'kaikki'; - const [selectedCategory, setSelectedCategory] = useState(currentCategory || allCategory); - const { t } = useClientTranslation('picture-galleries'); - const { sidebarVisible, setSidebarVisible } = props; - - useEffect(() => { - const category = findCorrectCategory(categories); - - if (!category) { - setSelectedCategory(allCategory); - } else { - setSelectedCategory(currentCategory); - } - }, [categories, currentCategory]); - - useEffect(() => { - const category = findCorrectCategory(categories); - - if (category) { - const translatedName = getCategoryTranslation(category.translations, language); - - if (translatedName && translatedName !== currentCategory) { - handleRouteChange(translatedName); - } - } else { - setSelectedCategory(allCategory); - } - }, [categories, lng]); - - const findCorrectCategory = (categories: Category[]) => { - if (!categories) return null; - const category = categories.find((cat) => - cat.translations.some((t) => t.name === currentCategory), - ); - return category ? category : null; - }; - - const handleRouteChange = (category: string) => { - const newPath = getRouteGalleryCategoryPage(category); - router.replace(newPath); - setSelectedCategory(category); - }; - - const mods: Mods = { - [cls.Hidden]: !sidebarVisible, - }; - - const getCategory = (category: Category, index: number) => { - const translatedCategory = getCategoryTranslation(category.translations, language); - - if (sidebarVisible) { - return ( -
handleRouteChange(translatedCategory)} - className={cls.Category} - style={ - selectedCategory === translatedCategory - ? { color: 'var(--secondary-color)' } - : {} - } - > - {translatedCategory.charAt(0).toUpperCase() + - translatedCategory.slice(1).replace('-', ' ')} -
- ); - } else { - return ( -
- {translatedCategory.charAt(0).toUpperCase() + - translatedCategory.slice(1).replace('-', ' ')} -
- ); - } - }; - - const getList = ( -
-

{t('category-menu-title')}

- -
handleRouteChange(allCategory)} - style={selectedCategory === allCategory ? { color: 'var(--secondary-color)' } : {}} - > - {allCategory.charAt(0).toUpperCase() + allCategory.slice(1)} -
- - {categories.map((cat, index) => getCategory(cat, index))} -
- ); - - return ( -
-
setSidebarVisible(!sidebarVisible)} - > - {sidebarVisible ? '<' : '>'} -
- {getList} -
- ); -}; - -export default GalleryNavMenuAsSidebar; diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/index.ts b/frontend-next-migration/src/preparedPages/PictureGalleryPages/index.ts index e53474048..622b812a2 100644 --- a/frontend-next-migration/src/preparedPages/PictureGalleryPages/index.ts +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/index.ts @@ -1,7 +1,5 @@ // todo fix it to not jump on loading -// export {default as PictureGalleryPage} from "./ui/PictureGalleryPage.async" -export { default as PictureGalleryPage } from './ui/PictureGalleryPage'; - -export type { Props as PictureGalleryPageProps } from './ui/PictureGalleryPage'; +export { default as PictureGalleryPage } from './ui/PictureGalleryPage.async'; +// export { default as PictureGalleryPage } from './ui/PictureGalleryPage'; export { default as cls } from './ui/PictureGalleryPage.module.scss'; diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/model/useFilterPhotoObjects.ts b/frontend-next-migration/src/preparedPages/PictureGalleryPages/model/useFilterPhotoObjects.ts new file mode 100644 index 000000000..c4af4f104 --- /dev/null +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/model/useFilterPhotoObjects.ts @@ -0,0 +1,96 @@ +import { useMemo, useState } from 'react'; +import { PhotoCategory, PhotoObject } from '@/entities/Gallery'; +import { useClientTranslation } from '@/shared/i18n'; + +/** + * Custom hook for filtering and sorting photo objects based on various criteria. + * @param photoObjects - The array of PhotoObject to filter and sort. + * @returns An object containing filtered images, current filters, and functions to set and reset filters. + */ +export const useFilterPhotoObjects = (photoObjects: PhotoObject[]) => { + const [searchQuery, setSearchQuery] = useState(''); + const { t } = useClientTranslation('picture-galleries'); + const [currentCategory, setCurrentCategory] = useState({ + id: 'all-categories', + name: t('all-categories') ?? 'All', + }); + const [sortBy, setSortBy] = useState<'date_asc' | 'date_desc' | 'title'>('date_desc'); + const [selectedAuthors, setSelectedAuthors] = useState([]); + + const filteredImages: PhotoObject[] = useMemo(() => { + let filtered = photoObjects; + + // filter by search query + if (searchQuery.trim() !== '') { + const query = searchQuery.trim().toLowerCase(); + filtered = filtered.filter( + (photo) => + photo.title?.toLowerCase().includes(query) || + photo.author?.toLowerCase().includes(query) || + photo.description?.toLowerCase().includes(query), + ); + } + + // filter by category + if (currentCategory && currentCategory.id !== 'all-categories') { + filtered = filtered.filter( + (photo) => photo.category && photo.category.id === currentCategory.id, + ); + } + + // filter by selected authors + if (selectedAuthors.length > 0) { + filtered = filtered.filter( + (photo) => photo.author && selectedAuthors.includes(photo.author), + ); + } + + // sort the filtered images + return [...filtered].sort((a, b) => { + switch (sortBy) { + case 'date_asc': + return new Date(a.date_created).getTime() - new Date(b.date_created).getTime(); + + case 'date_desc': + return new Date(b.date_created).getTime() - new Date(a.date_created).getTime(); + + case 'title': + return a.title?.localeCompare(b.title ? b.title : '') || 0; + + default: + return 0; + } + }); + }, [photoObjects, currentCategory, searchQuery, selectedAuthors, sortBy]); + + const resetAllFilters = () => { + setSearchQuery(''); + setCurrentCategory({ id: 'all-categories', name: t('all-categories') ?? 'All' }); + setSortBy('date_desc'); + setSelectedAuthors([]); + }; + const resetSelectionFilters = () => { + setSortBy('date_desc'); + setSelectedAuthors([]); + }; + + return { + filteredImages, + filters: { + searchQuery, + currentCategory, + sortBy, + selectedAuthors, + }, + setFilters: { + setSearchQuery, + setCurrentCategory, + setSortBy, + setSelectedAuthors, + }, + resetFilters: { + resetAllFilters, + resetSelectionFilters, + }, + }; +}; diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/AuthorsDropdown.tsx b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/AuthorsDropdown.tsx new file mode 100644 index 000000000..b7f2235a0 --- /dev/null +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/AuthorsDropdown.tsx @@ -0,0 +1,64 @@ +import cls from './FilterDropdowns.module.scss'; +import useSizes from '@/shared/lib/hooks/useSizes'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import { useClientTranslation } from '@/shared/i18n'; + +export const AuthorsDropdown = ({ + selectedAuthors, + setSelectedAuthors, + authorsList, + authorsOpen, + setAuthorsOpen, + dropdownsOpen, +}: { + selectedAuthors: string[]; + setSelectedAuthors: (authors: string[]) => void; + authorsList: string[]; + authorsOpen: boolean; + setAuthorsOpen: (open: boolean) => void; + dropdownsOpen: boolean; +}) => { + const { isMobileSize } = useSizes(); + const { t } = useClientTranslation('picture-galleries'); + + const handleAuthorChange = (author: string) => { + if (selectedAuthors.includes(author)) { + setSelectedAuthors(selectedAuthors.filter((a) => a !== author)); + } else { + setSelectedAuthors([...selectedAuthors, author]); + } + }; + + return ( +
+
+ + {authorsOpen && (!isMobileSize || dropdownsOpen) && ( +
+
    + {authorsList.map((author) => ( +
  • + handleAuthorChange(author)} + /> + +
  • + ))} +
+
+ )} +
+
+ ); +}; diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/FilterDropdowns.module.scss b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/FilterDropdowns.module.scss new file mode 100644 index 000000000..3a00f4df9 --- /dev/null +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/FilterDropdowns.module.scss @@ -0,0 +1,167 @@ +.dropdownContainer { + display: flex; + gap: 0.25rem; + justify-content: flex-end; + width: 100%; + @media (max-width: breakpoint(md)) { + background-color: var(--base-card-background); + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: 0.3rem 0.3rem var(--drop-shadows); + border: 2px solid var(--drop-shadows); + border-radius: var(--border-radius-figma); + gap: 0; + } + + label { + padding-left: 0.5rem; + font: var(--font-dm-s); + } +} + +.dropdownTitleContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; +} + +.divider { + @media (max-width: breakpoint(md)) { + background-color: var(--primary-color); + height: 1px; + width: 100%; + } +} + +.sortByDropdown { + width: 200px; + display: flex; + flex-direction: column; + @media (max-width: breakpoint(md)) { + width: 100%; + justify-content: center; + align-items: center; + } +} +.sortByDropdownContent { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + @media (min-width: breakpoint(md)) { + background-color: var(--base-card-background); + box-shadow: 0.3rem 0.3rem var(--drop-shadows); + border: 2px solid var(--drop-shadows); + border-radius: var(--border-radius-figma); + } +} + +.sortByOptions { + list-style: none; + accent-color: var(--primary-color); + padding: 0 1rem 0.5rem; +} + +.dropdownTitle { + font: var(--font-dm-m); + color: var(--primary-color); + background-color: var(--base-card-background); + padding: 0.5rem 1rem; + cursor: pointer; + border: none; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: var(--border-radius-figma); + width: 100%; + @media (max-width: breakpoint(md)) { + width: 100%; + border: none; + font: var(--font-sw-m); + } +} + +.authorsDropdown { + width: 300px; + display: flex; + flex-direction: column; + @media (min-width: breakpoint(md)) { + } + @media (max-width: breakpoint(md)) { + width: 100%; + justify-content: center; + align-items: center; + } +} +.authorsDropdownContent { + width: 100%; + display: flex; + flex-direction: column; + @media (min-width: breakpoint(md)) { + background-color: var(--base-card-background); + box-shadow: 0.3rem 0.3rem var(--drop-shadows); + border: 2px solid var(--drop-shadows); + border-radius: var(--border-radius-figma); + } +} + +.authorsOptionsContainer { + @media (max-width: breakpoint(md)) { + background-color: #2B516A; + margin: 0 1rem 0.5rem; + padding-top: 0.2rem; + border-radius: 4px; + outline: 1px solid var(--drop-shadows); + } +} + +.authorsOptions { + list-style: none; + width: calc(100% - 1.4rem); + max-height: 200px; + accent-color: var(--primary-color); + padding: 0 1rem; + margin-bottom: 0.5rem; + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: var(--primary-color) #2B516A; + &::-webkit-scrollbar { + width: 8px; + } + &::-webkit-scrollbar-track { + background: #2B516A; + } + &::-webkit-scrollbar-thumb { + background-color: var(--primary-color); + border-radius: 4px; + } + @media (max-width: breakpoint(md)) { + width: calc(100% - 0.3rem); + scrollbar-color: var(--primary-color) var(--base-card-background); + padding: 0 0.5rem; + margin-bottom: 0.2rem; + + } +} + +.hidden { + display: none; +} + +.resetButton { + background-color: var(--primary-color); + color: var(--drop-shadows); + width: 250px; + border: none; + margin: 1rem 0; + padding: 0.5rem 1rem; + border-radius: var(--border-radius-figma); + cursor: pointer; + font: var(--font-dm-m); + transition: + background-color 0.3s ease, + color 0.3s ease; +} diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/FilterDropdowns.tsx b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/FilterDropdowns.tsx new file mode 100644 index 000000000..db2be8de8 --- /dev/null +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/FilterDropdowns.tsx @@ -0,0 +1,91 @@ +import { PhotoObject } from '@/entities/Gallery'; +import cls from './FilterDropdowns.module.scss'; +import { useState } from 'react'; +import useSizes from '@/shared/lib/hooks/useSizes'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import { SortByDropdown } from './SortByDropdown'; +import { AuthorsDropdown } from './AuthorsDropdown'; +import { useClientTranslation } from '@/shared/i18n'; + +interface FilterDropdownsProps { + selectedAuthors: string[]; + setSelectedAuthors: (authors: string[]) => void; + sortBy: 'date_asc' | 'date_desc' | 'title'; + setSortBy: (sortBy: 'date_asc' | 'date_desc' | 'title') => void; + photoObjects: PhotoObject[]; + resetFilters: { + resetAllFilters: () => void; + resetSelectionFilters: () => void; + }; +} + +export const FilterDropdowns = ({ + selectedAuthors, + setSelectedAuthors, + sortBy, + setSortBy, + photoObjects, + resetFilters, +}: FilterDropdownsProps) => { + const [dropdownsOpen, setDropdownsOpen] = useState(false); + const [sortByOpen, setSortByOpen] = useState(false); + const [authorsOpen, setAuthorsOpen] = useState(false); + + const { isMobileSize } = useSizes(); + const { t } = useClientTranslation('picture-galleries'); + + const authorsList = [...photoObjects] + .map((photoObject) => photoObject.author) + .filter((author): author is string => !!author) + .filter((author, index, self) => self.indexOf(author) === index) // remove duplicates + .sort(); + + return ( +
+ {isMobileSize && ( +
+ +
+ )} +
+ + + {isMobileSize && dropdownsOpen && ( + <> +
+ + + )} +
+ ); +}; diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/SortByDropdown.tsx b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/SortByDropdown.tsx new file mode 100644 index 000000000..a47bd4b47 --- /dev/null +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/FilterDropdowns/SortByDropdown.tsx @@ -0,0 +1,59 @@ +import useSizes from '@/shared/lib/hooks/useSizes'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import cls from './FilterDropdowns.module.scss'; +import { useClientTranslation } from '@/shared/i18n'; + +export const SortByDropdown = ({ + sortBy, + setSortBy, + sortByOpen, + setSortByOpen, + dropdownsOpen, +}: { + sortBy: 'date_asc' | 'date_desc' | 'title'; + setSortBy: (value: 'date_asc' | 'date_desc' | 'title') => void; + sortByOpen: boolean; + setSortByOpen: (open: boolean) => void; + dropdownsOpen: boolean; +}) => { + const { isMobileSize } = useSizes(); + const { t } = useClientTranslation('picture-galleries'); + + const sortByOptions = [ + { value: 'date_desc', label: t('date-desc') }, + { value: 'date_asc', label: t('date-asc') }, + { value: 'title', label: t('title') }, + ] as const; + + return ( +
+
+ + {sortByOpen && (!isMobileSize || dropdownsOpen) && ( +
    + {sortByOptions.map((option) => ( +
  • + setSortBy(option.value)} + /> + +
  • + ))} +
+ )} +
+
+ ); +}; diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/NavigateGalleryTabs.tsx b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/NavigateGalleryTabs.tsx new file mode 100644 index 000000000..93c623a27 --- /dev/null +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/NavigateGalleryTabs.tsx @@ -0,0 +1,77 @@ +import { useMemo } from 'react'; +import { PhotoCategory } from '@/entities/Gallery'; +import useSizes from '@/shared/lib/hooks/useSizes'; +import { TabNavigation } from '@/shared/ui/TabNavigation'; +import { useClientTranslation } from '@/shared/i18n'; + +interface NavigateGalleryTabsProps { + categories: PhotoCategory[]; + setBackgroundColor: (color: string) => void; + currentCategory: PhotoCategory; + setCurrentCategory: (category: PhotoCategory) => void; +} + +export const NavigateGalleryTabs = ({ + categories, + setBackgroundColor, + currentCategory, + setCurrentCategory, +}: NavigateGalleryTabsProps) => { + const { isMobileSize } = useSizes(); + const currentCategoryNumber = useMemo( + () => categories?.findIndex((category) => category.id === currentCategory.id), + [categories, currentCategory.id], + ); + + const { t } = useClientTranslation('picture-galleries'); + const categoryColors = [ + { tabColor: '#97C459', sectionBG: '#527259' }, + { tabColor: '#5DCAA5', sectionBG: '#0C5450' }, + { tabColor: '#85B7EB', sectionBG: '#2B516A' }, + { tabColor: '#AFA9EC', sectionBG: '#63688B' }, + ]; + const activeTabStyles = { + backgroundColor: + categoryColors[currentCategoryNumber % categoryColors.length]?.tabColor || + categoryColors[0].tabColor, + color: 'black', + }; + const mobileTabStylesList = categories.map((_category, index) => ({ + border: `2px solid`, + borderColor: + categoryColors[index % categoryColors.length]?.tabColor || categoryColors[0].tabColor, + color: + categoryColors[index % categoryColors.length]?.tabColor || categoryColors[0].tabColor, + })); + + const onActiveTabChange = (tab: string) => { + const selectedCategory = categories.find((category) => category.id === tab); + if (selectedCategory) { + const selectedCategoryNumber = categories.findIndex( + (category) => category.id === selectedCategory.id, + ); + const sectionBGColor = + categoryColors[selectedCategoryNumber % categoryColors.length]?.sectionBG || + categoryColors[0].sectionBG; + setBackgroundColor(sectionBGColor); + setCurrentCategory(selectedCategory); + } + }; + const tabs = categories.map((category) => ({ + id: category.id, + label: + (category.name || category.id).charAt(0).toUpperCase() + + (category.name || category.id).slice(1), + })); + + return ( + + ); +}; diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.async.tsx b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.async.tsx index 2871830e3..af37b88e2 100644 --- a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.async.tsx +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.async.tsx @@ -1,6 +1,5 @@ import dynamic from 'next/dynamic'; -import { Props } from './PictureGalleryPage'; -const PictureGalleryPage = dynamic(() => import('./PictureGalleryPage')); +const PictureGalleryPage = dynamic(() => import('./PictureGalleryPage')); export default PictureGalleryPage; diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.module.scss b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.module.scss index 4e5e1eb1f..782531cbd 100644 --- a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.module.scss +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.module.scss @@ -1,21 +1,18 @@ .Wrapper { - display: flex; flex-direction: column; min-height: 100vh; padding-bottom: 20px; + margin-inline: auto; + padding-inline: 20px; // background-color: rgba(109, 39, 39, 0.322); //@media (max-width: 1200px) { // margin-top: 50px; //} - -} -.LeftSidebar{ - margin-top: 6rem; -} -.Maincontainer{ - background-color: white !important; + @media (min-width: breakpoint(md)) { + width: 60%; + } } .Container{ @@ -23,27 +20,18 @@ border-radius: var(--border-radius-custom); // background-color: var(--transparent-bg-dark); //margin-top: 10%; - padding-bottom: 40px; - - .SocialsText { - width: 85%; - margin: auto; - text-align: center; - } - - display: flex; flex-direction: column; - gap: 20px; + gap: 0.25rem; } .Header{ - background-color: #1E3544; - box-shadow: 0.3rem 0.3rem black; - border: 2px solid black; + background-color: var(--base-card-background); + box-shadow: 0.3rem 0.3rem var(--drop-shadows); + border: 2px solid var(--drop-shadows); border-radius: 5px; - margin-bottom: 20px; + margin-bottom: 2rem; padding: 20px; margin-inline:0; } @@ -58,51 +46,25 @@ margin-top: 10px; display:inline; font: var(--font-dm-m); - color: #faf9f6c0; -} - - -.galleries{ - display: flex; - gap: 30px; - justify-content: center; - flex-wrap: wrap; - + color: var(--white-text); } @media (min-width: breakpoint(md)) { - .Container { - //margin-top: 10vh; - - .SocialsText { - width: 75%; - } - } - .InfoText { width: 75%; font: var(--font-dm-m); } - -} - -.videoWrapper{ - padding: 0 40px; - margin-bottom: 20px; -} - -.videoWrapper>div { - margin-left: auto; - margin-right: auto; - max-width: 700px; - border-radius: var(--border-radius-custom); } .TitleBar { display: flex; justify-content: space-between; align-items: center; - margin-inline: 1rem; + margin-bottom: 2rem; + @media (max-width: breakpoint(lg)) { + flex-direction: column; + gap: 1rem; + } } .SearchBarDesktop { @@ -111,24 +73,8 @@ .short { width: 17.5rem; - @media (max-width: breakpoint(lg)){ margin-inline: auto; - } @media (max-width: breakpoint(md)){ width: 100%; - margin-inline: 0; } } - - - -@media (max-width: breakpoint(lg)) { - .mainTitle{ - text-align: center; - font: var(--font-sw-xxl); - } - .Maincontainer{ - width: 100%; - } - -} \ No newline at end of file diff --git a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.tsx b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.tsx index 113960eea..16a408636 100644 --- a/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.tsx +++ b/frontend-next-migration/src/preparedPages/PictureGalleryPages/ui/PictureGalleryPage.tsx @@ -1,103 +1,68 @@ 'use client'; -import React, { useState, useMemo } from 'react'; -import { AnimationGallerySection } from '@/widgets/SectionGallery/ui/SectionGalleryV2/SectionGallery'; -import { - getLanguageCode, - PhotoObject, - useGetDirectusGalleryImages, - getCategoryTranslation, -} from '@/entities/Gallery'; +import React, { useState, useMemo, useEffect } from 'react'; +import { AnimationGallerySection } from '@/widgets/SectionGallery'; +import { getLanguageCode, PhotoCategory, useGetDirectusGalleryImages } from '@/entities/Gallery'; import { Container } from '@/shared/ui/Container'; import cls from './PictureGalleryPage.module.scss'; import { useClientTranslation } from '@/shared/i18n'; import useSizes from '@/shared/lib/hooks/useSizes'; import { SearchBar } from '@/preparedPages/DefenseGalleryPages/ui/SingleDefensePage'; import { PageTitle } from '@/shared/ui/PageTitle'; -import { classNames } from '@/shared/lib/classNames/classNames'; -// import buttonImg from '@/shared/assets/images/gallery/Frame 526.png'; -// import { SectionGalleryV2 } from '@/widgets/SectionGallery'; import { useParams } from 'next/navigation'; -// import { useGetDirectusGalleryImages, getLanguageCode, getCategoryTranslation, } from '@/entities/Gallery'; - -export interface Props { - title: string; - infoText: string; - socialsText: string; - socialMediaLinks: string[]; - videoLink: string; -} -//This may be useful later -/* const PictureGalleryPage = (props: Props) => { - const { socialMediaLinks } = props; - const params = useParams(); - const lng = params.lng as string; - const category = params.category as string; - const language = getLanguageCode(lng); - const { photoObjects, isLoading } = useGetDirectusGalleryImages(language); - const [filteredImages, setFilteredImages] = useState(photoObjects); - const allCategory = lng === 'en' ? 'all' : 'kaikki'; - - useEffect(() => { - if (!category || category === allCategory) { - setFilteredImages(photoObjects); - } else { - setFilteredImages( - photoObjects.filter( - (photo) => - getCategoryTranslation(photo.category.translations, language) === category, - ), - ); - } - }, [photoObjects, category]); - - if (isLoading)

Loading...

; - */ +import { useFilterPhotoObjects } from '../model/useFilterPhotoObjects'; +import { NavigateGalleryTabs } from './NavigateGalleryTabs'; +import { FilterDropdowns } from './FilterDropdowns/FilterDropdowns'; const PictureGalleryPage = () => { const { t } = useClientTranslation('picture-galleries'); - const { isMobileSize, isDesktopSize, isWidescreenSize } = useSizes(); - const [searchQuery, setSearchQuery] = useState(''); + const { isDesktopSize, isWidescreenSize } = useSizes(); + const isBigDevice = isDesktopSize || isWidescreenSize; + const [sectionBG, setSectionBG] = useState('#527259'); const params = useParams(); const lng = params.lng as string; - const categorySlug = params.category as string | undefined; - const languageCode = getLanguageCode(lng); - const { photoObjects, isLoading, error } = useGetDirectusGalleryImages(languageCode); - const isBigDevice = isDesktopSize || isWidescreenSize; - const allCategory = lng === 'en' ? 'all' : 'kaikki'; + const { photoObjects, categories, isLoading, error } = + useGetDirectusGalleryImages(languageCode); - const showCreativity = useMemo( - () => !isMobileSize && searchQuery.length === 0, - [isMobileSize, searchQuery], - ); + // filters + const { filteredImages, filters, setFilters, resetFilters } = + useFilterPhotoObjects(photoObjects); + + // add "all categories" to categories + const allCategories: PhotoCategory[] = useMemo(() => { + if (!categories) return []; + return [ + { + id: 'all-categories', + name: t('all-categories') ?? 'All', + }, + ...categories, + ]; + }, [categories, t]); + + // useEffect for smooth scrolling to anchor when hash changes or page loads with hash + useEffect(() => { + if (isLoading || error || filteredImages.length === 0) return; - // filter images by category from URL params - const categoryFilteredImages: PhotoObject[] = useMemo(() => { - if (!photoObjects) return []; - if (!categorySlug || categorySlug === allCategory) { - return photoObjects; - } - - return photoObjects.filter((photo) => { - if (!photo.category) return false; - const translatedCategory = getCategoryTranslation( - photo.category.translations, - languageCode, - ); - return translatedCategory === categorySlug; - }); - }, [photoObjects, categorySlug, allCategory, languageCode]); - - const filteredImages: PhotoObject[] = useMemo(() => { - const query = searchQuery.trim().toLowerCase(); - if (!query) return categoryFilteredImages; - return categoryFilteredImages.filter((photo) => { - const fields = [photo.title, photo.author, photo.description]; - return fields.some((find) => (find || '').toLowerCase().includes(query)); - }); - }, [searchQuery, categoryFilteredImages]); + const scrollToHash = () => { + const anchorId = window.location.hash.slice(1); + if (!anchorId) return; + + const element = document.getElementById(anchorId.toLowerCase()); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } + }; + + scrollToHash(); + window.addEventListener('hashchange', scrollToHash); + + return () => { + window.removeEventListener('hashchange', scrollToHash); + }; + }, [isLoading, error, filteredImages]); if (isLoading) { return ( @@ -122,46 +87,51 @@ const PictureGalleryPage = () => { return (
- {isBigDevice ? ( -
- +
+ + {isBigDevice && ( -
- ) : ( + )} +
+ +
+

{t('picture-galleries')}

+

{t('info-text')}

+
+ {!isBigDevice && ( )} - {showCreativity && ( -
-

{t('picture-galleries')}

-

{t('info-text')}

-
- )} + + + + ({ - // Adjust these mappings as needed based on your PhotoObject and FrameSet definitions - title: photo.title || '', - author: photo.author || '', - description: photo.description || '', - frames: - photo.frames && photo.frames.length > 0 - ? photo.frames - : photo.versions?.preview - ? [[photo.versions.preview.image]] - : [], - // Add other FrameSet properties if required - }))} + animations={filteredImages} + backgroundColor={sectionBG} />
diff --git a/frontend-next-migration/src/shared/appLinks/RoutePaths.ts b/frontend-next-migration/src/shared/appLinks/RoutePaths.ts index 68bff0802..00f6f10ea 100644 --- a/frontend-next-migration/src/shared/appLinks/RoutePaths.ts +++ b/frontend-next-migration/src/shared/appLinks/RoutePaths.ts @@ -15,7 +15,6 @@ export enum AppRoutesLinks { PICTURE_GALLERY = 'PICTURE_GALLERY', COMICS_GALLERY = 'COMICS_GALLERY', - PICTURE_GALLERY_CATEGORY = 'PICTURE_GALLERY_CATEGORY', MAIN = 'MAIN', ABOUT = 'ABOUT', @@ -67,7 +66,6 @@ export const RoutePaths: Record = { [AppRoutesLinks.PICTURE_GALLERY]: '/picture-galleries', [AppRoutesLinks.COMICS_GALLERY]: '/comics', - [AppRoutesLinks.PICTURE_GALLERY_CATEGORY]: '/picture-galleries/:category', [AppRoutesLinks.MAIN]: '/', [AppRoutesLinks.ABOUT]: '/about', @@ -116,7 +114,6 @@ export const getRouteOneHeroDevPage = (slug: string) => `/hero-development/${slu export const getRouteComicsPage = () => '/comics'; export const getRouteGalleryPage = () => '/picture-galleries'; -export const getRouteGalleryCategoryPage = (category: string) => `/picture-galleries/${category}`; export const getRouteGameArtPage = () => '/artGame'; export const getRouteMyClanPage = () => '/clans/myclan'; diff --git a/frontend-next-migration/src/shared/assets/images/heros/people-pleaser/miellyttaja.png b/frontend-next-migration/src/shared/assets/images/heros/people-pleaser/miellyttaja.png new file mode 100644 index 000000000..b1c6de87b Binary files /dev/null and b/frontend-next-migration/src/shared/assets/images/heros/people-pleaser/miellyttaja.png differ diff --git a/frontend-next-migration/src/shared/i18n/locales/en/picture-galleries.json b/frontend-next-migration/src/shared/i18n/locales/en/picture-galleries.json index c166941dd..e2fc1f322 100644 --- a/frontend-next-migration/src/shared/i18n/locales/en/picture-galleries.json +++ b/frontend-next-migration/src/shared/i18n/locales/en/picture-galleries.json @@ -12,5 +12,20 @@ "socials-text": "Also, check out our socials!", "explore": "Explore", "category-menu-title": "Categories", - "picture-galleries-title": "GALLERY" + "all-categories": "All", + "picture-galleries-title": "How We Made the Graphics", + "no-animations-title": "No Results Found", + "no-animations-text": "Please check your spelling or try a new search.", + "loading": "Loading...", + "error-text": "Error loading gallery.", + "filters": "Filters", + "date-asc": "Oldest first", + "date-desc": "Newest first", + "title": "Alphabetical", + "sort-by": "Sort by", + "authors": "Authors", + "reset-filters": "Reset", + "page-planner": "Page designed by", + "image-alt": "image", + "animation-alt": "animation" } diff --git a/frontend-next-migration/src/shared/i18n/locales/fi/picture-galleries.json b/frontend-next-migration/src/shared/i18n/locales/fi/picture-galleries.json index 704cd56c2..f90b2a852 100644 --- a/frontend-next-migration/src/shared/i18n/locales/fi/picture-galleries.json +++ b/frontend-next-migration/src/shared/i18n/locales/fi/picture-galleries.json @@ -1,9 +1,9 @@ { - "picture-galleries": "Luovuuden Kulissit", + "picture-galleries": "Luovuuden kulissit", "Chat": "Chat", "Hahmot": "Hahmot", "Huonekalut": "Huonekalut", - "head-title": "Luovuuden Kulissit", + "head-title": "Luovuuden kulissit", "head-description": "Selaa kehittäjien, taiteilijoiden ja suunnittelijoiden töitä ja löydä inspiroivia projekteja.", "head-keywords": "altzone, kuvagalleriat, taide, projektit, kehittäjät, suunnittelijat", "og-title": "ALT Zone Kuvagalleriat — Luovuuden Kulissit", @@ -12,5 +12,20 @@ "socials-text": "Löydä lisää meidän someista!", "explore": "Tutustu", "category-menu-title": "Kategoriat", - "picture-galleries-title": "GALLERIA" + "all-categories": "Kaikki", + "picture-galleries-title": "Näin teimme grafiikan", + "no-animations-title": "Ei hakutuloksia", + "no-animations-text": "Tarkista kirjoititko oikein tai kokeile uutta hakua.", + "loading": "Ladataan...", + "error-text": "Gallerian lataus epäonnistui.", + "filters": "Suodattimet", + "date-asc": "Vanhin ensin", + "date-desc": "Uusin ensin", + "title": "A - Ö", + "sort-by": "Järjestä", + "authors": "Tekijät", + "reset-filters": "Tyhjennä", + "page-planner": "Sivun suunnitellut", + "image-alt": "kuva", + "animation-alt": "animaatio" } diff --git a/frontend-next-migration/src/shared/ui/SocialMediaIcons/index.ts b/frontend-next-migration/src/shared/ui/SocialMediaIcons/index.ts new file mode 100644 index 000000000..cb4efd4dd --- /dev/null +++ b/frontend-next-migration/src/shared/ui/SocialMediaIcons/index.ts @@ -0,0 +1 @@ +export { SocialMediaIcons } from './ui/SocialMediaIcons'; diff --git a/frontend-next-migration/src/shared/ui/SocialMediaIcons/ui/SocialMediaIcons.module.scss b/frontend-next-migration/src/shared/ui/SocialMediaIcons/ui/SocialMediaIcons.module.scss new file mode 100644 index 000000000..5acdcab2e --- /dev/null +++ b/frontend-next-migration/src/shared/ui/SocialMediaIcons/ui/SocialMediaIcons.module.scss @@ -0,0 +1,19 @@ +.socialMediaIcons { + display: flex; + flex-direction: row; + align-items: center; +} + +.iconLink { + display: inline-flex; + align-items: center; + justify-content: center; + font: var(--font-dm-l); + cursor: pointer; + color: var(--primary-color); + @include link-scale-effect('icon'); + + @media (max-width: breakpoint(sm)) { + font: var(--font-dm-m); + } +} \ No newline at end of file diff --git a/frontend-next-migration/src/shared/ui/SocialMediaIcons/ui/SocialMediaIcons.tsx b/frontend-next-migration/src/shared/ui/SocialMediaIcons/ui/SocialMediaIcons.tsx new file mode 100644 index 000000000..f003906e6 --- /dev/null +++ b/frontend-next-migration/src/shared/ui/SocialMediaIcons/ui/SocialMediaIcons.tsx @@ -0,0 +1,46 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faGithub, faLinkedin, faInstagram, faFacebook } from '@fortawesome/free-brands-svg-icons'; +import { faGlobe } from '@fortawesome/free-solid-svg-icons'; +import cls from './SocialMediaIcons.module.scss'; + +const SoMeIcons = { + website: faGlobe, + github: faGithub, + linkedin: faLinkedin, + facebook: faFacebook, + instagram: faInstagram, +}; + +interface SocialMediaIconsProps { + links: { name: 'website' | 'github' | 'linkedin' | 'facebook' | 'instagram'; url: string }[]; +} + +/** + * Component to render social media icons with links. + * @param props - The properties for the `SocialMediaIcons` component. + * @param props.links - An array of objects containing the name of the social media platform and its corresponding URL. + * @returns The rendered social media icons. + */ +export const SocialMediaIcons = ({ links }: SocialMediaIconsProps) => { + return ( +
+ {links.map((link) => { + const icon = SoMeIcons[link.name]; + if (!icon) return null; // skip if the icon is not found + return ( + + + + ); + })} +
+ ); +}; diff --git a/frontend-next-migration/src/shared/ui/TabNavigation/index.ts b/frontend-next-migration/src/shared/ui/TabNavigation/index.ts new file mode 100644 index 000000000..c967785f6 --- /dev/null +++ b/frontend-next-migration/src/shared/ui/TabNavigation/index.ts @@ -0,0 +1 @@ +export { TabNavigation } from './ui/TabNavigation'; diff --git a/frontend-next-migration/src/shared/ui/TabNavigation/ui/TabNavigation.module.scss b/frontend-next-migration/src/shared/ui/TabNavigation/ui/TabNavigation.module.scss new file mode 100644 index 000000000..a39c9a9b7 --- /dev/null +++ b/frontend-next-migration/src/shared/ui/TabNavigation/ui/TabNavigation.module.scss @@ -0,0 +1,62 @@ +.tabNavigation { + display: flex; + background-color: var(--base-card-background); + box-shadow: 0.3rem 0.3rem var(--drop-shadows); + border: 2px solid var(--drop-shadows); + border-radius: var(--border-radius-figma); + flex-direction: column; +} + +.tabsContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + flex-wrap: wrap; + @media (max-width: breakpoint(md)) { + padding: 1rem 1.5rem; + gap: 0.5rem; + } +} + +.tabsTitle { + font: var(--font-sw-xl); + color: var(--primary-color); + display: flex; + align-items: center; + justify-content: center; + padding-top: 1rem; +} + +.tab { + padding: 12px; + width: 250px; + height: 50px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + color: var(--primary-color); + font: var(--font-dm-bold-l); + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; + border-radius: var(--border-radius-figma); + + @media (max-width: breakpoint(md)) { + border-radius: var(--border-radius-figma); + font: var(--font-dm-m); + height: 35px; + width: 140px; + } +} + +.tab:hover { + text-decoration: underline; +} + +.activeTab { + outline: 2px solid var(--drop-shadows); + color: black; + background-color: var(--primary-color); +} \ No newline at end of file diff --git a/frontend-next-migration/src/shared/ui/TabNavigation/ui/TabNavigation.tsx b/frontend-next-migration/src/shared/ui/TabNavigation/ui/TabNavigation.tsx new file mode 100644 index 000000000..e06b6e341 --- /dev/null +++ b/frontend-next-migration/src/shared/ui/TabNavigation/ui/TabNavigation.tsx @@ -0,0 +1,57 @@ +import cls from './TabNavigation.module.scss'; +import useSizes from '@/shared/lib/hooks/useSizes'; + +interface TabNavigationProps { + tabs: { id: string; label: string }[]; + tabsTitle: string; + activeTab: string; + onTabClick: (tab: string) => void; + tabStylesList?: React.CSSProperties[]; + activeTabStyles?: React.CSSProperties; +} + +/** + * TabNavigation component renders a navigation bar with tabs. + * @param tabs - An array of tab objects containing id and label. + * @param tabsTitle - The title displayed above the tabs on mobile devices. + * @param activeTab - The id of the currently active tab. + * @param onTabClick - Callback function triggered when a tab is clicked. + * @param tabStylesList - Optional array of styles for each tab. + * @param activeTabStyles - Optional styles for the active tab. + */ +export const TabNavigation = ({ + tabs, + tabsTitle, + activeTab, + onTabClick, + tabStylesList, + activeTabStyles, +}: TabNavigationProps) => { + const { isMobileSize } = useSizes(); + + const handleTabClick = (tabId: string) => { + onTabClick(tabId); + }; + + return ( +
+ {isMobileSize &&
{tabsTitle}
} +
+ {tabs.map((tab) => ( +
handleTabClick(tab.id)} + > + {tab.label} +
+ ))} +
+
+ ); +}; diff --git a/frontend-next-migration/src/widgets/SectionGallery/index.ts b/frontend-next-migration/src/widgets/SectionGallery/index.ts index 6ef7469b8..a528008dc 100644 --- a/frontend-next-migration/src/widgets/SectionGallery/index.ts +++ b/frontend-next-migration/src/widgets/SectionGallery/index.ts @@ -2,4 +2,4 @@ export { SectionGallery as SectionGalleryV1 } from './ui/SectionGalleryV1/SectionGallery'; // Version 2 (with ImageWall) -export { AnimationGallerySection as SectionGalleryV2 } from './ui/SectionGalleryV2/SectionGallery'; +export { AnimationGallerySection } from './ui/SectionGalleryV2/SectionGallery'; diff --git a/frontend-next-migration/src/widgets/SectionGallery/ui/SectionGalleryV2/SectionGallery.tsx b/frontend-next-migration/src/widgets/SectionGallery/ui/SectionGalleryV2/SectionGallery.tsx index 7ef5d5ac1..56cb3e272 100644 --- a/frontend-next-migration/src/widgets/SectionGallery/ui/SectionGalleryV2/SectionGallery.tsx +++ b/frontend-next-migration/src/widgets/SectionGallery/ui/SectionGalleryV2/SectionGallery.tsx @@ -1,76 +1,99 @@ 'use client'; -import { useInView } from 'react-intersection-observer'; -import { classNames } from '@/shared/lib/classNames/classNames'; -import { Button, ButtonTheme } from '@/shared/ui/Button'; import cls from './SectionGallery2.module.scss'; import Image from 'next/image'; - -interface FrameSet { - title: string; - author: string; - description: string; - frames: string[][]; // each row is an array of image paths -} +import { SocialMediaIcons } from '@/shared/ui/SocialMediaIcons'; +import { PhotoObject } from '@/entities/Gallery'; +import miellyttaja from '@/shared/assets/images/heros/people-pleaser/miellyttaja.png'; +import { useClientTranslation } from '@/shared/i18n'; interface AnimationGalleryProps { - animations: FrameSet[]; + animations: PhotoObject[]; + backgroundColor?: string; } -export const AnimationGallerySection = ({ animations }: AnimationGalleryProps) => { - const { inView } = useInView({ - rootMargin: '-150px 0px', - triggerOnce: true, - }); - - const mods = { - [cls.inView]: inView, - }; - +export const AnimationGallerySection = ({ animations, backgroundColor }: AnimationGalleryProps) => { + const { t } = useClientTranslation('picture-galleries'); + if (!animations || animations.length === 0) { + return ( +
+
+
+ {t('no-animations-title')} +
+

{t('no-animations-title')}

+

{t('no-animations-text')}

+
+
+ {t('page-planner')}:Sanna-Kaisa Mastokangas +
+
+ ); + } return ( -
+
{animations.map((set, index) => (

{set.title}

{set.author}

+
+ {set.links && ( +
+ +
+ )} +

{set.description}

- {set.frames.map((row, rowIndex) => ( -
- {row.map((imgSrc, imgIndex) => ( -
- {`Frame -
- ))} -
- +
+ {set.frames?.map((row, rowIndex) => ( +
+ {`${set.title}
+ ))} +
+ {set.animation && ( +
+ {`${set.title}
- ))} + )}
))} +
+ {t('page-planner')}:Sanna-Kaisa Mastokangas +
); }; diff --git a/frontend-next-migration/src/widgets/SectionGallery/ui/SectionGalleryV2/SectionGallery2.module.scss b/frontend-next-migration/src/widgets/SectionGallery/ui/SectionGalleryV2/SectionGallery2.module.scss index ded383548..8032375af 100644 --- a/frontend-next-migration/src/widgets/SectionGallery/ui/SectionGalleryV2/SectionGallery2.module.scss +++ b/frontend-next-migration/src/widgets/SectionGallery/ui/SectionGalleryV2/SectionGallery2.module.scss @@ -1,52 +1,27 @@ -.buttonContainer { - text-align: center; -} - -.SeeMore { - margin-top: 3rem; - opacity: 0; - filter: blur(5px); - transform: scale(0.9) rotate(-5deg); - background-color: rgba(0, 0, 0, 0); - transition: opacity .5s ease-out, filter .5s ease-out, transform .5s ease-out, background-color 1.5s ease-out; -} - -.SeeMore.inView { - opacity: 1; - filter: blur(0); - background-color: rgba(0, 0, 0, 0.7); - transform: scale(1) rotate(0deg); -} - -.SeeMore:hover { - filter: blur(0); - background-color: rgba(0, 0, 0, 0.9); - transform: scale(1.05) rotate(0deg); - transition: filter 0.3s ease, background-color 0.3s ease, transform 0.3s ease; -} - -.AnimationGallerySection { +.animationGallerySection { display: flex; flex-direction: column; - gap: 2rem; - padding: 2rem 0; + gap: 3rem; + padding: 3rem; + box-shadow: 0.3rem 0.3rem var(--drop-shadows); + border: 2px solid var(--drop-shadows); + border-radius: 10px; + background-color: var(--base-card-background); } .block { display: flex; flex-direction: column; - align-items: start; - gap: 2rem; - background-color: #1E3544; - box-shadow: 0.3rem 0.3rem black; - border: 2px solid black; + align-items: flex-start; + background-color: var(--base-card-background); + box-shadow: 0.3rem 0.3rem var(--drop-shadows); + border: 2px solid var(--drop-shadows); border-radius: 10px; - margin-bottom: 20px; padding: 20px; } .textBlock { - text-align: start; + text-align: flex-start; max-width: 800px; width: 100%; } @@ -60,19 +35,29 @@ } .author { - font: var(--font-sw-m); + font: var(--font-sw-l); +} + +.socialsContainer { + display: flex; + margin-top: 0.5rem; } .description { font: var(--font-dm-m); - color: #faf9f6c0; - margin-top: 1rem; + color: var(--white-text); + margin: 0.5rem 0; } .framesContainer { display: flex; flex-direction: column; gap: 1rem; + justify-content: space-between; + justify-items: center; + align-items: center; + width: 100%; + margin-top: 1rem; } .frameRow { @@ -83,14 +68,14 @@ gap: 1rem; } -.imageWrapper, -.buttonWrapper { +.imageWrapper { position: relative; border-radius: 8px; overflow: hidden; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); flex: 1 1 0; aspect-ratio: 16 / 9; + width: 374px; + height: 246px; min-height: 180px; } @@ -102,53 +87,143 @@ object-position: center; } -.animateButton { - width: 100%; - height: 100%; +.animationContainer { border-radius: 8px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + position: relative; + overflow: hidden; + aspect-ratio: 16 / 9; + width: 720px; + height: 472px; + display: flex; + max-width: 100%; + justify-content: center; + align-items: center; + flex: 1 1 0; } -@media (max-width: breakpoint(md)) { - .framesContainer { +.animationImage { width: 100%; - } + height: 100%; + display: block; + object-fit: contain; + object-position: center; +} - .frameRow { - width: 100%; +.noAnimationsFoundContainer { + position: relative; display: flex; flex-direction: column; - gap: 1rem; - align-items: stretch; - } + align-items: center; + justify-content: center; + padding: 2rem; + background-color: var(--base-card-background); + box-shadow: 0.3rem 0.3rem var(--drop-shadows); + border: 2px solid var(--drop-shadows); + border-radius: 10px; +} - .imageWrapper { - width: 100%; - flex: 0 0 auto; - aspect-ratio: auto; - min-height: 0; - height: clamp(280px, 55vh, 560px); - } +.noAnimationImageContainer { + position: relative; + width: 160px; + height: 160px; + margin-bottom: 1rem; +} - .frameImage { +.noAnimationsImage { + width: 160px; + height: 160px; object-fit: contain; - object-position: center; - } + margin-bottom: 1rem; +} - .buttonWrapper { - width: 100%; - flex: 0 0 auto; - aspect-ratio: auto; - min-height: 0; - } +.noAnimationsTitle { + font: var(--font-sw-l); + font-weight: bold; + margin-bottom: 0.5rem; + font: var(--font-sw-l); + color: var(--white-text); +} - .animateButton { - width: 100%; - height: clamp(120px, 18vh, 180px); - } +.noAnimationsText { + font: var(--font-dm-m); + color: var(--white-text); + text-align: center; +} - .textBlock { text-align: center; } - .author { text-align: center; } - .description { text-align: start; } - .title { font: var(--font-sw-l); } +@media (max-width: breakpoint(md)) { + .framesContainer { + width: 100%; + } + + .frameRow { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + + .imageWrapper { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + aspect-ratio: auto; + height: auto; + } + + .frameImage { + object-fit: contain; + object-position: center; + width: 100%; + height: 100%; + } + + .animationContainer { + width: 100%; + } + + .buttonWrapper { + width: 100%; + flex: 0 0 auto; + aspect-ratio: auto; + min-height: 0; + } + + .textBlock { + text-align: center; + } + .author { + text-align: center; + } + .description { + text-align: flex-start; + } + .title { + font: var(--font-sw-l); + } +} + +.footer { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + background-color: var(--base-card-background); + box-shadow: 0.3rem 0.3rem var(--drop-shadows); + border: 2px solid var(--drop-shadows); + border-radius: var(--border-radius-figmadesktop); + padding: 20px; + text-align: center; + font: var(--font-dm-m); + color: var(--white-text); + + span { + color: var(--primary-color); + margin-left: 5px; + } + @media (max-width: breakpoint(md)) { + flex-direction: column; + } }