diff --git a/README.md b/README.md index 4dda80da..f4cf22aa 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,10 @@ The site is built using TypeScript and the Next.js framework and is hosted on Ve - **Collection Owner, Text and Concept** — Leo Shirokov - **Development and Implementation, "Design"** — Alexander Shenshin + + Clear image cache + ```shell + rm -rf /var/cache/nginx/imgcache/* + ``` + \ No newline at end of file diff --git a/app/about/page.tsx b/app/about/page.tsx index f10a6692..937bf7ef 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -15,8 +15,6 @@ import { } from '@mantine/core' import styles from './styles.module.scss' import { ShowMore } from '../components/showmore' -import leo from '@/public/images/leo-shirokov.png' -import aboutCircle from '@/public/images/about-circle.png' export const dynamic = 'force-static' @@ -36,10 +34,9 @@ export default function About() { width={79} height={125} sizes="(max-width: 768px) 100vw, 50vw" - src={leo} + src="/shared/leo-shirokov.png" className={styles.leoImage} component={NextImage} - placeholder="blur" /> I am a collector and restorer of antique barometers, a member of the Society for the History of Technology (SHOT), European Society for Environmental History (ESEH) and the @@ -73,7 +70,7 @@ export default function About() { - My collection features more than 100 rare and exceptional items, including mercury and + My collection features more than 150 rare and exceptional items, including mercury and aneroid barometers, as well as barographs, mainly from the Victorian era. Some of the most esteemed manufacturers in my collection include Negretti & Zambra, Short & Mason, Joseph Hicks, Peter Dollond, Thomas Mason, Dominicus Sala, Breguet, J.C. Vickery, Gottlieb Lufft, @@ -121,9 +118,8 @@ export default function About() { width={160} height={160} sizes="(max-width: 576px) 70vw, 160px" - src={aboutCircle} + src="/shared/about-circle.png" component={NextImage} - placeholder="blur" /> diff --git a/app/admin/add-barometer/file-upload.tsx b/app/admin/add-barometer/file-upload.tsx index c4b5f9e6..3fa7f138 100644 --- a/app/admin/add-barometer/file-upload.tsx +++ b/app/admin/add-barometer/file-upload.tsx @@ -16,7 +16,7 @@ import { import { IconPhotoPlus, IconXboxX } from '@tabler/icons-react' import { showError } from '@/utils/notification' import { deleteImage, uploadFileToCloud, createImageUrls } from '@/utils/fetch' -import { googleStorageImagesFolder } from '@/utils/constants' +import { imageStorage } from '@/utils/constants' interface FileUploadProps { fileNames: string[] @@ -32,7 +32,7 @@ export function FileUpload({ }: FileUploadProps) { const [isUploading, setIsUploading] = useState(false) - const googleUploadImages = async (files: File[] | null) => { + const uploadImages = async (files: File[] | null) => { if (clearValidateError) clearValidateError() if (!files || !Array.isArray(files) || files.length === 0) return setIsUploading(true) @@ -69,7 +69,7 @@ export function FileUpload({
- + {props => ( @@ -93,7 +93,7 @@ export function FileUpload({ bg="white" onClick={() => handleDeleteFile(i)} /> - + ))} diff --git a/app/admin/add-barometer/page.tsx b/app/admin/add-barometer/page.tsx index 18a95437..2e56f98a 100644 --- a/app/admin/add-barometer/page.tsx +++ b/app/admin/add-barometer/page.tsx @@ -14,7 +14,7 @@ import { Dimensions } from './dimensions' import { type BarometerFormProps } from '../../types' import { createBarometer } from '@/utils/fetch' import { slug, getThumbnailBase64 } from '@/utils/misc' -import { googleStorageImagesFolder } from '@/utils/constants' +import { imageStorage } from '@/utils/constants' export default function AddCard() { const { condition, categories, manufacturers } = useBarometers() @@ -50,7 +50,7 @@ export default function AddCard() { url, order: i, name: values.name, - blurData: await getThumbnailBase64(googleStorageImagesFolder + url), + blurData: await getThumbnailBase64(imageStorage + url), })), ), slug: slug(values.name), diff --git a/app/api/v2/barometers/[slug]/deleteFromStorage.ts b/app/api/v2/barometers/[slug]/deleteFromStorage.ts index 79a6a15b..c9d7090c 100644 --- a/app/api/v2/barometers/[slug]/deleteFromStorage.ts +++ b/app/api/v2/barometers/[slug]/deleteFromStorage.ts @@ -1,17 +1,17 @@ import { Image } from '@prisma/client' -import bucket from '@/utils/googleStorage' +import { minioClient, minioBucket } from '@/utils/minio' /** - * Deletes selected Google Storage images + * Deletes selected images from storage */ -export async function deleteImagesFromGoogleStorage(images: Image[]) { +export async function deleteImagesFromStorage(images: Image[]) { await Promise.all( images.map(async image => { try { - await bucket.file(image.url).delete() + await minioClient.removeObject(minioBucket, image.url) } catch (error) { // don't throw error if image was not deleted - console.error(`Could not delete ${image.url} from Google Storage`) + console.error(`Could not delete ${image.url} from storage`) console.error(error) } }), diff --git a/app/api/v2/barometers/[slug]/route.ts b/app/api/v2/barometers/[slug]/route.ts index 40fd7fc1..fb572e6d 100644 --- a/app/api/v2/barometers/[slug]/route.ts +++ b/app/api/v2/barometers/[slug]/route.ts @@ -5,7 +5,7 @@ import { withPrisma } from '@/prisma/prismaClient' import { NotFoundError } from '@/app/errors' import { revalidateCategory } from '../revalidate' import { FrontRoutes } from '@/utils/routes-front' -import { deleteImagesFromGoogleStorage } from './deleteFromStorage' +import { deleteImagesFromStorage } from './deleteFromStorage' import { trimTrailingSlash } from '@/utils/misc' interface Props { @@ -63,7 +63,7 @@ export const DELETE = withPrisma(async (prisma, _req: NextRequest, { params: { s }, }) }) - await deleteImagesFromGoogleStorage(imagesBeforeDbUpdate) + await deleteImagesFromStorage(imagesBeforeDbUpdate) revalidatePath(FrontRoutes.Barometer + barometer.slug) revalidatePath(trimTrailingSlash(FrontRoutes.NewArrivals)) await revalidateCategory(prisma, barometer.categoryId) diff --git a/app/api/v2/barometers/revalidate.ts b/app/api/v2/barometers/revalidate.ts index 04dce1ad..eb0c3771 100644 --- a/app/api/v2/barometers/revalidate.ts +++ b/app/api/v2/barometers/revalidate.ts @@ -25,5 +25,7 @@ export async function revalidateCategory(prisma: PrismaClient, categoryId: strin (_, i) => `${FrontRoutes.Categories}${[categoryName, sort, String(i + 1)].join('/')}`, ), ) - await Promise.all(pathsToRevalidate.map(path => revalidatePath(path))) + for (const path of pathsToRevalidate) { + revalidatePath(path) + } } diff --git a/app/api/v2/report/route.ts b/app/api/v2/report/route.ts index 0020dacd..f245d2e5 100644 --- a/app/api/v2/report/route.ts +++ b/app/api/v2/report/route.ts @@ -1,4 +1,4 @@ -import { Redis } from '@upstash/redis' +import Redis from 'ioredis' import { NextRequest, NextResponse } from 'next/server' import { InaccuracyReport } from '@prisma/client' import { revalidatePath } from 'next/cache' @@ -12,7 +12,7 @@ import { FrontRoutes } from '@/utils/routes-front' const REPORT_COOL_DOWN = 10 const REPORT_MAX_ATTEMPTS = 3 -const redis = Redis.fromEnv() +const redis = new Redis(process.env.REDIS_URL!) /** * Fetches a paginated list of inaccuracy reports for barometers. diff --git a/app/api/v2/upload/images/route.ts b/app/api/v2/upload/images/route.ts index 3d661192..3327bb04 100644 --- a/app/api/v2/upload/images/route.ts +++ b/app/api/v2/upload/images/route.ts @@ -1,30 +1,21 @@ import { NextRequest, NextResponse } from 'next/server' import path from 'path' import { v4 as uuid } from 'uuid' -import { GetSignedUrlConfig } from '@google-cloud/storage' +import { minioClient, minioBucket } from '@/utils/minio' import { FileDto, UrlDto, UrlProps } from './types' -import bucket from '@/utils/googleStorage' export async function POST(req: NextRequest) { try { const { files }: FileDto = await req.json() const signedUrls = await Promise.all( - files.map>(async ({ fileName, contentType }) => { + files.map>(async ({ fileName }) => { // give unique names to files const extension = path.extname(fileName).toLowerCase() const newFileName = `gallery/${uuid()}${extension}` - const options: GetSignedUrlConfig = { - version: 'v4', - action: 'write', - expires: Date.now() + 15 * 60 * 1000, // 15 min - contentType, - } - const cloudFile = bucket.file(newFileName) - // generate signed URL for each file - const [signedUrl] = await cloudFile.getSignedUrl(options) + const signedUrl = await minioClient.presignedPutObject(minioBucket, newFileName) return { signed: signedUrl, - public: cloudFile.name, + public: newFileName, } }), ) @@ -47,9 +38,8 @@ export async function DELETE(req: NextRequest) { const fileName = searchParams.get('fileName') try { if (!fileName) return NextResponse.json({ message: 'File name is required' }, { status: 400 }) - // delete file from google cloud storage - const file = bucket.file(fileName) - await file.delete() + // delete file from Minio storage + await minioClient.removeObject(minioBucket, fileName) return NextResponse.json({ message: `${fileName} was deleted` }, { status: 200 }) } catch (error) { return NextResponse.json( diff --git a/app/brands/[slug]/page.tsx b/app/brands/[slug]/page.tsx index d164acbb..2aff20f9 100644 --- a/app/brands/[slug]/page.tsx +++ b/app/brands/[slug]/page.tsx @@ -8,7 +8,6 @@ import { title } from '@/app/metadata' import { BarometerCardWithIcon } from '@/app/components/barometer-card' import { FrontRoutes } from '@/utils/routes-front' import { MD } from '@/app/components/md' -import { googleStorageImagesFolder } from '@/utils/constants' import { ImageLightbox } from '@/app/components/modal' interface Props { @@ -55,7 +54,6 @@ export default async function Manufacturer({ params: { slug } }: Props) { const manufacturer = await getManufacturer(slug) const barometers = await getBarometersByManufacturer(slug) const fullName = `${manufacturer.firstName ?? ''} ${manufacturer.name}` - return ( @@ -67,11 +65,7 @@ export default async function Manufacturer({ params: { slug } }: Props) {
{manufacturer.images.map(image => ( - + ))}
{manufacturer.description} diff --git a/app/brands/page.tsx b/app/brands/page.tsx index a5d0b4de..67e8d408 100644 --- a/app/brands/page.tsx +++ b/app/brands/page.tsx @@ -6,8 +6,8 @@ import { IconCircleArrowUp } from '@tabler/icons-react' import { withPrisma } from '@/prisma/prismaClient' import { FrontRoutes } from '@/utils/routes-front' import { title } from '../metadata' -import { googleStorageImagesFolder } from '@/utils/constants' import { DynamicOptions } from '../types' +import customImageLoader from '@/utils/image-loader' export const dynamic: DynamicOptions = 'force-static' @@ -67,38 +67,43 @@ const BrandsOfCountry = ({ country, }: { country: Awaited>[number] -}) => ( -
- - {country.name} - +}) => { + const width = 32 + const quality = 75 + return ( +
+ + {country.name} + -
- {country.manufacturers.map(({ id, firstName, name, slug, image }) => ( - -
- {image ? ( - {name} - ) : ( - - )} -

- {name + (firstName ? `, ${firstName}` : '')} -

-
- - ))} +
+ {country.manufacturers.map(({ id, firstName, name, slug, image }) => ( + +
+ {image ? ( + {name} + ) : ( + + )} +

+ {name + (firstName ? `, ${firstName}` : '')} +

+
+ + ))} +
-
-) + ) +} export default async function Manufacturers() { const countries = await getBrandsByCountry() diff --git a/app/collection/categories/[...category]/page.tsx b/app/collection/categories/[...category]/page.tsx index 68fa828f..b660d455 100644 --- a/app/collection/categories/[...category]/page.tsx +++ b/app/collection/categories/[...category]/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next' import capitalize from 'lodash/capitalize' import { Container, Grid, GridCol, Stack, Title } from '@mantine/core' -import { googleStorageImagesFolder, BAROMETERS_PER_CATEGORY_PAGE } from '@/utils/constants' +import { imageStorage, BAROMETERS_PER_CATEGORY_PAGE } from '@/utils/constants' import { FrontRoutes } from '@/utils/routes-front' import { BarometerCard } from '@/app/components/barometer-card' import { SortValue, SortOptions, DynamicOptions } from '@/app/types' @@ -13,8 +13,7 @@ import { withPrisma } from '@/prisma/prismaClient' import { getCategory, getBarometersByParams } from '@/app/services' import { FooterVideo } from '@/app/components/footer-video' -// all non-generated posts will give 404 -export const dynamicParams = false +export const dynamicParams = true export const dynamic: DynamicOptions = 'force-static' interface CollectionProps { @@ -34,7 +33,7 @@ export async function generateMetadata({ const barometerImages = barometers .filter(({ images }) => images && images.length > 0) .map(({ images, name }) => ({ - url: googleStorageImagesFolder + images.at(0)!.url, + url: imageStorage + images.at(0)!.url, alt: name, })) const url = `${FrontRoutes.Categories}${category.join('/')}` @@ -76,7 +75,7 @@ export default async function Collection({ params: { category } }: CollectionPro {barometers.map(({ name, id, images, manufacturer, slug }, i) => ( - + {barometer.images.map((image, i) => ( - + {barometer.name} diff --git a/app/collection/items/[slug]/components/edit-fields/estimated-price-edit.tsx b/app/collection/items/[slug]/components/edit-fields/estimated-price-edit.tsx index d3f7d9b1..44976fa9 100644 --- a/app/collection/items/[slug]/components/edit-fields/estimated-price-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/estimated-price-edit.tsx @@ -34,7 +34,7 @@ export function EstimatedPriceEdit({ size = 18, barometer, ...props }: Props) { const form = useForm({ initialValues: { estimatedPrice: '' }, validate: { - estimatedPrice: value => (isDecimal(value) ? null : 'Wrong decimal number'), + estimatedPrice: (value: string) => (isDecimal(value) ? null : 'Wrong decimal number'), }, }) const [opened, { open, close }] = useDisclosure(false) diff --git a/app/collection/items/[slug]/components/edit-fields/images-edit.tsx b/app/collection/items/[slug]/components/edit-fields/images-edit.tsx index a7338e88..4d220e34 100644 --- a/app/collection/items/[slug]/components/edit-fields/images-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/images-edit.tsx @@ -26,7 +26,7 @@ import { import { useEffect, useMemo, useState } from 'react' import { useDisclosure } from '@mantine/hooks' import { BarometerDTO } from '@/app/types' -import { googleStorageImagesFolder } from '@/utils/constants' +import { imageStorage } from '@/utils/constants' import { FrontRoutes } from '@/utils/routes-front' import { showError, showInfo } from '@/utils/notification' import { createImageUrls, deleteImage, updateBarometer, uploadFileToCloud } from '@/utils/fetch' @@ -78,7 +78,7 @@ function SortableImage({ className="h-auto w-auto" alt="Barometer" key={image} - src={googleStorageImagesFolder + image} + src={imageStorage + image} width={100} height={200} quality={50} @@ -137,7 +137,7 @@ export function ImagesEdit({ barometer, size, ...props }: ImagesEditProps) { id: barometer.id, images: await Promise.all( values.images.map(async (url, i) => { - const blurData = await getThumbnailBase64(googleStorageImagesFolder + url) + const blurData = await getThumbnailBase64(imageStorage + url) return { url, order: i, @@ -162,7 +162,7 @@ export function ImagesEdit({ barometer, size, ...props }: ImagesEditProps) { } } /** - * Upload images to google storage + * Upload images to storage */ const uploadImages = async (files: File[]) => { if (!files || !Array.isArray(files) || files.length === 0) return diff --git a/app/collection/items/[slug]/components/edit-fields/manufacturer-edit.tsx b/app/collection/items/[slug]/components/edit-fields/manufacturer-edit.tsx index d1beef98..be1411a4 100644 --- a/app/collection/items/[slug]/components/edit-fields/manufacturer-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/manufacturer-edit.tsx @@ -29,7 +29,7 @@ import { deleteImage, updateBarometer, updateManufacturer } from '@/utils/fetch' import { ManufacturerImageEdit } from './manufacturer-image-edit' import { type ManufacturerForm } from './types' import { getThumbnailBase64 } from '@/utils/misc' -import { googleStorageImagesFolder } from '@/utils/constants' +import { imageStorage } from '@/utils/constants' interface ManufacturerEditProps extends UnstyledButtonProps { size?: string | number | undefined @@ -149,7 +149,7 @@ export function ManufacturerEdit({ size = 18, barometer, ...props }: Manufacture countries: formValues.countries.map(id => ({ id })), images: await Promise.all( formValues.images.map(async (url, i) => { - const blurData = await getThumbnailBase64(googleStorageImagesFolder + url) + const blurData = await getThumbnailBase64(imageStorage + url) return { url, order: i, diff --git a/app/collection/items/[slug]/components/edit-fields/manufacturer-image-edit.tsx b/app/collection/items/[slug]/components/edit-fields/manufacturer-image-edit.tsx index b103a2e3..945ea67b 100644 --- a/app/collection/items/[slug]/components/edit-fields/manufacturer-image-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/manufacturer-image-edit.tsx @@ -16,7 +16,6 @@ import { UseFormReturnType } from '@mantine/form' import { createImageUrls, deleteImage, uploadFileToCloud } from '@/utils/fetch' import { ManufacturerForm } from './types' import { showError } from '@/utils/notification' -import { googleStorageImagesFolder } from '@/utils/constants' interface Props { imageUrls: string[] @@ -62,7 +61,7 @@ function SortableImage({ className="h-auto w-auto" alt="Barometer" key={image} - src={googleStorageImagesFolder + image} + src={image} width={100} height={200} /> @@ -73,7 +72,7 @@ function SortableImage({ export function ManufacturerImageEdit({ imageUrls, form, setLoading }: Props) { /** - * Upload images to google storage + * Upload images to storage */ const uploadImages = useCallback(async (files: File[]) => { if (!files || !Array.isArray(files) || files.length === 0) return diff --git a/app/collection/items/[slug]/components/inaccuracy-report.tsx b/app/collection/items/[slug]/components/inaccuracy-report.tsx index b27a046c..7c7f033c 100644 --- a/app/collection/items/[slug]/components/inaccuracy-report.tsx +++ b/app/collection/items/[slug]/components/inaccuracy-report.tsx @@ -36,10 +36,10 @@ export function InaccuracyReport({ barometer, ...props }: Props) { description: '', }, validate: { - reporterEmail: value => (!isEmail(value) ? 'Invalid email' : null), - reporterName: value => + reporterEmail: (value: string) => (!isEmail(value) ? 'Invalid email' : null), + reporterName: (value: string) => !isLength(value, { min: 2, max: 50 }) ? 'Value must be between 2 and 50 characters' : null, - description: value => + description: (value: string) => !isLength(value, { min: 5, max: maxFeedbackLen }) ? `Value must be between 5 and ${maxFeedbackLen} characters` : null, diff --git a/app/collection/items/[slug]/layout.tsx b/app/collection/items/[slug]/layout.tsx index ea618f2e..4f665b75 100644 --- a/app/collection/items/[slug]/layout.tsx +++ b/app/collection/items/[slug]/layout.tsx @@ -2,7 +2,7 @@ import { Metadata } from 'next/types' import { PropsWithChildren } from 'react' import capitalize from 'lodash/capitalize' import { getBarometer } from '@/app/services' -import { googleStorageImagesFolder } from '@/utils/constants' +import { imageStorage } from '@/utils/constants' import { title, openGraph, twitter } from '@/app/metadata' import { FrontRoutes } from '@/utils/routes-front' @@ -16,7 +16,7 @@ export async function generateMetadata({ const barometerImages = images && images.slice(0, 1).map(image => ({ - url: googleStorageImagesFolder + image.url, + url: imageStorage + image.url, alt: name, })) const url = FrontRoutes.Barometer + slug diff --git a/app/collection/items/[slug]/page.tsx b/app/collection/items/[slug]/page.tsx index 36a5637f..6c43524f 100644 --- a/app/collection/items/[slug]/page.tsx +++ b/app/collection/items/[slug]/page.tsx @@ -50,6 +50,7 @@ import { SubcategoryEdit } from './components/edit-fields/subcategory-edit' import { MaterialsEdit } from './components/edit-fields/materials-edit' export const dynamic = 'force-static' +export const dynamicParams = true interface Props { params: { diff --git a/app/collection/new-arrivals/page.tsx b/app/collection/new-arrivals/page.tsx index c292a35c..24b44891 100644 --- a/app/collection/new-arrivals/page.tsx +++ b/app/collection/new-arrivals/page.tsx @@ -17,7 +17,6 @@ export default async function NewArrivals({ searchParams }: newArrivalsProps) { page: searchParams.page ?? 1, size: searchParams.size ?? itemsOnPage, }) - return ( diff --git a/app/components/barometer-card/barometer-card.tsx b/app/components/barometer-card/barometer-card.tsx index b67f8e29..94648189 100644 --- a/app/components/barometer-card/barometer-card.tsx +++ b/app/components/barometer-card/barometer-card.tsx @@ -1,9 +1,8 @@ -import { Box, Text, Anchor, BoxProps } from '@mantine/core' -import Link from 'next/link' +import { Box, Anchor, BoxProps } from '@mantine/core' import NextImage from 'next/image' -import styles from './styles.module.scss' +import Link from 'next/link' +import customImageLoader from '@/utils/image-loader' import { BarometerListDTO } from '@/app/types' -import { googleStorageImagesFolder } from '@/utils/constants' interface BarometerCardProps extends BoxProps { image?: BarometerListDTO['barometers'][number]['images'][number] @@ -23,32 +22,30 @@ export async function BarometerCard({ }: BarometerCardProps) { return ( - - + +
{image ? ( ) : ( - No image +

No image

)} - - +
+

{name} - +

{manufacturer && manufacturer.toLowerCase() !== 'unknown' && ( - +

{manufacturer} - +

)}
diff --git a/app/components/barometer-card/styles.module.scss b/app/components/barometer-card/styles.module.scss deleted file mode 100644 index 496f6033..00000000 --- a/app/components/barometer-card/styles.module.scss +++ /dev/null @@ -1,17 +0,0 @@ -.bg_gradient { - height: 15rem; - width: 100%; - background-image: linear-gradient(180deg, #fbfbfb, #efefef); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - border-radius: 3px; - position: relative; - justify-content: flex-end; -} -.anchor { - display: block; - & > p, & span { - color: inherit !important; - } -} diff --git a/app/components/category-card/category-card.tsx b/app/components/category-card/category-card.tsx index e7790d78..e0519da2 100644 --- a/app/components/category-card/category-card.tsx +++ b/app/components/category-card/category-card.tsx @@ -5,7 +5,7 @@ import NextImage from 'next/image' import { FC } from 'react' import { CategoryDTO } from '@/app/types' import { CategoryIcon } from '../category-icon' -import { googleStorageImagesFolder } from '@/utils/constants' +import customImageLoader from '@/utils/image-loader' interface CategoryCardProps { name: string @@ -32,12 +32,11 @@ export const CategoryCard: FC = ({ name, link, image, priorit /> {image && ( - + ) } diff --git a/app/components/header/header.tsx b/app/components/header/header.tsx index fcaffaf3..9dbeecb3 100644 --- a/app/components/header/header.tsx +++ b/app/components/header/header.tsx @@ -4,6 +4,7 @@ import { Group, Anchor, Title, Container, Box, Flex, Tooltip } from '@mantine/co import { IconSearch } from '@tabler/icons-react' import { Navigation } from './navigation' import { getCategories } from '@/app/services' +import customImageLoader from '@/utils/image-loader' // server component @@ -34,10 +35,13 @@ export async function Header() { {/* Logo image */} diff --git a/app/components/heading-image/heading-image.tsx b/app/components/heading-image/heading-image.tsx index 85d0feca..a9ece5bb 100644 --- a/app/components/heading-image/heading-image.tsx +++ b/app/components/heading-image/heading-image.tsx @@ -2,20 +2,18 @@ import { Box, Title, Container } from '@mantine/core' import NextImage from 'next/image' import { FC } from 'react' import styles from './heading-image.module.scss' -import headingImage from '@/public/images/landing-header.png' +import customImageLoader from '@/utils/image-loader' export const HeadingImage: FC = () => { return ( diff --git a/app/components/modal/image-lightbox.tsx b/app/components/modal/image-lightbox.tsx index 34c1f058..ee5eb04c 100644 --- a/app/components/modal/image-lightbox.tsx +++ b/app/components/modal/image-lightbox.tsx @@ -3,6 +3,7 @@ import NextImage from 'next/image' import { useDisclosure } from '@mantine/hooks' import { ZoomModal } from './zoom-modal' +import customImageLoader from '@/utils/image-loader' interface ImageLightboxProps { src: string @@ -16,9 +17,10 @@ export function ImageLightbox({ src, name }: ImageLightboxProps) { return ( <> {({ onLoad }) => ( { - if (!mounted) return undefined - document.body.style.overflow = isOpened ? 'hidden' : '' - return () => { - document.body.style.overflow = '' - } - }, [isOpened, mounted]) + useScrollLock(isOpened) if (!mounted) return null diff --git a/app/history/page.tsx b/app/history/page.tsx index bbb66a08..13974e6c 100644 --- a/app/history/page.tsx +++ b/app/history/page.tsx @@ -1,8 +1,8 @@ import { Container, Title, Text, Box, Center, Paper, Image } from '@mantine/core' import NextImage from 'next/image' import sx from './styles.module.scss' -import { googleStorageImagesFolder } from '@/utils/constants' import { ShowMore } from '../components/showmore' +import customImageLoader from '@/utils/image-loader' export const dynamic = 'force-static' @@ -10,11 +10,12 @@ function Figure({ src }: { src: string }) { return (
Figure
diff --git a/app/hooks/useScrollLock.ts b/app/hooks/useScrollLock.ts new file mode 100644 index 00000000..fe486532 --- /dev/null +++ b/app/hooks/useScrollLock.ts @@ -0,0 +1,37 @@ +import { useEffect } from 'react' + +let scrollLockCount = 0 + +/** + * Disables page scrolling while the provided condition is true. + * + * This hook is typically used to prevent background scrolling + * when modals, sidebars, or other overlay components are visible. + * + * Scrolling is automatically re-enabled when the condition becomes false + * or when the component using the hook is unmounted. + * + * @param lock - A boolean indicating whether scrolling should be disabled. + */ +export function useScrollLock(lock: boolean) { + useEffect(() => { + if (typeof document === 'undefined') return undefined + + if (lock) { + scrollLockCount += 1 + if (scrollLockCount === 1) { + document.body.style.overflow = 'hidden' + } + } + + return () => { + if (lock) { + scrollLockCount -= 1 + if (scrollLockCount <= 0) { + document.body.style.overflow = '' + scrollLockCount = 0 + } + } + } + }, [lock]) +} diff --git a/app/layout.tsx b/app/layout.tsx index fe44d8fe..9600e598 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,4 @@ import { Viewport } from 'next' -import { Analytics as VercelAnalytics } from '@vercel/analytics/react' -import { SpeedInsights } from '@vercel/speed-insights/next' import { GoogleAnalytics } from '@next/third-parties/google' import { ColorSchemeScript, Box, Stack } from '@mantine/core' import { Notifications } from '@mantine/notifications' @@ -48,7 +46,7 @@ export const generateMetadata = withPrisma(async prisma => { export default function RootLayout({ children }: { children: any }) { return ( - + @@ -73,8 +71,6 @@ export default function RootLayout({ children }: { children: any }) {