Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c996eba
fix(about): update collection item count from 100 to 150
cybervoid0 Apr 20, 2025
d4067a7
Refactor database connection and update Prisma adapter
cybervoid0 Apr 24, 2025
a7cfec1
fix: restore prisma schema
cybervoid0 Apr 24, 2025
05ebf28
fix: restore db binary dump
cybervoid0 Apr 24, 2025
78c6cf5
refactor: replace Upstash Redis with ioredis and remove unused analyt…
cybervoid0 Apr 24, 2025
2ead9c3
fix: add logging for Redis URL during initialization
cybervoid0 Apr 24, 2025
7545d7c
refactor: improve error handling in GET and POST routes for inaccurac…
cybervoid0 Apr 24, 2025
40b2bd3
fix: update build script to include prisma generate
cybervoid0 Apr 24, 2025
25458de
fix: update Google Storage URL to new S3 endpoint and add file proces…
cybervoid0 Apr 25, 2025
6322f3d
feat: custom image loader
cybervoid0 Apr 25, 2025
adf1158
refactor: update image and video sources to use minio storage
cybervoid0 Apr 26, 2025
05a0da1
fix: update image sizes for better responsiveness in various components
cybervoid0 Apr 30, 2025
3874ae3
feat: add warmImages function to preload image URLs for optimization
cybervoid0 Apr 30, 2025
ee47a6d
feat: add p-limit dependency and implement concurrency control in war…
cybervoid0 Apr 30, 2025
a36005b
feat: integrate warmImages function for preloading images across vari…
cybervoid0 Apr 30, 2025
ce76e69
fix: log response status when caching images in warmImages function
cybervoid0 Apr 30, 2025
8a3a17a
docs: add instructions to clear image cache in README
cybervoid0 May 1, 2025
a1b7b98
refactor: relative paths in images
cybervoid0 May 1, 2025
358fd79
fix: ensure query parameters are correctly formatted and log response…
cybervoid0 May 1, 2025
40c03f6
refactor: replace warmImages function with markForWarming for improve…
cybervoid0 May 2, 2025
1a4c37d
Add Nixpack configuration for Bun setup, installation, and build phases
cybervoid0 May 2, 2025
0633474
build: replace bun with npm
cybervoid0 May 18, 2025
e1ee064
docs: add detailed instructions for restoring PostgreSQL database fro…
cybervoid0 May 18, 2025
d86f4ad
fix: optimize warmImages function for improved image caching
cybervoid0 May 18, 2025
df3d576
doc: update PostgreSQL database restore instructions for clarity and …
cybervoid0 May 18, 2025
d0feae2
fix: update warm script to include build step for warm-images
cybervoid0 May 18, 2025
b1c0fc2
fix: update build:warm script to use tsconfig.build.json for TypeScri…
cybervoid0 May 18, 2025
a4aa119
fix: update build:warm script to use esbuild for TypeScript compilation
cybervoid0 May 18, 2025
d878881
fix: update package.json and package-lock.json to include esbuild ver…
cybervoid0 May 18, 2025
0b276c3
fix: update build:warm script to remove target specification for esbuild
cybervoid0 May 18, 2025
16b6dbb
fix: update warm script to use CommonJS format for warm-images output
cybervoid0 May 18, 2025
ed37d1c
fix: update import statements to use .js extension for consistency
cybervoid0 May 18, 2025
406380e
fix: update import statements to use .ts extension for consistency
cybervoid0 May 18, 2025
06d5f8e
feat: warm-up page
cybervoid0 May 18, 2025
d2ca4cc
fix: remove warm-up page component
cybervoid0 May 18, 2025
e5c9a71
refactor: migrate from Google Cloud Storage to MinIO for image storage
cybervoid0 May 18, 2025
e6d3436
refactor: replace image loading logic with customImageLoader for opti…
cybervoid0 May 19, 2025
33f23c0
refactor: update image warming parameters for optimization across var…
cybervoid0 May 19, 2025
1b9bce9
refactor: update warmImages function to use customImageLoader for imp…
cybervoid0 May 19, 2025
0e104e4
refactor: remove cache control in warm function
cybervoid0 May 19, 2025
2e2600b
refactor: remove empty line in README.md
cybervoid0 May 19, 2025
02a6186
refactor: add dynamicParams export for static generation
cybervoid0 May 19, 2025
ac7c41e
refactor: add logging for revalidation process in PUT handler
cybervoid0 May 19, 2025
aa81904
refactor: add logging for category revalidation in PUT handler
cybervoid0 May 19, 2025
90bc291
refactor: enhance logging and revalidation process in category revali…
cybervoid0 May 19, 2025
56ed069
refactor: enable dynamicParams for category pages
cybervoid0 May 19, 2025
925ed62
refactor: remove debug logging from revalidateCategory function
cybervoid0 May 19, 2025
13279ee
fix: add missing newline at end of README.md
cybervoid0 May 19, 2025
6aa2528
refactor: increase image quality in BarometerCard component
cybervoid0 May 20, 2025
0176f24
refactor: remove image warming functionality
cybervoid0 May 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
```


10 changes: 3 additions & 7 deletions app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
Expand Down Expand Up @@ -73,7 +70,7 @@ export default function About() {
</Anchor>
</div>
<Text className={styles.paragraph}>
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,
Expand Down Expand Up @@ -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"
/>
</Flex>
<Divider my="md" mx={{ base: 'sm', sm: 'xl' }} />
Expand Down
8 changes: 4 additions & 4 deletions app/admin/add-barometer/file-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -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)
Expand Down Expand Up @@ -69,7 +69,7 @@ export function FileUpload({
<Fieldset m={0} mt="0.2rem" p="sm" pt="0.3rem" legend="Images">
<Stack gap="xs" align="flex-start">
<Group w="100%" justify="space-between">
<FileButton onChange={googleUploadImages} accept="image/*" multiple>
<FileButton onChange={uploadImages} accept="image/*" multiple>
{props => (
<Tooltip color="dark.3" withArrow label="Add image">
<ActionIcon loading={isUploading} variant="default" {...props}>
Expand All @@ -93,7 +93,7 @@ export function FileUpload({
bg="white"
onClick={() => handleDeleteFile(i)}
/>
<Image h="3rem" w="3rem" src={googleStorageImagesFolder + fileName} />
<Image h="3rem" w="3rem" src={imageStorage + fileName} />
</Paper>
))}
</Group>
Expand Down
4 changes: 2 additions & 2 deletions app/admin/add-barometer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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),
Expand Down
10 changes: 5 additions & 5 deletions app/api/v2/barometers/[slug]/deleteFromStorage.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}),
Expand Down
4 changes: 2 additions & 2 deletions app/api/v2/barometers/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion app/api/v2/barometers/revalidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
4 changes: 2 additions & 2 deletions app/api/v2/report/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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.
Expand Down
22 changes: 6 additions & 16 deletions app/api/v2/upload/images/route.ts
Original file line number Diff line number Diff line change
@@ -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<Promise<UrlProps>>(async ({ fileName, contentType }) => {
files.map<Promise<UrlProps>>(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,
}
}),
)
Expand All @@ -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(
Expand Down
8 changes: 1 addition & 7 deletions app/brands/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 (
<Container size="xl">
<Box className="mb-4">
Expand All @@ -67,11 +65,7 @@ export default async function Manufacturer({ params: { slug } }: Props) {
</Box>
<div className="my-8 flex flex-col items-center gap-8 sm:flex-row">
{manufacturer.images.map(image => (
<ImageLightbox
src={googleStorageImagesFolder + image.url}
name={image.name}
key={image.id}
/>
<ImageLightbox src={image.url} name={image.name} key={image.id} />
))}
</div>
<MD className="my-8">{manufacturer.description}</MD>
Expand Down
67 changes: 36 additions & 31 deletions app/brands/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -67,38 +67,43 @@ const BrandsOfCountry = ({
country,
}: {
country: Awaited<ReturnType<typeof getBrandsByCountry>>[number]
}) => (
<div className="mb-5 mr-4">
<Title order={3} className="!mb-5 border-b border-solid border-neutral-400 px-5 py-[0.1rem]">
{country.name}
</Title>
}) => {
const width = 32
const quality = 75
return (
<div className="mb-5 mr-4">
<Title order={3} className="!mb-5 border-b border-solid border-neutral-400 px-5 py-[0.1rem]">
{country.name}
</Title>

<div className="flex flex-col gap-4">
{country.manufacturers.map(({ id, firstName, name, slug, image }) => (
<Link className="w-fit" key={id} href={FrontRoutes.Brands + slug}>
<div className="flex flex-nowrap items-center gap-3">
{image ? (
<Image
height={32}
width={32}
alt={name}
src={googleStorageImagesFolder + image.url}
blurDataURL={image.blurData}
className="h-8 w-8 object-contain"
sizes="32px"
/>
) : (
<IconCircleArrowUp size={32} />
)}
<p className="w-fit font-medium capitalize">
{name + (firstName ? `, ${firstName}` : '')}
</p>
</div>
</Link>
))}
<div className="flex flex-col gap-4">
{country.manufacturers.map(({ id, firstName, name, slug, image }) => (
<Link className="w-fit" key={id} href={FrontRoutes.Brands + slug}>
<div className="flex flex-nowrap items-center gap-3">
{image ? (
<Image
unoptimized
width={width}
height={width}
alt={name}
loading="lazy"
src={customImageLoader({ src: image.url, width, quality })}
blurDataURL={image.blurData}
className="h-8 w-8 object-contain"
/>
) : (
<IconCircleArrowUp size={32} />
)}
<p className="w-fit font-medium capitalize">
{name + (firstName ? `, ${firstName}` : '')}
</p>
</div>
</Link>
))}
</div>
</div>
</div>
)
)
}

export default async function Manufacturers() {
const countries = await getBrandsByCountry()
Expand Down
9 changes: 4 additions & 5 deletions app/collection/categories/[...category]/page.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 {
Expand All @@ -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('/')}`
Expand Down Expand Up @@ -76,7 +75,7 @@ export default async function Collection({ params: { category } }: CollectionPro
<Sort sortBy={sort as SortValue} style={{ alignSelf: 'flex-end' }} />
<Grid justify="center" gutter="xl">
{barometers.map(({ name, id, images, manufacturer, slug }, i) => (
<GridCol span={{ base: 6, xs: 3, lg: 3 }} key={id}>
<GridCol span={{ base: 6, md: 4, lg: 3 }} key={id}>
<BarometerCard
priority={i < 5}
image={images[0]}
Expand Down
Loading
Loading