Skip to content

Commit

Permalink
feat: add model Media to allow user to select a color during photo cr…
Browse files Browse the repository at this point in the history
…eation
  • Loading branch information
claireso committed Sep 24, 2024
1 parent c758e36 commit aef3a02
Show file tree
Hide file tree
Showing 52 changed files with 1,282 additions and 715 deletions.
5 changes: 3 additions & 2 deletions app/(admin)/admin/photos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Heading1 } from '@components/Headings'
import Modal from '@components/Modal'
import Pager from '@components/Pager'
import EmptyZone from '@components/EmptyZone'
import { Photo } from '@models'

enum Action {
CREATE = 'create',
Expand Down Expand Up @@ -94,7 +95,7 @@ const Photos = () => {
)

const onCreatePhoto = useCallback(
(data: FormData) => {
(data: Partial<Photo>) => {
createPhoto(data, {
onSuccess() {
onCloseModal({ scroll: true })
Expand All @@ -114,7 +115,7 @@ const Photos = () => {
)

const onEditPhoto = useCallback(
(data: { id: number; data: FormData }) => {
(data: { id: number; data: Partial<Photo> }) => {
editPhoto(data, {
onSettled(data, err) {
if (err instanceof api.getErrorConstructor()) {
Expand Down
37 changes: 37 additions & 0 deletions app/api/media/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextRequest } from 'next/server'
import { createRouteHandler, withAuth } from '@services/middlewares'
import { pool, queries } from '@services/db'
import { MediaRequestSchema, Media, formatMedia } from '@models'
import uploadFile from '@utils/uploadFile'

// endpoint POST media
const createMedia = async (request: NextRequest) => {
const formData = await request.formData()

const body = Object.fromEntries(formData)

const { file } = MediaRequestSchema.parse(body)

const { filename, width, height } = await uploadFile(file)

const mediaPhoto = {
type: 'image',
name: filename,
width: width,
height: height
}

// todo: on error, delete file in the directory
const response = await pool.query(queries.insert_media(), [
mediaPhoto.type,
mediaPhoto.name,
mediaPhoto.width,
mediaPhoto.height
])

const media: Media = formatMedia(response.rows[0])

return Response.json(media, { status: 201 })
}

export const POST = createRouteHandler(withAuth, createMedia)
88 changes: 49 additions & 39 deletions app/api/photos/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { NextRequest } from 'next/server'
import { revalidateTag, unstable_cache } from 'next/cache'
import { createRouteHandler, withAuth } from '@services/middlewares'
import { pool, queries } from '@services/db'
import uploadFile from '@utils/uploadFile'
import { Photo, PhotoRequestSchema, formatPhoto, createPhoto as createPhotoHelper } from '@models'
import {
Photo,
PhotoRequestSchema,
LegacyPhotoRequestSchema,
formatPhoto as formatPhotoHelper,
createPhoto as createPhotoHelper
} from '@models'

interface RequestContext {
params: { id: string }
Expand All @@ -32,14 +37,13 @@ const getPhotoById = async (request: NextRequest, { params }: RequestContext) =>

const response = await getCachedPhoto(id)

// const response = await pool.query(queries.get_photo(id))
const photo: Photo = response.rows[0]

if (photo === undefined) {
if (response.rowCount === 0) {
return Response.json({}, { status: 404 })
}

return Response.json(formatPhoto(photo), { status: 200 })
const photo: Photo = formatPhotoHelper(response.rows[0])

return Response.json(photo, { status: 200 })
}

// Endpoint edit photo
Expand All @@ -51,34 +55,25 @@ const editPhoto = async (request: NextRequest, { params }: RequestContext) => {
}

let response = await getCachedPhoto(id)
const photo: Photo = response.rows[0]

if (photo === undefined) {
if (response.rowCount === 0) {
return Response.json({}, { status: 404 })
}

const formData = await request.formData()

const body = Object.fromEntries(formData)

const result = PhotoRequestSchema.parse(body)

const { file, ...partialPhoto } = result

const data: Partial<Photo> = {
title: partialPhoto.title ?? photo.title,
description: partialPhoto.description ?? photo.description,
name: photo.name,
position: partialPhoto.position ?? photo.position,
portrait: photo.portrait,
square: photo.square,
color: partialPhoto.color ?? photo.color,
updated_at: new Date()
}
const photo: Photo = formatPhotoHelper(response.rows[0])
const isPhotoLegacy = !photo.media_id

const uploadedFile = file.name ? await uploadFile(file) : undefined
const body = await request.json()
const schema = isPhotoLegacy ? LegacyPhotoRequestSchema : PhotoRequestSchema
const result = schema.parse(body)

const newPhoto = createPhotoHelper(data, uploadedFile)
const newPhoto: Partial<Photo> = createPhotoHelper({
title: result.title ?? photo.title,
description: result.description ?? photo.description,
media_id: result.media_id ?? photo.media_id,
color: result.color ?? photo.color,
position: result.position ?? photo.position
})

const fields = Object.entries(newPhoto)
.map((entry, index) => `${entry[0]}=($${index + 1})`)
Expand All @@ -88,7 +83,8 @@ const editPhoto = async (request: NextRequest, { params }: RequestContext) => {

revalidateTag('photos')
revalidateTag(`photo_${id}`)
return Response.json(formatPhoto(response.rows[0]), { status: 200 })

return Response.json(formatPhotoHelper(response.rows[0]), { status: 200 })
}

// endpoint delete photo
Expand All @@ -100,21 +96,35 @@ const deletePhoto = async (request: NextRequest, { params }: RequestContext) =>
}

const response = await getCachedPhoto(id)
const photo: Photo = response.rows[0]

if (photo === undefined) {
if (response.rowCount === 0) {
return Response.json({}, { status: 404 })
}

// delete photo from the folder
await unlink(path.resolve('uploads', photo.name))
// on delete photo, delete linked media
try {
// start transaction
await pool.query('BEGIN')
const photo: Photo = formatPhotoHelper(response.rows[0])
// delete photo from database
await pool.query(queries.delete_photo(id))

if (photo.media_id) {
// delete media from database
await pool.query(queries.delete_media(photo.media_id))
}

// delete photo from database
await pool.query(queries.delete_photo(id))
// delete photo from the folder
await unlink(path.resolve('uploads', photo.media.name))

revalidateTag('photos')
revalidateTag(`photo_${id}`)
return Response.json({}, { status: 200 })
await pool.query('COMMIT')
revalidateTag('photos')
revalidateTag(`photo_${id}`)
return Response.json({}, { status: 200 })
} catch (e) {
await pool.query('ROLLBACK')
throw e
}
}

export const GET = createRouteHandler(getPhotoById)
Expand Down
50 changes: 34 additions & 16 deletions app/api/photos/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import { revalidateTag } from 'next/cache'
import { differenceInMinutes } from 'date-fns'
import { createRouteHandler, withPagination, withAuth } from '@services/middlewares'
import { pool, queries } from '@services/db'
import logger from '@services/logger'
import { IS_NOTIFICATIONS_ENABLED, sendNotification, NOTIFICATION_NEW_PHOTO } from '@services/web-push'
import {
PhotoRequestSchema,
Photo,
Media,
Subscription,
Pager,
createPhoto as createPhotoHelper,
formatPhoto as formatPhotoHelper
} from '@models'
import uploadFile from '@utils/uploadFile'

// endpoint list photos
const getAllPhotos = async (request: NextRequest & { pager: Pager }) => {
Expand All @@ -22,9 +23,11 @@ const getAllPhotos = async (request: NextRequest & { pager: Pager }) => {
})
)

const photos: Array<Photo> = response.rows.map(formatPhotoHelper)

return Response.json(
{
items: response.rows.map(formatPhotoHelper),
items: photos,
pager: request.pager
},
{ status: 200 }
Expand All @@ -33,27 +36,42 @@ const getAllPhotos = async (request: NextRequest & { pager: Pager }) => {

// endpoint POST photo
const createPhoto = async (request: NextRequest) => {
const formData = await request.formData()

const body = Object.fromEntries(formData)
const body = await request.json()

const result = PhotoRequestSchema.parse(body)

const { file, ...partialPhoto } = result
// idea: check if it is a good idea to add the check in zod media schema
const mediaResponse = await pool.query(queries.get_media(result.media_id))

if (mediaResponse.rowCount === 0) {
const message = 'Media is required and must exist'
logger.error(message)
return Response.json({ message }, { status: 422 })
}

const media: Media = mediaResponse.rows[0]

const photoByMediaResponse = await pool.query(queries.get_photo_by_media(media.id))

const uploadedFile = await uploadFile(file)
if (photoByMediaResponse.rowCount === 1) {
const message = `Media ${media.id} is already linked to a photo`
logger.error(message)
return Response.json({ message }, { status: 422 })
}

const photo = createPhotoHelper(partialPhoto, uploadedFile)
const data = createPhotoHelper(result)

const response = await pool.query(queries.insert_photo(), [
photo.title,
photo.description,
photo.name,
photo.position,
photo.portrait,
photo.square
const responsePhoto = await pool.query(queries.insert_photo(), [
media.name, // set a value for "name" to keep legacy working (a database migration is required in the future)
data.title,
data.description,
data.position,
data.color,
data.media_id
])

const photo = formatPhotoHelper(responsePhoto.rows[0])

// send web-push notification
if (IS_NOTIFICATIONS_ENABLED) {
const responseForPreviousPhoto = await pool.query(queries.get_previous_photo())
Expand Down Expand Up @@ -81,7 +99,7 @@ const createPhoto = async (request: NextRequest) => {
}

revalidateTag('photos')
return Response.json(formatPhotoHelper(response.rows[0]), { status: 201 })
return Response.json(photo, { status: 201 })
}

export const GET = createRouteHandler(withPagination('photos'), getAllPhotos)
Expand Down
30 changes: 18 additions & 12 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,27 @@ const customJestConfig = {
id: 199,
title: '',
description: 'Février 2019',
name: '01d2y7jt2j24dv0s82m9xq729d.jpg',
source: '/uploads/01d2y7jt2j24dv0s82m9xq729d.jpg',
media: {
name: '01d2y7jt2j24dv0s82m9xq729d.jpg',
source: '/uploads/01d2y7jt2j24dv0s82m9xq729d.jpg',
portrait: false,
square: false
},
position: 'right',
portrait: false,
square: false,
created_at: '2019-02-05T07:05:02.548Z',
updated_at: '2019-02-05T08:24:09.612Z'
},
{
id: 198,
title: '',
description: 'Janvier 2019',
name: '01d2tf2h38pwcd953ans2f64p7.jpg',
source: '/uploads/01d2tf2h38pwcd953ans2f64p7.jpg',
media: {
name: '01d2tf2h38pwcd953ans2f64p7.jpg',
source: '/uploads/01d2tf2h38pwcd953ans2f64p7.jpg',
portrait: false,
square: false
},
position: 'center',
portrait: false,
square: false,
created_at: '2019-02-03T19:59:00.088Z',
updated_at: '2019-02-03T19:59:00.088Z'
}
Expand All @@ -60,11 +64,13 @@ const customJestConfig = {
id: 1,
title: 'Single photography',
description: 'Janvier 2019',
name: '01d2tf2h38pwcd953ans2f64p7.jpg',
source: '/uploads/01d2tf2h38pwcd953ans2f64p7.jpg',
media: {
name: '01d2tf2h38pwcd953ans2f64p7.jpg',
source: '/uploads/01d2tf2h38pwcd953ans2f64p7.jpg',
portrait: false,
square: false
},
position: 'center',
portrait: false,
square: false,
created_at: '2019-02-03T19:59:00.088Z',
updated_at: '2019-02-03T19:59:00.088Z'
},
Expand Down
Loading

0 comments on commit aef3a02

Please sign in to comment.