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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,5 @@ dist
public/uploads
.vercel

# webstorrm
.idea
18 changes: 18 additions & 0 deletions actions/barometer-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use server'

import { connectMongoose } from '@/utils/mongoose'
import BarometerType, { IBarometerType } from '@/models/type'

export async function getType(type: string): Promise<IBarometerType> {
await connectMongoose()
const barometerType = await BarometerType.findOne({ name: { $regex: type, $options: 'i' } })
if (!barometerType) throw new Error('Unknown barometer type')
return { ...barometerType.toObject(), _id: String(barometerType._id) }
}

export async function listTypes(): Promise<IBarometerType[]> {
await connectMongoose()
return (await BarometerType.find().sort({ order: 1 })).map(res =>
res.toObject({ flattenObjectIds: true }),
)
}
104 changes: 104 additions & 0 deletions actions/barometers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use server'

import { revalidatePath } from 'next/cache'
import { connectMongoose } from '@/utils/mongoose'
import Barometer, { IBarometer } from '@/models/barometer'
import { cleanObject, slug as slugify, parseDate } from '@/utils/misc'
import { SortValue } from '@/app/collection/types/[type]/types'
import BarometerType from '@/models/type'
import Manufacturer from '@/models/manufacturer'
import '@/models/condition'

/**
* Related tables to include in database response
*/
const dependencies = ['type', 'condition', 'manufacturer']

export async function getBarometer(slug: string): Promise<IBarometer> {
await connectMongoose()
const barometer = await Barometer.findOne({ slug }).populate(dependencies)
if (!barometer) throw new Error('Unknown slug')
return barometer.toObject({ flattenObjectIds: true })
}

/**
* Server function which creates new barometer in the database and generates corresponding page
* @returns created barometer slug
*/
export async function createBarometer(barometerData: IBarometer) {
await connectMongoose()
const cleanData = cleanObject(barometerData)
const slug = slugify(cleanData.name)
cleanData.slug = slug
const newBarometer = new Barometer(cleanData)
await newBarometer.save()
revalidatePath(`/collection/items/${slug}`)
return slug
}

/**
* Server function which updates existing barometer with new data and regenerates corresponding page
* @returns updated barometer slug (may differ from the original)
*/
export async function updateBarometer(barometerData: IBarometer): Promise<string> {
await connectMongoose()
const slug = slugify(barometerData.name)
barometerData.slug = slug
await Manufacturer.findByIdAndUpdate(barometerData.manufacturer?._id, barometerData.manufacturer)
const updatedBarometer = await Barometer.findByIdAndUpdate(barometerData._id, barometerData)
if (!updatedBarometer) throw new Error('Barometer not found')
revalidatePath(`/collection/items/${slug}`)
return slug
}

export async function listBarometers(options?: {
type?: string
sort?: SortValue
limit?: number
}): Promise<IBarometer[]> {
await connectMongoose()
const sortBy = options?.sort ?? 'date'
if (!options?.type)
return sortBarometers(
await Barometer.find()
.limit(options?.limit ?? 0)
.populate(dependencies),
sortBy,
)
const barometerType = await BarometerType.findOne({
name: { $regex: new RegExp(`^${options.type}$`, 'i') },
})
if (!barometerType) throw new Error('Unknown barometer type')
return sortBarometers(
(
await Barometer.find({ type: barometerType._id })
.limit(options.limit ?? 0)
.populate(dependencies)
).map(res => res.toObject({ flattenObjectIds: true })),
sortBy,
)
}

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
}
})
}
10 changes: 10 additions & 0 deletions actions/conditions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use server'

import { connectMongoose } from '@/utils/mongoose'
import BarometerCondition, { IBarometerCondition } from '@/models/condition'

export async function listConditions(): Promise<IBarometerCondition[]> {
await connectMongoose()
const conditions = await BarometerCondition.find()
return conditions.map(condition => condition.toObject({ flattenObjectIds: true }))
}
46 changes: 46 additions & 0 deletions actions/images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use server'

import path from 'path'
import { v4 as uuid } from 'uuid'
import { GetSignedUrlConfig, Storage } from '@google-cloud/storage'

const decodedPrivateKey = Buffer.from(process.env.GCP_PRIVATE_KEY, 'base64').toString('utf-8')
const storage = new Storage({
projectId: process.env.GCP_PROJECT_ID,
credentials: {
client_email: process.env.GCP_CLIENT_EMAIL,
private_key: decodedPrivateKey,
},
})
const bucket = storage.bucket(process.env.GCP_BUCKET_NAME)

export async function uploadImages(files: { fileName: string; contentType: string }[]) {
const signedUrls = await Promise.all(
files.map(async ({ fileName, contentType }) => {
// give unique names to files
const extension = path.extname(fileName).toLowerCase()
const newFileName = 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)
return {
signed: signedUrl,
public: cloudFile.publicUrl(),
}
}),
)
return signedUrls
}

// delete file from google cloud storage
export async function deleteImage(url?: string) {
if (!url) throw new Error('Unknown image file')
const file = bucket.file(url)
await file.delete()
}
25 changes: 25 additions & 0 deletions actions/manufacturers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use server'

import { connectMongoose } from '@/utils/mongoose'
import Manufacturer, { IManufacturer } from '@/models/manufacturer'
import { cleanObject } from '@/utils/misc'

