Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ module.exports = {
'mantine',
'plugin:@next/next/recommended',
'plugin:jest/recommended',
'plugin:prettier/recommended',
'plugin:@tanstack/eslint-plugin-query/recommended',
'plugin:react-hooks/recommended',
'plugin:prettier/recommended',
],
plugins: ['testing-library', 'jest'],
overrides: [
Expand Down
134 changes: 80 additions & 54 deletions app/api/barometers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,101 @@ import Barometer, { IBarometer } from '@/models/barometer'
import BarometerType from '@/models/type'
import '@/models/condition'
import Manufacturer from '@/models/manufacturer'
import { cleanObject, slug as slugify, parseDate } from '@/utils/misc'
import { cleanObject, slug as slugify } from '@/utils/misc'
import { SortValue } from '@/app/collection/types/[type]/types'
import { DEFAULT_PAGE_SIZE, type PaginationDTO } from '../types'

// dependencies to include in resulting barometers array
const deps = ['type', 'condition', 'manufacturer']

/**
* Sort barometer list by one of the parameters: manufacturer, name, date or catalogue no.
*/
function sortBarometers(barometers: IBarometer[], sortBy: SortValue | null): IBarometer[] {
return barometers.toSorted((a, b) => {
switch (sortBy) {
case 'manufacturer':
return (a.manufacturer?.name ?? '').localeCompare(b.manufacturer?.name ?? '')
case 'name':
return a.name.localeCompare(b.name)
case 'date': {
if (!a.dating || !b.dating) return 0
const yearA = parseDate(a.dating)?.[0]
const yearB = parseDate(b.dating)?.[0]
if (!yearA || !yearB) return 0
const dateA = new Date(yearA, 0, 1).getTime()
const dateB = new Date(yearB, 0, 1).getTime()
return dateA - dateB
}
case 'cat-no':
return a.collectionId.localeCompare(b.collectionId)
default:
return 0
}
})
}

/**
* Search barometers matching a query
*/
async function searchBarometers(query: string) {
const quotedQuery = `"${query}"`
const barometers = await Barometer.find({ $text: { $search: quotedQuery } }).populate(deps)
return NextResponse.json(barometers, { status: 200 })
function getSortCriteria(sortBy: SortValue | null): Record<string, 1 | -1> {
switch (sortBy) {
case 'manufacturer':
return { 'manufacturer.name': 1 }
case 'name':
return { name: 1 }
case 'date':
return { 'dating.year': 1 }
case 'cat-no':
return { collectionId: 1 }
default:
return { name: 1 }
}
}

/**
* Find a list of barometers of a certain type
*/
async function getBarometersByType(typeName: string, limit: number, sortBy: SortValue | null) {
async function getBarometersByType(
typeName: string,
page: number,
pageSize: number,
sortBy: SortValue | null,
) {
// perform case-insensitive compare with the stored types
const barometerType = await BarometerType.findOne({
name: { $regex: new RegExp(`^${typeName}$`, 'i') },
})
if (!barometerType) return NextResponse.json([], { status: 404 })

// if existing barometer type match the `type` param, return all corresponding barometers
const barometers = sortBarometers(
await Barometer.find({ type: barometerType._id })
.limit(limit)
.populate(['type', 'condition', 'manufacturer']),
sortBy,
const skip = (page - 1) * pageSize
const sortCriteria = getSortCriteria(sortBy)

const barometers = await Barometer.aggregate([
{
$match: { type: barometerType._id },
},
{
$lookup: {
from: 'manufacturers',
localField: 'manufacturer',
foreignField: '_id',
as: 'manufacturer',
},
},
{ $unwind: { path: '$manufacturer', preserveNullAndEmptyArrays: true } },
{
$lookup: {
from: 'barometerTypes',
localField: 'type',
foreignField: '_id',
as: 'type',
},
},
{ $unwind: { path: '$type', preserveNullAndEmptyArrays: true } },
{
$lookup: {
from: 'barometerConditions',
localField: 'condition',
foreignField: '_id',
as: 'condition',
},
},
{ $unwind: { path: '$condition', preserveNullAndEmptyArrays: true } },
{
$sort: sortCriteria,
},
{ $skip: skip },
{ $limit: pageSize },
])

const totalItems = await Barometer.countDocuments({ type: barometerType._id })

return NextResponse.json<PaginationDTO>(
{
barometers,
page,
totalItems,
pageSize,
totalPages: Math.ceil(totalItems / pageSize),
},
{ status: barometers.length > 0 ? 200 : 404 },
)
return NextResponse.json(barometers, { status: barometers.length > 0 ? 200 : 404 })
}

/**
* Find all barometers
* List all barometers
*/
async function getAllBarometers(limit: number, sortBy: SortValue | null) {
const barometers = sortBarometers(await Barometer.find().limit(limit).populate(deps), sortBy)
async function getAllBarometers() {
const barometers = await Barometer.find().populate(['type', 'condition', 'manufacturer'])
return NextResponse.json(barometers, { status: 200 })
}

Expand All @@ -86,14 +114,12 @@ export async function GET(req: NextRequest) {
const { searchParams } = req.nextUrl
const typeName = searchParams.get('type')
const sortBy = searchParams.get('sort') as SortValue | null
const limit = Number(searchParams.get('limit')) ?? 0
const query = searchParams.get('q')
// query param was received: ?q=aneroid%20barometer
if (query) return await searchBarometers(query)
const size = Math.max(Number(searchParams.get('size')) || DEFAULT_PAGE_SIZE, 1)
const page = Math.max(Number(searchParams.get('page')) || 1, 1)
// if `type` search param was not passed return all barometers list
if (!typeName || !typeName.trim()) return await getAllBarometers(limit, sortBy)
if (!typeName || !typeName.trim()) return await getAllBarometers()
// type was passed
return await getBarometersByType(typeName, limit, sortBy)
return await getBarometersByType(typeName, page, size, sortBy)
} catch (error) {
return NextResponse.json(
{ message: error instanceof Error ? error.message : 'Could not retrieve barometer list' },
Expand Down
59 changes: 59 additions & 0 deletions app/api/barometers/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server'
import Barometer from '@/models/barometer'
import { DEFAULT_PAGE_SIZE, type PaginationDTO } from '../../types'
import { connectMongoose } from '@/utils/mongoose'

// dependencies to include in resulting barometers array
const deps = ['type', 'condition', 'manufacturer']

/**
* Search barometers matching a query
*/
async function searchBarometers(query: string, page: number, pageSize: number) {
const quotedQuery = `"${query.trim()}"`
const skip = (page - 1) * pageSize
const [barometers, totalItems] = await Promise.all([
Barometer.find({ $text: { $search: quotedQuery } })
.populate(deps)
.skip(skip)
.limit(pageSize),
Barometer.countDocuments({ $text: { $search: quotedQuery } }),
])
return NextResponse.json<PaginationDTO>(
{
barometers,
totalItems,
page,
totalPages: Math.ceil(totalItems / pageSize),
pageSize,
},
{ status: 200 },
)
}

export async function GET(req: NextRequest) {
try {
await connectMongoose()
const { searchParams } = req.nextUrl
const query = searchParams.get('q')
const pageSize = Math.max(Number(searchParams.get('pageSize')) || DEFAULT_PAGE_SIZE, 1)
const page = Math.max(Number(searchParams.get('page')) || 1, 1)
if (!query)
return NextResponse.json<PaginationDTO>(
{
barometers: [],
page: 0,
pageSize,
totalItems: 0,
totalPages: 0,
},
{ status: 200 },
)
return await searchBarometers(query, page, pageSize)
} catch (error) {
return NextResponse.json(
{ message: error instanceof Error ? error.message : 'Could not execute the query' },
{ status: 500 },
)
}
}
12 changes: 12 additions & 0 deletions app/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IBarometer } from '@/models/barometer'

export interface PaginationDTO {
barometers: IBarometer[]
totalItems: number
totalPages: number
page: number
pageSize: number
}

// pagination page size
export const DEFAULT_PAGE_SIZE = 12
2 changes: 1 addition & 1 deletion app/collection/items/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function generateMetadata({
* to be used as static parameters for Next.js static generation.
*/
export async function generateStaticParams(): Promise<Slug[]> {
const barometers = await fetchBarometers()
const { barometers } = await fetchBarometers()
return barometers.map(({ slug, name }) => ({
slug: slug ?? slugify(name),
}))
Expand Down
23 changes: 17 additions & 6 deletions app/collection/types/[type]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,25 @@ import Sort from './sort'
import { fetchBarometers, fetchTypes } from '@/utils/fetch'
import { DescriptionText } from '@/app/components/description-text'
import { title, openGraph, twitter } from '@/app/metadata'
import { Pagination } from '@/app/components/pagination'

interface CollectionProps {
params: {
type: string
}
searchParams: {
sort?: SortValue
page: string
}
}

const PAGE_SIZE = '12'

export async function generateMetadata({ params: { type } }: CollectionProps): Promise<Metadata> {
const { description } = await fetchTypes({ type })
const barometersOfType = await fetchBarometers({ type, limit: '5' })
const barometersOfType = await fetchBarometers({ type, size: '5', page: '1' })
const collectionTitle = `${title}: ${capitalize(type)} Barometers Collection`
const barometerImages = barometersOfType
const barometerImages = barometersOfType.barometers
.filter(({ images }) => images && images.length > 0)
.map(({ images, name }) => ({
url: googleStorageImagesFolder + images!.at(0),
Expand All @@ -50,8 +54,14 @@ export async function generateMetadata({ params: { type } }: CollectionProps): P
}

export default async function Collection({ params: { type }, searchParams }: CollectionProps) {
const sortBy = searchParams.sort ?? 'date'
const barometersOfType = await fetchBarometers({ type, sort: sortBy })
const sort = searchParams.sort ?? 'date'
const page = searchParams.page ?? 1
const { barometers, totalPages } = await fetchBarometers({
type,
sort,
pageSize: PAGE_SIZE,
page,
})
// selected barometer type details
const { description } = await fetchTypes({ type })
return (
Expand All @@ -61,9 +71,9 @@ export default async function Collection({ params: { type }, searchParams }: Col
{type}
</Title>
{description && <DescriptionText size="sm" description={description} />}
<Sort sortBy={sortBy} style={{ alignSelf: 'flex-end' }} />
<Sort sortBy={sort} style={{ alignSelf: 'flex-end' }} />
<Grid justify="center" gutter="xl">
{barometersOfType.map(({ name, _id, images, manufacturer }, i) => (
{barometers.map(({ name, _id, images, manufacturer }, i) => (
<GridCol span={{ base: 6, xs: 3, lg: 3 }} key={String(_id)}>
<BarometerCard
priority={i < 8}
Expand All @@ -75,6 +85,7 @@ export default async function Collection({ params: { type }, searchParams }: Col
</GridCol>
))}
</Grid>
{totalPages > 1 && <Pagination total={totalPages} value={+page} />}
</Stack>
</Container>
)
Expand Down
7 changes: 6 additions & 1 deletion app/components/header/menudata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ export const menuData: MenuItem[] = [
},
{
id: 2,
label: 'Search',
link: 'search',
},
{
id: 3,
label: 'History',
link: 'history',
},
{
id: 3,
id: 4,
label: 'About',
link: 'about',
},
Expand Down
1 change: 1 addition & 0 deletions app/components/pagination/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './pagination'
31 changes: 31 additions & 0 deletions app/components/pagination/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client'

import { Pagination as MantinePagination, PaginationProps } from '@mantine/core'
import { useRouter, useSearchParams, usePathname } from 'next/navigation'

export function Pagination(props: PaginationProps) {
const router = useRouter()
const searchParams = useSearchParams()
const pathname = usePathname()
const updateQueryParams = (key: string, value: string | number) => {
const params = new URLSearchParams(searchParams?.toString() || '')
params.set(key, String(value))
return params.toString()
}
const handlePageChange = (newPage: number) => {
const updatedQuery = updateQueryParams('page', newPage)
router.push(`${pathname}?${updatedQuery}`)
}
return (
<MantinePagination
mt="lg"
style={{
alignSelf: 'center',
}}
c="dark"
color="dark"
onChange={handlePageChange}
{...props}
/>
)
}
Loading
Loading