diff --git a/lib/schemas/barometer-form.schema.ts b/app/admin/add-barometer/barometer-form.schema.ts similarity index 100% rename from lib/schemas/barometer-form.schema.ts rename to app/admin/add-barometer/barometer-form.schema.ts diff --git a/app/admin/add-barometer/barometer-form.tsx b/app/admin/add-barometer/barometer-form.tsx index 481bc8f3..7e822e3c 100644 --- a/app/admin/add-barometer/barometer-form.tsx +++ b/app/admin/add-barometer/barometer-form.tsx @@ -33,18 +33,18 @@ import { SelectValue, } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' -import { createBarometer } from '@/lib/barometers/actions' -import type { AllBrandsDTO } from '@/lib/brands/queries' -import type { CategoriesDTO } from '@/lib/categories/queries' -import type { ConditionsDTO } from '@/lib/conditions/queries' -import type { MaterialsDTO } from '@/lib/materials/queries' -import type { MovementsDTO } from '@/lib/movements/queries' +import { createBarometer } from '@/server/barometers/actions' +import type { AllBrandsDTO } from '@/server/brands/queries' +import type { CategoriesDTO } from '@/server/categories/queries' +import type { ConditionsDTO } from '@/server/conditions/queries' +import type { MaterialsDTO } from '@/server/materials/queries' +import type { MovementsDTO } from '@/server/movements/queries' +import { cn } from '@/utils' import { type BarometerFormData, BarometerFormTransformSchema, BarometerFormValidationSchema, -} from '@/lib/schemas/barometer-form.schema' -import { cn } from '@/utils' +} from './barometer-form.schema' import { Dimensions } from './dimensions' import { FileUpload } from './file-upload' diff --git a/app/admin/add-barometer/file-upload.tsx b/app/admin/add-barometer/file-upload.tsx index 14757f98..68795726 100644 --- a/app/admin/add-barometer/file-upload.tsx +++ b/app/admin/add-barometer/file-upload.tsx @@ -10,7 +10,8 @@ import { Card } from '@/components/ui/card' import { FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { imageStorage } from '@/constants/globals' -import { createImageUrls, deleteImage, uploadFileToCloud } from '@/services/fetch' +import { createImageUrls, deleteImage } from '@/server/images/actions' +import { uploadFileToCloud } from '@/server/images/upload' interface FileUploadProps { name: string diff --git a/app/admin/add-barometer/page.tsx b/app/admin/add-barometer/page.tsx index 2e5d77d5..fcf7a628 100644 --- a/app/admin/add-barometer/page.tsx +++ b/app/admin/add-barometer/page.tsx @@ -1,10 +1,10 @@ import 'server-only' -import { getAllBrands } from '@/lib/brands/queries' -import { getCategories } from '@/lib/categories/queries' -import { getConditions } from '@/lib/conditions/queries' -import { getMaterials } from '@/lib/materials/queries' -import { getMovements } from '@/lib/movements/queries' +import { getAllBrands } from '@/server/brands/queries' +import { getCategories } from '@/server/categories/queries' +import { getConditions } from '@/server/conditions/queries' +import { getMaterials } from '@/server/materials/queries' +import { getMovements } from '@/server/movements/queries' import type { DynamicOptions } from '@/types' import BarometerForm from './barometer-form' diff --git a/app/admin/add-brand/brand-add-form.tsx b/app/admin/add-brand/brand-add-form.tsx index 3141d75c..4cd3650f 100644 --- a/app/admin/add-brand/brand-add-form.tsx +++ b/app/admin/add-brand/brand-add-form.tsx @@ -8,10 +8,11 @@ import { z } from 'zod' import { IconUpload, ImageUpload, MultiSelect, RequiredFieldMark } from '@/components/elements' import * as UI from '@/components/ui' import { imageStorage } from '@/constants' -import { createBrand } from '@/lib/brands/actions' -import type { AllBrandsDTO } from '@/lib/brands/queries' -import type { CountryListDTO } from '@/lib/counties/queries' -import { createImageUrls, uploadFileToCloud } from '@/services/fetch' +import { createBrand } from '@/server/brands/actions' +import type { AllBrandsDTO } from '@/server/brands/queries' +import type { CountryListDTO } from '@/server/counties/queries' +import { createImageUrls } from '@/server/images/actions' +import { uploadFileToCloud } from '@/server/images/upload' import { generateIcon, getThumbnailBase64 } from '@/utils' // Zod validation schema diff --git a/app/admin/add-brand/page.tsx b/app/admin/add-brand/page.tsx index eb45d2c8..9edb829a 100644 --- a/app/admin/add-brand/page.tsx +++ b/app/admin/add-brand/page.tsx @@ -1,7 +1,7 @@ import 'server-only' -import { getAllBrands } from '@/lib/brands/queries' -import { getCountries } from '@/lib/counties/queries' +import { getAllBrands } from '@/server/brands/queries' +import { getCountries } from '@/server/counties/queries' import type { DynamicOptions } from '@/types' import BrandAddForm from './brand-add-form' diff --git a/app/admin/add-document/document-form.tsx b/app/admin/add-document/document-form.tsx index 1c024374..b7a5f39e 100644 --- a/app/admin/add-document/document-form.tsx +++ b/app/admin/add-document/document-form.tsx @@ -10,9 +10,9 @@ import { z } from 'zod' import { MultiSelect, RequiredFieldMark } from '@/components/elements' import * as UI from '@/components/ui' import { imageStorage } from '@/constants/globals' -import type { AllBarometersDTO } from '@/lib/barometers/queries' -import type { ConditionsDTO } from '@/lib/conditions/queries' -import { createDocument } from '@/lib/documents/actions' +import type { AllBarometersDTO } from '@/server/barometers/queries' +import type { ConditionsDTO } from '@/server/conditions/queries' +import { createDocument } from '@/server/documents/actions' import { getThumbnailBase64 } from '@/utils' import { FileUpload } from '../add-barometer/file-upload' diff --git a/app/admin/add-document/page.tsx b/app/admin/add-document/page.tsx index 2d90d6ec..ed8b4100 100644 --- a/app/admin/add-document/page.tsx +++ b/app/admin/add-document/page.tsx @@ -1,7 +1,7 @@ import 'server-only' -import { getAllBarometers } from '@/lib/barometers/queries' -import { getConditions } from '@/lib/conditions/queries' +import { getAllBarometers } from '@/server/barometers/queries' +import { getConditions } from '@/server/conditions/queries' import type { DynamicOptions } from '@/types' import { DocumentForm } from './document-form' diff --git a/app/admin/reports/page.tsx b/app/admin/reports/page.tsx index d43ad0c9..831fdb22 100644 --- a/app/admin/reports/page.tsx +++ b/app/admin/reports/page.tsx @@ -1,78 +1,25 @@ -'use client' - -import { useQuery } from '@tanstack/react-query' -import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table' -import dayjs from 'dayjs' -import Link from 'next/link' -import { useSearchParams } from 'next/navigation' -import { Table } from '@/components/elements' import { Pagination } from '@/components/ui' -import { FrontRoutes } from '@/constants' -import { fetchReportList } from '@/services' -import type { InaccuracyReportListDTO } from '@/types' +import { getInaccuracyReports } from '@/server/reports/queries' +import type { DynamicOptions } from '@/types' +import { ReportTable } from './report-table' + +export const dynamic: DynamicOptions = 'force-dynamic' +const itemsOnPage = 12 + +interface Props { + searchParams: Promise<{ page: string }> +} -const itemsOnPage = 6 +export default async function ReportList({ searchParams }: Props) { + const { page } = await searchParams + const pageNo = Number.parseInt(page, 10) || 1 + const { reports, totalPages } = await getInaccuracyReports(pageNo, itemsOnPage) -export default function ReportList() { - const searchParams = useSearchParams() - const page = searchParams.get('page') ?? '1' - const { data } = useQuery({ - queryKey: ['inaccuracyReport', page], - queryFn: () => - fetchReportList({ - page, - size: String(itemsOnPage), - }), - }) - const { accessor } = createColumnHelper() - const columns = [ - accessor('barometer.name', { - header: 'Barometer', - cell: info => ( - - {info.getValue()} - - ), - }), - accessor('reporterName', { - header: 'Name', - }), - accessor('reporterEmail', { - header: 'Email', - cell: info => ( - - {info.getValue()} - - ), - }), - accessor('createdAt', { - header: 'Created at', - cell: info => dayjs(info.getValue()).format('MMMM D, YYYY HH:mm'), - }), - accessor('description', { - header: 'Description', - cell: info =>
{info.getValue()}
, - }), - accessor('status', { - header: 'Status', - }), - ] - const table = useReactTable({ - columns, - data: data?.reports ?? [], - getCoreRowModel: getCoreRowModel(), - }) return ( <>

Inaccuracy Reports

- - + + ) } diff --git a/app/admin/reports/report-table.tsx b/app/admin/reports/report-table.tsx new file mode 100644 index 00000000..c0b94fd2 --- /dev/null +++ b/app/admin/reports/report-table.tsx @@ -0,0 +1,60 @@ +'use client' + +import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import dayjs from 'dayjs' +import Link from 'next/link' +import { Table } from '@/components/elements' +import { FrontRoutes } from '@/constants' +import type { InaccuracyReportsDTO } from '@/server/reports/queries' + +interface Props { + reports: InaccuracyReportsDTO['reports'] +} + +export function ReportTable({ reports }: Props) { + const { accessor } = createColumnHelper() + const columns = [ + accessor('barometer.name', { + header: 'Barometer', + cell: info => ( + + {info.getValue()} + + ), + }), + accessor('reporterName', { + header: 'Name', + }), + accessor('reporterEmail', { + header: 'Email', + cell: info => ( + + {info.getValue()} + + ), + }), + accessor('createdAt', { + header: 'Created at', + cell: info => dayjs(info.getValue()).format('MMMM D, YYYY HH:mm'), + }), + accessor('description', { + header: 'Description', + cell: info =>
{info.getValue()}
, + }), + accessor('status', { + header: 'Status', + }), + ] + const table = useReactTable({ + columns, + data: reports ?? [], + getCoreRowModel: getCoreRowModel(), + }) + return
+} diff --git a/app/api/v2/parameters.ts b/app/api/v2/parameters.ts deleted file mode 100644 index f5c2bb45..00000000 --- a/app/api/v2/parameters.ts +++ /dev/null @@ -1,2 +0,0 @@ -// pagination page size -export const DEFAULT_PAGE_SIZE = 12 diff --git a/app/api/v2/report/getters.ts b/app/api/v2/report/getters.ts deleted file mode 100644 index 1a69a771..00000000 --- a/app/api/v2/report/getters.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { withPrisma } from '@/prisma/prismaClient' - -export const getInaccuracyReportList = withPrisma( - async (prisma, page: number, pageSize: number) => { - const [reports, totalItems] = await Promise.all([ - prisma.inaccuracyReport.findMany({ - skip: (page - 1) * pageSize, - take: pageSize, - orderBy: [{ createdAt: 'desc' }, { reporterName: 'desc' }], - include: { - barometer: { - select: { - name: true, - slug: true, - }, - }, - }, - }), - prisma.inaccuracyReport.count(), - ]) - return { - reports, - page, - totalItems, - pageSize, - totalPages: Math.ceil(totalItems / pageSize), - } - }, -) - -export type InaccuracyReportListDTO = Awaited> diff --git a/app/api/v2/report/route.ts b/app/api/v2/report/route.ts deleted file mode 100644 index a7bac795..00000000 --- a/app/api/v2/report/route.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { InaccuracyReport } from '@prisma/client' -import Redis from 'ioredis' -import { revalidatePath } from 'next/cache' -import { type NextRequest, NextResponse } from 'next/server' -import { FrontRoutes } from '@/constants/routes-front' -import { cleanObject, trimTrailingSlash } from '@/utils' -import { DEFAULT_PAGE_SIZE } from '../parameters' -import { getInaccuracyReportList } from './getters' -import { createReport } from './setters' - -// inaccuracy report TTL, minutes -const REPORT_COOL_DOWN = 10 -const REPORT_MAX_ATTEMPTS = 3 - -const redis = new Redis(process.env.REDIS_URL ?? '') - -/** - * Fetches a paginated list of inaccuracy reports for barometers. - * Returns 200 if reports are found, 404 if not, and 500 on error. - */ -export async function GET(req: NextRequest) { - try { - const { searchParams } = req.nextUrl - const size = Math.max(Number(searchParams.get('size')) || DEFAULT_PAGE_SIZE, 1) - const page = Math.max(Number(searchParams.get('page')) || 1, 1) - const dbResponse = await getInaccuracyReportList(page, size) - return NextResponse.json(dbResponse, { status: dbResponse.reports.length > 0 ? 200 : 404 }) - } catch (error) { - console.error('Error fetching inaccuracy report list:', error) - const message = error instanceof Error ? error.message : 'Could not get inaccuracy report list' - return NextResponse.json({ message }, { status: 500 }) - } -} - -/** - * Route for creating a new inaccuracy report for a barometer - * - Allows up to three reports to be submitted for a single key. - * - The key is generated using a combination of the user's IP address and the barometer ID (barometerId). - * - After exceeding the request limit, returns a 429 (Too Many Requests) error. - */ -export async function POST(req: NextRequest) { - try { - // getting sender IP address - const ip = - req.headers.get('x-forwarded-for')?.split(',')[0] || req.headers.get('remote-addr') || null - if (!ip) { - return NextResponse.json({ message: 'Could not determine IP address' }, { status: 400 }) - } - const body: Partial = await req.json() - const { reporterEmail, reporterName, description, barometerId } = cleanObject(body) - if (!reporterEmail || !reporterName || !description || !barometerId) - throw new Error('Report params are not complete') - const redisKey = `rate-limit:${ip}:${barometerId}` - const attempts = await redis.incr(redisKey) - if (attempts <= REPORT_MAX_ATTEMPTS) { - await redis.expire(redisKey, REPORT_COOL_DOWN * 60) - } else { - const ttl = await redis.ttl(redisKey) // TTL in seconds - const minutesLeft = Math.ceil(ttl / 60) - return NextResponse.json( - { - message: `Too many requests. Please try again after ${minutesLeft} minute(s).`, - }, - { status: 429 }, - ) - } - const { id } = await createReport(barometerId, reporterEmail, reporterName, description) - revalidatePath(trimTrailingSlash(FrontRoutes.Reports)) - return NextResponse.json({ message: 'Inaccuracy report created', id }, { status: 201 }) - } catch (error) { - console.error('Error sending inaccuracy report:', error) - const message = error instanceof Error ? error.message : 'Could not send inaccuracy report' - return NextResponse.json({ message }, { status: 500 }) - } -} diff --git a/app/api/v2/report/setters.ts b/app/api/v2/report/setters.ts deleted file mode 100644 index ad54b475..00000000 --- a/app/api/v2/report/setters.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { withPrisma } from '@/prisma/prismaClient' - -export const createReport = withPrisma( - async ( - prisma, - barometerId: string, - reporterEmail: string, - reporterName: string, - description: string, - ) => { - const newReport = await prisma.inaccuracyReport.create({ - data: { - barometerId, - reporterEmail, - reporterName, - description, - }, - }) - return { id: newReport.id } - }, -) diff --git a/app/api/v2/upload/images/route.ts b/app/api/v2/upload/images/route.ts deleted file mode 100644 index 5b4c25d3..00000000 --- a/app/api/v2/upload/images/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import path from 'node:path' -import { type NextRequest, NextResponse } from 'next/server' -import { v4 as uuid } from 'uuid' -import { minioBucket, minioClient } from '@/services/minio' -import type { FileDto, UrlDto, UrlProps } from './types' - -export async function POST(req: NextRequest) { - try { - const { files }: FileDto = await req.json() - const signedUrls = await Promise.all( - files.map>(async ({ fileName }) => { - // give unique names to files - const extension = path.extname(fileName).toLowerCase() - const newFileName = `gallery/${uuid()}${extension}` - const signedUrl = await minioClient.presignedPutObject(minioBucket, newFileName) - return { - signed: signedUrl, - public: newFileName, - } - }), - ) - return NextResponse.json( - { - urls: signedUrls, - }, - { status: 201 }, - ) - } catch (error) { - return NextResponse.json( - { message: error instanceof Error ? error.message : 'Error uploading files' }, - { status: 500 }, - ) - } -} - -export async function DELETE(req: NextRequest) { - const { searchParams } = new URL(req.url) - const fileName = searchParams.get('fileName') - try { - if (!fileName) return NextResponse.json({ message: 'File name is required' }, { status: 400 }) - // delete file from Minio storage - await minioClient.removeObject(minioBucket, fileName) - return NextResponse.json({ message: `${fileName} was deleted` }, { status: 200 }) - } catch (_error) { - return NextResponse.json( - { message: `${fileName ?? 'Your file'} is already deleted` }, - { status: 200 }, - ) - } -} diff --git a/app/api/v2/upload/images/types.ts b/app/api/v2/upload/images/types.ts deleted file mode 100644 index 46c3109e..00000000 --- a/app/api/v2/upload/images/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type FileProps = { fileName: string; contentType: string } -export type FileDto = { files: FileProps[] } -export type UrlProps = { signed: string; public: string } -export type UrlDto = { urls: UrlProps[] } diff --git a/app/brands/[slug]/page.tsx b/app/brands/[slug]/page.tsx index dd30568a..cd1c3ae0 100644 --- a/app/brands/[slug]/page.tsx +++ b/app/brands/[slug]/page.tsx @@ -7,8 +7,8 @@ import { BarometerCardWithIcon, ImageLightbox, MD } from '@/components/elements' import { Card } from '@/components/ui' import { FrontRoutes } from '@/constants' import { title } from '@/constants/metadata' -import { type BrandDTO, getBrand } from '@/lib/brands/queries' import { withPrisma } from '@/prisma/prismaClient' +import { type BrandDTO, getBrand } from '@/server/brands/queries' import type { DynamicOptions } from '@/types' interface Props { diff --git a/app/brands/brand-edit.tsx b/app/brands/brand-edit.tsx index 0a708a69..09715bae 100644 --- a/app/brands/brand-edit.tsx +++ b/app/brands/brand-edit.tsx @@ -8,10 +8,10 @@ import { toast } from 'sonner' import { IconUpload, RequiredFieldMark } from '@/components/elements' import * as UI from '@/components/ui' import { imageStorage } from '@/constants' -import { deleteBrand, updateBrand } from '@/lib/brands/actions' -import type { AllBrandsDTO, BrandDTO } from '@/lib/brands/queries' -import type { CountryListDTO } from '@/lib/counties/queries' -import { deleteImages } from '@/lib/images/actions' +import { deleteBrand, updateBrand } from '@/server/brands/actions' +import type { AllBrandsDTO, BrandDTO } from '@/server/brands/queries' +import type { CountryListDTO } from '@/server/counties/queries' +import { deleteImages } from '@/server/images/actions' import { generateIcon, getThumbnailBase64 } from '@/utils' import { type BrandEditForm, brandEditSchema } from './brand-edit-schema' import { BrandImageEdit } from './brand-image-edit' diff --git a/app/brands/brand-image-edit.tsx b/app/brands/brand-image-edit.tsx index 2ae7b37a..18dc5021 100644 --- a/app/brands/brand-image-edit.tsx +++ b/app/brands/brand-image-edit.tsx @@ -13,7 +13,8 @@ import NextImage from 'next/image' import { type TransitionStartFunction, useCallback } from 'react' import type { UseFormReturn } from 'react-hook-form' import { Button } from '@/components/ui/button' -import { createImageUrls, deleteImage, uploadFileToCloud } from '@/services/fetch' +import { createImageUrls, deleteImage } from '@/server/images/actions' +import { uploadFileToCloud } from '@/server/images/upload' import type { BrandEditForm } from './brand-edit-schema' interface Props { diff --git a/app/brands/page.tsx b/app/brands/page.tsx index 550d39c9..f9e78cd6 100644 --- a/app/brands/page.tsx +++ b/app/brands/page.tsx @@ -13,8 +13,8 @@ import { type BrandsByCountryDTO, getAllBrands, getBrandsByCountry, -} from '@/lib/brands/queries' -import { type CountryListDTO, getCountries } from '@/lib/counties/queries' +} from '@/server/brands/queries' +import { type CountryListDTO, getCountries } from '@/server/counties/queries' import { title } from '../../constants/metadata' import type { DynamicOptions } from '../../types' import { BrandEdit } from './brand-edit' diff --git a/app/collection/categories/[...category]/page.tsx b/app/collection/categories/[...category]/page.tsx index e88332b0..98e1bf3e 100644 --- a/app/collection/categories/[...category]/page.tsx +++ b/app/collection/categories/[...category]/page.tsx @@ -8,9 +8,9 @@ import { Card, Pagination } from '@/components/ui' import { DEFAULT_PAGE_SIZE, imageStorage } from '@/constants' import { openGraph, title, twitter } from '@/constants/metadata' import { FrontRoutes } from '@/constants/routes-front' -import { getBarometersByParams } from '@/lib/barometers/queries' -import { getCategory } from '@/lib/categories/queries' import { withPrisma } from '@/prisma/prismaClient' +import { getBarometersByParams } from '@/server/barometers/queries' +import { getCategory } from '@/server/categories/queries' import { type DynamicOptions, SortOptions, type SortValue } from '@/types' import Sort from './sort' diff --git a/app/collection/items/[slug]/components/carousel.tsx b/app/collection/items/[slug]/components/carousel.tsx index 4c92d1c2..4c644b85 100644 --- a/app/collection/items/[slug]/components/carousel.tsx +++ b/app/collection/items/[slug]/components/carousel.tsx @@ -4,7 +4,7 @@ import Image from 'next/image' import { Navigation, Pagination, Zoom } from 'swiper/modules' import { Swiper, SwiperSlide } from 'swiper/react' import { IsAdmin } from '@/components/elements' -import type { BarometerDTO } from '@/lib/barometers/queries' +import type { BarometerDTO } from '@/server/barometers/queries' import { customImageLoader } from '@/utils' import { ImagesEdit } from './edit-fields/images-edit' import 'swiper/css' diff --git a/app/collection/items/[slug]/components/condition.tsx b/app/collection/items/[slug]/components/condition.tsx index 3f2e6552..2357523c 100644 --- a/app/collection/items/[slug]/components/condition.tsx +++ b/app/collection/items/[slug]/components/condition.tsx @@ -1,7 +1,7 @@ import { Info } from 'lucide-react' import { Button } from '@/components/ui' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import type { ConditionsDTO } from '@/lib/conditions/queries' +import type { ConditionsDTO } from '@/server/conditions/queries' interface ConditionProps { condition: ConditionsDTO[number] diff --git a/app/collection/items/[slug]/components/delete-barometer/delete-barometer.tsx b/app/collection/items/[slug]/components/delete-barometer/delete-barometer.tsx index 1b358fee..cb86ed99 100644 --- a/app/collection/items/[slug]/components/delete-barometer/delete-barometer.tsx +++ b/app/collection/items/[slug]/components/delete-barometer/delete-barometer.tsx @@ -15,8 +15,8 @@ import { DialogTrigger, } from '@/components/ui' import { FrontRoutes } from '@/constants' -import { deleteBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' +import { deleteBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' import { cn } from '@/utils' interface Props { diff --git a/app/collection/items/[slug]/components/edit-fields/brand-edit.tsx b/app/collection/items/[slug]/components/edit-fields/brand-edit.tsx index d935b4cf..5adcb3bc 100644 --- a/app/collection/items/[slug]/components/edit-fields/brand-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/brand-edit.tsx @@ -1,15 +1,34 @@ 'use client' import { zodResolver } from '@hookform/resolvers/zod' -import { Edit } from 'lucide-react' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@radix-ui/react-select' import { useCallback, useEffect, useState, useTransition } from 'react' -import { useForm } from 'react-hook-form' +import { FormProvider, useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' -import * as UI from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' -import type { AllBrandsDTO } from '@/lib/brands/queries' +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' +import type { AllBrandsDTO } from '@/server/brands/queries' +import { EditButton } from './edit-button' interface Props { barometer: NonNullable @@ -60,57 +79,51 @@ function BrandEdit({ brands, barometer }: Props) { [barometer.manufacturerId, barometer.name], ) return ( - - - - - - - - + + + +
- - Change Brand - - Update the manufacturer for this barometer. - - + + Change Brand + Update the manufacturer for this barometer. +
- ( - - Brand - - - - - - + + Brand + + + + + )} />
- +
-
-
-
+ + + ) } diff --git a/app/collection/items/[slug]/components/edit-fields/condition-edit.tsx b/app/collection/items/[slug]/components/edit-fields/condition-edit.tsx index ebeb43c7..98abbde4 100644 --- a/app/collection/items/[slug]/components/edit-fields/condition-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/condition-edit.tsx @@ -24,9 +24,9 @@ import { SelectTrigger, SelectValue, } from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' -import type { ConditionsDTO } from '@/lib/conditions/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' +import type { ConditionsDTO } from '@/server/conditions/queries' import { EditButton } from './edit-button' interface ConditionEditProps { diff --git a/app/collection/items/[slug]/components/edit-fields/date-edit.tsx b/app/collection/items/[slug]/components/edit-fields/date-edit.tsx index 2aefc5c3..b5e6777f 100644 --- a/app/collection/items/[slug]/components/edit-fields/date-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/date-edit.tsx @@ -8,8 +8,8 @@ import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import * as UI from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' import { cn } from '@/utils' interface DateEditProps extends ComponentProps<'button'> { diff --git a/app/collection/items/[slug]/components/edit-fields/dimensions-edit.tsx b/app/collection/items/[slug]/components/edit-fields/dimensions-edit.tsx index 0837350d..4de76118 100644 --- a/app/collection/items/[slug]/components/edit-fields/dimensions-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/dimensions-edit.tsx @@ -8,8 +8,8 @@ import { useFieldArray, useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import * as UI from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' import type { Dimensions } from '@/types' import { cn } from '@/utils' diff --git a/app/collection/items/[slug]/components/edit-fields/edit-category.tsx b/app/collection/items/[slug]/components/edit-fields/edit-category.tsx index 4054ee60..0ea24c1a 100644 --- a/app/collection/items/[slug]/components/edit-fields/edit-category.tsx +++ b/app/collection/items/[slug]/components/edit-fields/edit-category.tsx @@ -23,9 +23,9 @@ import { SelectTrigger, SelectValue, } from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' -import type { CategoriesDTO } from '@/lib/categories/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' +import type { CategoriesDTO } from '@/server/categories/queries' import { EditButton } from './edit-button' interface Props { 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 62de4f99..8234ad25 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 @@ -7,8 +7,8 @@ import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import * as UI from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' import { cn } from '@/utils' interface EstimatedPriceEditProps extends ComponentProps<'button'> { 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 236c7981..ff379413 100644 --- a/app/collection/items/[slug]/components/edit-fields/images-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/images-edit.tsx @@ -17,9 +17,10 @@ import { toast } from 'sonner' import { z } from 'zod' import * as UI from '@/components/ui' import { imageStorage } from '@/constants/globals' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' -import { createImageUrls, deleteImage, uploadFileToCloud } from '@/services/fetch' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' +import { createImageUrls, deleteImage, deleteImages } from '@/server/images/actions' +import { uploadFileToCloud } from '@/server/images/upload' import { cn, customImageLoader, getThumbnailBase64 } from '@/utils' interface ImagesEditProps extends ComponentProps<'button'> { @@ -113,16 +114,7 @@ export function ImagesEdit({ barometer, size, className, ...props }: ImagesEditP try { // erase deleted images const extraFiles = barometerImages?.filter(img => !values.images.includes(img)) - if (extraFiles) - await Promise.all( - extraFiles?.map(async file => { - try { - await deleteImage(file) - } catch (_error) { - // don't mind if it was not possible to delete the file - } - }), - ) + if (extraFiles) await deleteImages(extraFiles) const imageData = await Promise.all( values.images.map(async (url, i) => { diff --git a/app/collection/items/[slug]/components/edit-fields/materials-edit.tsx b/app/collection/items/[slug]/components/edit-fields/materials-edit.tsx index 247450dd..f4567b7e 100644 --- a/app/collection/items/[slug]/components/edit-fields/materials-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/materials-edit.tsx @@ -8,9 +8,9 @@ import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import * as UI from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' -import type { MaterialsDTO } from '@/lib/materials/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' +import type { MaterialsDTO } from '@/server/materials/queries' import { cn } from '@/utils' interface MaterialsEditProps extends ComponentProps<'button'> { diff --git a/app/collection/items/[slug]/components/edit-fields/movements-edit.tsx b/app/collection/items/[slug]/components/edit-fields/movements-edit.tsx index d294d6c0..a7fef462 100644 --- a/app/collection/items/[slug]/components/edit-fields/movements-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/movements-edit.tsx @@ -7,9 +7,9 @@ import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import * as UI from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' -import type { MovementsDTO } from '@/lib/movements/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' +import type { MovementsDTO } from '@/server/movements/queries' import { cn } from '@/utils' interface SubcategoryEditProps extends ComponentProps<'button'> { diff --git a/app/collection/items/[slug]/components/edit-fields/purchased-at-edit.tsx b/app/collection/items/[slug]/components/edit-fields/purchased-at-edit.tsx index 7dc104ff..07446fad 100644 --- a/app/collection/items/[slug]/components/edit-fields/purchased-at-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/purchased-at-edit.tsx @@ -9,8 +9,8 @@ import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import * as UI from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' import { cn } from '@/utils' dayjs.extend(utc) diff --git a/app/collection/items/[slug]/components/edit-fields/textarea-edit.tsx b/app/collection/items/[slug]/components/edit-fields/textarea-edit.tsx index 916b65b1..28b4106d 100644 --- a/app/collection/items/[slug]/components/edit-fields/textarea-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/textarea-edit.tsx @@ -8,8 +8,8 @@ import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import * as UI from '@/components/ui' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' import { cn } from '@/utils' interface TextAreaEditProps extends ComponentProps<'button'> { diff --git a/app/collection/items/[slug]/components/edit-fields/textfield-edit.tsx b/app/collection/items/[slug]/components/edit-fields/textfield-edit.tsx index 01984e5e..df1eb439 100644 --- a/app/collection/items/[slug]/components/edit-fields/textfield-edit.tsx +++ b/app/collection/items/[slug]/components/edit-fields/textfield-edit.tsx @@ -9,8 +9,8 @@ import { toast } from 'sonner' import { z } from 'zod' import * as UI from '@/components/ui' import { FrontRoutes } from '@/constants/routes-front' -import { updateBarometer } from '@/lib/barometers/actions' -import type { BarometerDTO } from '@/lib/barometers/queries' +import { updateBarometer } from '@/server/barometers/actions' +import type { BarometerDTO } from '@/server/barometers/queries' import { cn } from '@/utils' interface TextFieldEditProps { diff --git a/app/collection/items/[slug]/components/inaccuracy-report.tsx b/app/collection/items/[slug]/components/inaccuracy-report.tsx index cd5169f3..93a63b58 100644 --- a/app/collection/items/[slug]/components/inaccuracy-report.tsx +++ b/app/collection/items/[slug]/components/inaccuracy-report.tsx @@ -1,14 +1,30 @@ 'use client' -import { yupResolver } from '@hookform/resolvers/yup' -import { useMutation, useQueryClient } from '@tanstack/react-query' -import React from 'react' -import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import React, { useEffect, useTransition } from 'react' +import { FormProvider, useForm } from 'react-hook-form' import { toast } from 'sonner' -import * as yup from 'yup' -import * as UI from '@/components/ui' -import type { BarometerDTO } from '@/lib/barometers/queries' -import { createReport } from '@/services/fetch' +import { z } from 'zod' +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, + Textarea, + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui' +import type { BarometerDTO } from '@/server/barometers/queries' +import { createReport } from '@/server/reports/actions' interface Props extends React.ComponentProps<'button'> { barometer: NonNullable @@ -16,64 +32,61 @@ interface Props extends React.ComponentProps<'button'> { const maxFeedbackLen = 1000 -const validationSchema = yup.object({ - reporterName: yup +const validationSchema = z.object({ + reporterName: z .string() - .required('Name is required') + .min(1, 'Name is required') .min(2, 'Name must be at least 2 characters') .max(50, 'Name must be less than 50 characters'), - reporterEmail: yup + reporterEmail: z.email('Please enter a valid email address').min(1, 'Email is required'), + description: z .string() - .required('Email is required') - .email('Please enter a valid email address'), - description: yup - .string() - .required('Description is required') + .min(1, 'Description is required') .min(5, 'Description must be at least 5 characters') .max(maxFeedbackLen, `Description must be less than ${maxFeedbackLen} characters`), }) -type ReportForm = yup.InferType +type ReportForm = z.infer export function InaccuracyReport({ barometer, ...props }: Props) { - const queryClient = useQueryClient() const [isOpened, setIsOpened] = React.useState(false) - const { - register, - handleSubmit, - watch, - reset, - formState: { errors, isSubmitting }, - } = useForm({ - resolver: yupResolver(validationSchema), + const [isPending, startTransition] = useTransition() + + const form = useForm({ + mode: 'onSubmit', + reValidateMode: 'onChange', + resolver: zodResolver(validationSchema), defaultValues: { reporterName: '', reporterEmail: '', description: '' }, - mode: 'onBlur', }) - const { mutate, isPending } = useMutation({ - mutationFn: createReport, - onSuccess: ({ id }) => { - queryClient.invalidateQueries({ queryKey: ['inaccuracyReport'] }) - setIsOpened(false) - reset() - toast.success( - `Thank you! Your report was registered with ID ${id}. We will contact you at the provided email`, - ) - }, - onError: err => toast.error(err.message), - }) - const onSubmit = handleSubmit(values => { - mutate({ ...values, barometerId: barometer.id }) + const onSubmit = form.handleSubmit(values => { + startTransition(async () => { + try { + const result = await createReport({ ...values, barometerId: barometer.id }) + setIsOpened(false) + form.reset() + toast.success( + `Thank you! Your report was registered with ID ${result.id}. We will contact you at the provided email`, + ) + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Failed to submit report') + } + }) }) - const descriptionValue = watch('description') ?? '' + const descriptionValue = form.watch('description') ?? '' const symbolsLeft = maxFeedbackLen - descriptionValue.length + useEffect(() => { + if (!isOpened) return + form.reset() + }, [form.reset, isOpened]) + return ( <> - - - + + + + Report issues in the description of {barometer.name} - - - - - - - Report Inaccuracy in {barometer.name} - - - We will contact you using the provided email. - - -
-
- Name - + + + + + Report inaccuracy + + Found an error in the description of {barometer.name}? Let us know. + + + + + ( + + Name + + + + + + )} /> - {errors.reporterName && ( -

{errors.reporterName.message}

- )} -
-
- Email - ( + + Email + + + + + + )} /> - {errors.reporterEmail && ( -

- {errors.reporterEmail.message} -

- )} -
-
- Feedback - ( + + Feedback + +