export async function listManufacturers(): Promise<IManufacturer[]> {
await connectMongoose()
const conditions = await Manufacturer.find()
return conditions.map(obj => obj.toObject({ flattenObjectIds: true }))
}

export async function setManufacturer(manufData: IManufacturer): Promise<IManufacturer> {
await connectMongoose()
const cleanData = cleanObject(manufData)
const newManufacturer = new Manufacturer(cleanData)
await newManufacturer.save()
return newManufacturer.toObject({ flattenObjectIds: true })
}

export async function deleteManufacturer(id: string) {
await connectMongoose()
const deletedManufacturer = await Manufacturer.findByIdAndDelete(id)
if (!deletedManufacturer) throw new Error('Unknown manufacturer')
}
69 changes: 29 additions & 40 deletions app/admin/components/add-card.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
'use client'

import { useSWRConfig } from 'swr'
import { Box, Title, Button, TextInput, Select, Textarea } from '@mantine/core'
import { useForm } from '@mantine/form'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import axios, { AxiosError } from 'axios'
import { useEffect, useState } from 'react'
import { isLength } from 'validator'
import { showInfo, showError } from '@/utils/notification'
import { useBarometers } from '@/app/hooks/useBarometers'
import { FileUpload } from './file-upload'
import { AddManufacturer } from './add-manufacturer'
import { Dimensions } from './dimensions'
import type { BarometerFormProps } from '../types'
import { barometersApiRoute } from '@/app/constants'
import { useBarometers } from '@/app/hooks/useBarometers'
import { createBarometer } from '@/actions/barometers'

export function AddCard() {
const { mutate } = useSWRConfig()
const [uploadedImages, setUploadedImages] = useState<string[]>([])
const { condition, types, manufacturers } = useBarometers()
const { types, manufacturers, conditions } = useBarometers()

const form = useForm<BarometerFormProps>({
initialValues: {
Expand All @@ -35,57 +35,46 @@ export function AddCard() {
},
})

const queryClient = useQueryClient()
const { mutate } = useMutation({
mutationFn: async (values: BarometerFormProps) => {
const saveBarometer = async (values: BarometerFormProps) => {
try {
const barometerWithImages = {
...values,
manufacturer: manufacturers.data.find(({ _id }) => _id === values.manufacturer),
manufacturer: manufacturers?.find(({ _id }) => _id === values.manufacturer),
images: uploadedImages.map(image => image.split('/').at(-1)),
}
const { data } = await axios.post(barometersApiRoute, barometerWithImages)
return data
},
onSuccess: (_, { name }) => {
queryClient.invalidateQueries({
queryKey: ['barometers'],
})
} as any
await createBarometer(barometerWithImages)
form.reset()
setUploadedImages([])
showInfo(`Added ${name} to the database`)
},
onError: (error: AxiosError) => {
showError(
(error.response?.data as { message: string })?.message ||
error.message ||
'Error adding barometer',
)
},
})
showInfo(`Added ${values.name} to the database`)
} catch (error) {
showError(error instanceof Error ? error.message : 'Error adding barometer')
}
}

// set default barometer type
useEffect(() => {
if (types.data.length === 0) return
form.setFieldValue('type', String(types.data[0]._id))
if (types?.length === 0) return
form.setFieldValue('type', String(types?.[0]._id))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [types.data])
}, [types])

// set default barometer condition
useEffect(() => {
if (condition.data.length === 0) return
form.setFieldValue('condition', String(condition.data.at(-1)?._id))
if (conditions?.length === 0) return
form.setFieldValue('condition', String(conditions?.at(-1)?._id))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [condition.data])
}, [conditions])

// set default manufacturer
useEffect(() => {
// if there are no manufacturers or manufacturer is already set, do nothing
if (manufacturers.data.length === 0 || form.values.manufacturer) return
form.setFieldValue('manufacturer', String(manufacturers.data[0]._id))
if (manufacturers?.length === 0 || form.values.manufacturer) return
form.setFieldValue('manufacturer', String(manufacturers?.[0]._id))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [manufacturers.data])
}, [manufacturers])

const onAddManufacturer = (id: string) => {
mutate('manufacturers')
form.setFieldValue('manufacturer', id)
}

Expand All @@ -94,12 +83,12 @@ export function AddCard() {
<Title mb="lg" order={3} tt="capitalize">
Add new barometer
</Title>
<Box component="form" onSubmit={form.onSubmit(values => mutate(values))}>
<Box component="form" onSubmit={form.onSubmit(values => saveBarometer(values))}>
<TextInput label="Catalogue No." required {...form.getInputProps('collectionId')} />
<TextInput label="Title" required id="barometer-name" {...form.getInputProps('name')} />
<TextInput label="Dating" key={form.key('dating')} {...form.getInputProps('dating')} />
<Select
data={types.data.map(({ name, _id }) => ({
data={types?.map(({ name, _id }) => ({
label: name,
value: String(_id),
}))}
Expand All @@ -109,7 +98,7 @@ export function AddCard() {
{...form.getInputProps('type')}
/>
<Select
data={manufacturers.data.map(({ name, _id }) => ({
data={manufacturers?.map(({ name, _id }) => ({
label: name,
value: _id!,
}))}
Expand All @@ -125,7 +114,7 @@ export function AddCard() {
/>
<Select
label="Condition"
data={condition.data.map(({ name, _id }) => ({
data={conditions?.map(({ name, _id }) => ({
label: name,
value: String(_id),
}))}
Expand Down
Loading
Loading