diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml new file mode 100644 index 0000000..f06f991 --- /dev/null +++ b/.github/workflows/check-formatting.yml @@ -0,0 +1,13 @@ +name: Pre-commit Checks + +on: + pull_request: + paths: + - '**/*.{js,jsx,ts,tsx,json,css,scss,md}' + +jobs: + pre-commit-checks: + runs-on: ubuntu-latest + steps: + - name: Run Prettier + run: yarn format-check diff --git a/.gitignore b/.gitignore index 6381d42..7f40deb 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ yarn-error.log* next-env.d.ts userbase.csv -pscale_dump_apunts-dades_main_20240310_112108 \ No newline at end of file +pscale_dump_apunts-dades_main_20240310_112108 +prisma/migrations diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..900f52e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": false, + "semi": false, + "tabWidth": 2, + "printWidth": 80 +} diff --git a/.vscode/settings.json b/.vscode/settings.json index d067910..fae8e3d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true -} \ No newline at end of file +} diff --git a/.vscode/typescriptreact.json.code-snippets b/.vscode/typescriptreact.json.code-snippets index 3dcc777..d629595 100644 --- a/.vscode/typescriptreact.json.code-snippets +++ b/.vscode/typescriptreact.json.code-snippets @@ -1,4 +1,5 @@ -"Typescript React Function Component": { +{ + "Typescript React Function Component": { "prefix": "fc", "body": [ "import { FC } from 'react'", @@ -11,7 +12,8 @@ " return
$TM_FILENAME_BASE
", "}", "", - "export default $TM_FILENAME_BASE" + "export default $TM_FILENAME_BASE", ], - "description": "Typescript React Function Component" - }, \ No newline at end of file + "description": "Typescript React Function Component", + }, +} diff --git a/README.md b/README.md index 28b3c1c..2f1ca0a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # Benvinguts al repo d'Apunts Dades + ## M'agradaria contribuir: + Obre un [Issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-an-issue) explicant una mica què t'agradaria que s'afegís, preferiblement [etiqueta l'issue segons el tipus](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels). Si t'animes a fer la implementació de qualsevol issue no assignat a ningú afegeix un comentari avisant que estàs en ello i quan estigui resolt envia una [PR enllaçada a l'Issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) explicant una mica com ho has solucionat. No fa falta que estigui tot perfecte ni que tinguis molta experiència en software. Des de l'AED animem a tothom qui en tingui ganes a contribuir i intentarem donar suport en la mesura del que ens sigui possible amb els recursos dels que disposem. -Si necessites credencials de dev per alguna api escriu-nos. \ No newline at end of file +Si necessites credencials de dev per alguna api escriu-nos. diff --git a/components.json b/components.json index c19f2eb..1e8d33a 100644 --- a/components.json +++ b/components.json @@ -14,4 +14,4 @@ "components": "@/components", "utils": "@/lib/utils" } -} \ No newline at end of file +} diff --git a/next.config.js b/next.config.js index 39656ff..569946b 100644 --- a/next.config.js +++ b/next.config.js @@ -1,11 +1,11 @@ /** @type {import('next').NextConfig} */ const nextConfig = { images: { - domains: ['uploadthing.com', 'lh3.googleusercontent.com'], + domains: ["uploadthing.com", "lh3.googleusercontent.com"], }, experimental: { - appDir: true - } + appDir: true, + }, } module.exports = nextConfig diff --git a/package.json b/package.json index 97bdf36..5a31876 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,24 @@ "build": "prisma db push && prisma db seed && next build", "start": "next start", "lint": "next lint", - "postinstall": "prisma generate" + "postinstall": "prisma generate", + "format": "prettier --write .", + "format-check": "prettier --check ." }, "prisma": { "seed": "node prisma/seed.js" }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{js,jsx,ts,tsx,json,css,scss,md}": [ + "prettier --write", + "git add" + ] + }, "dependencies": { "@editorjs/attaches": "^1.3.0", "@editorjs/code": "^2.8.0", @@ -86,6 +99,9 @@ "devDependencies": { "@types/editorjs__header": "^2.6.0", "@types/lodash.debounce": "^4.0.7", + "husky": "^9.0.11", + "lint-staged": "^15.2.2", + "prettier": "^3.2.5", "prisma": "^5.9.1" } } diff --git a/prisma/accountVerify.js b/prisma/accountVerify.js index 3be883d..07bf185 100644 --- a/prisma/accountVerify.js +++ b/prisma/accountVerify.js @@ -1,29 +1,29 @@ -const { PrismaClient } = require("@prisma/client"); -const Papa = require("papaparse"); -const fs = require("fs"); +const { PrismaClient } = require("@prisma/client") +const Papa = require("papaparse") +const fs = require("fs") -const prisma = new PrismaClient(); +const prisma = new PrismaClient() async function processUserRequests() { - const userbasePath = "userbase.csv"; - const userBaseFileData = fs.readFileSync(userbasePath, "utf8"); - const verifiedValid = ["sí", "si"]; + const userbasePath = "userbase.csv" + const userBaseFileData = fs.readFileSync(userbasePath, "utf8") + const verifiedValid = ["sí", "si"] const parsedUserBase = Papa.parse(userBaseFileData, { header: true, // Assuming the first row contains the headers dynamicTyping: true, // Automatically convert strings to their appropriate data type skipEmptyLines: true, // Skip empty lines in the CSV complete: function async(result) { result.data.forEach(async (row) => { - const email = row.email; - const generacio = row.generacio; - const verificat = row.verificat; + const email = row.email + const generacio = row.generacio + const verificat = row.verificat if (verificat && verifiedValid.includes(verificat.toLowerCase())) { const existingUser = await prisma.authorizedUsers.findUnique({ where: { email: email, }, - }); + }) if (!existingUser) { await prisma.authorizedUsers.create({ data: { @@ -31,13 +31,13 @@ async function processUserRequests() { generacio: generacio, createdAt: new Date(), }, - }); + }) } } - }); + }) }, - }); - await prisma.$disconnect(); + }) + await prisma.$disconnect() } -processUserRequests(); \ No newline at end of file +processUserRequests() diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 29e23af..71d06f2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -57,7 +57,7 @@ model User { } model AuthorizedUsers { - email String @id + email String @id generacio Int createdAt DateTime @default(now()) } @@ -86,26 +86,27 @@ model Question { updatedAt DateTime @updatedAt subjectId String authorId String - subject Subject @relation(fields: [subjectId], references: [id]) + subject Subject @relation(fields: [subjectId], references: [id]) author User @relation(fields: [authorId], references: [id]) answers Answer[] votes QuestionVote[] } model Post { - id String @id @default(cuid()) - title String - content String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - subjectId String - authorId String - tipus TipusType - year Int - subject Subject @relation(fields: [subjectId], references: [id]) - author User @relation(fields: [authorId], references: [id]) - comments Comment[] - votes PostVote[] + id String @id @default(cuid()) + title String + content String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + subjectId String + authorId String + tipus TipusType + year Int + isAnonymous Boolean @default(false) + subject Subject @relation(fields: [subjectId], references: [id]) + author User @relation(fields: [authorId], references: [id]) + comments Comment[] + votes PostVote[] } model Answer { diff --git a/prisma/seed.js b/prisma/seed.js index 2145ee0..cc30397 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -1,59 +1,204 @@ -const { PrismaClient } = require('@prisma/client') +const { PrismaClient } = require("@prisma/client") const prisma = new PrismaClient() async function main() { const aed = await prisma.user.upsert({ - where: { email: "info@aed.cat" }, - update: {}, - create: { - email: "info@aed.cat", - name: "Associació d'Estudiants de Dades", - username: "AED", - generacio: 2017, - }, - }); - const createManySubjects = await prisma.subject.createMany({ - data: [ - { name: "Àlgebra", acronym: "ALG", semester: "Q1", creatorId: aed.id }, - { name: "Algorísmia i Programació I", acronym: "AP1", semester: "Q1", creatorId: aed.id }, - { name: "Càlcul", acronym: "CAL", semester: "Q1", creatorId: aed.id }, - { name: "Lògica i Matemàtica Discreta", acronym: "LMD", semester: "Q1", creatorId: aed.id }, - { name: "Àlgebra i Càlcul Avançats", acronym: "AC2", semester: "Q2", creatorId: aed.id }, - { name: "Algorísmia i Programació II", acronym: "AP2", semester: "Q2", creatorId: aed.id }, - { name: "Computadors", acronym: "COM", semester: "Q2", creatorId: aed.id }, - { name: "Probabilitat i Estadística I", acronym: "PIE1", semester: "Q2", creatorId: aed.id }, - { name: "Algorísmia i Programació III", acronym: "AP3", semester: "Q3", creatorId: aed.id }, - { name: "Bases de Dades", acronym: "BD", semester: "Q3", creatorId: aed.id }, - { name: "Probabilitat i Estadística II", acronym: "PIE2", semester: "Q3", creatorId: aed.id }, - { name: "Senyals i Sistemes", acronym: "SIS", semester: "Q3", creatorId: aed.id }, - { name: "Teoria de la Informació", acronym: "TEOI", semester: "Q3", creatorId: aed.id }, - { name: "Aprenentatge Automàtic I", acronym: "AA1", semester: "Q4", creatorId: aed.id }, - { name: "Anàlisi de Dades", acronym: "AD", semester: "Q4", creatorId: aed.id }, - { name: "Introducció al Processament Audiovisual", acronym: "IPA", semester: "Q4", creatorId: aed.id }, - { name: "Optimització Matemàtica", acronym: "OM", semester: "Q4", creatorId: aed.id }, - { name: "Paral·lelisme i Sistemes Distribuïts", acronym: "PSD", semester: "Q4", creatorId: aed.id }, - { name: "Aprenentatge Automàtic II", acronym: "AA2", semester: "Q5", creatorId: aed.id }, - { name: "Bases de Dades Avançades", acronym: "BDA", semester: "Q5", creatorId: aed.id }, - { name: "Cerca i Anàlisi de la Informació", acronym: "CAI", semester: "Q5", creatorId: aed.id }, - { name: "Emprenedoria i Innovació", acronym: "EI", semester: "Q5", creatorId: aed.id }, - { name: "Visualització de la Informació", acronym: "VI", semester: "Q5", creatorId: aed.id }, - { name: "Projectes d'Enginyeria", acronym: "PE", semester: "Q6", creatorId: aed.id }, - { name: "Processament d'Imatge i Visió Artificial", acronym: "PIVA", semester: "Q6", creatorId: aed.id }, - { name: "Processament del Llenguatge Oral i Escrit", acronym: "POE", semester: "Q6", creatorId: aed.id }, - { name: "Temes Avançats d'Enginyeria de Dades I", acronym: "TAED1", semester: "Q6", creatorId: aed.id }, - { name: "Temes Avançats d'Enginyeria de Dades II", acronym: "TAED2", semester: "Q7", creatorId: aed.id }, - { name: "Altres", acronym: "Altres", semester: "Q8", creatorId: aed.id }, - ], - skipDuplicates: true, - }); - const AuthorizedUsers = await prisma.authorizedUsers.createMany({ - data: [ - { email: "pau.matas@estudiantat.upc.edu", generacio: 2019 }, - { email: "aina.luis@estudiantat.upc.edu", generacio: 2020 }, - { email: "pol.puigdemont@estudiantat.upc.edu", generacio: 2020 }, - ], - skipDuplicates: true, - }); + where: { email: "info@aed.cat" }, + update: {}, + create: { + email: "info@aed.cat", + name: "Associació d'Estudiants de Dades", + username: "AED", + generacio: 2017, + }, + }) + const createManySubjects = await prisma.subject.createMany({ + data: [ + { + name: "Àlgebra", + acronym: "ALG", + semester: "Q1", + creatorId: aed.id, + }, + { + name: "Algorísmia i Programació I", + acronym: "AP1", + semester: "Q1", + creatorId: aed.id, + }, + { + name: "Càlcul", + acronym: "CAL", + semester: "Q1", + creatorId: aed.id, + }, + { + name: "Lògica i Matemàtica Discreta", + acronym: "LMD", + semester: "Q1", + creatorId: aed.id, + }, + { + name: "Àlgebra i Càlcul Avançats", + acronym: "AC2", + semester: "Q2", + creatorId: aed.id, + }, + { + name: "Algorísmia i Programació II", + acronym: "AP2", + semester: "Q2", + creatorId: aed.id, + }, + { + name: "Computadors", + acronym: "COM", + semester: "Q2", + creatorId: aed.id, + }, + { + name: "Probabilitat i Estadística I", + acronym: "PIE1", + semester: "Q2", + creatorId: aed.id, + }, + { + name: "Algorísmia i Programació III", + acronym: "AP3", + semester: "Q3", + creatorId: aed.id, + }, + { + name: "Bases de Dades", + acronym: "BD", + semester: "Q3", + creatorId: aed.id, + }, + { + name: "Probabilitat i Estadística II", + acronym: "PIE2", + semester: "Q3", + creatorId: aed.id, + }, + { + name: "Senyals i Sistemes", + acronym: "SIS", + semester: "Q3", + creatorId: aed.id, + }, + { + name: "Teoria de la Informació", + acronym: "TEOI", + semester: "Q3", + creatorId: aed.id, + }, + { + name: "Aprenentatge Automàtic I", + acronym: "AA1", + semester: "Q4", + creatorId: aed.id, + }, + { + name: "Anàlisi de Dades", + acronym: "AD", + semester: "Q4", + creatorId: aed.id, + }, + { + name: "Introducció al Processament Audiovisual", + acronym: "IPA", + semester: "Q4", + creatorId: aed.id, + }, + { + name: "Optimització Matemàtica", + acronym: "OM", + semester: "Q4", + creatorId: aed.id, + }, + { + name: "Paral·lelisme i Sistemes Distribuïts", + acronym: "PSD", + semester: "Q4", + creatorId: aed.id, + }, + { + name: "Aprenentatge Automàtic II", + acronym: "AA2", + semester: "Q5", + creatorId: aed.id, + }, + { + name: "Bases de Dades Avançades", + acronym: "BDA", + semester: "Q5", + creatorId: aed.id, + }, + { + name: "Cerca i Anàlisi de la Informació", + acronym: "CAI", + semester: "Q5", + creatorId: aed.id, + }, + { + name: "Emprenedoria i Innovació", + acronym: "EI", + semester: "Q5", + creatorId: aed.id, + }, + { + name: "Visualització de la Informació", + acronym: "VI", + semester: "Q5", + creatorId: aed.id, + }, + { + name: "Projectes d'Enginyeria", + acronym: "PE", + semester: "Q6", + creatorId: aed.id, + }, + { + name: "Processament d'Imatge i Visió Artificial", + acronym: "PIVA", + semester: "Q6", + creatorId: aed.id, + }, + { + name: "Processament del Llenguatge Oral i Escrit", + acronym: "POE", + semester: "Q6", + creatorId: aed.id, + }, + { + name: "Temes Avançats d'Enginyeria de Dades I", + acronym: "TAED1", + semester: "Q6", + creatorId: aed.id, + }, + { + name: "Temes Avançats d'Enginyeria de Dades II", + acronym: "TAED2", + semester: "Q7", + creatorId: aed.id, + }, + { + name: "Altres", + acronym: "Altres", + semester: "Q8", + creatorId: aed.id, + }, + ], + skipDuplicates: true, + }) + const AuthorizedUsers = await prisma.authorizedUsers.createMany({ + data: [ + { email: "pau.matas@estudiantat.upc.edu", generacio: 2019 }, + { email: "aina.luis@estudiantat.upc.edu", generacio: 2020 }, + { email: "pol.puigdemont@estudiantat.upc.edu", generacio: 2020 }, + ], + skipDuplicates: true, + }) } main() .then(async () => { @@ -64,4 +209,3 @@ main() await prisma.$disconnect() process.exit(1) }) - \ No newline at end of file diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx index b46a783..5f69528 100644 --- a/src/app/(auth)/sign-in/page.tsx +++ b/src/app/(auth)/sign-in/page.tsx @@ -1,24 +1,28 @@ -import SignIn from "@/components/SignIn"; -import { buttonVariants } from "@/components/ui/Button"; -import { cn } from "@/lib/utils"; -import { ChevronLeft } from "lucide-react"; -import Link from "next/link"; +import SignIn from "@/components/SignIn" +import { buttonVariants } from "@/components/ui/Button" +import { cn } from "@/lib/utils" +import { ChevronLeft } from "lucide-react" +import Link from "next/link" const page = () => { - return ( -
-
- - - Home - + return ( +
+
+ + + Home + - -
-
- ); -}; + +
+
+ ) +} -export default page; +export default page diff --git a/src/app/@authModal/(.)sign-in/page.tsx b/src/app/@authModal/(.)sign-in/page.tsx index d632d5c..3b1dfbf 100644 --- a/src/app/@authModal/(.)sign-in/page.tsx +++ b/src/app/@authModal/(.)sign-in/page.tsx @@ -1,23 +1,23 @@ -import CloseModal from "@/components/CloseModal"; -import SignIn from "@/components/SignIn"; -import { FC } from "react"; +import CloseModal from "@/components/CloseModal" +import SignIn from "@/components/SignIn" +import { FC } from "react" interface pageProps {} const page: FC = ({}) => { - return ( -
-
-
-
- -
+ return ( +
+
+
+
+ +
- -
-
-
- ); -}; + +
+
+
+ ) +} -export default page; +export default page diff --git a/src/app/@authModal/default.tsx b/src/app/@authModal/default.tsx index 3e4bcd3..86b9e9a 100644 --- a/src/app/@authModal/default.tsx +++ b/src/app/@authModal/default.tsx @@ -1,3 +1,3 @@ export default function Default() { - return null; + return null } diff --git a/src/app/[slug]/layout.tsx b/src/app/[slug]/layout.tsx index cf9100a..19c7362 100644 --- a/src/app/[slug]/layout.tsx +++ b/src/app/[slug]/layout.tsx @@ -1,194 +1,213 @@ -import SubscribeLeaveToggle from "@/components/SubscribeLeaveToggle"; -import { buttonVariants } from "@/components/ui/Button"; -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { HomeIcon, FileQuestionIcon, FileTextIcon, InfoIcon } from "lucide-react"; -import Link from "next/link"; -import { notFound } from "next/navigation"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; - -const Layout = async ({ children, params: { slug } }: { children: React.ReactNode; params: { slug: string } }) => { - const session = await getAuthSession(); - - const subject = await db.subject.findFirst({ - where: { acronym: slug }, - include: { - posts: { - include: { - author: true, - votes: true, - }, - }, - questions: { - include: { - answers: true, - }, - }, - }, - }); - - const subscription = !session?.user - ? undefined - : await db.subscription.findFirst({ - where: { - subject: { - acronym: slug, - }, - user: { - id: session.user.id, - }, - }, - }); - - const isSubscribed = !!subscription; - - if (!subject) return notFound(); - - const memberCount = await db.subscription.count({ - where: { - subject: { - acronym: slug, - }, - }, - }); - - const mostRecentPostYear = subject.posts.reduce((acc, post) => { - if (post.year > acc) return post.year; - return acc; - }, 0); - - const questionCount = await db.question.count({ - where: { - subject: { - acronym: slug, - }, - }, - }); - - const unAnsweredQuestionCount = await db.question.count({ - where: { - subject: { - acronym: slug, - }, - answers: { - none: {}, - }, - }, - }); - - const startsWithVowel = /^[aeiouàáâãäåæçèéêëìíîïðòóôõöøùúûüýÿ]/i; - const subjectNameArticle = subject.name.match(startsWithVowel) ? "d'" : "de "; - - return ( -
-
- - - Inici - - - - - Apunts - - - - - Preguntes - - - - - - - - Més sobre els apunts {subjectNameArticle} - {subject.name} - - - -
-
-

- Apunts {subjectNameArticle} - {subject.name} -

-
- -
-
-
Membres
-
{memberCount}
-
-
-
Subscripció
-
{isSubscribed ? "Subscrit" : "No subscrit"}
-
-
-
Quadrimestre
-
{subject.semester}
-
-
-
Apunts més recents
-
{mostRecentPostYear}
-
-
-
Preguntes
-
{questionCount}
-
-
-
Preguntes sense respondre
-
{unAnsweredQuestionCount}
-
- {subject.creatorId === session?.user?.id ? ( -
-

Pots editar aquesta assignatura

-
- ) : null} - - {subject.creatorId !== session?.user?.id ? ( - - ) : null} - - - Comparteix Apunts - - - - Llança una pregunta - -
-
-
-
-
- -
-
{children}
-
-
-
- ); -}; - -export default Layout; +import SubscribeLeaveToggle from "@/components/SubscribeLeaveToggle" +import { buttonVariants } from "@/components/ui/Button" +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { + HomeIcon, + FileQuestionIcon, + FileTextIcon, + InfoIcon, +} from "lucide-react" +import Link from "next/link" +import { notFound } from "next/navigation" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +const Layout = async ({ + children, + params: { slug }, +}: { + children: React.ReactNode + params: { slug: string } +}) => { + const session = await getAuthSession() + + const subject = await db.subject.findFirst({ + where: { acronym: slug }, + include: { + posts: { + include: { + author: true, + votes: true, + }, + }, + questions: { + include: { + answers: true, + }, + }, + }, + }) + + const subscription = !session?.user + ? undefined + : await db.subscription.findFirst({ + where: { + subject: { + acronym: slug, + }, + user: { + id: session.user.id, + }, + }, + }) + + const isSubscribed = !!subscription + + if (!subject) return notFound() + + const memberCount = await db.subscription.count({ + where: { + subject: { + acronym: slug, + }, + }, + }) + + const mostRecentPostYear = subject.posts.reduce((acc, post) => { + if (post.year > acc) return post.year + return acc + }, 0) + + const questionCount = await db.question.count({ + where: { + subject: { + acronym: slug, + }, + }, + }) + + const unAnsweredQuestionCount = await db.question.count({ + where: { + subject: { + acronym: slug, + }, + answers: { + none: {}, + }, + }, + }) + + const startsWithVowel = /^[aeiouàáâãäåæçèéêëìíîïðòóôõöøùúûüýÿ]/i + const subjectNameArticle = subject.name.match(startsWithVowel) ? "d'" : "de " + + return ( +
+
+ + + Inici + + + + + Apunts + + + + + Preguntes + + + + + + + + Més sobre els apunts {subjectNameArticle} + {subject.name} + + + +
+
+

+ Apunts {subjectNameArticle} + {subject.name} +

+
+ +
+
+
Membres
+
{memberCount}
+
+
+
Subscripció
+
+ {isSubscribed ? "Subscrit" : "No subscrit"} +
+
+
+
Quadrimestre
+
{subject.semester}
+
+
+
Apunts més recents
+
{mostRecentPostYear}
+
+
+
Preguntes
+
{questionCount}
+
+
+
Preguntes sense respondre
+
{unAnsweredQuestionCount}
+
+ {subject.creatorId === session?.user?.id ? ( +
+

+ Pots editar aquesta assignatura +

+
+ ) : null} + + {subject.creatorId !== session?.user?.id ? ( + + ) : null} + + + Comparteix Apunts + + + + Llança una pregunta + +
+
+
+
+
+ +
+
{children}
+
+
+
+ ) +} + +export default Layout diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index 88a7402..e1d174a 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -1,57 +1,54 @@ -import MiniCreatePost from "@/components/MiniCreatePost"; -import PostFeed from "@/components/PostFeed"; -import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config"; -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { notFound } from "next/navigation"; +import MiniCreatePost from "@/components/MiniCreatePost" +import PostFeed from "@/components/PostFeed" +import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { notFound } from "next/navigation" interface PageProps { - params: { - slug: string; - }; + params: { + slug: string + } } const page = async ({ params }: PageProps) => { - const { slug } = params; - - const session = await getAuthSession(); - - const subject = await db.subject.findFirst({ - where: { acronym: slug }, - include: { - posts: { - include: { - author: true, - votes: true, - comments: true, - subject: true, - }, - orderBy: { - createdAt: "desc", - }, - - take: INFINITE_SCROLL_PAGINATION_RESULTS, - }, - }, - }); - - if (!subject) return notFound(); - - return ( - <> -

- {subject.acronym}/ - {subject.name} -

- - - - - - ); -}; - -export default page; + const { slug } = params + + const session = await getAuthSession() + + const subject = await db.subject.findFirst({ + where: { acronym: slug }, + include: { + posts: { + include: { + author: true, + votes: true, + comments: true, + subject: true, + }, + orderBy: { + createdAt: "desc", + }, + + take: INFINITE_SCROLL_PAGINATION_RESULTS, + }, + }, + }) + + if (!subject) return notFound() + + return ( + <> +

+ {subject.acronym}/ + {subject.name} +

+ + + + + + ) +} + +export default page diff --git a/src/app/[slug]/post/[postId]/page.tsx b/src/app/[slug]/post/[postId]/page.tsx index c05becf..a5b5c7d 100644 --- a/src/app/[slug]/post/[postId]/page.tsx +++ b/src/app/[slug]/post/[postId]/page.tsx @@ -1,54 +1,54 @@ -import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config"; -import { db } from "@/lib/db"; -import { notFound } from "next/navigation"; -import { PostView } from "@/components/PostView"; +import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" +import { db } from "@/lib/db" +import { notFound } from "next/navigation" +import { PostView } from "@/components/PostView" interface PageProps { - params: { - slug: string; - postId: string; - }; + params: { + slug: string + postId: string + } } const page = async ({ params }: PageProps) => { - const { slug, postId } = params; - const post = await db.post.findFirst({ - where: { id: postId, subject: { acronym: slug } }, - include: { - author: true, - votes: true, - subject: true, - comments: { - include: { - post: true, - votes: true, - author: true, - }, - orderBy: { - createdAt: "desc", - }, - take: INFINITE_SCROLL_PAGINATION_RESULTS, - }, - }, - }); - const comments = await db.comment.findMany({ - where: { - postId: postId, - }, - include: { - author: true, - votes: true, - }, - orderBy: { - createdAt: "asc", - }, - }); - if (!post) return notFound(); - return ( -
- -
- ); -}; + const { slug, postId } = params + const post = await db.post.findFirst({ + where: { id: postId, subject: { acronym: slug } }, + include: { + author: true, + votes: true, + subject: true, + comments: { + include: { + post: true, + votes: true, + author: true, + }, + orderBy: { + createdAt: "desc", + }, + take: INFINITE_SCROLL_PAGINATION_RESULTS, + }, + }, + }) + const comments = await db.comment.findMany({ + where: { + postId: postId, + }, + include: { + author: true, + votes: true, + }, + orderBy: { + createdAt: "asc", + }, + }) + if (!post) return notFound() + return ( +
+ +
+ ) +} -export default page; +export default page diff --git a/src/app/[slug]/q/[questionId]/page.tsx b/src/app/[slug]/q/[questionId]/page.tsx index 14e2af7..df56f25 100644 --- a/src/app/[slug]/q/[questionId]/page.tsx +++ b/src/app/[slug]/q/[questionId]/page.tsx @@ -1,63 +1,60 @@ -import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config"; -import { db } from "@/lib/db"; -import { notFound } from "next/navigation"; -import { AnswersView } from "@/components/QuestionView"; +import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" +import { db } from "@/lib/db" +import { notFound } from "next/navigation" +import { AnswersView } from "@/components/QuestionView" interface PageProps { - params: { - slug: string; - questionId: string; - }; + params: { + slug: string + questionId: string + } } const page = async ({ params }: PageProps) => { - const { slug, questionId } = params; - const question = await db.question.findFirst({ - where: { id: questionId, subject: { acronym: slug } }, - include: { - subject: true, - votes: true, - author: true, - answers: { - include: { - question: true, - votes: true, - author: true, - }, - orderBy: { - createdAt: "desc", - }, - take: INFINITE_SCROLL_PAGINATION_RESULTS, - }, - }, - }); - if (!question || question.subject === null) return notFound(); - const answers = await db.answer.findMany({ - where: { - questionId: questionId, - }, - include: { - question: true, - votes: true, - author: true, - }, - orderBy: [ - { - accepted: "desc", - }, - { - createdAt: "asc", - }, - ], - }); - return ( -
- -
- ); -}; + const { slug, questionId } = params + const question = await db.question.findFirst({ + where: { id: questionId, subject: { acronym: slug } }, + include: { + subject: true, + votes: true, + author: true, + answers: { + include: { + question: true, + votes: true, + author: true, + }, + orderBy: { + createdAt: "desc", + }, + take: INFINITE_SCROLL_PAGINATION_RESULTS, + }, + }, + }) + if (!question || question.subject === null) return notFound() + const answers = await db.answer.findMany({ + where: { + questionId: questionId, + }, + include: { + question: true, + votes: true, + author: true, + }, + orderBy: [ + { + accepted: "desc", + }, + { + createdAt: "asc", + }, + ], + }) + return ( +
+ +
+ ) +} -export default page; +export default page diff --git a/src/app/[slug]/q/page.tsx b/src/app/[slug]/q/page.tsx index 493defd..07dc4cb 100644 --- a/src/app/[slug]/q/page.tsx +++ b/src/app/[slug]/q/page.tsx @@ -1,56 +1,53 @@ -import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config"; -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import MiniCreateQuestion from "@/components/MiniCreateQuestion"; -import QuestionFeed from "@/components/QuestionFeed"; -import { notFound } from "next/navigation"; +import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import MiniCreateQuestion from "@/components/MiniCreateQuestion" +import QuestionFeed from "@/components/QuestionFeed" +import { notFound } from "next/navigation" interface PageProps { - params: { - slug: string; - }; + params: { + slug: string + } } const page = async ({ params }: PageProps) => { - const { slug } = params; - const session = await getAuthSession(); - const subject = await db.subject.findFirst({ - where: { acronym: slug }, - include: { - questions: { - include: { - author: true, - votes: true, - subject: true, - answers: true, - }, - orderBy: { - createdAt: "desc", - }, - take: INFINITE_SCROLL_PAGINATION_RESULTS, - }, - }, - }); + const { slug } = params + const session = await getAuthSession() + const subject = await db.subject.findFirst({ + where: { acronym: slug }, + include: { + questions: { + include: { + author: true, + votes: true, + subject: true, + answers: true, + }, + orderBy: { + createdAt: "desc", + }, + take: INFINITE_SCROLL_PAGINATION_RESULTS, + }, + }, + }) - if (!subject) return notFound(); + if (!subject) return notFound() - return ( - <> -

- {subject.acronym} Questions: -

+ return ( + <> +

+ {subject.acronym} Questions: +

- + - - - ); -}; + + + ) +} -export default page; +export default page diff --git a/src/app/[slug]/submit/page.tsx b/src/app/[slug]/submit/page.tsx index 2003ee5..2d0d22b 100644 --- a/src/app/[slug]/submit/page.tsx +++ b/src/app/[slug]/submit/page.tsx @@ -1,34 +1,34 @@ -import { SmallProfileForm } from "@/components/Form"; -import { db } from "@/lib/db"; -import { notFound } from "next/navigation"; +import { SmallProfileForm } from "@/components/Form" +import { db } from "@/lib/db" +import { notFound } from "next/navigation" interface PageProps { - params: { - slug: string; - }; + params: { + slug: string + } } const page = async ({ params }: PageProps) => { - const { slug } = params; + const { slug } = params - const subject = await db.subject.findFirst({ - where: { acronym: slug }, - }); + const subject = await db.subject.findFirst({ + where: { acronym: slug }, + }) - if (!subject) return notFound(); + if (!subject) return notFound() - const startsWithVowel = /^[aeiouàáâãäåæçèéêëìíîïðòóôõöøùúûüýÿ]/i; - const subjectNameArticle = subject.name.match(startsWithVowel) ? "d'" : "de "; + const startsWithVowel = /^[aeiouàáâãäåæçèéêëìíîïðòóôõöøùúûüýÿ]/i + const subjectNameArticle = subject.name.match(startsWithVowel) ? "d'" : "de " - return ( - <> -

- Penja apunts {subjectNameArticle} - {subject.name} -

- - - ); -}; + return ( + <> +

+ Penja apunts {subjectNameArticle} + {subject.name} +

+ + + ) +} -export default page; +export default page diff --git a/src/app/api/a/route.ts b/src/app/api/a/route.ts index e79b0bb..ceee2bd 100644 --- a/src/app/api/a/route.ts +++ b/src/app/api/a/route.ts @@ -1,87 +1,87 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { z } from "zod" export async function GET(req: Request) { - const url = new URL(req.url); + const url = new URL(req.url) - const session = await getAuthSession(); + const session = await getAuthSession() - let followedCommunitiesIds: string[] = []; + let followedCommunitiesIds: string[] = [] - if (session && session.user) { - const followedCommunities = await db.subscription.findMany({ - where: { - userId: session.user.id, - }, - include: { - subject: true, - }, - }); + if (session && session.user) { + const followedCommunities = await db.subscription.findMany({ + where: { + userId: session.user.id, + }, + include: { + subject: true, + }, + }) - followedCommunitiesIds = followedCommunities.map((sub) => sub.subject.id); - } + followedCommunitiesIds = followedCommunities.map((sub) => sub.subject.id) + } - try { - const { limit, page, subjectAcronym, questionId } = z - .object({ - limit: z.string(), - page: z.string(), - subjectAcronym: z.string().nullish().optional(), - questionId: z.string().nullish().optional(), - }) - .parse({ - subjectAcronym: url.searchParams.get("subjectAcronym"), - limit: url.searchParams.get("limit"), - page: url.searchParams.get("page"), - questionId: url.searchParams.get("questionId"), - }); + try { + const { limit, page, subjectAcronym, questionId } = z + .object({ + limit: z.string(), + page: z.string(), + subjectAcronym: z.string().nullish().optional(), + questionId: z.string().nullish().optional(), + }) + .parse({ + subjectAcronym: url.searchParams.get("subjectAcronym"), + limit: url.searchParams.get("limit"), + page: url.searchParams.get("page"), + questionId: url.searchParams.get("questionId"), + }) - let whereClause = {}; + let whereClause = {} - if (subjectAcronym) { - whereClause = { - subject: { - acronym: subjectAcronym, - }, - }; - } else if (session) { - whereClause = { - subject: { - id: { - in: followedCommunitiesIds, - }, - }, - }; - } - if (questionId) { - whereClause = { - question: { - id: questionId, - }, - }; - } + if (subjectAcronym) { + whereClause = { + subject: { + acronym: subjectAcronym, + }, + } + } else if (session) { + whereClause = { + subject: { + id: { + in: followedCommunitiesIds, + }, + }, + } + } + if (questionId) { + whereClause = { + question: { + id: questionId, + }, + } + } - const answers = await db.answer.findMany({ - take: parseInt(limit), - skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 - orderBy: [ - { - accepted: "desc", - }, - { - createdAt: "asc", - }, - ], - include: { - votes: true, - author: true, - question: true, - }, - where: whereClause, - }); - return new Response(JSON.stringify(answers)); - } catch (error) { - return new Response("Could not fetch answers", { status: 500 }); - } + const answers = await db.answer.findMany({ + take: parseInt(limit), + skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 + orderBy: [ + { + accepted: "desc", + }, + { + createdAt: "asc", + }, + ], + include: { + votes: true, + author: true, + question: true, + }, + where: whereClause, + }) + return new Response(JSON.stringify(answers)) + } catch (error) { + return new Response("Could not fetch answers", { status: 500 }) + } } diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 347f967..68933da 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -3,4 +3,4 @@ import NextAuth from "next-auth/next" const handler = NextAuth(authOptions) -export {handler as GET, handler as POST} \ No newline at end of file +export { handler as GET, handler as POST } diff --git a/src/app/api/comments/route.ts b/src/app/api/comments/route.ts index 61874a2..2ecb808 100644 --- a/src/app/api/comments/route.ts +++ b/src/app/api/comments/route.ts @@ -1,82 +1,82 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { z } from "zod" export async function GET(req: Request) { - const url = new URL(req.url); + const url = new URL(req.url) - const session = await getAuthSession(); + const session = await getAuthSession() - let followedCommunitiesIds: string[] = []; + let followedCommunitiesIds: string[] = [] - if (session && session.user) { - const followedCommunities = await db.subscription.findMany({ - where: { - userId: session.user.id, - }, - include: { - subject: true, - }, - }); + if (session && session.user) { + const followedCommunities = await db.subscription.findMany({ + where: { + userId: session.user.id, + }, + include: { + subject: true, + }, + }) - followedCommunitiesIds = followedCommunities.map((sub) => sub.subject.id); - } + followedCommunitiesIds = followedCommunities.map((sub) => sub.subject.id) + } - try { - const { limit, page, subjectAcronym, postId } = z - .object({ - limit: z.string(), - page: z.string(), - subjectAcronym: z.string().nullish().optional(), - postId: z.string().nullish().optional(), - }) - .parse({ - subjectAcronym: url.searchParams.get("subjectAcronym"), - limit: url.searchParams.get("limit"), - page: url.searchParams.get("page"), - postId: url.searchParams.get("postId"), - }); + try { + const { limit, page, subjectAcronym, postId } = z + .object({ + limit: z.string(), + page: z.string(), + subjectAcronym: z.string().nullish().optional(), + postId: z.string().nullish().optional(), + }) + .parse({ + subjectAcronym: url.searchParams.get("subjectAcronym"), + limit: url.searchParams.get("limit"), + page: url.searchParams.get("page"), + postId: url.searchParams.get("postId"), + }) - let whereClause = {}; + let whereClause = {} - if (subjectAcronym) { - whereClause = { - subject: { - acronym: subjectAcronym, - }, - }; - } else if (session) { - whereClause = { - subject: { - id: { - in: followedCommunitiesIds, - }, - }, - }; - } - if (postId) { - whereClause = { - post: { - id: postId, - }, - }; - } + if (subjectAcronym) { + whereClause = { + subject: { + acronym: subjectAcronym, + }, + } + } else if (session) { + whereClause = { + subject: { + id: { + in: followedCommunitiesIds, + }, + }, + } + } + if (postId) { + whereClause = { + post: { + id: postId, + }, + } + } - const comments = await db.comment.findMany({ - take: parseInt(limit), - skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 - orderBy: { - createdAt: "desc", - }, - include: { - votes: true, - author: true, - post: true, - }, - where: whereClause, - }); - return new Response(JSON.stringify(comments)); - } catch (error) { - return new Response("Could not fetch comments", { status: 500 }); - } + const comments = await db.comment.findMany({ + take: parseInt(limit), + skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 + orderBy: { + createdAt: "desc", + }, + include: { + votes: true, + author: true, + post: true, + }, + where: whereClause, + }) + return new Response(JSON.stringify(comments)) + } catch (error) { + return new Response("Could not fetch comments", { status: 500 }) + } } diff --git a/src/app/api/link/route.ts b/src/app/api/link/route.ts index 2894dba..ee4daa5 100644 --- a/src/app/api/link/route.ts +++ b/src/app/api/link/route.ts @@ -1,36 +1,38 @@ -import axios from "axios"; +import axios from "axios" export async function GET(req: Request) { - const url = new URL(req.url); - const href = url.searchParams.get("url"); + const url = new URL(req.url) + const href = url.searchParams.get("url") - if (!href) { - return new Response("Invalid href", { status: 400 }); - } + if (!href) { + return new Response("Invalid href", { status: 400 }) + } - const res = await axios.get(href); + const res = await axios.get(href) - // Parse the HTML using regular expressions - const titleMatch = res.data.match(/(.*?)<\/title>/); - const title = titleMatch ? titleMatch[1] : ""; + // Parse the HTML using regular expressions + const titleMatch = res.data.match(/<title>(.*?)<\/title>/) + const title = titleMatch ? titleMatch[1] : "" - const descriptionMatch = res.data.match(/<meta name="description" content="(.*?)"/); - const description = descriptionMatch ? descriptionMatch[1] : ""; + const descriptionMatch = res.data.match( + /<meta name="description" content="(.*?)"/, + ) + const description = descriptionMatch ? descriptionMatch[1] : "" - const imageMatch = res.data.match(/<meta property="og:image" content="(.*?)"/); - const imageUrl = imageMatch ? imageMatch[1] : ""; + const imageMatch = res.data.match(/<meta property="og:image" content="(.*?)"/) + const imageUrl = imageMatch ? imageMatch[1] : "" - // Return the data in the format required by the editor tool - return new Response( - JSON.stringify({ - success: 1, - meta: { - title, - description, - image: { - url: imageUrl, - }, - }, - }) - ); + // Return the data in the format required by the editor tool + return new Response( + JSON.stringify({ + success: 1, + meta: { + title, + description, + image: { + url: imageUrl, + }, + }, + }), + ) } diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index 44742b7..61af3ed 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -1,75 +1,75 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { z } from "zod" export async function GET(req: Request) { - const url = new URL(req.url); + const url = new URL(req.url) - const session = await getAuthSession(); + const session = await getAuthSession() - let followedCommunitiesIds: string[] = []; + let followedCommunitiesIds: string[] = [] - if (session && session.user) { - const followedCommunities = await db.subscription.findMany({ - where: { - userId: session.user.id, - }, - include: { - subject: true, - }, - }); + if (session && session.user) { + const followedCommunities = await db.subscription.findMany({ + where: { + userId: session.user.id, + }, + include: { + subject: true, + }, + }) - followedCommunitiesIds = followedCommunities.map((sub) => sub.subject.id); - } + followedCommunitiesIds = followedCommunities.map((sub) => sub.subject.id) + } - try { - const { limit, page, subjectAcronym } = z - .object({ - limit: z.string(), - page: z.string(), - subjectAcronym: z.string().nullish().optional(), - }) - .parse({ - subjectAcronym: url.searchParams.get("subjectAcronym"), - limit: url.searchParams.get("limit"), - page: url.searchParams.get("page"), - }); + try { + const { limit, page, subjectAcronym } = z + .object({ + limit: z.string(), + page: z.string(), + subjectAcronym: z.string().nullish().optional(), + }) + .parse({ + subjectAcronym: url.searchParams.get("subjectAcronym"), + limit: url.searchParams.get("limit"), + page: url.searchParams.get("page"), + }) - let whereClause = {}; + let whereClause = {} - if (subjectAcronym) { - whereClause = { - subject: { - acronym: subjectAcronym, - }, - }; - } else if (session) { - whereClause = { - subject: { - id: { - in: followedCommunitiesIds, - }, - }, - }; - } + if (subjectAcronym) { + whereClause = { + subject: { + acronym: subjectAcronym, + }, + } + } else if (session) { + whereClause = { + subject: { + id: { + in: followedCommunitiesIds, + }, + }, + } + } - const posts = await db.post.findMany({ - take: parseInt(limit), - skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 - orderBy: { - createdAt: "desc", - }, - include: { - subject: true, - votes: true, - author: true, - comments: true, - }, - where: whereClause, - }); + const posts = await db.post.findMany({ + take: parseInt(limit), + skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 + orderBy: { + createdAt: "desc", + }, + include: { + subject: true, + votes: true, + author: true, + comments: true, + }, + where: whereClause, + }) - return new Response(JSON.stringify(posts)); - } catch (error) { - return new Response("Could not fetch posts", { status: 500 }); - } + return new Response(JSON.stringify(posts)) + } catch (error) { + return new Response("Could not fetch posts", { status: 500 }) + } } diff --git a/src/app/api/q/route.ts b/src/app/api/q/route.ts index 3f1cfb9..171846f 100644 --- a/src/app/api/q/route.ts +++ b/src/app/api/q/route.ts @@ -1,75 +1,75 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { z } from "zod" export async function GET(req: Request) { - const url = new URL(req.url); + const url = new URL(req.url) - const session = await getAuthSession(); + const session = await getAuthSession() - let followedCommunitiesIds: string[] = []; + let followedCommunitiesIds: string[] = [] - if (session && session.user) { - const followedCommunities = await db.subscription.findMany({ - where: { - userId: session.user.id, - }, - include: { - subject: true, - }, - }); + if (session && session.user) { + const followedCommunities = await db.subscription.findMany({ + where: { + userId: session.user.id, + }, + include: { + subject: true, + }, + }) - followedCommunitiesIds = followedCommunities.map((sub) => sub.subject.id); - } + followedCommunitiesIds = followedCommunities.map((sub) => sub.subject.id) + } - try { - const { limit, page, subjectAcronym } = z - .object({ - limit: z.string(), - page: z.string(), - subjectAcronym: z.string().nullish().optional(), - }) - .parse({ - subjectAcronym: url.searchParams.get("subjectAcronym"), - limit: url.searchParams.get("limit"), - page: url.searchParams.get("page"), - }); + try { + const { limit, page, subjectAcronym } = z + .object({ + limit: z.string(), + page: z.string(), + subjectAcronym: z.string().nullish().optional(), + }) + .parse({ + subjectAcronym: url.searchParams.get("subjectAcronym"), + limit: url.searchParams.get("limit"), + page: url.searchParams.get("page"), + }) - let whereClause = {}; + let whereClause = {} - if (subjectAcronym) { - whereClause = { - subject: { - acronym: subjectAcronym, - }, - }; - } else if (session) { - whereClause = { - subject: { - id: { - in: followedCommunitiesIds, - }, - }, - }; - } + if (subjectAcronym) { + whereClause = { + subject: { + acronym: subjectAcronym, + }, + } + } else if (session) { + whereClause = { + subject: { + id: { + in: followedCommunitiesIds, + }, + }, + } + } - const questions = await db.question.findMany({ - take: parseInt(limit), - skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 - orderBy: { - createdAt: "desc", - }, - include: { - subject: true, - votes: true, - author: true, - answers: true, - }, - where: whereClause, - }); + const questions = await db.question.findMany({ + take: parseInt(limit), + skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 + orderBy: { + createdAt: "desc", + }, + include: { + subject: true, + votes: true, + author: true, + answers: true, + }, + where: whereClause, + }) - return new Response(JSON.stringify(questions)); - } catch (error) { - return new Response("Could not fetch posts", { status: 500 }); - } + return new Response(JSON.stringify(questions)) + } catch (error) { + return new Response("Could not fetch posts", { status: 500 }) + } } diff --git a/src/app/api/subject/answer/accept/route.ts b/src/app/api/subject/answer/accept/route.ts index a34dbb8..e67b305 100644 --- a/src/app/api/subject/answer/accept/route.ts +++ b/src/app/api/subject/answer/accept/route.ts @@ -1,88 +1,91 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { AnswerAcceptedValidator } from "@/lib/validators/vote"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { AnswerAcceptedValidator } from "@/lib/validators/vote" +import { z } from "zod" export async function PATCH(req: Request) { - try { - const body = await req.json(); + try { + const body = await req.json() - const { answerId, accepted } = AnswerAcceptedValidator.parse(body); + const { answerId, accepted } = AnswerAcceptedValidator.parse(body) - const session = await getAuthSession(); + const session = await getAuthSession() - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } - // // check if user has already voted on this answer - // const existingAccepted = await db.answerVote.findFirst({ - // where: { - // userId: session.user.id, - // answerId, - // }, - // }); + // // check if user has already voted on this answer + // const existingAccepted = await db.answerVote.findFirst({ + // where: { + // userId: session.user.id, + // answerId, + // }, + // }); - const answer = await db.answer.findUnique({ - where: { - id: answerId, - }, - include: { - author: true, - votes: true, - }, - }); + const answer = await db.answer.findUnique({ + where: { + id: answerId, + }, + include: { + author: true, + votes: true, + }, + }) - if (!answer) { - return new Response("Answer not found", { status: 404 }); - } + if (!answer) { + return new Response("Answer not found", { status: 404 }) + } - const question = await db.question.findFirst({ - where: { - id: answer.questionId, - }, - select: { - author: true, - }, - }); + const question = await db.question.findFirst({ + where: { + id: answer.questionId, + }, + select: { + author: true, + }, + }) - if (question?.author.id !== session.user.id) { - return new Response("Unauthorized", { status: 401 }); - } + if (question?.author.id !== session.user.id) { + return new Response("Unauthorized", { status: 401 }) + } - if (answer.accepted) { - // if vote type is the same as existing vote, delete the vote - if (accepted) { - await db.answer.update({ - where: { - id: answerId, - }, - data: { - accepted: false, - }, - }); + if (answer.accepted) { + // if vote type is the same as existing vote, delete the vote + if (accepted) { + await db.answer.update({ + where: { + id: answerId, + }, + data: { + accepted: false, + }, + }) - return new Response("OK"); - } - } + return new Response("OK") + } + } - // if no existing vote, create a new vote - await db.answer.update({ - where: { - id: answerId, - }, - data: { - accepted: true, - }, - }); + // if no existing vote, create a new vote + await db.answer.update({ + where: { + id: answerId, + }, + data: { + accepted: true, + }, + }) - return new Response("OK"); - } catch (error) { - error; - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 400 }); - } + return new Response("OK") + } catch (error) { + error + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 400 }) + } - return new Response("Could not accept the answer at this time. Please try later", { status: 500 }); - } + return new Response( + "Could not accept the answer at this time. Please try later", + { status: 500 }, + ) + } } diff --git a/src/app/api/subject/answer/create/route.ts b/src/app/api/subject/answer/create/route.ts index 6b7ee08..c58ba95 100644 --- a/src/app/api/subject/answer/create/route.ts +++ b/src/app/api/subject/answer/create/route.ts @@ -1,33 +1,35 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { AnswerValidator } from "@/lib/validators/question"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { AnswerValidator } from "@/lib/validators/question" +import { z } from "zod" export async function POST(req: Request) { - try { - const session = await getAuthSession(); + try { + const session = await getAuthSession() - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } - const body = await req.json(); + const body = await req.json() - const { title, content, questionId } = AnswerValidator.parse(body); + const { title, content, questionId } = AnswerValidator.parse(body) - await db.answer.create({ - data: { - title: title, - content: content, - questionId: questionId, - authorId: session.user.id, - }, - }); - return new Response("Answer created", { status: 201 }); - } catch (error) { - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 422 }); - } - return new Response("Could not create post, please try again", { status: 500 }); - } + await db.answer.create({ + data: { + title: title, + content: content, + questionId: questionId, + authorId: session.user.id, + }, + }) + return new Response("Answer created", { status: 201 }) + } catch (error) { + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 422 }) + } + return new Response("Could not create post, please try again", { + status: 500, + }) + } } diff --git a/src/app/api/subject/answer/vote/route.ts b/src/app/api/subject/answer/vote/route.ts index dd56418..92af5c7 100644 --- a/src/app/api/subject/answer/vote/route.ts +++ b/src/app/api/subject/answer/vote/route.ts @@ -1,153 +1,155 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { redis } from "@/lib/redis"; -import { AnswerVoteValidator } from "@/lib/validators/vote"; -import { CachedAnswer } from "@/types/redis"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { redis } from "@/lib/redis" +import { AnswerVoteValidator } from "@/lib/validators/vote" +import { CachedAnswer } from "@/types/redis" +import { z } from "zod" -const CACHE_AFTER_UPVOTES = 1; +const CACHE_AFTER_UPVOTES = 1 export async function PATCH(req: Request) { - try { - const body = await req.json(); - - const { answerId, voteType } = AnswerVoteValidator.parse(body); - - const session = await getAuthSession(); - - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } - - // check if user has already voted on this answer - const existingVote = await db.answerVote.findFirst({ - where: { - userId: session.user.id, - answerId, - }, - }); - - const answer = await db.answer.findUnique({ - where: { - id: answerId, - }, - include: { - author: true, - votes: true, - }, - }); - - if (!answer) { - return new Response("Answer not found", { status: 404 }); - } - - if (existingVote) { - // if vote type is the same as existing vote, delete the vote - if (existingVote.type === voteType) { - await db.answerVote.delete({ - where: { - userId_answerId: { - answerId, - userId: session.user.id, - }, - }, - }); - - // Recount the votes - const votesAmt = answer.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedAnswer = { - authorName: answer.author.name ?? "", - content: JSON.stringify(answer.content), - id: answer.id, - title: answer.title, - currentVote: null, - createdAt: answer.createdAt, - }; - - await redis.hset(`answer:${answerId}`, cachePayload); // Store the answer data as a hash - } - - return new Response("OK"); - } - - // if vote type is different, update the vote - await db.answerVote.update({ - where: { - userId_answerId: { - answerId, - userId: session.user.id, - }, - }, - data: { - type: voteType, - }, - }); - - // Recount the votes - const votesAmt = answer.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedAnswer = { - authorName: answer.author.name ?? "", - content: JSON.stringify(answer.content), - id: answer.id, - title: answer.title, - currentVote: voteType, - createdAt: answer.createdAt, - }; - - await redis.hset(`answer:${answerId}`, cachePayload); // Store the answer data as a hash - } - - return new Response("OK"); - } - - // if no existing vote, create a new vote - await db.answerVote.create({ - data: { - type: voteType, - userId: session.user.id, - answerId, - }, - }); - - // Recount the votes - const votesAmt = answer.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedAnswer = { - authorName: answer.author.name ?? "", - content: JSON.stringify(answer.content), - id: answer.id, - title: answer.title, - currentVote: voteType, - createdAt: answer.createdAt, - }; - - await redis.hset(`answer:${answerId}`, cachePayload); // Store the answer data as a hash - } - - return new Response("OK"); - } catch (error) { - error; - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 400 }); - } - - return new Response("Could not vote at this time. Please try later", { status: 500 }); - } + try { + const body = await req.json() + + const { answerId, voteType } = AnswerVoteValidator.parse(body) + + const session = await getAuthSession() + + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } + + // check if user has already voted on this answer + const existingVote = await db.answerVote.findFirst({ + where: { + userId: session.user.id, + answerId, + }, + }) + + const answer = await db.answer.findUnique({ + where: { + id: answerId, + }, + include: { + author: true, + votes: true, + }, + }) + + if (!answer) { + return new Response("Answer not found", { status: 404 }) + } + + if (existingVote) { + // if vote type is the same as existing vote, delete the vote + if (existingVote.type === voteType) { + await db.answerVote.delete({ + where: { + userId_answerId: { + answerId, + userId: session.user.id, + }, + }, + }) + + // Recount the votes + const votesAmt = answer.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedAnswer = { + authorName: answer.author.name ?? "", + content: JSON.stringify(answer.content), + id: answer.id, + title: answer.title, + currentVote: null, + createdAt: answer.createdAt, + } + + await redis.hset(`answer:${answerId}`, cachePayload) // Store the answer data as a hash + } + + return new Response("OK") + } + + // if vote type is different, update the vote + await db.answerVote.update({ + where: { + userId_answerId: { + answerId, + userId: session.user.id, + }, + }, + data: { + type: voteType, + }, + }) + + // Recount the votes + const votesAmt = answer.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedAnswer = { + authorName: answer.author.name ?? "", + content: JSON.stringify(answer.content), + id: answer.id, + title: answer.title, + currentVote: voteType, + createdAt: answer.createdAt, + } + + await redis.hset(`answer:${answerId}`, cachePayload) // Store the answer data as a hash + } + + return new Response("OK") + } + + // if no existing vote, create a new vote + await db.answerVote.create({ + data: { + type: voteType, + userId: session.user.id, + answerId, + }, + }) + + // Recount the votes + const votesAmt = answer.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedAnswer = { + authorName: answer.author.name ?? "", + content: JSON.stringify(answer.content), + id: answer.id, + title: answer.title, + currentVote: voteType, + createdAt: answer.createdAt, + } + + await redis.hset(`answer:${answerId}`, cachePayload) // Store the answer data as a hash + } + + return new Response("OK") + } catch (error) { + error + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 400 }) + } + + return new Response("Could not vote at this time. Please try later", { + status: 500, + }) + } } diff --git a/src/app/api/subject/comment/create/route.ts b/src/app/api/subject/comment/create/route.ts index 6d32f5e..e110373 100644 --- a/src/app/api/subject/comment/create/route.ts +++ b/src/app/api/subject/comment/create/route.ts @@ -1,34 +1,36 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { CommentValidator } from "@/lib/validators/post"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { CommentValidator } from "@/lib/validators/post" +import { z } from "zod" export async function POST(req: Request) { - try { - const session = await getAuthSession(); + try { + const session = await getAuthSession() - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } - const body = await req.json(); + const body = await req.json() - const { content, postId } = CommentValidator.parse(body); + const { content, postId } = CommentValidator.parse(body) - await db.comment.create({ - data: { - content: content, - postId: postId, - authorId: session.user.id, - }, - }); + await db.comment.create({ + data: { + content: content, + postId: postId, + authorId: session.user.id, + }, + }) - return new Response("OK"); - } catch (error) { - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 422 }); - } + return new Response("OK") + } catch (error) { + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 422 }) + } - return new Response("Could not create post, please try again", { status: 500 }); - } + return new Response("Could not create post, please try again", { + status: 500, + }) + } } diff --git a/src/app/api/subject/comment/vote/route.ts b/src/app/api/subject/comment/vote/route.ts index 8f0bc3e..18cf994 100644 --- a/src/app/api/subject/comment/vote/route.ts +++ b/src/app/api/subject/comment/vote/route.ts @@ -1,150 +1,152 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { redis } from "@/lib/redis"; -import { CommentVoteValidator } from "@/lib/validators/vote"; -import { CachedComment } from "@/types/redis"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { redis } from "@/lib/redis" +import { CommentVoteValidator } from "@/lib/validators/vote" +import { CachedComment } from "@/types/redis" +import { z } from "zod" -const CACHE_AFTER_UPVOTES = 1; +const CACHE_AFTER_UPVOTES = 1 export async function PATCH(req: Request) { - try { - const body = await req.json(); - - const { commentId, voteType } = CommentVoteValidator.parse(body); - - const session = await getAuthSession(); - - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } - - // check if user has already voted on this comment - const existingVote = await db.commentVote.findFirst({ - where: { - userId: session.user.id, - commentId, - }, - }); - - const comment = await db.comment.findUnique({ - where: { - id: commentId, - }, - include: { - author: true, - votes: true, - }, - }); - - if (!comment) { - return new Response("Comment not found", { status: 404 }); - } - - if (existingVote) { - // if vote type is the same as existing vote, delete the vote - if (existingVote.type === voteType) { - await db.commentVote.delete({ - where: { - userId_commentId: { - commentId, - userId: session.user.id, - }, - }, - }); - - // Recount the votes - const votesAmt = comment.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedComment = { - authorName: comment.author.name ?? "", - content: JSON.stringify(comment.content), - id: comment.id, - currentVote: null, - createdAt: comment.createdAt, - }; - - await redis.hset(`comment:${commentId}`, cachePayload); // Store the comment data as a hash - } - - return new Response("OK"); - } - - // if vote type is different, update the vote - await db.commentVote.update({ - where: { - userId_commentId: { - commentId, - userId: session.user.id, - }, - }, - data: { - type: voteType, - }, - }); - - // Recount the votes - const votesAmt = comment.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedComment = { - authorName: comment.author.name ?? "", - content: JSON.stringify(comment.content), - id: comment.id, - currentVote: voteType, - createdAt: comment.createdAt, - }; - - await redis.hset(`comment:${commentId}`, cachePayload); // Store the comment data as a hash - } - - return new Response("OK"); - } - - // if no existing vote, create a new vote - await db.commentVote.create({ - data: { - type: voteType, - userId: session.user.id, - commentId, - }, - }); - - // Recount the votes - const votesAmt = comment.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedComment = { - authorName: comment.author.name ?? "", - content: JSON.stringify(comment.content), - id: comment.id, - currentVote: voteType, - createdAt: comment.createdAt, - }; - - await redis.hset(`comment:${commentId}`, cachePayload); // Store the comment data as a hash - } - - return new Response("OK"); - } catch (error) { - error; - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 400 }); - } - - return new Response("Could not vote at this time. Please try later", { status: 500 }); - } + try { + const body = await req.json() + + const { commentId, voteType } = CommentVoteValidator.parse(body) + + const session = await getAuthSession() + + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } + + // check if user has already voted on this comment + const existingVote = await db.commentVote.findFirst({ + where: { + userId: session.user.id, + commentId, + }, + }) + + const comment = await db.comment.findUnique({ + where: { + id: commentId, + }, + include: { + author: true, + votes: true, + }, + }) + + if (!comment) { + return new Response("Comment not found", { status: 404 }) + } + + if (existingVote) { + // if vote type is the same as existing vote, delete the vote + if (existingVote.type === voteType) { + await db.commentVote.delete({ + where: { + userId_commentId: { + commentId, + userId: session.user.id, + }, + }, + }) + + // Recount the votes + const votesAmt = comment.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedComment = { + authorName: comment.author.name ?? "", + content: JSON.stringify(comment.content), + id: comment.id, + currentVote: null, + createdAt: comment.createdAt, + } + + await redis.hset(`comment:${commentId}`, cachePayload) // Store the comment data as a hash + } + + return new Response("OK") + } + + // if vote type is different, update the vote + await db.commentVote.update({ + where: { + userId_commentId: { + commentId, + userId: session.user.id, + }, + }, + data: { + type: voteType, + }, + }) + + // Recount the votes + const votesAmt = comment.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedComment = { + authorName: comment.author.name ?? "", + content: JSON.stringify(comment.content), + id: comment.id, + currentVote: voteType, + createdAt: comment.createdAt, + } + + await redis.hset(`comment:${commentId}`, cachePayload) // Store the comment data as a hash + } + + return new Response("OK") + } + + // if no existing vote, create a new vote + await db.commentVote.create({ + data: { + type: voteType, + userId: session.user.id, + commentId, + }, + }) + + // Recount the votes + const votesAmt = comment.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedComment = { + authorName: comment.author.name ?? "", + content: JSON.stringify(comment.content), + id: comment.id, + currentVote: voteType, + createdAt: comment.createdAt, + } + + await redis.hset(`comment:${commentId}`, cachePayload) // Store the comment data as a hash + } + + return new Response("OK") + } catch (error) { + error + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 400 }) + } + + return new Response("Could not vote at this time. Please try later", { + status: 500, + }) + } } diff --git a/src/app/api/subject/post/create/route.ts b/src/app/api/subject/post/create/route.ts index e699bc8..76a0a2b 100644 --- a/src/app/api/subject/post/create/route.ts +++ b/src/app/api/subject/post/create/route.ts @@ -1,57 +1,63 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { ApuntsPostValidator } from "@/lib/validators/post"; -import { z } from "zod"; -import { TipusType } from "@prisma/client"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { ApuntsPostValidator } from "@/lib/validators/post" +import { z } from "zod" +import { TipusType } from "@prisma/client" export async function POST(req: Request) { - try { - const session = await getAuthSession(); - - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } - - const body = await req.json(); - - const { pdf, title, assignatura, tipus } = ApuntsPostValidator.parse(body); - - const subject = await db.subject.findFirst({ - where: { - acronym: assignatura.toUpperCase(), - }, - }); - - if (!subject) { - return new Response("Subject not found", { status: 404 }); - } - - const semester = subject.semester; - const semesterNumber = semester[0] === "Q" ? parseInt(semester[1]) : 8; - if (typeof session.user.generacio !== "number") { - return new Response("Invalid generacio", { status: 409 }); - } - const year: number = session.user.generacio + Math.floor((semesterNumber - 1) / 2); - - if (!["apunts", "examens", "exercicis", "diapositives", "altres"].includes(tipus)) { - return new Response("Invalid tipus", { status: 422 }); - } - - await db.post.create({ - data: { - title: title, - content: pdf, - subjectId: subject.id, - authorId: session.user.id, - tipus: tipus as TipusType, - year: year, - }, - }); - return new Response(JSON.stringify(subject.acronym), { status: 201 }); - } catch (error) { - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 422 }); - } - return new Response("Something went wrong", { status: 500 }); - } + try { + const session = await getAuthSession() + + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } + + const body = await req.json() + + const { pdf, title, assignatura, tipus, anonim } = + ApuntsPostValidator.parse(body) + + const subject = await db.subject.findFirst({ + where: { + acronym: assignatura.toUpperCase(), + }, + }) + + if (!subject) { + return new Response("Subject not found", { status: 404 }) + } + + const semester = subject.semester + const semesterNumber = semester[0] === "Q" ? parseInt(semester[1]) : 8 + if (typeof session.user.generacio !== "number") { + return new Response("Invalid generacio", { status: 409 }) + } + const year: number = + session.user.generacio + Math.floor((semesterNumber - 1) / 2) + + if ( + !["apunts", "examens", "exercicis", "diapositives", "altres"].includes( + tipus, + ) + ) { + return new Response("Invalid tipus", { status: 422 }) + } + await db.post.create({ + data: { + title: title, + content: pdf, + subjectId: subject.id, + authorId: session.user.id, + tipus: tipus as TipusType, + year: year, + isAnonymous: anonim, + }, + }) + return new Response(JSON.stringify(subject.acronym), { status: 201 }) + } catch (error) { + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 422 }) + } + return new Response("Something went wrong", { status: 500 }) + } } diff --git a/src/app/api/subject/post/vote/route.ts b/src/app/api/subject/post/vote/route.ts index 8206509..e600d42 100644 --- a/src/app/api/subject/post/vote/route.ts +++ b/src/app/api/subject/post/vote/route.ts @@ -1,153 +1,155 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { redis } from "@/lib/redis"; -import { PostVoteValidator } from "@/lib/validators/vote"; -import { CachedPost } from "@/types/redis"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { redis } from "@/lib/redis" +import { PostVoteValidator } from "@/lib/validators/vote" +import { CachedPost } from "@/types/redis" +import { z } from "zod" -const CACHE_AFTER_UPVOTES = 1; +const CACHE_AFTER_UPVOTES = 1 export async function PATCH(req: Request) { - try { - const body = await req.json(); - - const { postId, voteType } = PostVoteValidator.parse(body); - - const session = await getAuthSession(); - - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } - - // check if user has already voted on this post - const existingVote = await db.postVote.findFirst({ - where: { - userId: session.user.id, - postId, - }, - }); - - const post = await db.post.findUnique({ - where: { - id: postId, - }, - include: { - author: true, - votes: true, - }, - }); - - if (!post) { - return new Response("Post not found", { status: 404 }); - } - - if (existingVote) { - // if vote type is the same as existing vote, delete the vote - if (existingVote.type === voteType) { - await db.postVote.delete({ - where: { - userId_postId: { - postId, - userId: session.user.id, - }, - }, - }); - - // Recount the votes - const votesAmt = post.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedPost = { - authorName: post.author.name ?? "", - content: post.content ?? "", - id: post.id, - title: post.title, - currentVote: null, - createdAt: post.createdAt, - }; - - await redis.hset(`post:${postId}`, cachePayload); // Store the post data as a hash - } - - return new Response("OK"); - } - - // if vote type is different, update the vote - await db.postVote.update({ - where: { - userId_postId: { - postId, - userId: session.user.id, - }, - }, - data: { - type: voteType, - }, - }); - - // Recount the votes - const votesAmt = post.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedPost = { - authorName: post.author.name ?? "", - content: post.content ?? "", - id: post.id, - title: post.title, - currentVote: voteType, - createdAt: post.createdAt, - }; - - await redis.hset(`post:${postId}`, cachePayload); // Store the post data as a hash - } - - return new Response("OK"); - } - - // if no existing vote, create a new vote - await db.postVote.create({ - data: { - type: voteType, - userId: session.user.id, - postId, - }, - }); - - // Recount the votes - const votesAmt = post.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedPost = { - authorName: post.author.name ?? "", - content: post.content ?? "", - id: post.id, - title: post.title, - currentVote: voteType, - createdAt: post.createdAt, - }; - - await redis.hset(`post:${postId}`, cachePayload); // Store the post data as a hash - } - - return new Response("OK"); - } catch (error) { - error; - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 400 }); - } - - return new Response("Could not vote at this time. Please try later", { status: 500 }); - } + try { + const body = await req.json() + + const { postId, voteType } = PostVoteValidator.parse(body) + + const session = await getAuthSession() + + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } + + // check if user has already voted on this post + const existingVote = await db.postVote.findFirst({ + where: { + userId: session.user.id, + postId, + }, + }) + + const post = await db.post.findUnique({ + where: { + id: postId, + }, + include: { + author: true, + votes: true, + }, + }) + + if (!post) { + return new Response("Post not found", { status: 404 }) + } + + if (existingVote) { + // if vote type is the same as existing vote, delete the vote + if (existingVote.type === voteType) { + await db.postVote.delete({ + where: { + userId_postId: { + postId, + userId: session.user.id, + }, + }, + }) + + // Recount the votes + const votesAmt = post.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedPost = { + authorName: post.author.name ?? "", + content: post.content ?? "", + id: post.id, + title: post.title, + currentVote: null, + createdAt: post.createdAt, + } + + await redis.hset(`post:${postId}`, cachePayload) // Store the post data as a hash + } + + return new Response("OK") + } + + // if vote type is different, update the vote + await db.postVote.update({ + where: { + userId_postId: { + postId, + userId: session.user.id, + }, + }, + data: { + type: voteType, + }, + }) + + // Recount the votes + const votesAmt = post.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedPost = { + authorName: post.author.name ?? "", + content: post.content ?? "", + id: post.id, + title: post.title, + currentVote: voteType, + createdAt: post.createdAt, + } + + await redis.hset(`post:${postId}`, cachePayload) // Store the post data as a hash + } + + return new Response("OK") + } + + // if no existing vote, create a new vote + await db.postVote.create({ + data: { + type: voteType, + userId: session.user.id, + postId, + }, + }) + + // Recount the votes + const votesAmt = post.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedPost = { + authorName: post.author.name ?? "", + content: post.content ?? "", + id: post.id, + title: post.title, + currentVote: voteType, + createdAt: post.createdAt, + } + + await redis.hset(`post:${postId}`, cachePayload) // Store the post data as a hash + } + + return new Response("OK") + } catch (error) { + error + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 400 }) + } + + return new Response("Could not vote at this time. Please try later", { + status: 500, + }) + } } diff --git a/src/app/api/subject/question/create/route.ts b/src/app/api/subject/question/create/route.ts index 246ed3c..bdf32f5 100644 --- a/src/app/api/subject/question/create/route.ts +++ b/src/app/api/subject/question/create/route.ts @@ -1,36 +1,38 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { QuestionValidator } from "@/lib/validators/question"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { QuestionValidator } from "@/lib/validators/question" +import { z } from "zod" export async function POST(req: Request) { - try { - const session = await getAuthSession(); + try { + const session = await getAuthSession() - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } - const body = await req.json(); + const body = await req.json() - const { title, subjectId, content } = QuestionValidator.parse(body); + const { title, subjectId, content } = QuestionValidator.parse(body) - const createdQuestion = await db.question.create({ - data: { - title: title, - content: content, - authorId: session.user.id, - subjectId: subjectId, - }, - }); + const createdQuestion = await db.question.create({ + data: { + title: title, + content: content, + authorId: session.user.id, + subjectId: subjectId, + }, + }) - const createdQuestionId = createdQuestion.id; // Get the ID of the created question + const createdQuestionId = createdQuestion.id // Get the ID of the created question - return new Response(JSON.stringify(createdQuestionId), { status: 201 }); - } catch (error) { - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 422 }); - } - return new Response("Could not create post, please try again", { status: 500 }); - } + return new Response(JSON.stringify(createdQuestionId), { status: 201 }) + } catch (error) { + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 422 }) + } + return new Response("Could not create post, please try again", { + status: 500, + }) + } } diff --git a/src/app/api/subject/question/vote/route.ts b/src/app/api/subject/question/vote/route.ts index ebb777c..d53756e 100644 --- a/src/app/api/subject/question/vote/route.ts +++ b/src/app/api/subject/question/vote/route.ts @@ -1,153 +1,153 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { redis } from "@/lib/redis"; -import { QuestionVoteValidator } from "@/lib/validators/vote"; -import { CachedQuestion } from "@/types/redis"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { redis } from "@/lib/redis" +import { QuestionVoteValidator } from "@/lib/validators/vote" +import { CachedQuestion } from "@/types/redis" +import { z } from "zod" -const CACHE_AFTER_UPVOTES = 1; +const CACHE_AFTER_UPVOTES = 1 export async function PATCH(req: Request) { - try { - const body = await req.json(); - - const { questionId, voteType } = QuestionVoteValidator.parse(body); - - const session = await getAuthSession(); - - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } - - // check if user has already voted on this question - const existingVote = await db.questionVote.findFirst({ - where: { - userId: session.user.id, - questionId, - }, - }); - - const question = await db.question.findUnique({ - where: { - id: questionId, - }, - include: { - author: true, - votes: true, - }, - }); - - if (!question) { - return new Response("Question not found", { status: 404 }); - } - - if (existingVote) { - // if vote type is the same as existing vote, delete the vote - if (existingVote.type === voteType) { - await db.questionVote.delete({ - where: { - userId_questionId: { - questionId, - userId: session.user.id, - }, - }, - }); - - // Recount the votes - const votesAmt = question.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedQuestion = { - authorName: question.author.name ?? "", - content: JSON.stringify(question.content), - id: question.id, - title: question.title, - currentVote: null, - createdAt: question.createdAt, - }; - - await redis.hset(`question:${questionId}`, cachePayload); // Store the question data as a hash - } - - return new Response("OK"); - } - - // if vote type is different, update the vote - await db.questionVote.update({ - where: { - userId_questionId: { - questionId, - userId: session.user.id, - }, - }, - data: { - type: voteType, - }, - }); - - // Recount the votes - const votesAmt = question.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedQuestion = { - authorName: question.author.name ?? "", - content: JSON.stringify(question.content), - id: question.id, - title: question.title, - currentVote: voteType, - createdAt: question.createdAt, - }; - - await redis.hset(`question:${questionId}`, cachePayload); // Store the question data as a hash - } - - return new Response("OK"); - } - - // if no existing vote, create a new vote - await db.questionVote.create({ - data: { - type: voteType, - userId: session.user.id, - questionId, - }, - }); - - // Recount the votes - const votesAmt = question.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedQuestion = { - authorName: question.author.name ?? "", - content: JSON.stringify(question.content), - id: question.id, - title: question.title, - currentVote: voteType, - createdAt: question.createdAt, - }; - - await redis.hset(`question:${questionId}`, cachePayload); // Store the question data as a hash - } - - return new Response("OK"); - } catch (error) { - error; - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 400 }); - } - - return new Response("Internal Server Error", { status: 500 }); - } + try { + const body = await req.json() + + const { questionId, voteType } = QuestionVoteValidator.parse(body) + + const session = await getAuthSession() + + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } + + // check if user has already voted on this question + const existingVote = await db.questionVote.findFirst({ + where: { + userId: session.user.id, + questionId, + }, + }) + + const question = await db.question.findUnique({ + where: { + id: questionId, + }, + include: { + author: true, + votes: true, + }, + }) + + if (!question) { + return new Response("Question not found", { status: 404 }) + } + + if (existingVote) { + // if vote type is the same as existing vote, delete the vote + if (existingVote.type === voteType) { + await db.questionVote.delete({ + where: { + userId_questionId: { + questionId, + userId: session.user.id, + }, + }, + }) + + // Recount the votes + const votesAmt = question.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedQuestion = { + authorName: question.author.name ?? "", + content: JSON.stringify(question.content), + id: question.id, + title: question.title, + currentVote: null, + createdAt: question.createdAt, + } + + await redis.hset(`question:${questionId}`, cachePayload) // Store the question data as a hash + } + + return new Response("OK") + } + + // if vote type is different, update the vote + await db.questionVote.update({ + where: { + userId_questionId: { + questionId, + userId: session.user.id, + }, + }, + data: { + type: voteType, + }, + }) + + // Recount the votes + const votesAmt = question.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedQuestion = { + authorName: question.author.name ?? "", + content: JSON.stringify(question.content), + id: question.id, + title: question.title, + currentVote: voteType, + createdAt: question.createdAt, + } + + await redis.hset(`question:${questionId}`, cachePayload) // Store the question data as a hash + } + + return new Response("OK") + } + + // if no existing vote, create a new vote + await db.questionVote.create({ + data: { + type: voteType, + userId: session.user.id, + questionId, + }, + }) + + // Recount the votes + const votesAmt = question.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + if (votesAmt >= CACHE_AFTER_UPVOTES) { + const cachePayload: CachedQuestion = { + authorName: question.author.name ?? "", + content: JSON.stringify(question.content), + id: question.id, + title: question.title, + currentVote: voteType, + createdAt: question.createdAt, + } + + await redis.hset(`question:${questionId}`, cachePayload) // Store the question data as a hash + } + + return new Response("OK") + } catch (error) { + error + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 400 }) + } + + return new Response("Internal Server Error", { status: 500 }) + } } diff --git a/src/app/api/subject/route.ts b/src/app/api/subject/route.ts index a966e0c..40a23cd 100644 --- a/src/app/api/subject/route.ts +++ b/src/app/api/subject/route.ts @@ -1,52 +1,52 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { SubjectValidator } from "@/lib/validators/subject"; -import { z } from "zod"; -import { SemesterType } from "@prisma/client"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { SubjectValidator } from "@/lib/validators/subject" +import { z } from "zod" +import { SemesterType } from "@prisma/client" export async function POST(req: Request) { - try { - const session = await getAuthSession(); - - if (!session) { - return new Response("Unauthorized", { status: 401 }); - } - - const body = await req.json(); - const { name, acronym, semester } = SubjectValidator.parse(body); - - const subjectExists = await db.subject.findFirst({ - where: { - OR: [{ name }, { acronym }], - }, - }); - - if (subjectExists) { - return new Response("Subject already exists", { status: 409 }); - } - - const subject = await db.subject.create({ - data: { - name, - acronym, - creatorId: session.user.id, - semester: semester as SemesterType, - }, - }); - - await db.subscription.create({ - data: { - userId: session.user.id, - subjectId: subject.id, - }, - }); - - return new Response(subject.acronym); - } catch (error) { - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 422 }); - } - - return new Response("Could not create subject", { status: 500 }); - } + try { + const session = await getAuthSession() + + if (!session) { + return new Response("Unauthorized", { status: 401 }) + } + + const body = await req.json() + const { name, acronym, semester } = SubjectValidator.parse(body) + + const subjectExists = await db.subject.findFirst({ + where: { + OR: [{ name }, { acronym }], + }, + }) + + if (subjectExists) { + return new Response("Subject already exists", { status: 409 }) + } + + const subject = await db.subject.create({ + data: { + name, + acronym, + creatorId: session.user.id, + semester: semester as SemesterType, + }, + }) + + await db.subscription.create({ + data: { + userId: session.user.id, + subjectId: subject.id, + }, + }) + + return new Response(subject.acronym) + } catch (error) { + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 422 }) + } + + return new Response("Could not create subject", { status: 500 }) + } } diff --git a/src/app/api/subject/subscribe/route.ts b/src/app/api/subject/subscribe/route.ts index 2e72310..dad6c96 100644 --- a/src/app/api/subject/subscribe/route.ts +++ b/src/app/api/subject/subscribe/route.ts @@ -1,44 +1,46 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { SubjectSubscriptionValidator } from "@/lib/validators/subject"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { SubjectSubscriptionValidator } from "@/lib/validators/subject" +import { z } from "zod" export async function POST(req: Request) { - try { - const session = await getAuthSession(); - - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } - - const body = await req.json(); - - const { subjectId } = SubjectSubscriptionValidator.parse(body); - - const subscriptionExists = await db.subscription.findFirst({ - where: { - subjectId, - userId: session.user.id, - }, - }); - - if (subscriptionExists) { - return new Response("Already subscribed", { status: 400 }); - } - - await db.subscription.create({ - data: { - subjectId, - userId: session.user.id, - }, - }); - - return new Response(subjectId); - } catch (error) { - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 422 }); - } - - return new Response("Could not subscribe, please try again", { status: 500 }); - } + try { + const session = await getAuthSession() + + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } + + const body = await req.json() + + const { subjectId } = SubjectSubscriptionValidator.parse(body) + + const subscriptionExists = await db.subscription.findFirst({ + where: { + subjectId, + userId: session.user.id, + }, + }) + + if (subscriptionExists) { + return new Response("Already subscribed", { status: 400 }) + } + + await db.subscription.create({ + data: { + subjectId, + userId: session.user.id, + }, + }) + + return new Response(subjectId) + } catch (error) { + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 422 }) + } + + return new Response("Could not subscribe, please try again", { + status: 500, + }) + } } diff --git a/src/app/api/subject/unsubscribe/route.ts b/src/app/api/subject/unsubscribe/route.ts index c8a99a4..86a6bae 100644 --- a/src/app/api/subject/unsubscribe/route.ts +++ b/src/app/api/subject/unsubscribe/route.ts @@ -1,57 +1,61 @@ -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { SubjectSubscriptionValidator } from "@/lib/validators/subject"; -import { z } from "zod"; +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import { SubjectSubscriptionValidator } from "@/lib/validators/subject" +import { z } from "zod" export async function POST(req: Request) { - try { - const session = await getAuthSession(); - - if (!session?.user) { - return new Response("Unauthorized", { status: 401 }); - } - - const body = await req.json(); - - const { subjectId } = SubjectSubscriptionValidator.parse(body); - - const subscriptionExists = await db.subscription.findFirst({ - where: { - subjectId, - userId: session.user.id, - }, - }); - - if (!subscriptionExists) { - return new Response("Not subscribed yet", { status: 400 }); - } - - const subject = await db.subject.findFirst({ - where: { - id: subjectId, - creatorId: session.user.id, - }, - }); - - if (subject) { - return new Response("Cannot unsubscribe from your own subject", { status: 400 }); - } - - await db.subscription.delete({ - where: { - userId_subjectId: { - subjectId, - userId: session.user.id, - }, - }, - }); - - return new Response(subjectId); - } catch (error) { - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 422 }); - } - - return new Response("Could not unsubscribe, please try again", { status: 500 }); - } + try { + const session = await getAuthSession() + + if (!session?.user) { + return new Response("Unauthorized", { status: 401 }) + } + + const body = await req.json() + + const { subjectId } = SubjectSubscriptionValidator.parse(body) + + const subscriptionExists = await db.subscription.findFirst({ + where: { + subjectId, + userId: session.user.id, + }, + }) + + if (!subscriptionExists) { + return new Response("Not subscribed yet", { status: 400 }) + } + + const subject = await db.subject.findFirst({ + where: { + id: subjectId, + creatorId: session.user.id, + }, + }) + + if (subject) { + return new Response("Cannot unsubscribe from your own subject", { + status: 400, + }) + } + + await db.subscription.delete({ + where: { + userId_subjectId: { + subjectId, + userId: session.user.id, + }, + }, + }) + + return new Response(subjectId) + } catch (error) { + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 422 }) + } + + return new Response("Could not unsubscribe, please try again", { + status: 500, + }) + } } diff --git a/src/app/api/uploadthing/core.ts b/src/app/api/uploadthing/core.ts index aff8475..d317f1e 100644 --- a/src/app/api/uploadthing/core.ts +++ b/src/app/api/uploadthing/core.ts @@ -1,37 +1,41 @@ -import { createUploadthing, type FileRouter } from "uploadthing/next"; -import { UploadThingError } from "@uploadthing/shared"; -import { getToken } from "next-auth/jwt"; +import { createUploadthing, type FileRouter } from "uploadthing/next" +import { UploadThingError } from "@uploadthing/shared" +import { getToken } from "next-auth/jwt" -const f = createUploadthing(); +const f = createUploadthing() // FileRouter for your app, can contain multiple FileRoutes export const ourFileRouter = { - // Define as many FileRoutes as you like, each with a unique routeSlug - imageUploader: f({ image: { maxFileSize: "4MB" } }) - // Set permissions and file types for this FileRoute - .middleware(async (req) => { - // This code runs on your server before upload - const user = await getToken({ req }); + // Define as many FileRoutes as you like, each with a unique routeSlug + imageUploader: f({ image: { maxFileSize: "4MB" } }) + // Set permissions and file types for this FileRoute + .middleware(async (req) => { + // This code runs on your server before upload + const user = await getToken({ req }) - // If you throw, the user will not be able to upload - if (!user) throw new UploadThingError({ code: "FORBIDDEN", message: "Unauthorized" }); + // If you throw, the user will not be able to upload + if (!user) + throw new UploadThingError({ + code: "FORBIDDEN", + message: "Unauthorized", + }) - // Whatever is returned here is accessible in onUploadComplete as `metadata` - return { userId: user.id }; - }) - .onUploadComplete(async ({}) => {}), + // Whatever is returned here is accessible in onUploadComplete as `metadata` + return { userId: user.id } + }) + .onUploadComplete(async ({}) => {}), - // Another FileRoute (made by myself, not by the library) - fileUploader: f({ - pdf: { maxFileCount: 1, maxFileSize: "128MB" }, - text: { maxFileCount: 5 }, - }) - .middleware(async (req) => { - const user = await getToken({ req }); - if (!user) throw new UploadThingError({ code: "FORBIDDEN" }); - return { userId: user.id }; - }) - .onUploadComplete(async ({}) => {}), -} satisfies FileRouter; + // Another FileRoute (made by myself, not by the library) + fileUploader: f({ + pdf: { maxFileCount: 1, maxFileSize: "128MB" }, + text: { maxFileCount: 5 }, + }) + .middleware(async (req) => { + const user = await getToken({ req }) + if (!user) throw new UploadThingError({ code: "FORBIDDEN" }) + return { userId: user.id } + }) + .onUploadComplete(async ({}) => {}), +} satisfies FileRouter -export type OurFileRouter = typeof ourFileRouter; +export type OurFileRouter = typeof ourFileRouter diff --git a/src/app/api/uploadthing/route.ts b/src/app/api/uploadthing/route.ts index b41db7d..b4310d6 100644 --- a/src/app/api/uploadthing/route.ts +++ b/src/app/api/uploadthing/route.ts @@ -1,8 +1,8 @@ -import { createNextRouteHandler } from "uploadthing/next"; +import { createNextRouteHandler } from "uploadthing/next" -import { ourFileRouter } from "./core"; +import { ourFileRouter } from "./core" // Export routes for Next App Router export const { GET, POST } = createNextRouteHandler({ - router: ourFileRouter, -}); + router: ourFileRouter, +}) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 01b55dc..a5bdfcb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,37 +1,49 @@ -import { cn } from "@/lib/utils"; -import "@/styles/globals.css"; -import { Inter } from "next/font/google"; -import Navbar from "@/components/Navbar"; -import { Toaster } from "@/components/ui/Toaster"; -import Providers from "@/components/Providers"; +import { cn } from "@/lib/utils" +import "@/styles/globals.css" +import { Inter } from "next/font/google" +import Navbar from "@/components/Navbar" +import { Toaster } from "@/components/ui/Toaster" +import Providers from "@/components/Providers" export const metadata = { - title: "Apunts Dades", - description: - "Un forum per a compartir, recomanar i discutir sobre apunts del Grau en Ciència i Enginyeria de Dades (GCED) de la UPC", -}; + title: "Apunts Dades", + description: + "Un forum per a compartir, recomanar i discutir sobre apunts del Grau en Ciència i Enginyeria de Dades (GCED) de la UPC", +} -const inter = Inter({ subsets: ["latin"] }); // TODO: Fer servir la font de l'AED +const inter = Inter({ subsets: ["latin"] }) // TODO: Fer servir la font de l'AED -function RootLayout({ children, authModal }: { children: React.ReactNode; authModal: React.ReactNode }) { - return ( - <html - lang="en" - className={cn("bg-white text-slate-900 antialiased light", inter.className)}> - <body className="min-h-screen pt-12 bg-slate-50 antialiased"> - <Providers> - {/* @ts-expect-error server component */} - <Navbar /> +function RootLayout({ + children, + authModal, +}: { + children: React.ReactNode + authModal: React.ReactNode +}) { + return ( + <html + lang="en" + className={cn( + "bg-white text-slate-900 antialiased light", + inter.className, + )} + > + <body className="min-h-screen pt-12 bg-slate-50 antialiased"> + <Providers> + {/* @ts-expect-error server component */} + <Navbar /> - {authModal} + {authModal} - <div className="container max-w-7xl mx-auto h-full pt-12">{children}</div> + <div className="container max-w-7xl mx-auto h-full pt-12"> + {children} + </div> - <Toaster /> - </Providers> - </body> - </html> - ); + <Toaster /> + </Providers> + </body> + </html> + ) } -export default RootLayout; \ No newline at end of file +export default RootLayout diff --git a/src/app/page.tsx b/src/app/page.tsx index 965c956..a660de4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,67 +1,67 @@ // import CustomFeed from "@/components/CustomFeed"; -import { buttonVariants } from "@/components/ui/Button"; +import { buttonVariants } from "@/components/ui/Button" // import { getAuthSession } from "@/lib/auth"; -import { HomeIcon } from "lucide-react"; -import Link from "next/link"; +import { HomeIcon } from "lucide-react" +import Link from "next/link" -import { db } from "@/lib/db"; -import { BookIcon } from "lucide-react"; +import { db } from "@/lib/db" +import { BookIcon } from "lucide-react" // import { HeartIcon, HeartPulseIcon } from "lucide-react"; -import { Badge } from "@/components/ui/Badge"; -import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/Badge" +import { cn } from "@/lib/utils" export default async function Home() { - // const session = await getAuthSession(); + // const session = await getAuthSession(); - const subjects = await db.subject.findMany({ - select: { - id: true, - acronym: true, - name: true, - semester: true, - }, - }); + const subjects = await db.subject.findMany({ + select: { + id: true, + acronym: true, + name: true, + semester: true, + }, + }) - // const subscription = !session?.user - // ? undefined - // : await db.subscription.findFirst({ - // where: { - // userId: session.user.id, - // subjectId: subjects.id, - // }, - // }); + // const subscription = !session?.user + // ? undefined + // : await db.subscription.findFirst({ + // where: { + // userId: session.user.id, + // subjectId: subjects.id, + // }, + // }); - // const isSubscribed = !!subscription; - // const ColorClass = isSubscribed ? "text-red-500" : "text-black"; + // const isSubscribed = !!subscription; + // const ColorClass = isSubscribed ? "text-red-500" : "text-black"; - function semesterColor(semester: string) { - switch (semester) { - case "Q1": - return "bg-emerald-100"; - case "Q2": - return "bg-rose-100"; - case "Q3": - return "bg-cyan-100"; - case "Q4": - return "bg-amber-100"; - case "Q5": - return "bg-violet-100"; - case "Q6": - return "bg-blue-100"; - default: - return "bg-gray-100"; - } - } + function semesterColor(semester: string) { + switch (semester) { + case "Q1": + return "bg-emerald-100" + case "Q2": + return "bg-rose-100" + case "Q3": + return "bg-cyan-100" + case "Q4": + return "bg-amber-100" + case "Q5": + return "bg-violet-100" + case "Q6": + return "bg-blue-100" + default: + return "bg-gray-100" + } + } - return ( - <> - <h1 className="font-bold text-3xl md:text-4xl">El teu espai</h1> - <div className="grid grid-cols-1 md:grid-cols-3 gap-y-4 md:gap-x-4 py-6"> - {/* Feed + return ( + <> + <h1 className="font-bold text-3xl md:text-4xl">El teu espai</h1> + <div className="grid grid-cols-1 md:grid-cols-3 gap-y-4 md:gap-x-4 py-6"> + {/* Feed {session ? <CustomFeed /> : null} */} - {/* subjects info */} - {/* <div className="overflow-hidden h-fit rounded-lg border border-gray-200 order-first md:order-last mb-4"> + {/* subjects info */} + {/* <div className="overflow-hidden h-fit rounded-lg border border-gray-200 order-first md:order-last mb-4"> <div className="bg-emerald-100 px-6 py-4"> <p className="font-semibold py-3 flex items-center gap-1.5"> <HomeIcon className="w-4 h-4" /> @@ -86,51 +86,55 @@ export default async function Home() { </Link> </div> </div> */} - <div className="overflow-hidden h-fit rounded-lg border border-gray-200 order-first mb-4"> - <div className="bg-pink-100 px-6 py-4"> - <p className="</div>font-semibold py-3 flex items-center gap-1.5"> - <HomeIcon className="w-4 h-4" /> - Inici - </p> - </div> + <div className="overflow-hidden h-fit rounded-lg border border-gray-200 order-first mb-4"> + <div className="bg-pink-100 px-6 py-4"> + <p className="</div>font-semibold py-3 flex items-center gap-1.5"> + <HomeIcon className="w-4 h-4" /> + Inici + </p> + </div> - <div className="-my-3 divide-y divide-gray-100 px-6 py-4 text-sm leading-6"> - <Link - className={buttonVariants({ - className: "w-full mt-4 mb-6", - })} - href="/submit"> - Penja Apunts - </Link> - </div> - </div> - </div> + <div className="-my-3 divide-y divide-gray-100 px-6 py-4 text-sm leading-6"> + <Link + className={buttonVariants({ + className: "w-full mt-4 mb-6", + })} + href="/submit" + > + Penja Apunts + </Link> + </div> + </div> + </div> - <div className="grid grid-cols-1 md:grid-cols-3 gap-y-4 md:gap-x-4 py-6"> - {subjects.map((subject, index) => { - return ( - <Link - key={index} - className="w-full mt-4 mb-6" - href={`/${subject.acronym}`}> - <div className="overflow-hidden h-fit rounded-lg border border-gray-200 order-first md:order-last"> - <div className={cn("px-6 py-2", semesterColor(subject.semester))}> - <p className="font-semibold py-1 flex items-center gap-1.5"> - <BookIcon className="w-4 h-4" /> - {subject.name} - {/* <HeartIcon className={cn("h-5 w-5", ColorClass)} /> */} - </p> - </div> + <div className="grid grid-cols-1 md:grid-cols-3 gap-y-4 md:gap-x-4 py-6"> + {subjects.map((subject, index) => { + return ( + <Link + key={index} + className="w-full mt-4 mb-6" + href={`/${subject.acronym}`} + > + <div className="overflow-hidden h-fit rounded-lg border border-gray-200 order-first md:order-last"> + <div + className={cn("px-6 py-2", semesterColor(subject.semester))} + > + <p className="font-semibold py-1 flex items-center gap-1.5"> + <BookIcon className="w-4 h-4" /> + {subject.name} + {/* <HeartIcon className={cn("h-5 w-5", ColorClass)} /> */} + </p> + </div> - <div className="-my-3 divide-y divide-gray-100 px-6 py-4 text-sm leading-6 space-x-2"> - <Badge variant="outline">{subject.semester}</Badge> - <Badge variant="outline">{subject.acronym}</Badge> - </div> - </div> - </Link> - ); - })} - </div> - </> - ); + <div className="-my-3 divide-y divide-gray-100 px-6 py-4 text-sm leading-6 space-x-2"> + <Badge variant="outline">{subject.semester}</Badge> + <Badge variant="outline">{subject.acronym}</Badge> + </div> + </div> + </Link> + ) + })} + </div> + </> + ) } diff --git a/src/app/privacyandterms/page.tsx b/src/app/privacyandterms/page.tsx index c090c19..0ce4ccf 100644 --- a/src/app/privacyandterms/page.tsx +++ b/src/app/privacyandterms/page.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC } from "react" interface PageProps {} @@ -148,7 +148,7 @@ const Page: FC<PageProps> = ({}) => { acceptance of the updated terms. </p> </div> - ); -}; + ) +} -export default Page; +export default Page diff --git a/src/app/submit/page.tsx b/src/app/submit/page.tsx index 8d19989..3f3818f 100644 --- a/src/app/submit/page.tsx +++ b/src/app/submit/page.tsx @@ -1,10 +1,10 @@ -import { FC } from "react"; -import { ProfileForm } from "@/components/Form"; +import { FC } from "react" +import { ProfileForm } from "@/components/Form" interface pageProps {} const page: FC<pageProps> = ({}) => { - return <ProfileForm />; -}; + return <ProfileForm /> +} -export default page; +export default page diff --git a/src/components/AnswerComponent.tsx b/src/components/AnswerComponent.tsx index 0c970a3..2b5014c 100644 --- a/src/components/AnswerComponent.tsx +++ b/src/components/AnswerComponent.tsx @@ -1,86 +1,91 @@ -"use client"; +"use client" -import { formatTimeToNow } from "@/lib/utils"; -import { Answer, User, AnswerVote } from "@prisma/client"; -import { FC, useRef } from "react"; -import EditorOutput from "./EditorOutput"; -import AnswerVoteClient from "./votes/AnswerVoteClient"; -import AnswerAcceptClient from "./votes/AnswerAcceptClient"; +import { formatTimeToNow } from "@/lib/utils" +import { Answer, User, AnswerVote } from "@prisma/client" +import { FC, useRef } from "react" +import EditorOutput from "./EditorOutput" +import AnswerVoteClient from "./votes/AnswerVoteClient" +import AnswerAcceptClient from "./votes/AnswerAcceptClient" -type PartialVote = Pick<AnswerVote, "type">; +type PartialVote = Pick<AnswerVote, "type"> interface AnswerProps { - answer: Answer & { - author: User; - votes: AnswerVote[]; - }; - votesAmt: number; - subjectName: string; - currentVote?: PartialVote; - subjectAcronym: string; - questionId: string; - questionAuthorId: string; + answer: Answer & { + author: User + votes: AnswerVote[] + } + votesAmt: number + subjectName: string + currentVote?: PartialVote + subjectAcronym: string + questionId: string + questionAuthorId: string } const AnswerComponent: FC<AnswerProps> = ({ - answer, - votesAmt: _votesAmt, - currentVote: _currentVote, - subjectName, - subjectAcronym, - questionId, - questionAuthorId, + answer, + votesAmt: _votesAmt, + currentVote: _currentVote, + subjectName, + subjectAcronym, + questionId, + questionAuthorId, }) => { - const pRef = useRef<HTMLParagraphElement>(null); + const pRef = useRef<HTMLParagraphElement>(null) - return ( - <div className="rounded-md bg-white shadow my-2"> - <div className="px-6 py-4 flex justify-between"> - <div className="h-auto flex flex-col"> - <AnswerAcceptClient - questionAuthorId={questionAuthorId} - initialAccepted={answer.accepted} - answerId={answer.id} - answer={answer} - /> + return ( + <div className="rounded-md bg-white shadow my-2"> + <div className="px-6 py-4 flex justify-between"> + <div className="h-auto flex flex-col"> + <AnswerAcceptClient + questionAuthorId={questionAuthorId} + initialAccepted={answer.accepted} + answerId={answer.id} + answer={answer} + /> - <AnswerVoteClient - initialVotesAmt={_votesAmt} - answerId={answer.id} - initialVote={_currentVote?.type} - /> - </div> + <AnswerVoteClient + initialVotesAmt={_votesAmt} + answerId={answer.id} + initialVote={_currentVote?.type} + /> + </div> - <div className="w-0 flex-1"> - <div className="max-h-40 mt-1 text-xs text-gray-500"> - {subjectName ? ( - <> - <a - className="underline text-zinc-900 text-sm underline-offset-2" - href={`/${subjectAcronym}`}> - {subjectAcronym} - </a> - <span className="px-1">•</span> - </> - ) : null} - <span>Compartit per {answer.author.name}</span> {formatTimeToNow(new Date(answer.createdAt))} - </div> - <a href={`/${subjectAcronym}/q/${questionId}`}> - <h1 className="text-lg font-semibold py-2 leading-6 text-gray-900">{answer.title}</h1> - </a> + <div className="w-0 flex-1"> + <div className="max-h-40 mt-1 text-xs text-gray-500"> + {subjectName ? ( + <> + <a + className="underline text-zinc-900 text-sm underline-offset-2" + href={`/${subjectAcronym}`} + > + {subjectAcronym} + </a> + <span className="px-1">•</span> + </> + ) : null} + <span>Compartit per {answer.author.name}</span>{" "} + {formatTimeToNow(new Date(answer.createdAt))} + </div> + <a href={`/${subjectAcronym}/q/${questionId}`}> + <h1 className="text-lg font-semibold py-2 leading-6 text-gray-900"> + {answer.title} + </h1> + </a> - <div - className="relative text-sm max-h-40 w-full overflow-clip" - ref={pRef}> - <EditorOutput content={answer.content} /> - {pRef.current?.clientHeight === 160 ? ( - // blur bottom if content is too long - <div className="absolute bottom-0 left-0 h-24 w-full bg-gradient-to-t from-white to-transparent"></div> - ) : null} - </div> - </div> - </div> - </div> - ); -}; -export default AnswerComponent; + <div + className="relative text-sm max-h-40 w-full overflow-clip" + ref={pRef} + > + <EditorOutput content={answer.content} /> + {pRef.current?.clientHeight === 160 ? ( + // blur bottom if content is too long + <div className="absolute bottom-0 left-0 h-24 w-full bg-gradient-to-t from-white to-transparent"></div> + ) : null} + </div> + </div> + </div> + </div> + ) +} +export default AnswerComponent diff --git a/src/components/AnswerFeed.tsx b/src/components/AnswerFeed.tsx index 5b8423f..161cd50 100644 --- a/src/components/AnswerFeed.tsx +++ b/src/components/AnswerFeed.tsx @@ -1,108 +1,113 @@ -"use client"; +"use client" -import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config"; -import { ExtendedAnswer } from "@/types/db"; -import { useIntersection } from "@mantine/hooks"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { Loader2 } from "lucide-react"; -import { FC, useEffect, useRef } from "react"; -import { useSession } from "next-auth/react"; -import AnswerComponent from "@/components/AnswerComponent"; +import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" +import { ExtendedAnswer } from "@/types/db" +import { useIntersection } from "@mantine/hooks" +import { useInfiniteQuery } from "@tanstack/react-query" +import axios from "axios" +import { Loader2 } from "lucide-react" +import { FC, useEffect, useRef } from "react" +import { useSession } from "next-auth/react" +import AnswerComponent from "@/components/AnswerComponent" interface AnswerFeedProps { - initialAnswers: ExtendedAnswer[]; - subjectName: string; - subjectAcronym: string; - questionId: string; + initialAnswers: ExtendedAnswer[] + subjectName: string + subjectAcronym: string + questionId: string } -const AnswerFeed: FC<AnswerFeedProps> = ({ initialAnswers, subjectName, subjectAcronym, questionId }) => { - const lastAnswerRef = useRef<HTMLElement>(null); - const { ref, entry } = useIntersection({ - root: lastAnswerRef.current, - threshold: 1, - }); - const { data: session } = useSession(); +const AnswerFeed: FC<AnswerFeedProps> = ({ + initialAnswers, + subjectName, + subjectAcronym, + questionId, +}) => { + const lastAnswerRef = useRef<HTMLElement>(null) + const { ref, entry } = useIntersection({ + root: lastAnswerRef.current, + threshold: 1, + }) + const { data: session } = useSession() - const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( - ["infinite-query"], - async ({ pageParam = 1 }) => { - const query = - `/api/a?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}` + - (!!subjectAcronym ? `&subjectAcronym=${subjectAcronym}` : "") + - (!!questionId ? `&questionId=${questionId}` : ""); - const { data } = await axios.get(query); - return data as ExtendedAnswer[]; - }, + const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( + ["infinite-query"], + async ({ pageParam = 1 }) => { + const query = + `/api/a?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}` + + (!!subjectAcronym ? `&subjectAcronym=${subjectAcronym}` : "") + + (!!questionId ? `&questionId=${questionId}` : "") + const { data } = await axios.get(query) + return data as ExtendedAnswer[] + }, - { - getNextPageParam: (_, pages) => { - return pages.length + 1; - }, - initialData: { pages: [initialAnswers], pageParams: [1] }, - } - ); + { + getNextPageParam: (_, pages) => { + return pages.length + 1 + }, + initialData: { pages: [initialAnswers], pageParams: [1] }, + }, + ) - useEffect(() => { - if (entry?.isIntersecting) { - fetchNextPage(); // Load more answers when the last answer comes into view - } - }, [entry, fetchNextPage]); + useEffect(() => { + if (entry?.isIntersecting) { + fetchNextPage() // Load more answers when the last answer comes into view + } + }, [entry, fetchNextPage]) - const answers = data?.pages.flatMap((page) => page) ?? initialAnswers; + const answers = data?.pages.flatMap((page) => page) ?? initialAnswers - return ( - <ul className="flex flex-col col-span-2 space-y-6"> - {answers.map((answer, index) => { - const votesAmt = answer.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); + return ( + <ul className="flex flex-col col-span-2 space-y-6"> + {answers.map((answer, index) => { + const votesAmt = answer.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) - const currentVote = answer.votes.find((vote) => vote.userId === session?.user.id); + const currentVote = answer.votes.find( + (vote) => vote.userId === session?.user.id, + ) - if (index === answers.length - 1) { - // Add a ref to the last answer in the list - return ( - <li - key={answer.id} - ref={ref}> - <AnswerComponent - subjectName={subjectName} - subjectAcronym={subjectAcronym} - answer={answer} - votesAmt={votesAmt} - currentVote={currentVote} - questionId={questionId} - questionAuthorId={answer.question.authorId} - /> - </li> - ); - } else { - return ( - <AnswerComponent - subjectName={subjectName} - subjectAcronym={subjectAcronym} - key={answer.id} - answer={answer} - votesAmt={votesAmt} - currentVote={currentVote} - questionId={questionId} - questionAuthorId={answer.question.authorId} - /> - ); - } - })} + if (index === answers.length - 1) { + // Add a ref to the last answer in the list + return ( + <li key={answer.id} ref={ref}> + <AnswerComponent + subjectName={subjectName} + subjectAcronym={subjectAcronym} + answer={answer} + votesAmt={votesAmt} + currentVote={currentVote} + questionId={questionId} + questionAuthorId={answer.question.authorId} + /> + </li> + ) + } else { + return ( + <AnswerComponent + subjectName={subjectName} + subjectAcronym={subjectAcronym} + key={answer.id} + answer={answer} + votesAmt={votesAmt} + currentVote={currentVote} + questionId={questionId} + questionAuthorId={answer.question.authorId} + /> + ) + } + })} - {isFetchingNextPage && ( - <li className="flex justify-center"> - <Loader2 className="w-6 h-6 text-zinc-500 animate-spin" /> - </li> - )} - </ul> - ); -}; + {isFetchingNextPage && ( + <li className="flex justify-center"> + <Loader2 className="w-6 h-6 text-zinc-500 animate-spin" /> + </li> + )} + </ul> + ) +} -export default AnswerFeed; +export default AnswerFeed diff --git a/src/components/CloseModal.tsx b/src/components/CloseModal.tsx index fa04d0b..2bf12e8 100644 --- a/src/components/CloseModal.tsx +++ b/src/components/CloseModal.tsx @@ -1,21 +1,22 @@ -"use client"; +"use client" -import { X } from "lucide-react"; -import { Button } from "./ui/Button"; -import { useRouter } from "next/navigation"; +import { X } from "lucide-react" +import { Button } from "./ui/Button" +import { useRouter } from "next/navigation" const CloseModal = ({}) => { - const router = useRouter(); + const router = useRouter() - return ( - <Button - variant="subtle" - className="h-6 w-6 p-0 rounded-md" - onClick={() => router.back()} - aria-label="Tancar Modal"> - <X className="h-4 w-4" /> - </Button> - ); -}; + return ( + <Button + variant="subtle" + className="h-6 w-6 p-0 rounded-md" + onClick={() => router.back()} + aria-label="Tancar Modal" + > + <X className="h-4 w-4" /> + </Button> + ) +} -export default CloseModal; +export default CloseModal diff --git a/src/components/Combobox.tsx b/src/components/Combobox.tsx index aeeed26..b7258ed 100644 --- a/src/components/Combobox.tsx +++ b/src/components/Combobox.tsx @@ -17,54 +17,62 @@ import { } from "@/components/ui/Popover" export interface Option { - value: string; - label: string; + value: string + label: string } -export function Combobox({ options, value, setValue }: { options: Option[], value: string, setValue: Function}) { - const [open, setOpen] = React.useState(false); +export function Combobox({ + options, + value, + setValue, +}: { + options: Option[] + value: string + setValue: Function +}) { + const [open, setOpen] = React.useState(false) return ( <div> - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger asChild> - <Button - variant="outline" - role="combobox" - aria-expanded={open} - className="w-[200px] justify-between" - > - {value - ? options.find((option) => option.value === value)?.label - : "Select..."} - <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> - </Button> - </PopoverTrigger> - <PopoverContent className="w-[200px] p-0"> - <Command> - <CommandInput placeholder="Search..." /> - <CommandEmpty>No option found.</CommandEmpty> - <CommandGroup> - {options.map((option) => ( - <CommandItem - key={option.value} - value={option.value} - onSelect={(currentValue) => { - setValue(currentValue === value ? "" : currentValue); - setOpen(false); - }} - > - <Check - className={cn( - "mr-2 h-4 w-4", - value === option.value ? "opacity-100" : "opacity-0" - )} - /> - {option.label} - </CommandItem> - ))} - </CommandGroup> - </Command> - </PopoverContent> - </Popover> + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={open} + className="w-[200px] justify-between" + > + {value + ? options.find((option) => option.value === value)?.label + : "Select..."} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[200px] p-0"> + <Command> + <CommandInput placeholder="Search..." /> + <CommandEmpty>No option found.</CommandEmpty> + <CommandGroup> + {options.map((option) => ( + <CommandItem + key={option.value} + value={option.value} + onSelect={(currentValue) => { + setValue(currentValue === value ? "" : currentValue) + setOpen(false) + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + value === option.value ? "opacity-100" : "opacity-0", + )} + /> + {option.label} + </CommandItem> + ))} + </CommandGroup> + </Command> + </PopoverContent> + </Popover> </div> - ); -} \ No newline at end of file + ) +} diff --git a/src/components/CommentComponent.tsx b/src/components/CommentComponent.tsx index d60c4c8..e8f83ea 100644 --- a/src/components/CommentComponent.tsx +++ b/src/components/CommentComponent.tsx @@ -1,69 +1,72 @@ -"use client"; +"use client" -import { formatTimeToNow } from "@/lib/utils"; -import { Comment, User, CommentVote } from "@prisma/client"; -import { FC, useRef } from "react"; +import { formatTimeToNow } from "@/lib/utils" +import { Comment, User, CommentVote } from "@prisma/client" +import { FC, useRef } from "react" // import EditorOutput from "./EditorOutput"; -import CommentVoteClient from "./votes/CommentVoteClient"; +import CommentVoteClient from "./votes/CommentVoteClient" -type PartialVote = Pick<CommentVote, "type">; +type PartialVote = Pick<CommentVote, "type"> interface CommentProps { - comment: Comment & { - author: User; - votes: CommentVote[]; - }; - votesAmt: number; - subjectName: string; - currentVote?: PartialVote; - subjectAcronym: string; - postId: string; + comment: Comment & { + author: User + votes: CommentVote[] + } + votesAmt: number + subjectName: string + currentVote?: PartialVote + subjectAcronym: string + postId: string } const CommentComponent: FC<CommentProps> = ({ - comment, - votesAmt: _votesAmt, - currentVote: _currentVote, - subjectName, - subjectAcronym, + comment, + votesAmt: _votesAmt, + currentVote: _currentVote, + subjectName, + subjectAcronym, }) => { - const pRef = useRef<HTMLParagraphElement>(null); + const pRef = useRef<HTMLParagraphElement>(null) - return ( - <div className="rounded-md bg-white shadow"> - <div className="px-6 py-4 flex justify-between"> - <CommentVoteClient - initialVotesAmt={_votesAmt} - commentId={comment.id} - initialVote={_currentVote?.type} - /> + return ( + <div className="rounded-md bg-white shadow"> + <div className="px-6 py-4 flex justify-between"> + <CommentVoteClient + initialVotesAmt={_votesAmt} + commentId={comment.id} + initialVote={_currentVote?.type} + /> - <div className="w-0 flex-1"> - <div className="max-h-40 mt-1 text-xs text-gray-500"> - {subjectName ? ( - <> - <a - className="underline text-zinc-900 text-sm underline-offset-2" - href={`/${subjectAcronym}`}> - {subjectAcronym} - </a> - <span className="px-1">•</span> - </> - ) : null} - <span>Compartit per {comment.author.name}</span> {formatTimeToNow(new Date(comment.createdAt))} - </div> - <div - className="relative text-sm max-h-40 w-full overflow-clip" - ref={pRef}> - <div>{comment.content}</div> - {pRef.current?.clientHeight === 160 ? ( - // blur bottom if content is too long - <div className="absolute bottom-0 left-0 h-24 w-full bg-gradient-to-t from-white to-transparent"></div> - ) : null} - </div> - </div> - </div> - </div> - ); -}; -export default CommentComponent; + <div className="w-0 flex-1"> + <div className="max-h-40 mt-1 text-xs text-gray-500"> + {subjectName ? ( + <> + <a + className="underline text-zinc-900 text-sm underline-offset-2" + href={`/${subjectAcronym}`} + > + {subjectAcronym} + </a> + <span className="px-1">•</span> + </> + ) : null} + <span>Compartit per {comment.author.name}</span>{" "} + {formatTimeToNow(new Date(comment.createdAt))} + </div> + <div + className="relative text-sm max-h-40 w-full overflow-clip" + ref={pRef} + > + <div>{comment.content}</div> + {pRef.current?.clientHeight === 160 ? ( + // blur bottom if content is too long + <div className="absolute bottom-0 left-0 h-24 w-full bg-gradient-to-t from-white to-transparent"></div> + ) : null} + </div> + </div> + </div> + </div> + ) +} +export default CommentComponent diff --git a/src/components/CommentFeed.tsx b/src/components/CommentFeed.tsx index 741eb9e..110c9a6 100644 --- a/src/components/CommentFeed.tsx +++ b/src/components/CommentFeed.tsx @@ -1,106 +1,111 @@ -"use client"; +"use client" -import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config"; -import { ExtendedComment } from "@/types/db"; -import { useIntersection } from "@mantine/hooks"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { Loader2 } from "lucide-react"; -import { FC, useEffect, useRef } from "react"; -import { useSession } from "next-auth/react"; -import CommentComponent from "@/components/CommentComponent"; +import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" +import { ExtendedComment } from "@/types/db" +import { useIntersection } from "@mantine/hooks" +import { useInfiniteQuery } from "@tanstack/react-query" +import axios from "axios" +import { Loader2 } from "lucide-react" +import { FC, useEffect, useRef } from "react" +import { useSession } from "next-auth/react" +import CommentComponent from "@/components/CommentComponent" interface CommentFeedProps { - initialComments: ExtendedComment[]; - subjectName: string; - subjectAcronym: string; - postId: string; + initialComments: ExtendedComment[] + subjectName: string + subjectAcronym: string + postId: string } -const CommentFeed: FC<CommentFeedProps> = ({ initialComments, subjectName, subjectAcronym, postId }) => { - const lastCommentRef = useRef<HTMLElement>(null); - const { ref, entry } = useIntersection({ - root: lastCommentRef.current, - threshold: 1, - }); - const { data: session } = useSession(); +const CommentFeed: FC<CommentFeedProps> = ({ + initialComments, + subjectName, + subjectAcronym, + postId, +}) => { + const lastCommentRef = useRef<HTMLElement>(null) + const { ref, entry } = useIntersection({ + root: lastCommentRef.current, + threshold: 1, + }) + const { data: session } = useSession() - const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( - ["infinite-query"], - async ({ pageParam = 1 }) => { - const query = - `/api/comments?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}` + - (!!subjectAcronym ? `&subjectAcronym=${subjectAcronym}` : "") + - (!!postId ? `&postId=${postId}` : ""); - const { data } = await axios.get(query); - return data as ExtendedComment[]; - }, + const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( + ["infinite-query"], + async ({ pageParam = 1 }) => { + const query = + `/api/comments?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}` + + (!!subjectAcronym ? `&subjectAcronym=${subjectAcronym}` : "") + + (!!postId ? `&postId=${postId}` : "") + const { data } = await axios.get(query) + return data as ExtendedComment[] + }, - { - getNextPageParam: (_, pages) => { - return pages.length + 1; - }, - initialData: { pages: [initialComments], pageParams: [1] }, - } - ); + { + getNextPageParam: (_, pages) => { + return pages.length + 1 + }, + initialData: { pages: [initialComments], pageParams: [1] }, + }, + ) - useEffect(() => { - if (entry?.isIntersecting) { - fetchNextPage(); // Load more comments when the last comment comes into view - } - }, [entry, fetchNextPage]); + useEffect(() => { + if (entry?.isIntersecting) { + fetchNextPage() // Load more comments when the last comment comes into view + } + }, [entry, fetchNextPage]) - const comments = data?.pages.flatMap((page) => page) ?? initialComments; + const comments = data?.pages.flatMap((page) => page) ?? initialComments - return ( - <ul className="flex flex-col col-span-2 space-y-6"> - {comments.map((comment, index) => { - const votesAmt = comment.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); + return ( + <ul className="flex flex-col col-span-2 space-y-6"> + {comments.map((comment, index) => { + const votesAmt = comment.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) - const currentVote = comment.votes.find((vote) => vote.userId === session?.user.id); + const currentVote = comment.votes.find( + (vote) => vote.userId === session?.user.id, + ) - if (index === comments.length - 1) { - // Add a ref to the last comment in the list - return ( - <li - key={comment.id} - ref={ref}> - <CommentComponent - subjectName={subjectName} - subjectAcronym={subjectAcronym} - comment={comment} - votesAmt={votesAmt} - currentVote={currentVote} - postId={postId} - /> - </li> - ); - } else { - return ( - <CommentComponent - subjectName={subjectName} - subjectAcronym={subjectAcronym} - key={comment.id} - comment={comment} - votesAmt={votesAmt} - currentVote={currentVote} - postId={postId} - /> - ); - } - })} + if (index === comments.length - 1) { + // Add a ref to the last comment in the list + return ( + <li key={comment.id} ref={ref}> + <CommentComponent + subjectName={subjectName} + subjectAcronym={subjectAcronym} + comment={comment} + votesAmt={votesAmt} + currentVote={currentVote} + postId={postId} + /> + </li> + ) + } else { + return ( + <CommentComponent + subjectName={subjectName} + subjectAcronym={subjectAcronym} + key={comment.id} + comment={comment} + votesAmt={votesAmt} + currentVote={currentVote} + postId={postId} + /> + ) + } + })} - {isFetchingNextPage && ( - <li className="flex justify-center"> - <Loader2 className="w-6 h-6 text-zinc-500 animate-spin" /> - </li> - )} - </ul> - ); -}; + {isFetchingNextPage && ( + <li className="flex justify-center"> + <Loader2 className="w-6 h-6 text-zinc-500 animate-spin" /> + </li> + )} + </ul> + ) +} -export default CommentFeed; +export default CommentFeed diff --git a/src/components/CustomFeed.tsx b/src/components/CustomFeed.tsx index 02a6ea3..08ff8a0 100644 --- a/src/components/CustomFeed.tsx +++ b/src/components/CustomFeed.tsx @@ -1,45 +1,45 @@ -import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config"; -import { getAuthSession } from "@/lib/auth"; -import { db } from "@/lib/db"; -import PostFeed from "./PostFeed"; -import { notFound } from "next/navigation"; +import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" +import { getAuthSession } from "@/lib/auth" +import { db } from "@/lib/db" +import PostFeed from "./PostFeed" +import { notFound } from "next/navigation" const CustomFeed = async () => { - const session = await getAuthSession(); + const session = await getAuthSession() - // only rendered if session exists, so this will not happen - if (!session) return notFound(); + // only rendered if session exists, so this will not happen + if (!session) return notFound() - const followedCommunities = await db.subscription.findMany({ - where: { - userId: session.user.id, - }, - include: { - subject: true, - }, - }); + const followedCommunities = await db.subscription.findMany({ + where: { + userId: session.user.id, + }, + include: { + subject: true, + }, + }) - const posts = await db.post.findMany({ - where: { - subject: { - name: { - in: followedCommunities.map((sub) => sub.subject.name), - }, - }, - }, - orderBy: { - createdAt: "desc", - }, - include: { - votes: true, - author: true, - comments: true, - subject: true, - }, - take: INFINITE_SCROLL_PAGINATION_RESULTS, - }); + const posts = await db.post.findMany({ + where: { + subject: { + name: { + in: followedCommunities.map((sub) => sub.subject.name), + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + include: { + votes: true, + author: true, + comments: true, + subject: true, + }, + take: INFINITE_SCROLL_PAGINATION_RESULTS, + }) - return <PostFeed initialPosts={posts} />; -}; + return <PostFeed initialPosts={posts} /> +} -export default CustomFeed; +export default CustomFeed diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 1ef2d96..c5a0a89 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -1,25 +1,25 @@ -"use client"; +"use client" -import { FC, useCallback, useEffect, useRef, useState } from "react"; -import TextareaAutosize from "react-textarea-autosize"; -import { useForm } from "react-hook-form"; +import { FC, useCallback, useEffect, useRef, useState } from "react" +import TextareaAutosize from "react-textarea-autosize" +import { useForm } from "react-hook-form" import { QuestionCreationRequest, QuestionValidator, AnswerCreationRequest, AnswerValidator, -} from "@/lib/validators/question"; -import { zodResolver } from "@hookform/resolvers/zod"; -import type EditorJS from "@editorjs/editorjs"; -import { toast } from "@/hooks/use-toast"; -import { useMutation } from "@tanstack/react-query"; -import axios from "axios"; -import { usePathname, useRouter } from "next/navigation"; +} from "@/lib/validators/question" +import { zodResolver } from "@hookform/resolvers/zod" +import type EditorJS from "@editorjs/editorjs" +import { toast } from "@/hooks/use-toast" +import { useMutation } from "@tanstack/react-query" +import axios from "axios" +import { usePathname, useRouter } from "next/navigation" interface EditorProps { - subjectId: string; - contentType: "question" | "answer"; - questionId?: string; + subjectId: string + contentType: "question" | "answer" + questionId?: string } const Editor: FC<EditorProps> = ({ subjectId, contentType, questionId }) => { @@ -28,7 +28,9 @@ const Editor: FC<EditorProps> = ({ subjectId, contentType, questionId }) => { handleSubmit, formState: { errors }, } = useForm<QuestionCreationRequest | AnswerCreationRequest>({ - resolver: zodResolver(contentType === "question" ? QuestionValidator : AnswerValidator), + resolver: zodResolver( + contentType === "question" ? QuestionValidator : AnswerValidator, + ), defaultValues: { subjectId, title: `${ @@ -37,30 +39,30 @@ const Editor: FC<EditorProps> = ({ subjectId, contentType, questionId }) => { content: null, questionId: questionId || "", // Add questionId as a value in the form }, - }); + }) - const ref = useRef<EditorJS>(); - const [isMounted, setIsMounted] = useState<boolean>(false); - const _titleRef = useRef<HTMLTextAreaElement>(null); - const pathname = usePathname(); - const router = useRouter(); + const ref = useRef<EditorJS>() + const [isMounted, setIsMounted] = useState<boolean>(false) + const _titleRef = useRef<HTMLTextAreaElement>(null) + const pathname = usePathname() + const router = useRouter() const initializeEditor = useCallback(async () => { - const EditorJS = (await import("@editorjs/editorjs")).default; - const Header = (await import("@editorjs/header")).default; - const Embed = (await import("@editorjs/embed")).default; - const Table = (await import("@editorjs/table")).default; - const List = (await import("@editorjs/list")).default; - const Code = (await import("@editorjs/code")).default; - const LinkTool = (await import("@editorjs/link")).default; - const InlineCode = (await import("@editorjs/inline-code")).default; + const EditorJS = (await import("@editorjs/editorjs")).default + const Header = (await import("@editorjs/header")).default + const Embed = (await import("@editorjs/embed")).default + const Table = (await import("@editorjs/table")).default + const List = (await import("@editorjs/list")).default + const Code = (await import("@editorjs/code")).default + const LinkTool = (await import("@editorjs/link")).default + const InlineCode = (await import("@editorjs/inline-code")).default // const ImageTool = (await import("@editorjs/image")).default; TODO: do this later if (!ref.current) { const editor = new EditorJS({ holder: "editor", onReady() { - ref.current = editor; + ref.current = editor }, placeholder: `Esciu aquí la teva ${ contentType === "question" ? "pregunta" : "resposta" @@ -81,15 +83,15 @@ const Editor: FC<EditorProps> = ({ subjectId, contentType, questionId }) => { table: Table, embed: Embed, }, - }); + }) } - }, []); + }, []) useEffect(() => { if (typeof window !== "undefined") { - setIsMounted(true); + setIsMounted(true) } - }, []); + }, []) useEffect(() => { if (Object.keys(errors).length) { @@ -98,29 +100,29 @@ const Editor: FC<EditorProps> = ({ subjectId, contentType, questionId }) => { title: "Alguna cosa no ha anat bé...", description: (value as { message: string }).message, variant: "destructive", - }); + }) } } - }, [errors]); + }, [errors]) useEffect(() => { const init = async () => { - await initializeEditor(); + await initializeEditor() setTimeout(() => { - _titleRef.current?.focus(); - }, 0); - }; + _titleRef.current?.focus() + }, 0) + } if (isMounted) { - init(); + init() return () => { - ref.current?.destroy(); - ref.current = undefined; - }; + ref.current?.destroy() + ref.current = undefined + } } - }, [isMounted, initializeEditor]); + }, [isMounted, initializeEditor]) const { mutate: createContent } = useMutation({ mutationFn: async ({ @@ -133,13 +135,13 @@ const Editor: FC<EditorProps> = ({ subjectId, contentType, questionId }) => { content, subjectId, ...(contentType === "answer" && { questionId: questionId }), - }; - const apiPath = contentType === "question" ? "/api/subject/question/create" : "/api/subject/answer/create"; - const { data } = await axios.post( - apiPath, - payload - ); - return data; + } + const apiPath = + contentType === "question" + ? "/api/subject/question/create" + : "/api/subject/answer/create" + const { data } = await axios.post(apiPath, payload) + return data }, onError: () => { toast({ @@ -148,42 +150,42 @@ const Editor: FC<EditorProps> = ({ subjectId, contentType, questionId }) => { contentType === "question" ? "pregunta" : "reposta" }. Torna-ho a provar més tard.`, variant: "destructive", - }); + }) }, onSuccess: (data) => { // return new Response(JSON.stringify(createdQuestionId), { status: 201 }); if (contentType === "question") { - const questionId = data as string; - const newPathname = pathname.replace("/q", `/q/${questionId}`); - router.push(newPathname); + const questionId = data as string + const newPathname = pathname.replace("/q", `/q/${questionId}`) + router.push(newPathname) } - router.refresh(); + router.refresh() return toast({ description: `La teva ${ contentType === "question" ? "pregunta" : "reposta" } s'ha creat correctament`, - }); + }) }, - }); + }) async function onSubmit() { - const blocks = await ref.current?.save(); - const title = (await _titleRef.current?.value) as string; + const blocks = await ref.current?.save() + const title = (await _titleRef.current?.value) as string const payload: QuestionCreationRequest | AnswerCreationRequest = { title: title, content: blocks, subjectId: subjectId, ...(contentType === "answer" && { questionId: questionId }), - }; - createContent(payload); + } + createContent(payload) } if (!isMounted) { - return null; + return null } - const { ref: titleRef, ...rest } = register("title"); + const { ref: titleRef, ...rest } = register("title") return ( <div className="w-full p-4 bg-zinc-50 rounded-lg border border-zinc-200 h-full"> <form @@ -194,9 +196,9 @@ const Editor: FC<EditorProps> = ({ subjectId, contentType, questionId }) => { <div className="prose prose-stone dark:prose-invert h-full"> <TextareaAutosize ref={(e) => { - titleRef(e); + titleRef(e) // @ts-ignore - _titleRef.current = e; + _titleRef.current = e }} placeholder="Títol" className="w-full resize-none appearance-none overflow-hidden bg-transparent text-xl font-bold focus:outline-none h-12" @@ -205,7 +207,7 @@ const Editor: FC<EditorProps> = ({ subjectId, contentType, questionId }) => { </div> </form> </div> - ); -}; + ) +} -export default Editor; +export default Editor diff --git a/src/components/EditorOutput.tsx b/src/components/EditorOutput.tsx index 6a6936d..5d33768 100644 --- a/src/components/EditorOutput.tsx +++ b/src/components/EditorOutput.tsx @@ -1,11 +1,11 @@ -import CustomCodeRenderer from '@/components/renderers/CustomCodeRenderer' -import CustomImageRenderer from '@/components/renderers/CustomImageRenderer' -import { FC } from 'react' -import dynamic from 'next/dynamic' +import CustomCodeRenderer from "@/components/renderers/CustomCodeRenderer" +import CustomImageRenderer from "@/components/renderers/CustomImageRenderer" +import { FC } from "react" +import dynamic from "next/dynamic" const Output = dynamic( - async () => (await import('editorjs-react-renderer')).default, - { ssr: false } + async () => (await import("editorjs-react-renderer")).default, + { ssr: false }, ) as FC<any> interface EditorOutputProps { @@ -19,8 +19,8 @@ const renderers = { const style = { paragraph: { - fontSize: '0.875rem', - lineHeight: '1.25rem', + fontSize: "0.875rem", + lineHeight: "1.25rem", }, } @@ -28,7 +28,7 @@ const EditorOutput: FC<EditorOutputProps> = ({ content }) => { return ( <Output style={style} - className='text-sm' + className="text-sm" renderers={renderers} data={content} /> diff --git a/src/components/Form.tsx b/src/components/Form.tsx index 1613ab0..cbb7d7f 100644 --- a/src/components/Form.tsx +++ b/src/components/Form.tsx @@ -1,41 +1,52 @@ -"use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; -import { z } from "zod"; -import { useMutation } from "@tanstack/react-query"; -import axios from "axios"; -import { useRouter } from "next/navigation"; -import { toast } from "@/hooks/use-toast"; +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { FieldValues, SubmitHandler, useForm } from "react-hook-form" +import { z } from "zod" +import { useMutation } from "@tanstack/react-query" +import axios from "axios" +import { useRouter } from "next/navigation" +import { toast } from "@/hooks/use-toast" -import { Button } from "@/components/ui/Button"; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/Form"; -import { Input } from "@/components/ui/Input"; -import { Combobox } from "@/components/Combobox"; -import { ApuntsPostCreationRequest } from "@/lib/validators/post"; -import { uploadFiles } from "@/lib/uploadthing"; +import { Button } from "@/components/ui/Button" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/Form" +import { Input } from "@/components/ui/Input" +import { Combobox } from "@/components/Combobox" +import { Checkbox } from "@/components/ui/checkbox" +import { ApuntsPostCreationRequest } from "@/lib/validators/post" +import { uploadFiles } from "@/lib/uploadthing" const formSchema = z.object({ - pdf: z.any(), - title: z.string({ - required_error: "Selecciona un usuari", - }), - assignatura: z.string({ - required_error: "Selecciona una assignatura.", - }), - tipus: z.string({ - required_error: "Selecciona un tipus.", - }), -}); + pdf: z.any(), + title: z.string({ + required_error: "Selecciona un usuari", + }), + assignatura: z.string({ + required_error: "Selecciona una assignatura.", + }), + tipus: z.string({ + required_error: "Selecciona un tipus.", + }), + anonim: z.boolean().default(false).optional(), +}) const smallFormSchema = z.object({ - pdf: z.any(), - title: z.string({ - required_error: "Selecciona un usuari", - }), - tipus: z.string({ - required_error: "Selecciona un tipus.", - }), -}); + pdf: z.any(), + title: z.string({ + required_error: "Selecciona un usuari", + }), + tipus: z.string({ + required_error: "Selecciona un tipus.", + }), + anonim: z.boolean().default(false).optional(), +}) // export function ComboboxForm() { // const form = useForm<z.infer<typeof FormSchema>>({ @@ -43,219 +54,232 @@ const smallFormSchema = z.object({ // }) export function ProfileForm() { - const router = useRouter(); + const router = useRouter() - const { mutate: createApuntsPost } = useMutation({ - mutationFn: async ({ pdf, title, assignatura, tipus }: ApuntsPostCreationRequest) => { - const payload: ApuntsPostCreationRequest = { - pdf, - title, - assignatura, - tipus, - }; - const { data } = await axios.post("/api/subject/post/create", payload); - return data; - }, - onError: () => { - toast({ - title: "Alguna cosa no ha anat bé", - description: "No s'ha pogut crear el post. Torna-ho a provar més tard.", - variant: "destructive", - }); - }, - onSuccess: (subjectAcronym) => { - router.push(`/${subjectAcronym}`); - router.refresh(); + const { mutate: createApuntsPost } = useMutation({ + mutationFn: async ({ + pdf, + title, + assignatura, + tipus, + anonim, + }: ApuntsPostCreationRequest) => { + const payload: ApuntsPostCreationRequest = { + pdf, + title, + assignatura, + tipus, + anonim, + } + const { data } = await axios.post("/api/subject/post/create", payload) + return data + }, + onError: () => { + toast({ + title: "Alguna cosa no ha anat bé", + description: "No s'ha pogut crear el post. Torna-ho a provar més tard.", + variant: "destructive", + }) + }, + onSuccess: (subjectAcronym) => { + router.push(`/${subjectAcronym}`) + router.refresh() - return toast({ - description: "El teu post s'ha creat correctament", - }); - }, - }); - const form = useForm({ resolver: zodResolver(formSchema) }); - async function onSubmit(data: ApuntsPostCreationRequest) { - const [res] = await uploadFiles([data.pdf], "fileUploader"); - const payload: ApuntsPostCreationRequest = { - pdf: res.fileUrl, - title: data.title, - assignatura: data.assignatura, - tipus: data.tipus, - }; + return toast({ + description: "El teu post s'ha creat correctament", + }) + }, + }) + const form = useForm({ + resolver: zodResolver(formSchema), + }) + async function onSubmit(data: ApuntsPostCreationRequest) { + const [res] = await uploadFiles([data.pdf], "fileUploader") + const payload: ApuntsPostCreationRequest = { + pdf: res.fileUrl, + title: data.title, + assignatura: data.assignatura, + tipus: data.tipus, + anonim: data.anonim, + } - createApuntsPost(payload); - } - // ------------------------------ - const assignatures = [ - { - value: "alg", - label: "ALG", - }, - { - value: "cal", - label: "CAL", - }, - { - value: "lmd", - label: "LMD", - }, - { - value: "ap1", - label: "AP1", - }, - { - value: "ap2", - label: "AP2", - }, - { - value: "ac2", - label: "AC2", - }, - { - value: "pie1", - label: "PIE1", - }, - { - value: "com", - label: "COM", - }, - { - value: "sis", - label: "SIS", - }, - { - value: "ap3", - label: "AP3", - }, - { - value: "teoi", - label: "TEOI", - }, - { - value: "pie2", - label: "PIE2", - }, - { - value: "bd", - label: "BD", - }, - { - value: "psd", - label: "PSD", - }, - { - value: "ipa", - label: "IPA", - }, - { - value: "om", - label: "OM", - }, - { - value: "ad", - label: "AD", - }, - { - value: "aa1", - label: "AA1", - }, - { - value: "vi", - label: "VI", - }, - { - value: "cai", - label: "CAI", - }, - { - value: "bda", - label: "BDA", - }, - { - value: "aa2", - label: "AA2", - }, - { - value: "ei", - label: "EI", - }, - { - value: "taed1", - label: "TAED1", - }, - { - value: "poe", - label: "POE", - }, - { - value: "piva", - label: "PIVA", - }, - { - value: "pe", - label: "PE", - }, - { - value: "taed2", - label: "TAED2", - }, - { - value: "altres", - label: "Altres", - }, - ]; - const tipus = [ - { - value: "apunts", - label: "Apunts", - }, - { - value: "examens", - label: "Exàmens", - }, - { - value: "exercicis", - label: "Exercicis", - }, - { - value: "diapositives", - label: "Diapositives", - }, - { - value: "altres", - label: "Altres", - }, - ]; - // ------------------------------ - return ( - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit as SubmitHandler<FieldValues>)} - className="space-y-8"> - <FormField - control={form.control} - name="pdf" - render={({ field }) => ( - <FormItem> - <FormLabel>Fitxers PDF</FormLabel> - <FormControl> - <div className="grid w-full max-w-sm items-center gap-1.5"> - <Input - id="pdf-file" - type="file" - onChange={(e) => { - if (e.target.files) { - field.onChange(e.target.files[0]); - } - }} - /> - </div> - </FormControl> - <FormDescription>Penja els teus apunts en format PDF.</FormDescription> - <FormMessage /> - </FormItem> - )} - /> - {/* TODO: Nomes admins + createApuntsPost(payload) + } + // ------------------------------ + const assignatures = [ + { + value: "alg", + label: "ALG", + }, + { + value: "cal", + label: "CAL", + }, + { + value: "lmd", + label: "LMD", + }, + { + value: "ap1", + label: "AP1", + }, + { + value: "ap2", + label: "AP2", + }, + { + value: "ac2", + label: "AC2", + }, + { + value: "pie1", + label: "PIE1", + }, + { + value: "com", + label: "COM", + }, + { + value: "sis", + label: "SIS", + }, + { + value: "ap3", + label: "AP3", + }, + { + value: "teoi", + label: "TEOI", + }, + { + value: "pie2", + label: "PIE2", + }, + { + value: "bd", + label: "BD", + }, + { + value: "psd", + label: "PSD", + }, + { + value: "ipa", + label: "IPA", + }, + { + value: "om", + label: "OM", + }, + { + value: "ad", + label: "AD", + }, + { + value: "aa1", + label: "AA1", + }, + { + value: "vi", + label: "VI", + }, + { + value: "cai", + label: "CAI", + }, + { + value: "bda", + label: "BDA", + }, + { + value: "aa2", + label: "AA2", + }, + { + value: "ei", + label: "EI", + }, + { + value: "taed1", + label: "TAED1", + }, + { + value: "poe", + label: "POE", + }, + { + value: "piva", + label: "PIVA", + }, + { + value: "pe", + label: "PE", + }, + { + value: "taed2", + label: "TAED2", + }, + { + value: "altres", + label: "Altres", + }, + ] + const tipus = [ + { + value: "apunts", + label: "Apunts", + }, + { + value: "examens", + label: "Exàmens", + }, + { + value: "exercicis", + label: "Exercicis", + }, + { + value: "diapositives", + label: "Diapositives", + }, + { + value: "altres", + label: "Altres", + }, + ] + // ------------------------------ + return ( + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit as SubmitHandler<FieldValues>)} + className="space-y-8" + > + <FormField + control={form.control} + name="pdf" + render={({ field }) => ( + <FormItem> + <FormLabel>Fitxers PDF</FormLabel> + <FormControl> + <div className="grid w-full max-w-sm items-center gap-1.5"> + <Input + id="pdf-file" + type="file" + onChange={(e) => { + if (e.target.files) { + field.onChange(e.target.files[0]) + } + }} + /> + </div> + </FormControl> + <FormDescription> + Penja els teus apunts en format PDF. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + {/* TODO: Nomes admins <FormField control={form.control} name="username" @@ -272,166 +296,199 @@ export function ProfileForm() { </FormItem> )} /> */} - <FormField - control={form.control} - name="title" - render={({ field }) => ( - <FormItem> - <FormLabel>Nom dels Apunts</FormLabel> - <FormControl> - <Input - placeholder="WhoIsGraf?" - {...field} - /> - </FormControl> - <FormDescription>El nom dels teus apunts.</FormDescription> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="assignatura" - render={({ field }) => ( - <FormItem> - <FormLabel>Assignatura</FormLabel> - <FormControl> - <Combobox - options={assignatures} - value={field.value} - setValue={field.onChange} - /> - </FormControl> - <FormDescription>Tria l'assignatura.</FormDescription> - </FormItem> - )} - /> + <FormField + control={form.control} + name="title" + render={({ field }) => ( + <FormItem> + <FormLabel>Nom dels Apunts</FormLabel> + <FormControl> + <Input placeholder="WhoIsGraf?" {...field} /> + </FormControl> + <FormDescription>El nom dels teus apunts.</FormDescription> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="assignatura" + render={({ field }) => ( + <FormItem> + <FormLabel>Assignatura</FormLabel> + <FormControl> + <Combobox + options={assignatures} + value={field.value} + setValue={field.onChange} + /> + </FormControl> + <FormDescription>Tria l'assignatura.</FormDescription> + </FormItem> + )} + /> - <FormField - control={form.control} - name="tipus" - render={({ field }) => ( - <FormItem> - <FormLabel>Tipus</FormLabel> - <FormControl> - <Combobox - options={tipus} - value={field.value} - setValue={field.onChange} - /> - </FormControl> - <FormDescription>Tria el tipus de document.</FormDescription> - </FormItem> - )} - /> - <Button - type="submit" - isLoading={form.formState.isSubmitting}> - Submit - </Button> - </form> - </Form> - ); + <FormField + control={form.control} + name="tipus" + render={({ field }) => ( + <FormItem> + <FormLabel>Tipus</FormLabel> + <FormControl> + <Combobox + options={tipus} + value={field.value} + setValue={field.onChange} + /> + </FormControl> + <FormDescription>Tria el tipus de document.</FormDescription> + </FormItem> + )} + /> + <FormField + control={form.control} + name="anonim" + render={({ field }) => ( + <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4 shadow"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <div className="space-y-1 leading-none"> + <FormLabel>Penjar com a anònim</FormLabel> + <FormDescription> + L'AED guarda sempre l'autor dels apunts. + L'opció d'anònim permet que no es mostrin als altres + usuaris. + </FormDescription> + </div> + </FormItem> + )} + /> + <Button type="submit" isLoading={form.formState.isSubmitting}> + Submit + </Button> + </form> + </Form> + ) } -export default ProfileForm; +export default ProfileForm -export const SmallProfileForm = ({ subjectAcronym }: { subjectAcronym: string }) => { - const router = useRouter(); +export const SmallProfileForm = ({ + subjectAcronym, +}: { + subjectAcronym: string +}) => { + const router = useRouter() - const { mutate: createApuntsPost } = useMutation({ - mutationFn: async ({ pdf, title, assignatura, tipus }: ApuntsPostCreationRequest) => { - const payload: ApuntsPostCreationRequest = { - pdf, - title, - assignatura, - tipus, - }; - const { data } = await axios.post("/api/subject/post/create", payload); - return data; - }, - onError: () => { - toast({ - title: "Alguna cosa no ha anat bé", - description: "No s'ha pogut crear el post. Torna-ho a provar més tard.", - variant: "destructive", - }); - }, - onSuccess: (subjectAcronym) => { - router.push(`/${subjectAcronym}`); - router.refresh(); + const { mutate: createApuntsPost } = useMutation({ + mutationFn: async ({ + pdf, + title, + assignatura, + tipus, + anonim, + }: ApuntsPostCreationRequest) => { + const payload: ApuntsPostCreationRequest = { + pdf, + title, + assignatura, + tipus, + anonim, + } + const { data } = await axios.post("/api/subject/post/create", payload) + return data + }, + onError: () => { + toast({ + title: "Alguna cosa no ha anat bé", + description: "No s'ha pogut crear el post. Torna-ho a provar més tard.", + variant: "destructive", + }) + }, + onSuccess: (subjectAcronym) => { + router.push(`/${subjectAcronym}`) + router.refresh() - return toast({ - description: "El teu post s'ha creat correctament", - }); - }, - }); - const form = useForm({ resolver: zodResolver(smallFormSchema) }); - async function onSubmit(data: ApuntsPostCreationRequest) { - const [res] = await uploadFiles([data.pdf], "fileUploader"); - const payload: ApuntsPostCreationRequest = { - pdf: res.fileUrl, - title: data.title, - assignatura: subjectAcronym, - tipus: data.tipus, - }; - - createApuntsPost(payload); - } - // ------------------------------ - const tipus = [ - { - value: "apunts", - label: "Apunts", - }, - { - value: "examens", - label: "Exàmens", - }, - { - value: "exercicis", - label: "Exercicis", - }, - { - value: "diapositives", - label: "Diapositives", - }, - { - value: "altres", - label: "Altres", - }, - ]; - // ------------------------------ - return ( - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit as SubmitHandler<FieldValues>)} - className="space-y-8"> - <FormField - control={form.control} - name="pdf" - render={({ field }) => ( - <FormItem> - <FormLabel>Fitxers PDF</FormLabel> - <FormControl> - <div className="grid w-full max-w-sm items-center gap-1.5"> - <Input - id="pdf-file" - type="file" - onChange={(e) => { - if (e.target.files) { - field.onChange(e.target.files[0]); - } - }} - /> - </div> - </FormControl> - <FormDescription>Penja els teus apunts en format PDF.</FormDescription> - <FormMessage /> - </FormItem> - )} - /> - {/* TODO: Nomes admins + return toast({ + description: "El teu post s'ha creat correctament", + }) + }, + }) + const form = useForm({ + resolver: zodResolver(smallFormSchema), + }) + async function onSubmit(data: ApuntsPostCreationRequest) { + const [res] = await uploadFiles([data.pdf], "fileUploader") + const payload: ApuntsPostCreationRequest = { + pdf: res.fileUrl, + title: data.title, + assignatura: subjectAcronym, + tipus: data.tipus, + anonim: data.anonim, + } + createApuntsPost(payload) + } + // ------------------------------ + const tipus = [ + { + value: "apunts", + label: "Apunts", + }, + { + value: "examens", + label: "Exàmens", + }, + { + value: "exercicis", + label: "Exercicis", + }, + { + value: "diapositives", + label: "Diapositives", + }, + { + value: "altres", + label: "Altres", + }, + ] + // ------------------------------ + return ( + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit as SubmitHandler<FieldValues>)} + className="space-y-8" + > + <FormField + control={form.control} + name="pdf" + render={({ field }) => ( + <FormItem> + <FormLabel>Fitxers PDF</FormLabel> + <FormControl> + <div className="grid w-full max-w-sm items-center gap-1.5"> + <Input + id="pdf-file" + type="file" + onChange={(e) => { + if (e.target.files) { + field.onChange(e.target.files[0]) + } + }} + /> + </div> + </FormControl> + <FormDescription> + Penja els teus apunts en format PDF. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + {/* TODO: Nomes admins <FormField control={form.control} name="username" @@ -448,47 +505,64 @@ export const SmallProfileForm = ({ subjectAcronym }: { subjectAcronym: string }) </FormItem> )} /> */} - <FormField - control={form.control} - name="title" - render={({ field }) => ( - <FormItem> - <FormLabel>Nom dels Apunts</FormLabel> - <FormControl> - <Input - placeholder="WhoIsGraf?" - {...field} - /> - </FormControl> - <FormDescription>El nom dels teus apunts.</FormDescription> - <FormMessage /> - </FormItem> - )} - /> + <FormField + control={form.control} + name="title" + render={({ field }) => ( + <FormItem> + <FormLabel>Nom dels Apunts</FormLabel> + <FormControl> + <Input placeholder="WhoIsGraf?" {...field} /> + </FormControl> + <FormDescription>El nom dels teus apunts.</FormDescription> + <FormMessage /> + </FormItem> + )} + /> - <FormField - control={form.control} - name="tipus" - render={({ field }) => ( - <FormItem> - <FormLabel>Tipus</FormLabel> - <FormControl> - <Combobox - options={tipus} - value={field.value} - setValue={field.onChange} - /> - </FormControl> - <FormDescription>Tria el tipus de document.</FormDescription> - </FormItem> - )} - /> - <Button - type="submit" - isLoading={form.formState.isSubmitting}> - Submit - </Button> - </form> - </Form> - ); -}; + <FormField + control={form.control} + name="tipus" + render={({ field }) => ( + <FormItem> + <FormLabel>Tipus</FormLabel> + <FormControl> + <Combobox + options={tipus} + value={field.value} + setValue={field.onChange} + /> + </FormControl> + <FormDescription>Tria el tipus de document.</FormDescription> + </FormItem> + )} + /> + <FormField + control={form.control} + name="anonim" + render={({ field }) => ( + <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4 shadow"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <div className="space-y-1 leading-none"> + <FormLabel>Penjar com a anònim</FormLabel> + <FormDescription> + L'AED guarda sempre l'autor dels apunts. + L'opció d'anònim permet que no es mostrin als altres + usuaris. + </FormDescription> + </div> + </FormItem> + )} + /> + <Button type="submit" isLoading={form.formState.isSubmitting}> + Submit + </Button> + </form> + </Form> + ) +} diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index ab2da91..6af2742 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -1,22 +1,23 @@ -import { LucideProps, MessageSquare, User } from "lucide-react"; +import { LucideProps, MessageSquare, User } from "lucide-react" export const Icons = { - user: User, - logo: (props: LucideProps) => ( - <svg - {...props} - version="1.1" - id="Layer_1" - x="0px" - y="0px" - width="100%" - viewBox="0 0 192 192" - enableBackground="new 0 0 192 192"> - <path - fill="#000000" - opacity="1.000000" - stroke="none" - d=" + user: User, + logo: (props: LucideProps) => ( + <svg + {...props} + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="100%" + viewBox="0 0 192 192" + enableBackground="new 0 0 192 192" + > + <path + fill="#000000" + opacity="1.000000" + stroke="none" + d=" M137.340088,136.147064 C139.400726,141.522293 138.114197,145.777649 134.055389,148.998413 C130.294128,151.983032 125.997803,152.258057 121.957733,149.576004 @@ -76,34 +77,29 @@ M98.335548,102.709183 C105.356140,113.258614 105.417824,112.337120 105.627090,111.556107 C107.174629,105.780388 103.389214,104.166473 98.335548,102.709183 z" - /> - </svg> - ), - google: (props: LucideProps) => ( - <svg - {...props} - viewBox="0 0 24 24"> - <path - d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" - fill="#4285F4" - /> - <path - d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" - fill="#34A853" - /> - <path - d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" - fill="#FBBC05" - /> - <path - d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" - fill="#EA4335" - /> - <path - d="M1 1h22v22H1z" - fill="none" - /> - </svg> - ), - commentReply: MessageSquare, -}; + /> + </svg> + ), + google: (props: LucideProps) => ( + <svg {...props} viewBox="0 0 24 24"> + <path + d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" + fill="#4285F4" + /> + <path + d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" + fill="#34A853" + /> + <path + d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" + fill="#FBBC05" + /> + <path + d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" + fill="#EA4335" + /> + <path d="M1 1h22v22H1z" fill="none" /> + </svg> + ), + commentReply: MessageSquare, +} diff --git a/src/components/MiniCreateAnswer.tsx b/src/components/MiniCreateAnswer.tsx index 421c6d1..190e25b 100644 --- a/src/components/MiniCreateAnswer.tsx +++ b/src/components/MiniCreateAnswer.tsx @@ -1,15 +1,15 @@ -"use client"; +"use client" -import { Session } from "next-auth"; -import { Button } from "@/components/ui/Button"; -import { FC } from "react"; -import UserAvatar from "./UserAvatar"; -import Editor from "@/components/Editor"; +import { Session } from "next-auth" +import { Button } from "@/components/ui/Button" +import { FC } from "react" +import UserAvatar from "./UserAvatar" +import Editor from "@/components/Editor" interface MiniCreateAnswer { - session: Session | null; - subjectId: string; - questionId: string; + session: Session | null + subjectId: string + questionId: string } const MiniCreateAnswer: FC<MiniCreateAnswer> = ({ session, @@ -32,19 +32,27 @@ const MiniCreateAnswer: FC<MiniCreateAnswer> = ({ {/* form */} <div className="flex-grow"> <div className="h-full"> - <Editor subjectId={subjectId} contentType={"answer"} questionId={questionId}/> + <Editor + subjectId={subjectId} + contentType={"answer"} + questionId={questionId} + /> </div> </div> </div> <div className="flex justify-end"> - <Button type="submit" className="w-full sm:w-auto mt-2" form="subject-question-form"> + <Button + type="submit" + className="w-full sm:w-auto mt-2" + form="subject-question-form" + > Compartir </Button> </div> </div> </div> - ); -}; + ) +} -export default MiniCreateAnswer; +export default MiniCreateAnswer diff --git a/src/components/MiniCreateComment.tsx b/src/components/MiniCreateComment.tsx index c044aa8..524a1a3 100644 --- a/src/components/MiniCreateComment.tsx +++ b/src/components/MiniCreateComment.tsx @@ -1,23 +1,20 @@ -"use client"; +"use client" -import { Session } from "next-auth"; -import { Button } from "@/components/ui/Button"; -import { FC, useState } from "react"; -import UserAvatar from "./UserAvatar"; -import axios from "axios"; -import { useMutation } from "@tanstack/react-query"; -import { toast } from "@/hooks/use-toast"; +import { Session } from "next-auth" +import { Button } from "@/components/ui/Button" +import { FC, useState } from "react" +import UserAvatar from "./UserAvatar" +import axios from "axios" +import { useMutation } from "@tanstack/react-query" +import { toast } from "@/hooks/use-toast" interface MiniCreateComment { - session: Session | null; - postId: string; + session: Session | null + postId: string } -const MiniCreateComment: FC<MiniCreateComment> = ({ - session, - postId, -}) => { - const [content, setContent] = useState(""); +const MiniCreateComment: FC<MiniCreateComment> = ({ session, postId }) => { + const [content, setContent] = useState("") // Define the mutation function using useMutation hook const { mutate: createComment } = useMutation({ @@ -25,25 +22,25 @@ const MiniCreateComment: FC<MiniCreateComment> = ({ const { data } = await axios.post("/api/subject/comment/create", { content: content, postId, - }); - return data; + }) + return data }, onSuccess: ({}) => { // Handle success and show toast toast({ description: `Comment created successfully`, - }); + }) // You can add any additional handling specific to your needs here }, onError: ({}) => { // Handle error if needed }, - }); + }) const handleSubmit = async () => { // Call the mutate function to initiate the mutation - createComment(); - }; + createComment() + } return ( <div className="overflow-hidden rounded-md bg-white shadow"> @@ -83,7 +80,7 @@ const MiniCreateComment: FC<MiniCreateComment> = ({ </div> </div> </div> - ); -}; + ) +} -export default MiniCreateComment; +export default MiniCreateComment diff --git a/src/components/MiniCreatePost.tsx b/src/components/MiniCreatePost.tsx index feb1955..e96c06d 100644 --- a/src/components/MiniCreatePost.tsx +++ b/src/components/MiniCreatePost.tsx @@ -1,40 +1,40 @@ -"use client"; +"use client" -import { Session } from "next-auth"; -import { usePathname, useRouter } from "next/navigation"; -import { FC } from "react"; -import UserAvatar from "./UserAvatar"; -import { Input } from "./ui/Input"; +import { Session } from "next-auth" +import { usePathname, useRouter } from "next/navigation" +import { FC } from "react" +import UserAvatar from "./UserAvatar" +import { Input } from "./ui/Input" interface MiniCreatePostProps { - session: Session | null; + session: Session | null } const MiniCreatePost: FC<MiniCreatePostProps> = ({ session }) => { - const router = useRouter(); - const pathname = usePathname(); - - return ( - <div className="overflow-hidden rounded-md bg-white shadow"> - <div className="h-full px-6 py-4 flex justify-between gap-6"> - <div className="relative"> - <UserAvatar - user={{ - name: session?.user?.name || null, - image: session?.user?.image || null, - }} - /> - - <span className="absolute bottom-0 right-0 rounded-full w-3 h-3 bg-green-500 outline outline-2 outline-white" /> - </div> - - <Input - readOnly - onClick={() => router.push(pathname + "/submit")} - placeholder="Comparteix els teus apunts" - /> - - {/* <Button + const router = useRouter() + const pathname = usePathname() + + return ( + <div className="overflow-hidden rounded-md bg-white shadow"> + <div className="h-full px-6 py-4 flex justify-between gap-6"> + <div className="relative"> + <UserAvatar + user={{ + name: session?.user?.name || null, + image: session?.user?.image || null, + }} + /> + + <span className="absolute bottom-0 right-0 rounded-full w-3 h-3 bg-green-500 outline outline-2 outline-white" /> + </div> + + <Input + readOnly + onClick={() => router.push(pathname + "/submit")} + placeholder="Comparteix els teus apunts" + /> + + {/* <Button onClick={() => router.push(parentPathname + "/submit")} variant="ghost"> <ImageIcon className="text-zinc-600" /> @@ -51,9 +51,9 @@ const MiniCreatePost: FC<MiniCreatePostProps> = ({ session }) => { variant="ghost"> <Link2 className="text-zinc-600" /> </Button> */} - </div> - </div> - ); -}; + </div> + </div> + ) +} -export default MiniCreatePost; +export default MiniCreatePost diff --git a/src/components/MiniCreateQuestion.tsx b/src/components/MiniCreateQuestion.tsx index f53f7b3..16a82a9 100644 --- a/src/components/MiniCreateQuestion.tsx +++ b/src/components/MiniCreateQuestion.tsx @@ -1,14 +1,14 @@ -"use client"; +"use client" -import { Session } from "next-auth"; -import { Button } from "@/components/ui/Button"; -import { FC } from "react"; -import UserAvatar from "./UserAvatar"; -import Editor from "@/components/Editor"; +import { Session } from "next-auth" +import { Button } from "@/components/ui/Button" +import { FC } from "react" +import UserAvatar from "./UserAvatar" +import Editor from "@/components/Editor" interface MiniCreateQuestionProps { - session: Session | null; - subjectId: string; + session: Session | null + subjectId: string } const MiniCreateQuestion: FC<MiniCreateQuestionProps> = ({ session, @@ -30,19 +30,23 @@ const MiniCreateQuestion: FC<MiniCreateQuestionProps> = ({ {/* form */} <div className="flex-grow"> <div className="h-full"> - <Editor subjectId={subjectId} contentType={"question"}/> + <Editor subjectId={subjectId} contentType={"question"} /> </div> </div> </div> <div className="flex justify-end"> - <Button type="submit" className="w-full sm:w-auto mt-2" form="subject-question-form"> + <Button + type="submit" + className="w-full sm:w-auto mt-2" + form="subject-question-form" + > Compartir </Button> </div> </div> </div> - ); -}; + ) +} -export default MiniCreateQuestion; +export default MiniCreateQuestion diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 74d27d3..cda49b5 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,36 +1,36 @@ -import Link from "next/link"; -import { Icons } from "@/components/Icons"; -import { buttonVariants } from "./ui/Button"; -import { getAuthSession } from "@/lib/auth"; -import UserAccountNav from "./UserAccountNav"; +import Link from "next/link" +import { Icons } from "@/components/Icons" +import { buttonVariants } from "./ui/Button" +import { getAuthSession } from "@/lib/auth" +import UserAccountNav from "./UserAccountNav" const Navbar = async () => { - const session = await getAuthSession(); + const session = await getAuthSession() - return ( - <div className="fixed top-0 inset-x-0 h-auto bg-zinc-100 border-b border-zinc-300 z-[10] py-2"> - <div className="container max-w-7xl h-full mx-auto flex items-center justify-between gap-2"> - <Link - href="/" - className="flex gap-2 items-center"> - <Icons.logo className="w-10 h-10 sm:h-6 sm:w-6" /> - <p className="hidden text-zinc-700 text-sm font-medium md:block">Apunts Dades</p> - </Link> + return ( + <div className="fixed top-0 inset-x-0 h-auto bg-zinc-100 border-b border-zinc-300 z-[10] py-2"> + <div className="container max-w-7xl h-full mx-auto flex items-center justify-between gap-2"> + <Link href="/" className="flex gap-2 items-center"> + <Icons.logo className="w-10 h-10 sm:h-6 sm:w-6" /> + <p className="hidden text-zinc-700 text-sm font-medium md:block"> + Apunts Dades + </p> + </Link> - {/* search bar */} + {/* search bar */} - {session && session.user ? ( - <UserAccountNav user={session.user} /> - ) : ( - <div> - <Link href="/sign-in" className={buttonVariants()}> - Sign In - </Link> - </div> - )} - </div> - </div> - ); -}; + {session && session.user ? ( + <UserAccountNav user={session.user} /> + ) : ( + <div> + <Link href="/sign-in" className={buttonVariants()}> + Sign In + </Link> + </div> + )} + </div> + </div> + ) +} -export default Navbar; +export default Navbar diff --git a/src/components/Post.tsx b/src/components/Post.tsx index 9cbb74e..15fa30e 100644 --- a/src/components/Post.tsx +++ b/src/components/Post.tsx @@ -1,89 +1,104 @@ -"use client"; +"use client" -import { formatTimeToNow } from "@/lib/utils"; -import { Post, User, PostVote } from "@prisma/client"; -import { MessageSquare } from "lucide-react"; -import Link from "next/link"; -import { buttonVariants } from "@/components/ui/Button"; -import { FC, useRef } from "react"; -import PostVoteClient from "./votes/PostVoteClient"; -import { Badge } from "@/components/ui/Badge"; +import { formatTimeToNow } from "@/lib/utils" +import { Post, User, PostVote } from "@prisma/client" +import { MessageSquare } from "lucide-react" +import Link from "next/link" +import { buttonVariants } from "@/components/ui/Button" +import { FC, useRef } from "react" +import PostVoteClient from "./votes/PostVoteClient" +import { Badge } from "@/components/ui/Badge" -type PartialVote = Pick<PostVote, "type">; +type PartialVote = Pick<PostVote, "type"> interface PostProps { - post: Post & { - author: User; - votes: PostVote[]; - }; - votesAmt: number; - subjectAcronym: string; - currentVote?: PartialVote; - commentAmt: number; + post: Post & { + author: User + votes: PostVote[] + } + votesAmt: number + subjectAcronym: string + currentVote?: PartialVote + commentAmt: number } -const Post: FC<PostProps> = ({ post, votesAmt: _votesAmt, currentVote: _currentVote, subjectAcronym, commentAmt }) => { - const pRef = useRef<HTMLParagraphElement>(null); +const Post: FC<PostProps> = ({ + post, + votesAmt: _votesAmt, + currentVote: _currentVote, + subjectAcronym, + commentAmt, +}) => { + const pRef = useRef<HTMLParagraphElement>(null) - return ( - <div className="rounded-md bg-white shadow"> - <div className="px-6 py-4 flex justify-between"> - <PostVoteClient - initialVotesAmt={_votesAmt} - postId={post.id} - initialVote={_currentVote?.type} - /> + return ( + <div className="rounded-md bg-white shadow"> + <div className="px-6 py-4 flex justify-between"> + <PostVoteClient + initialVotesAmt={_votesAmt} + postId={post.id} + initialVote={_currentVote?.type} + /> - <div className="w-0 flex-1"> - <div className="max-h-40 mt-1 text-xs text-gray-500"> - {subjectAcronym ? ( - <> - <a - className="underline text-zinc-900 text-sm underline-offset-2" - href={`/${subjectAcronym}`}> - {subjectAcronym} - </a> - <span className="px-1">•</span> - </> - ) : null} - <span>Compartit per {post.author.name}</span> {formatTimeToNow(new Date(post.createdAt))} - </div> - <a href={`/${subjectAcronym}/post/${post.id}`}> - <h1 className="text-lg font-semibold py-2 leading-6 text-gray-900">{post.title}</h1> - </a> - <div className="space-x-2"> - <Badge>{post.tipus}</Badge> - <Badge variant="secondary">{post.year}</Badge> - </div> + <div className="w-0 flex-1"> + <div className="max-h-40 mt-1 text-xs text-gray-500"> + {subjectAcronym ? ( + <> + <a + className="underline text-zinc-900 text-sm underline-offset-2" + href={`/${subjectAcronym}`} + > + {subjectAcronym} + </a> + <span className="px-1">•</span> + </> + ) : null} + <span> + Compartit per {post.isAnonymous ? "Anònim" : post.author.name} + </span>{" "} + {formatTimeToNow(new Date(post.createdAt))} + </div> + <a href={`/${subjectAcronym}/post/${post.id}`}> + <h1 className="text-lg font-semibold py-2 leading-6 text-gray-900"> + {post.title} + </h1> + </a> + <div className="space-x-2"> + <Badge>{post.tipus}</Badge> + <Badge variant="secondary">{post.year}</Badge> + </div> - <div - className="relative text-sm max-h-40 w-full overflow-clip" - ref={pRef}> - {post.content && post.content.endsWith(".pdf") ? ( - <Link - className={buttonVariants({ - className: "w-full mt-4 mb-6", - })} - href={post.content}> - Visualitza els Apunts - </Link> - ) : null} - {pRef.current?.clientHeight === 160 ? ( - // blur bottom if content is too long - <div className="absolute bottom-0 left-0 h-24 w-full bg-gradient-to-t from-white to-transparent"></div> - ) : null} - </div> - </div> - </div> + <div + className="relative text-sm max-h-40 w-full overflow-clip" + ref={pRef} + > + {post.content && post.content.endsWith(".pdf") ? ( + <Link + className={buttonVariants({ + className: "w-full mt-4 mb-6", + })} + href={post.content} + > + Visualitza els Apunts + </Link> + ) : null} + {pRef.current?.clientHeight === 160 ? ( + // blur bottom if content is too long + <div className="absolute bottom-0 left-0 h-24 w-full bg-gradient-to-t from-white to-transparent"></div> + ) : null} + </div> + </div> + </div> - <div className="bg-gray-50 z-20 text-sm px-4 py-4 sm:px-6"> - <Link - href={`/${subjectAcronym}/post/${post.id}`} - className="w-fit flex items-center gap-2"> - <MessageSquare className="h-4 w-4" /> {commentAmt} comments - </Link> - </div> - </div> - ); -}; -export default Post; + <div className="bg-gray-50 z-20 text-sm px-4 py-4 sm:px-6"> + <Link + href={`/${subjectAcronym}/post/${post.id}`} + className="w-fit flex items-center gap-2" + > + <MessageSquare className="h-4 w-4" /> {commentAmt} comments + </Link> + </div> + </div> + ) +} +export default Post diff --git a/src/components/PostFeed.tsx b/src/components/PostFeed.tsx index f34de05..3dcf25c 100644 --- a/src/components/PostFeed.tsx +++ b/src/components/PostFeed.tsx @@ -1,102 +1,102 @@ -"use client"; +"use client" -import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config"; -import { ExtendedPost } from "@/types/db"; -import { useIntersection } from "@mantine/hooks"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { Loader2 } from "lucide-react"; -import { FC, useEffect, useRef } from "react"; -import Post from "./Post"; -import { useSession } from "next-auth/react"; +import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" +import { ExtendedPost } from "@/types/db" +import { useIntersection } from "@mantine/hooks" +import { useInfiniteQuery } from "@tanstack/react-query" +import axios from "axios" +import { Loader2 } from "lucide-react" +import { FC, useEffect, useRef } from "react" +import Post from "./Post" +import { useSession } from "next-auth/react" interface PostFeedProps { - initialPosts: ExtendedPost[]; - subjectAcronym?: string; + initialPosts: ExtendedPost[] + subjectAcronym?: string } const PostFeed: FC<PostFeedProps> = ({ initialPosts, subjectAcronym }) => { - const lastPostRef = useRef<HTMLElement>(null); - const { ref, entry } = useIntersection({ - root: lastPostRef.current, - threshold: 1, - }); - const { data: session } = useSession(); + const lastPostRef = useRef<HTMLElement>(null) + const { ref, entry } = useIntersection({ + root: lastPostRef.current, + threshold: 1, + }) + const { data: session } = useSession() - const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( - ["infinite-query"], - async ({ pageParam = 1 }) => { - const query = - `/api/posts?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}` + - (!!subjectAcronym ? `&subjectAcronym=${subjectAcronym}` : ""); + const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( + ["infinite-query"], + async ({ pageParam = 1 }) => { + const query = + `/api/posts?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}` + + (!!subjectAcronym ? `&subjectAcronym=${subjectAcronym}` : "") - const { data } = await axios.get(query); - return data as ExtendedPost[]; - }, + const { data } = await axios.get(query) + return data as ExtendedPost[] + }, - { - getNextPageParam: (_, pages) => { - return pages.length + 1; - }, - initialData: { pages: [initialPosts], pageParams: [1] }, - } - ); + { + getNextPageParam: (_, pages) => { + return pages.length + 1 + }, + initialData: { pages: [initialPosts], pageParams: [1] }, + }, + ) - useEffect(() => { - if (entry?.isIntersecting) { - fetchNextPage(); // Load more posts when the last post comes into view - } - }, [entry, fetchNextPage]); + useEffect(() => { + if (entry?.isIntersecting) { + fetchNextPage() // Load more posts when the last post comes into view + } + }, [entry, fetchNextPage]) - const posts = data?.pages.flatMap((page) => page) ?? initialPosts; + const posts = data?.pages.flatMap((page) => page) ?? initialPosts - return ( - <ul className="flex flex-col col-span-2 space-y-6"> - {posts.map((post, index) => { - const votesAmt = post.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); + return ( + <ul className="flex flex-col col-span-2 space-y-6"> + {posts.map((post, index) => { + const votesAmt = post.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) - const currentVote = post.votes.find((vote) => vote.userId === session?.user.id); + const currentVote = post.votes.find( + (vote) => vote.userId === session?.user.id, + ) - if (index === posts.length - 1) { - // Add a ref to the last post in the list - return ( - <li - key={post.id} - ref={ref}> - <Post - post={post} - commentAmt={post.comments.length} - subjectAcronym={post.subject.acronym} - votesAmt={votesAmt} - currentVote={currentVote} - /> - </li> - ); - } else { - return ( - <Post - key={post.id} - post={post} - commentAmt={post.comments.length} - subjectAcronym={post.subject.acronym} - votesAmt={votesAmt} - currentVote={currentVote} - /> - ); - } - })} + if (index === posts.length - 1) { + // Add a ref to the last post in the list + return ( + <li key={post.id} ref={ref}> + <Post + post={post} + commentAmt={post.comments.length} + subjectAcronym={post.subject.acronym} + votesAmt={votesAmt} + currentVote={currentVote} + /> + </li> + ) + } else { + return ( + <Post + key={post.id} + post={post} + commentAmt={post.comments.length} + subjectAcronym={post.subject.acronym} + votesAmt={votesAmt} + currentVote={currentVote} + /> + ) + } + })} - {isFetchingNextPage && ( - <li className="flex justify-center"> - <Loader2 className="w-6 h-6 text-zinc-500 animate-spin" /> - </li> - )} - </ul> - ); -}; + {isFetchingNextPage && ( + <li className="flex justify-center"> + <Loader2 className="w-6 h-6 text-zinc-500 animate-spin" /> + </li> + )} + </ul> + ) +} -export default PostFeed; +export default PostFeed diff --git a/src/components/PostView.tsx b/src/components/PostView.tsx index 4ab1b79..0755cad 100644 --- a/src/components/PostView.tsx +++ b/src/components/PostView.tsx @@ -1,22 +1,22 @@ -"use client"; -import { FC } from "react"; -import { ExtendedPost, ExtendedComment } from "@/types/db"; -import { useSession } from "next-auth/react"; -import MiniCreateComment from "@/components/MiniCreateComment"; -import CommentFeed from "@/components/CommentFeed"; -import Post from "@/components/Post"; +"use client" +import { FC } from "react" +import { ExtendedPost, ExtendedComment } from "@/types/db" +import { useSession } from "next-auth/react" +import MiniCreateComment from "@/components/MiniCreateComment" +import CommentFeed from "@/components/CommentFeed" +import Post from "@/components/Post" interface PostViewProps { - post: ExtendedPost; - comments: ExtendedComment[]; + post: ExtendedPost + comments: ExtendedComment[] } export const PostView: FC<PostViewProps> = ({ post, comments }) => { - const { data: session } = useSession(); - const votesAmt = post.votes.length; + const { data: session } = useSession() + const votesAmt = post.votes.length const currentVote = post.votes.find( - (vote) => vote.userId === session?.user?.id - ); + (vote) => vote.userId === session?.user?.id, + ) return ( <div> <div> @@ -40,7 +40,7 @@ export const PostView: FC<PostViewProps> = ({ post, comments }) => { /> </div> </div> - ); -}; + ) +} -export default PostView; +export default PostView diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx index 92b1b35..78c574d 100644 --- a/src/components/Providers.tsx +++ b/src/components/Providers.tsx @@ -1,16 +1,16 @@ -"use client"; +"use client" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { SessionProvider } from "next-auth/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { SessionProvider } from "next-auth/react" const Providers = ({ children }: { children: React.ReactNode }) => { - const queryClient = new QueryClient(); + const queryClient = new QueryClient() - return ( - <QueryClientProvider client={queryClient}> - <SessionProvider>{children}</SessionProvider> - </QueryClientProvider> - ); -}; + return ( + <QueryClientProvider client={queryClient}> + <SessionProvider>{children}</SessionProvider> + </QueryClientProvider> + ) +} -export default Providers; +export default Providers diff --git a/src/components/QuestionComponent.tsx b/src/components/QuestionComponent.tsx index 6e290b9..a57d469 100644 --- a/src/components/QuestionComponent.tsx +++ b/src/components/QuestionComponent.tsx @@ -1,84 +1,90 @@ -"use client"; +"use client" -import { formatTimeToNow } from "@/lib/utils"; -import { Question, User, QuestionVote } from "@prisma/client"; -import { MessageSquare } from "lucide-react"; -import Link from "next/link"; -import { FC, useRef } from "react"; -import EditorOutput from "./EditorOutput"; -import QuestionVoteClient from "./votes/QuestionVoteClient"; +import { formatTimeToNow } from "@/lib/utils" +import { Question, User, QuestionVote } from "@prisma/client" +import { MessageSquare } from "lucide-react" +import Link from "next/link" +import { FC, useRef } from "react" +import EditorOutput from "./EditorOutput" +import QuestionVoteClient from "./votes/QuestionVoteClient" -type PartialVote = Pick<QuestionVote, "type">; +type PartialVote = Pick<QuestionVote, "type"> interface QuestionProps { - question: Question & { - author: User; - votes: QuestionVote[]; - }; - votesAmt: number; - subjectName: string; - currentVote?: PartialVote; - answerAmt: number; - subjectAcronym: string; + question: Question & { + author: User + votes: QuestionVote[] + } + votesAmt: number + subjectName: string + currentVote?: PartialVote + answerAmt: number + subjectAcronym: string } const QuestionComponent: FC<QuestionProps> = ({ - question, - votesAmt: _votesAmt, - currentVote: _currentVote, - subjectName, - answerAmt, - subjectAcronym, + question, + votesAmt: _votesAmt, + currentVote: _currentVote, + subjectName, + answerAmt, + subjectAcronym, }) => { - const pRef = useRef<HTMLParagraphElement>(null); + const pRef = useRef<HTMLParagraphElement>(null) - return ( - <div className="rounded-md bg-white shadow"> - <div className="px-6 py-4 flex justify-between"> - <QuestionVoteClient - initialVotesAmt={_votesAmt} - questionId={question.id} - initialVote={_currentVote?.type} - /> + return ( + <div className="rounded-md bg-white shadow"> + <div className="px-6 py-4 flex justify-between"> + <QuestionVoteClient + initialVotesAmt={_votesAmt} + questionId={question.id} + initialVote={_currentVote?.type} + /> - <div className="w-0 flex-1"> - <div className="max-h-40 mt-1 text-xs text-gray-500"> - {subjectName ? ( - <> - <a - className="underline text-zinc-900 text-sm underline-offset-2" - href={`/${subjectAcronym}`}> - {subjectAcronym} - </a> - <span className="px-1">•</span> - </> - ) : null} - <span>Compartit per {question.author.name}</span> {formatTimeToNow(new Date(question.createdAt))} - </div> - <a href={`/${subjectAcronym}/q/${question.id}`}> - <h1 className="text-lg font-semibold py-2 leading-6 text-gray-900">{question.title}</h1> - </a> + <div className="w-0 flex-1"> + <div className="max-h-40 mt-1 text-xs text-gray-500"> + {subjectName ? ( + <> + <a + className="underline text-zinc-900 text-sm underline-offset-2" + href={`/${subjectAcronym}`} + > + {subjectAcronym} + </a> + <span className="px-1">•</span> + </> + ) : null} + <span>Compartit per {question.author.name}</span>{" "} + {formatTimeToNow(new Date(question.createdAt))} + </div> + <a href={`/${subjectAcronym}/q/${question.id}`}> + <h1 className="text-lg font-semibold py-2 leading-6 text-gray-900"> + {question.title} + </h1> + </a> - <div - className="relative text-sm max-h-40 w-full overflow-clip" - ref={pRef}> - <EditorOutput content={question.content} /> - {pRef.current?.clientHeight === 160 ? ( - // blur bottom if content is too long - <div className="absolute bottom-0 left-0 h-24 w-full bg-gradient-to-t from-white to-transparent"></div> - ) : null} - </div> - </div> - </div> + <div + className="relative text-sm max-h-40 w-full overflow-clip" + ref={pRef} + > + <EditorOutput content={question.content} /> + {pRef.current?.clientHeight === 160 ? ( + // blur bottom if content is too long + <div className="absolute bottom-0 left-0 h-24 w-full bg-gradient-to-t from-white to-transparent"></div> + ) : null} + </div> + </div> + </div> - <div className="bg-gray-50 z-20 text-sm px-4 py-4 sm:px-6"> - <Link - href={`/${subjectName}/q/${question.id}`} - className="w-fit flex items-center gap-2"> - <MessageSquare className="h-4 w-4" /> {answerAmt} answers - </Link> - </div> - </div> - ); -}; -export default QuestionComponent; + <div className="bg-gray-50 z-20 text-sm px-4 py-4 sm:px-6"> + <Link + href={`/${subjectName}/q/${question.id}`} + className="w-fit flex items-center gap-2" + > + <MessageSquare className="h-4 w-4" /> {answerAmt} answers + </Link> + </div> + </div> + ) +} +export default QuestionComponent diff --git a/src/components/QuestionFeed.tsx b/src/components/QuestionFeed.tsx index fbda66c..c32ead4 100644 --- a/src/components/QuestionFeed.tsx +++ b/src/components/QuestionFeed.tsx @@ -1,104 +1,107 @@ -"use client"; +"use client" -import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config"; -import { ExtendedQuestion } from "@/types/db"; -import { useIntersection } from "@mantine/hooks"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { Loader2 } from "lucide-react"; -import { FC, useEffect, useRef } from "react"; -import QuestionComponent from "./QuestionComponent"; -import { useSession } from "next-auth/react"; +import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" +import { ExtendedQuestion } from "@/types/db" +import { useIntersection } from "@mantine/hooks" +import { useInfiniteQuery } from "@tanstack/react-query" +import axios from "axios" +import { Loader2 } from "lucide-react" +import { FC, useEffect, useRef } from "react" +import QuestionComponent from "./QuestionComponent" +import { useSession } from "next-auth/react" interface QuestionFeedProps { - initialQuestions: ExtendedQuestion[]; - subjectAcronym?: string; + initialQuestions: ExtendedQuestion[] + subjectAcronym?: string } -const QuestionFeed: FC<QuestionFeedProps> = ({ initialQuestions, subjectAcronym }) => { - const lastQuestionRef = useRef<HTMLElement>(null); - const { ref, entry } = useIntersection({ - root: lastQuestionRef.current, - threshold: 1, - }); - const { data: session } = useSession(); +const QuestionFeed: FC<QuestionFeedProps> = ({ + initialQuestions, + subjectAcronym, +}) => { + const lastQuestionRef = useRef<HTMLElement>(null) + const { ref, entry } = useIntersection({ + root: lastQuestionRef.current, + threshold: 1, + }) + const { data: session } = useSession() - const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( - ["infinite-query"], - async ({ pageParam = 1 }) => { - const query = - `/api/q?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}` + - (!!subjectAcronym ? `&subjectAcronym=${subjectAcronym}` : ""); + const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( + ["infinite-query"], + async ({ pageParam = 1 }) => { + const query = + `/api/q?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}` + + (!!subjectAcronym ? `&subjectAcronym=${subjectAcronym}` : "") - const { data } = await axios.get(query); - return data as ExtendedQuestion[]; - }, + const { data } = await axios.get(query) + return data as ExtendedQuestion[] + }, - { - getNextPageParam: (_, pages) => { - return pages.length + 1; - }, - initialData: { pages: [initialQuestions], pageParams: [1] }, - } - ); + { + getNextPageParam: (_, pages) => { + return pages.length + 1 + }, + initialData: { pages: [initialQuestions], pageParams: [1] }, + }, + ) - useEffect(() => { - if (entry?.isIntersecting) { - fetchNextPage(); // Load more questions when the last question comes into view - } - }, [entry, fetchNextPage]); + useEffect(() => { + if (entry?.isIntersecting) { + fetchNextPage() // Load more questions when the last question comes into view + } + }, [entry, fetchNextPage]) - const questions = data?.pages.flatMap((page) => page) ?? initialQuestions; + const questions = data?.pages.flatMap((page) => page) ?? initialQuestions - return ( - <ul className="flex flex-col col-span-2 space-y-6"> - {questions.map((question, index) => { - const votesAmt = question.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); + return ( + <ul className="flex flex-col col-span-2 space-y-6"> + {questions.map((question, index) => { + const votesAmt = question.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) - const currentVote = question.votes.find((vote) => vote.userId === session?.user.id); + const currentVote = question.votes.find( + (vote) => vote.userId === session?.user.id, + ) - if (index === questions.length - 1) { - // Add a ref to the last question in the list - return ( - <li - key={question.id} - ref={ref}> - <QuestionComponent - question={question} - answerAmt={question.answers.length} - subjectName={question.subject.name} - votesAmt={votesAmt} - currentVote={currentVote} - subjectAcronym={question.subject.acronym} - /> - </li> - ); - } else { - return ( - <QuestionComponent - key={question.id} - question={question} - answerAmt={question.answers.length} - subjectName={question.subject.name} - votesAmt={votesAmt} - currentVote={currentVote} - subjectAcronym={question.subject.acronym} - /> - ); - } - })} + if (index === questions.length - 1) { + // Add a ref to the last question in the list + return ( + <li key={question.id} ref={ref}> + <QuestionComponent + question={question} + answerAmt={question.answers.length} + subjectName={question.subject.name} + votesAmt={votesAmt} + currentVote={currentVote} + subjectAcronym={question.subject.acronym} + /> + </li> + ) + } else { + return ( + <QuestionComponent + key={question.id} + question={question} + answerAmt={question.answers.length} + subjectName={question.subject.name} + votesAmt={votesAmt} + currentVote={currentVote} + subjectAcronym={question.subject.acronym} + /> + ) + } + })} - {isFetchingNextPage && ( - <li className="flex justify-center"> - <Loader2 className="w-6 h-6 text-zinc-500 animate-spin" /> - </li> - )} - </ul> - ); -}; + {isFetchingNextPage && ( + <li className="flex justify-center"> + <Loader2 className="w-6 h-6 text-zinc-500 animate-spin" /> + </li> + )} + </ul> + ) +} -export default QuestionFeed; +export default QuestionFeed diff --git a/src/components/QuestionView.tsx b/src/components/QuestionView.tsx index e5dac4e..19137f0 100644 --- a/src/components/QuestionView.tsx +++ b/src/components/QuestionView.tsx @@ -1,25 +1,25 @@ -"use client"; -import { FC } from "react"; -import QuestionComponent from "@/components/QuestionComponent"; -import { ExtendedQuestion, ExtendedAnswer } from "@/types/db"; -import { useSession } from "next-auth/react"; -import MiniCreateAnswer from "@/components/MiniCreateAnswer"; -import AnswerFeed from "@/components/AnswerFeed"; +"use client" +import { FC } from "react" +import QuestionComponent from "@/components/QuestionComponent" +import { ExtendedQuestion, ExtendedAnswer } from "@/types/db" +import { useSession } from "next-auth/react" +import MiniCreateAnswer from "@/components/MiniCreateAnswer" +import AnswerFeed from "@/components/AnswerFeed" interface AnswersViewProps { - question: ExtendedQuestion; - answers: ExtendedAnswer[]; + question: ExtendedQuestion + answers: ExtendedAnswer[] } export const AnswersView: FC<AnswersViewProps> = ({ question, answers }) => { - const { data: session } = useSession(); - const votesAmt = question.votes.length; - const answerAmt = question.answers.length; - const subjectName = question.subject.name; - const subjectAcronym = question.subject.acronym; + const { data: session } = useSession() + const votesAmt = question.votes.length + const answerAmt = question.answers.length + const subjectName = question.subject.name + const subjectAcronym = question.subject.acronym const currentVote = question.votes.find( - (vote) => vote.userId === session?.user?.id - ); + (vote) => vote.userId === session?.user?.id, + ) return ( <div> <div> @@ -33,13 +33,22 @@ export const AnswersView: FC<AnswersViewProps> = ({ question, answers }) => { /> </div> <div className="mt-4"> - <MiniCreateAnswer session={session} subjectId={question.subject.id} questionId={question.id}/> + <MiniCreateAnswer + session={session} + subjectId={question.subject.id} + questionId={question.id} + /> </div> <div> - <AnswerFeed initialAnswers={answers} subjectAcronym={question.subject.acronym} subjectName={question.subject.name} questionId={question.id}/> + <AnswerFeed + initialAnswers={answers} + subjectAcronym={question.subject.acronym} + subjectName={question.subject.name} + questionId={question.id} + /> </div> </div> - ); -}; + ) +} -export default AnswersView; +export default AnswersView diff --git a/src/components/SignIn.tsx b/src/components/SignIn.tsx index d131f9b..24883fe 100644 --- a/src/components/SignIn.tsx +++ b/src/components/SignIn.tsx @@ -1,35 +1,35 @@ -import { Icons } from "@/components/Icons"; -import Link from "next/link"; -import UserAuthForm from "@/components/UserAuthForm"; +import { Icons } from "@/components/Icons" +import Link from "next/link" +import UserAuthForm from "@/components/UserAuthForm" const SignIn = () => { - return ( - <div className="container mx-auto flex w-full flex-col justify-center space-y-6 sm:w[400px]"> - <div className="flex flex-col space-y-2 text-center"> - <Icons.logo className="mx-auto h-6 w-6" /> - <h1 className="text-2xl font-semibold tracking-tight">Ben tornat!</h1> - <p className="text-sm max-w-xs mx-auto"> - Continuant, entraràs al teu un compte d'Apunts Dades sota les nostres Condicions d'Ús i Polítiques de - Privacitat. - </p> + return ( + <div className="container mx-auto flex w-full flex-col justify-center space-y-6 sm:w[400px]"> + <div className="flex flex-col space-y-2 text-center"> + <Icons.logo className="mx-auto h-6 w-6" /> + <h1 className="text-2xl font-semibold tracking-tight">Ben tornat!</h1> + <p className="text-sm max-w-xs mx-auto"> + Continuant, entraràs al teu un compte d'Apunts Dades sota les + nostres Condicions d'Ús i Polítiques de Privacitat. + </p> - {/* sign in form */} - <UserAuthForm /> + {/* sign in form */} + <UserAuthForm /> - <p className="px-8 text-center text-sm text-zinc-700"> - No tens un compte?{" "} - <Link - href="https://forms.gle/xBfXgAdVTWvBFRGb8" - className="hover:text-zinc-800 text-sm underline underline-offset-4" - target="_blank" - rel="noopener noreferrer" - > - Registra't - </Link> - </p> - </div> - </div> - ); -}; + <p className="px-8 text-center text-sm text-zinc-700"> + No tens un compte?{" "} + <Link + href="https://forms.gle/xBfXgAdVTWvBFRGb8" + className="hover:text-zinc-800 text-sm underline underline-offset-4" + target="_blank" + rel="noopener noreferrer" + > + Registra't + </Link> + </p> + </div> + </div> + ) +} -export default SignIn; +export default SignIn diff --git a/src/components/SubscribeLeaveToggle.tsx b/src/components/SubscribeLeaveToggle.tsx index b272e9f..7c5a76d 100644 --- a/src/components/SubscribeLeaveToggle.tsx +++ b/src/components/SubscribeLeaveToggle.tsx @@ -1,112 +1,119 @@ -"use client"; +"use client" -import { FC, startTransition } from "react"; -import { Button } from "./ui/Button"; -import { SubscribeToSubjectPayload } from "@/lib/validators/subject"; -import { useMutation } from "@tanstack/react-query"; -import axios, { AxiosError } from "axios"; -import { toast } from "@/hooks/use-toast"; -import { useRouter } from "next/navigation"; -import { useCustomToasts } from "@/hooks/use-custom-toasts"; +import { FC, startTransition } from "react" +import { Button } from "./ui/Button" +import { SubscribeToSubjectPayload } from "@/lib/validators/subject" +import { useMutation } from "@tanstack/react-query" +import axios, { AxiosError } from "axios" +import { toast } from "@/hooks/use-toast" +import { useRouter } from "next/navigation" +import { useCustomToasts } from "@/hooks/use-custom-toasts" interface SubscribeLeaveToggleProps { - subjectId: string; - subjectName: string; - isSubscribed: boolean; + subjectId: string + subjectName: string + isSubscribed: boolean } -const SubscribeLeaveToggle: FC<SubscribeLeaveToggleProps> = ({ subjectId, subjectName, isSubscribed }) => { - const { loginToast } = useCustomToasts(); - const router = useRouter(); +const SubscribeLeaveToggle: FC<SubscribeLeaveToggleProps> = ({ + subjectId, + subjectName, + isSubscribed, +}) => { + const { loginToast } = useCustomToasts() + const router = useRouter() - const { mutate: subscribe, isLoading: isSubLoading } = useMutation({ - mutationFn: async () => { - const payload: SubscribeToSubjectPayload = { - subjectId, - }; - const { data } = await axios.post("/api/subject/subscribe", payload); - return data as string; - }, - onError: (err) => { - if (err instanceof AxiosError) { - if (err.response?.status === 401) { - return loginToast(); - } - } + const { mutate: subscribe, isLoading: isSubLoading } = useMutation({ + mutationFn: async () => { + const payload: SubscribeToSubjectPayload = { + subjectId, + } + const { data } = await axios.post("/api/subject/subscribe", payload) + return data as string + }, + onError: (err) => { + if (err instanceof AxiosError) { + if (err.response?.status === 401) { + return loginToast() + } + } - return toast({ - title: "S'ha produït un error desconegut.", - description: "No s'ha pogut subscriure a l'assignatura. Siusplau, torna a intentar-ho més tard.", - variant: "destructive", - }); - }, - onSuccess: ({}) => { - startTransition(() => { - router.refresh(); - }); + return toast({ + title: "S'ha produït un error desconegut.", + description: + "No s'ha pogut subscriure a l'assignatura. Siusplau, torna a intentar-ho més tard.", + variant: "destructive", + }) + }, + onSuccess: ({}) => { + startTransition(() => { + router.refresh() + }) - const startsWithVowel = /^[aeiouàáâãäåæçèéêëìíîïðòóôõöøùúûüýÿ]/i; - const subjectArticle = subjectName.match(startsWithVowel) ? "d'" : "de "; + const startsWithVowel = /^[aeiouàáâãäåæçèéêëìíîïðòóôõöøùúûüýÿ]/i + const subjectArticle = subjectName.match(startsWithVowel) ? "d'" : "de " - return toast({ - title: `T'has subscrit als apunts ${subjectArticle}${subjectName}!`, - description: "", - }); - }, - }); + return toast({ + title: `T'has subscrit als apunts ${subjectArticle}${subjectName}!`, + description: "", + }) + }, + }) - const { mutate: unsubscribe, isLoading: isUnsubLoading } = useMutation({ - mutationFn: async () => { - const payload: SubscribeToSubjectPayload = { - subjectId, - }; - const { data } = await axios.post("/api/subject/unsubscribe", payload); - return data as string; - }, - onError: (err) => { - if (err instanceof AxiosError) { - if (err.response?.status === 401) { - return loginToast(); - } - } + const { mutate: unsubscribe, isLoading: isUnsubLoading } = useMutation({ + mutationFn: async () => { + const payload: SubscribeToSubjectPayload = { + subjectId, + } + const { data } = await axios.post("/api/subject/unsubscribe", payload) + return data as string + }, + onError: (err) => { + if (err instanceof AxiosError) { + if (err.response?.status === 401) { + return loginToast() + } + } - return toast({ - title: "S'ha produït un error desconegut.", - description: - "No s'ha pogut donar de baixa la subscripció a l'assignatura. Siusplau, torna a intentar-ho més tard.", - variant: "destructive", - }); - }, - onSuccess: ({}) => { - startTransition(() => { - router.refresh(); - }); + return toast({ + title: "S'ha produït un error desconegut.", + description: + "No s'ha pogut donar de baixa la subscripció a l'assignatura. Siusplau, torna a intentar-ho més tard.", + variant: "destructive", + }) + }, + onSuccess: ({}) => { + startTransition(() => { + router.refresh() + }) - const startsWithVowel = /^[aeiouàáâãäåæçèéêëìíîïðòóôõöøùúûüýÿ]/i; - const subjectArticle = subjectName.match(startsWithVowel) ? "d'" : "de "; + const startsWithVowel = /^[aeiouàáâãäåæçèéêëìíîïðòóôõöøùúûüýÿ]/i + const subjectArticle = subjectName.match(startsWithVowel) ? "d'" : "de " - return toast({ - title: `Has donat de baixa la teva subscripció als apunts ${subjectArticle}${subjectName}!`, - description: "", - }); - }, - }); + return toast({ + title: `Has donat de baixa la teva subscripció als apunts ${subjectArticle}${subjectName}!`, + description: "", + }) + }, + }) - return isSubscribed ? ( - <Button - isLoading={isUnsubLoading} - onClick={() => unsubscribe()} - className="w-full mt-1 mb-4"> - Deixar de seguir - </Button> - ) : ( - <Button - isLoading={isSubLoading} - onClick={() => subscribe()} - className="w-full mt-1 mb-4"> - Seguir - </Button> - ); -}; + return isSubscribed ? ( + <Button + isLoading={isUnsubLoading} + onClick={() => unsubscribe()} + className="w-full mt-1 mb-4" + > + Deixar de seguir + </Button> + ) : ( + <Button + isLoading={isSubLoading} + onClick={() => subscribe()} + className="w-full mt-1 mb-4" + > + Seguir + </Button> + ) +} -export default SubscribeLeaveToggle; +export default SubscribeLeaveToggle diff --git a/src/components/UserAccountNav.tsx b/src/components/UserAccountNav.tsx index 42e327c..13ac2c3 100644 --- a/src/components/UserAccountNav.tsx +++ b/src/components/UserAccountNav.tsx @@ -1,67 +1,72 @@ -"use client"; +"use client" -import { User } from "next-auth"; -import { FC } from "react"; +import { User } from "next-auth" +import { FC } from "react" import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, -} from "@/components/ui/DropdownMenu"; -import UserAvatar from "@/components/UserAvatar"; -import Link from "next/link"; -import { signOut } from "next-auth/react"; + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, +} from "@/components/ui/DropdownMenu" +import UserAvatar from "@/components/UserAvatar" +import Link from "next/link" +import { signOut } from "next-auth/react" interface UserAccountNavProps { - user: Pick<User, "name" | "image" | "email">; + user: Pick<User, "name" | "image" | "email"> } const UserAccountNav: FC<UserAccountNavProps> = ({ user }) => { - return ( - <DropdownMenu> - <DropdownMenuTrigger> - <UserAvatar - user={{ - name: user.name || null, - image: user.image || null, - }} - className="h-8 w-8" - /> - </DropdownMenuTrigger> - <DropdownMenuContent - className="bg-white" - align="end"> - <div className="flex items-center justify-start gap-2 p-2"> - <div className="flex flex-col space-y-1 leading-none"> - {user.name && <p className="font-medium">{user.name}</p>} - {user.email && <p className="w-[200px] truncate text-sm text-zinc-700">{user.email}</p>} - </div> - </div> + return ( + <DropdownMenu> + <DropdownMenuTrigger> + <UserAvatar + user={{ + name: user.name || null, + image: user.image || null, + }} + className="h-8 w-8" + /> + </DropdownMenuTrigger> + <DropdownMenuContent className="bg-white" align="end"> + <div className="flex items-center justify-start gap-2 p-2"> + <div className="flex flex-col space-y-1 leading-none"> + {user.name && <p className="font-medium">{user.name}</p>} + {user.email && ( + <p className="w-[200px] truncate text-sm text-zinc-700"> + {user.email} + </p> + )} + </div> + </div> - <DropdownMenuSeparator /> + <DropdownMenuSeparator /> - <DropdownMenuItem asChild> - <Link href="/">Inici</Link> - </DropdownMenuItem> + <DropdownMenuItem asChild> + <Link href="/">Inici</Link> + </DropdownMenuItem> - <DropdownMenuItem asChild> - <Link href="/privacyandterms">Privacitat i Termes de Servei</Link> - </DropdownMenuItem> + <DropdownMenuItem asChild> + <Link href="/privacyandterms">Privacitat i Termes de Servei</Link> + </DropdownMenuItem> - <DropdownMenuSeparator /> + <DropdownMenuSeparator /> - <DropdownMenuItem - onSelect={(event) => { - event.preventDefault(); - signOut({ callbackUrl: `${window.location.origin}/sign-in` }); - }} - className="cursor-pointer"> - Tancar Sessió - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - ); -}; + <DropdownMenuItem + onSelect={(event) => { + event.preventDefault() + signOut({ + callbackUrl: `${window.location.origin}/sign-in`, + }) + }} + className="cursor-pointer" + > + Tancar Sessió + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) +} -export default UserAccountNav; +export default UserAccountNav diff --git a/src/components/UserAuthForm.tsx b/src/components/UserAuthForm.tsx index 7121697..c5486e9 100644 --- a/src/components/UserAuthForm.tsx +++ b/src/components/UserAuthForm.tsx @@ -1,46 +1,48 @@ -"use client"; +"use client" -import { Button } from "./ui/Button"; -import { FC, useState } from "react"; -import { signIn } from "next-auth/react"; -import { cn } from "@/lib/utils"; -import { Icons } from "./Icons"; -import { useToast } from "@/hooks/use-toast"; +import { Button } from "./ui/Button" +import { FC, useState } from "react" +import { signIn } from "next-auth/react" +import { cn } from "@/lib/utils" +import { Icons } from "./Icons" +import { useToast } from "@/hooks/use-toast" interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {} const UserAuthForm: FC<UserAuthFormProps> = ({ className }) => { - const [isLoading, setIsLoading] = useState<boolean>(false); - const { toast } = useToast(); + const [isLoading, setIsLoading] = useState<boolean>(false) + const { toast } = useToast() - const loginWithGoogle = async () => { - setIsLoading(true); + const loginWithGoogle = async () => { + setIsLoading(true) - try { - await signIn("google", { callbackUrl: "/" }); - } catch (error) { - toast({ - title: "Hi ha hagut un problema.", - description: "Hi ha hagut un error al iniciar sessió amb Google. Si us plau, torna a intentar-ho.", - variant: "destructive", - }); - } finally { - setIsLoading(false); - } - }; + try { + await signIn("google", { callbackUrl: "/" }) + } catch (error) { + toast({ + title: "Hi ha hagut un problema.", + description: + "Hi ha hagut un error al iniciar sessió amb Google. Si us plau, torna a intentar-ho.", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } - return ( - <div className={cn("flex justify-center", className)}> - <Button - onClick={loginWithGoogle} - isLoading={isLoading} - size="sm" - className="w-full"> - {isLoading ? null : <Icons.google className="h-4 w-4 mr-2" />} - Google - </Button> - </div> - ); -}; + return ( + <div className={cn("flex justify-center", className)}> + <Button + onClick={loginWithGoogle} + isLoading={isLoading} + size="sm" + className="w-full" + > + {isLoading ? null : <Icons.google className="h-4 w-4 mr-2" />} + Google + </Button> + </div> + ) +} -export default UserAuthForm; +export default UserAuthForm diff --git a/src/components/UserAvatar.tsx b/src/components/UserAvatar.tsx index 5c9c332..e1f93ba 100644 --- a/src/components/UserAvatar.tsx +++ b/src/components/UserAvatar.tsx @@ -1,35 +1,35 @@ -import { User } from "next-auth"; -import { FC } from "react"; -import { Avatar, AvatarFallback } from "./ui/Avatar"; -import Image from "next/image"; -import { Icons } from "./Icons"; -import { AvatarProps } from "@radix-ui/react-avatar"; +import { User } from "next-auth" +import { FC } from "react" +import { Avatar, AvatarFallback } from "./ui/Avatar" +import Image from "next/image" +import { Icons } from "./Icons" +import { AvatarProps } from "@radix-ui/react-avatar" interface UserAvatarProps extends AvatarProps { - user: Pick<User, "name" | "image">; + user: Pick<User, "name" | "image"> } const UserAvatar: FC<UserAvatarProps> = ({ user, ...props }) => { - return ( - <Avatar {...props}> - {user.image ? ( - <div className="relative aspect-square h-full w-full"> - <Image - fill - sizes="100%" - src={user.image} - alt="Imatge del Perfil" - referrerPolicy="no-referrer" - /> - </div> - ) : ( - <AvatarFallback> - <span className="sr-only">{user?.name}</span> - <Icons.user /> - </AvatarFallback> - )} - </Avatar> - ); -}; + return ( + <Avatar {...props}> + {user.image ? ( + <div className="relative aspect-square h-full w-full"> + <Image + fill + sizes="100%" + src={user.image} + alt="Imatge del Perfil" + referrerPolicy="no-referrer" + /> + </div> + ) : ( + <AvatarFallback> + <span className="sr-only">{user?.name}</span> + <Icons.user /> + </AvatarFallback> + )} + </Avatar> + ) +} -export default UserAvatar; +export default UserAvatar diff --git a/src/components/renderers/CustomCodeRenderer.tsx b/src/components/renderers/CustomCodeRenderer.tsx index 4a6349f..67404d8 100644 --- a/src/components/renderers/CustomCodeRenderer.tsx +++ b/src/components/renderers/CustomCodeRenderer.tsx @@ -1,13 +1,13 @@ -"use client"; +"use client" function CustomCodeRenderer({ data }: any) { - data; + data - return ( - <pre className="bg-gray-800 rounded-md p-4"> - <code className="text-gray-100 text-sm">{data.code}</code> - </pre> - ); + return ( + <pre className="bg-gray-800 rounded-md p-4"> + <code className="text-gray-100 text-sm">{data.code}</code> + </pre> + ) } -export default CustomCodeRenderer; +export default CustomCodeRenderer diff --git a/src/components/renderers/CustomImageRenderer.tsx b/src/components/renderers/CustomImageRenderer.tsx index be6d13f..84d4a2b 100644 --- a/src/components/renderers/CustomImageRenderer.tsx +++ b/src/components/renderers/CustomImageRenderer.tsx @@ -1,20 +1,15 @@ -"use client"; +"use client" -import Image from "next/image"; +import Image from "next/image" function CustomImageRenderer({ data }: any) { - const src = data.file.url; + const src = data.file.url - return ( - <div className="relative w-full min-h-[15rem]"> - <Image - alt="image" - className="object-contain" - fill - src={src} - /> - </div> - ); + return ( + <div className="relative w-full min-h-[15rem]"> + <Image alt="image" className="object-contain" fill src={src} /> + </div> + ) } -export default CustomImageRenderer; +export default CustomImageRenderer diff --git a/src/components/ui/Avatar.tsx b/src/components/ui/Avatar.tsx index 51e507b..5955cbc 100644 --- a/src/components/ui/Avatar.tsx +++ b/src/components/ui/Avatar.tsx @@ -13,7 +13,7 @@ const Avatar = React.forwardRef< ref={ref} className={cn( "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className + className, )} {...props} /> @@ -40,7 +40,7 @@ const AvatarFallback = React.forwardRef< ref={ref} className={cn( "flex h-full w-full items-center justify-center rounded-full bg-muted", - className + className, )} {...props} /> diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx index f000e3e..265875e 100644 --- a/src/components/ui/Badge.tsx +++ b/src/components/ui/Badge.tsx @@ -20,7 +20,7 @@ const badgeVariants = cva( defaultVariants: { variant: "default", }, - } + }, ) export interface BadgeProps diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index c86da0d..d97895f 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -1,36 +1,34 @@ -import { cn } from '@/lib/utils' -import { cva, VariantProps } from 'class-variance-authority' -import { Loader2 } from 'lucide-react' -import * as React from 'react' +import { cn } from "@/lib/utils" +import { cva, VariantProps } from "class-variance-authority" +import { Loader2 } from "lucide-react" +import * as React from "react" const buttonVariants = cva( - 'active:scale-95 inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900', + "active:scale-95 inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900", { variants: { variant: { - default: - 'bg-zinc-900 text-zinc-100 hover:bg-zinc-800', - destructive: 'text-white hover:bg-red-600 dark:hover:bg-red-600', + default: "bg-zinc-900 text-zinc-100 hover:bg-zinc-800", + destructive: "text-white hover:bg-red-600 dark:hover:bg-red-600", outline: - 'bg-zinc-100 text-zinc-900 hover:bg-zinc-200 outline outline-1 outline-zinc-300', - subtle: - 'hover:bg-zinc-200 bg-zinc-100 text-zinc-900', + "bg-zinc-100 text-zinc-900 hover:bg-zinc-200 outline outline-1 outline-zinc-300", + subtle: "hover:bg-zinc-200 bg-zinc-100 text-zinc-900", ghost: - 'bg-transparent hover:bg-zinc-100 text-zinc-800 data-[state=open]:bg-transparent data-[state=open]:bg-transparent', - link: 'bg-transparent dark:bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent', + "bg-transparent hover:bg-zinc-100 text-zinc-800 data-[state=open]:bg-transparent data-[state=open]:bg-transparent", + link: "bg-transparent dark:bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent", }, size: { - default: 'h-10 py-2 px-4', - sm: 'h-9 px-2 rounded-md', - xs: 'h-8 px-1.5 rounded-sm', - lg: 'h-11 px-8 rounded-md', + default: "h-10 py-2 px-4", + sm: "h-9 px-2 rounded-md", + xs: "h-8 px-1.5 rounded-sm", + lg: "h-11 px-8 rounded-md", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, - } + }, ) export interface ButtonProps @@ -46,13 +44,14 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( className={cn(buttonVariants({ variant, size, className }))} ref={ref} disabled={isLoading} - {...props}> - {isLoading ? <Loader2 className='mr-2 h-4 w-4 animate-spin' /> : null} + {...props} + > + {isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} {children} </button> ) - } + }, ) -Button.displayName = 'Button' +Button.displayName = "Button" -export { Button, buttonVariants } \ No newline at end of file +export { Button, buttonVariants } diff --git a/src/components/ui/Command.tsx b/src/components/ui/Command.tsx index 32e0007..28c6a45 100644 --- a/src/components/ui/Command.tsx +++ b/src/components/ui/Command.tsx @@ -16,7 +16,7 @@ const Command = React.forwardRef< ref={ref} className={cn( "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", - className + className, )} {...props} /> @@ -47,7 +47,7 @@ const CommandInput = React.forwardRef< ref={ref} className={cn( "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", - className + className, )} {...props} /> @@ -90,7 +90,7 @@ const CommandGroup = React.forwardRef< ref={ref} className={cn( "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", - className + className, )} {...props} /> @@ -118,7 +118,7 @@ const CommandItem = React.forwardRef< ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className + className, )} {...props} /> @@ -134,7 +134,7 @@ const CommandShortcut = ({ <span className={cn( "ml-auto text-xs tracking-widest text-muted-foreground", - className + className, )} {...props} /> diff --git a/src/components/ui/Dialog.tsx b/src/components/ui/Dialog.tsx index 01ff19c..0b5735c 100644 --- a/src/components/ui/Dialog.tsx +++ b/src/components/ui/Dialog.tsx @@ -22,7 +22,7 @@ const DialogOverlay = React.forwardRef< ref={ref} className={cn( "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className + className, )} {...props} /> @@ -39,7 +39,7 @@ const DialogContent = React.forwardRef< ref={ref} className={cn( "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className + className, )} {...props} > @@ -60,7 +60,7 @@ const DialogHeader = ({ <div className={cn( "flex flex-col space-y-1.5 text-center sm:text-left", - className + className, )} {...props} /> @@ -74,7 +74,7 @@ const DialogFooter = ({ <div className={cn( "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", - className + className, )} {...props} /> @@ -89,7 +89,7 @@ const DialogTitle = React.forwardRef< ref={ref} className={cn( "text-lg font-semibold leading-none tracking-tight", - className + className, )} {...props} /> diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx index f69a0d6..576b13a 100644 --- a/src/components/ui/DropdownMenu.tsx +++ b/src/components/ui/DropdownMenu.tsx @@ -29,7 +29,7 @@ const DropdownMenuSubTrigger = React.forwardRef< className={cn( "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", inset && "pl-8", - className + className, )} {...props} > @@ -48,7 +48,7 @@ const DropdownMenuSubContent = React.forwardRef< ref={ref} className={cn( "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> @@ -66,7 +66,7 @@ const DropdownMenuContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> @@ -85,7 +85,7 @@ const DropdownMenuItem = React.forwardRef< className={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", inset && "pl-8", - className + className, )} {...props} /> @@ -100,7 +100,7 @@ const DropdownMenuCheckboxItem = React.forwardRef< ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className + className, )} checked={checked} {...props} @@ -124,7 +124,7 @@ const DropdownMenuRadioItem = React.forwardRef< ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className + className, )} {...props} > @@ -149,7 +149,7 @@ const DropdownMenuLabel = React.forwardRef< className={cn( "px-2 py-1.5 text-sm font-semibold", inset && "pl-8", - className + className, )} {...props} /> diff --git a/src/components/ui/Form.tsx b/src/components/ui/Form.tsx index d540b14..037b24d 100644 --- a/src/components/ui/Form.tsx +++ b/src/components/ui/Form.tsx @@ -17,18 +17,18 @@ const Form = FormProvider type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, > = { name: TName } const FormFieldContext = React.createContext<FormFieldContextValue>( - {} as FormFieldContextValue + {} as FormFieldContextValue, ) const FormField = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, >({ ...props }: ControllerProps<TFieldValues, TName>) => { @@ -67,7 +67,7 @@ type FormItemContextValue = { } const FormItemContext = React.createContext<FormItemContextValue>( - {} as FormItemContextValue + {} as FormItemContextValue, ) const FormItem = React.forwardRef< diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 677d05f..9900814 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -12,13 +12,13 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>( type={type} className={cn( "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", - className + className, )} ref={ref} {...props} /> ) - } + }, ) Input.displayName = "Input" diff --git a/src/components/ui/Label.tsx b/src/components/ui/Label.tsx index 5341821..afde563 100644 --- a/src/components/ui/Label.tsx +++ b/src/components/ui/Label.tsx @@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", ) const Label = React.forwardRef< diff --git a/src/components/ui/Popover.tsx b/src/components/ui/Popover.tsx index a0ec48b..cb5c141 100644 --- a/src/components/ui/Popover.tsx +++ b/src/components/ui/Popover.tsx @@ -20,7 +20,7 @@ const PopoverContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> diff --git a/src/components/ui/Toast.tsx b/src/components/ui/Toast.tsx index a822477..d61b0e0 100644 --- a/src/components/ui/Toast.tsx +++ b/src/components/ui/Toast.tsx @@ -15,7 +15,7 @@ const ToastViewport = React.forwardRef< ref={ref} className={cn( "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", - className + className, )} {...props} /> @@ -35,7 +35,7 @@ const toastVariants = cva( defaultVariants: { variant: "default", }, - } + }, ) const Toast = React.forwardRef< @@ -61,7 +61,7 @@ const ToastAction = React.forwardRef< ref={ref} className={cn( "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", - className + className, )} {...props} /> @@ -76,7 +76,7 @@ const ToastClose = React.forwardRef< ref={ref} className={cn( "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", - className + className, )} toast-close="" {...props} diff --git a/src/components/ui/Toaster.tsx b/src/components/ui/Toaster.tsx index 37c92fd..9461ca3 100644 --- a/src/components/ui/Toaster.tsx +++ b/src/components/ui/Toaster.tsx @@ -1,28 +1,35 @@ -"use client"; +"use client" -import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/Toast"; -import { useToast } from "@/hooks/use-toast"; +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/Toast" +import { useToast } from "@/hooks/use-toast" export function Toaster() { - const { toasts } = useToast(); + const { toasts } = useToast() - return ( - <ToastProvider> - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - <Toast - key={id} - {...props}> - <div className="grid gap-1"> - {title && <ToastTitle>{title}</ToastTitle>} - {description && <ToastDescription>{description}</ToastDescription>} - </div> - {action} - <ToastClose /> - </Toast> - ); - })} - <ToastViewport /> - </ToastProvider> - ); + return ( + <ToastProvider> + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + <Toast key={id} {...props}> + <div className="grid gap-1"> + {title && <ToastTitle>{title}</ToastTitle>} + {description && ( + <ToastDescription>{description}</ToastDescription> + )} + </div> + {action} + <ToastClose /> + </Toast> + ) + })} + <ToastViewport /> + </ToastProvider> + ) } diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index df61a13..b8e7c62 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -14,7 +14,7 @@ const Checkbox = React.forwardRef< ref={ref} className={cn( "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", - className + className, )} {...props} > diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 30fc44d..2373b49 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> diff --git a/src/components/votes/AnswerAcceptClient.tsx b/src/components/votes/AnswerAcceptClient.tsx index 0985ce3..082476c 100644 --- a/src/components/votes/AnswerAcceptClient.tsx +++ b/src/components/votes/AnswerAcceptClient.tsx @@ -1,104 +1,113 @@ -"use client"; +"use client" -import { useCustomToasts } from "@/hooks/use-custom-toasts"; -import { AnswerAcceptedRequest } from "@/lib/validators/vote"; -import { usePrevious } from "@mantine/hooks"; -import { useMutation } from "@tanstack/react-query"; -import axios, { AxiosError } from "axios"; -import { useEffect, useState } from "react"; -import { toast } from "../../hooks/use-toast"; -import { Button } from "../ui/Button"; -import { Check } from "lucide-react"; -import { cn } from "@/lib/utils"; -import debounce from "lodash.debounce"; -import { Answer, AnswerVote, User } from "@prisma/client"; -import { useSession } from "next-auth/react"; +import { useCustomToasts } from "@/hooks/use-custom-toasts" +import { AnswerAcceptedRequest } from "@/lib/validators/vote" +import { usePrevious } from "@mantine/hooks" +import { useMutation } from "@tanstack/react-query" +import axios, { AxiosError } from "axios" +import { useEffect, useState } from "react" +import { toast } from "../../hooks/use-toast" +import { Button } from "../ui/Button" +import { Check } from "lucide-react" +import { cn } from "@/lib/utils" +import debounce from "lodash.debounce" +import { Answer, AnswerVote, User } from "@prisma/client" +import { useSession } from "next-auth/react" interface AnswerAcceptClientProps { - questionAuthorId: string; - answerId: string; - initialAccepted: boolean | undefined; - answer: Answer & { - author: User; - votes: AnswerVote[]; - }; + questionAuthorId: string + answerId: string + initialAccepted: boolean | undefined + answer: Answer & { + author: User + votes: AnswerVote[] + } } -const AnswerAcceptClient = ({ questionAuthorId, answerId, initialAccepted, answer }: AnswerAcceptClientProps) => { - const { loginToast } = useCustomToasts(); - const [currentAccepted, setCurrentAccepted] = useState(initialAccepted); - const prevAccepted = usePrevious(currentAccepted); +const AnswerAcceptClient = ({ + questionAuthorId, + answerId, + initialAccepted, + answer, +}: AnswerAcceptClientProps) => { + const { loginToast } = useCustomToasts() + const [currentAccepted, setCurrentAccepted] = useState(initialAccepted) + const prevAccepted = usePrevious(currentAccepted) - // ensure sync with server - useEffect(() => { - setCurrentAccepted(initialAccepted); - }, [initialAccepted]); + // ensure sync with server + useEffect(() => { + setCurrentAccepted(initialAccepted) + }, [initialAccepted]) - const { mutate: accept } = useMutation({ - mutationFn: async (accepted: boolean) => { - const payload: AnswerAcceptedRequest = { - accepted: accepted, - answerId: answerId, - }; + const { mutate: accept } = useMutation({ + mutationFn: async (accepted: boolean) => { + const payload: AnswerAcceptedRequest = { + accepted: accepted, + answerId: answerId, + } - await axios.patch("/api/subject/answer/accept", payload); - }, - onError: (err, accept) => { - // reset current acceptation - setCurrentAccepted(prevAccepted); + await axios.patch("/api/subject/answer/accept", payload) + }, + onError: (err, accept) => { + // reset current acceptation + setCurrentAccepted(prevAccepted) - if (err instanceof AxiosError) { - if (err.response?.status === 401) { - return loginToast(); - } - } + if (err instanceof AxiosError) { + if (err.response?.status === 401) { + return loginToast() + } + } - return toast({ - title: "Something went wrong.", - description: "Your acceptation was not registered. Please try again.", - variant: "destructive", - }); - }, - onMutate: (accepted: boolean) => { - if (currentAccepted === accepted) { - // User is voting the same way again, so remove their acceptation - setCurrentAccepted(undefined); - } else { - // User is voting in the opposite direction, so subtract 2 - setCurrentAccepted(accepted); - } - }, - }); + return toast({ + title: "Something went wrong.", + description: "Your acceptation was not registered. Please try again.", + variant: "destructive", + }) + }, + onMutate: (accepted: boolean) => { + if (currentAccepted === accepted) { + // User is voting the same way again, so remove their acceptation + setCurrentAccepted(undefined) + } else { + // User is voting in the opposite direction, so subtract 2 + setCurrentAccepted(accepted) + } + }, + }) - const debouncedAccepted = debounce(accept, 1000, { leading: true, trailing: false }); + const debouncedAccepted = debounce(accept, 1000, { + leading: true, + trailing: false, + }) - const { data: session } = useSession(); + const { data: session } = useSession() - return ( - <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> - {questionAuthorId === session?.user.id ? ( - <Button - onClick={() => debouncedAccepted(true)} - size="sm" - variant="ghost" - aria-label="accept"> - <Check - strokeWidth={3} - className={cn("h-5 w-5 text-zinc-700", { - "text-emerald-500": currentAccepted, - })} - /> - </Button> - ) : ( - <Check - strokeWidth={3} - className={cn("h-5 w-5 text-zinc-700", { - "text-emerald-500": answer.accepted, - })} - /> - )} - </div> - ); -}; + return ( + <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> + {questionAuthorId === session?.user.id ? ( + <Button + onClick={() => debouncedAccepted(true)} + size="sm" + variant="ghost" + aria-label="accept" + > + <Check + strokeWidth={3} + className={cn("h-5 w-5 text-zinc-700", { + "text-emerald-500": currentAccepted, + })} + /> + </Button> + ) : ( + <Check + strokeWidth={3} + className={cn("h-5 w-5 text-zinc-700", { + "text-emerald-500": answer.accepted, + })} + /> + )} + </div> + ) +} -export default AnswerAcceptClient; +export default AnswerAcceptClient diff --git a/src/components/votes/AnswerVoteClient.tsx b/src/components/votes/AnswerVoteClient.tsx index 987a7a5..e928832 100644 --- a/src/components/votes/AnswerVoteClient.tsx +++ b/src/components/votes/AnswerVoteClient.tsx @@ -1,115 +1,127 @@ -"use client"; +"use client" -import { useCustomToasts } from "@/hooks/use-custom-toasts"; -import { AnswerVoteRequest } from "@/lib/validators/vote"; -import { usePrevious } from "@mantine/hooks"; -import { VoteType } from "@prisma/client"; -import { useMutation } from "@tanstack/react-query"; -import axios, { AxiosError } from "axios"; -import { useEffect, useState } from "react"; -import { toast } from "../../hooks/use-toast"; -import { Button } from "../ui/Button"; -import { ArrowBigDown, ArrowBigUp } from "lucide-react"; -import { cn } from "@/lib/utils"; -import debounce from "lodash.debounce"; +import { useCustomToasts } from "@/hooks/use-custom-toasts" +import { AnswerVoteRequest } from "@/lib/validators/vote" +import { usePrevious } from "@mantine/hooks" +import { VoteType } from "@prisma/client" +import { useMutation } from "@tanstack/react-query" +import axios, { AxiosError } from "axios" +import { useEffect, useState } from "react" +import { toast } from "../../hooks/use-toast" +import { Button } from "../ui/Button" +import { ArrowBigDown, ArrowBigUp } from "lucide-react" +import { cn } from "@/lib/utils" +import debounce from "lodash.debounce" interface AnswerVoteClientProps { - answerId: string; - initialVotesAmt: number; - initialVote?: VoteType | null; + answerId: string + initialVotesAmt: number + initialVote?: VoteType | null } -const AnswerVoteClient = ({ answerId, initialVotesAmt, initialVote }: AnswerVoteClientProps) => { - const { loginToast } = useCustomToasts(); - const [votesAmt, setVotesAmt] = useState<number>(initialVotesAmt); - const [currentVote, setCurrentVote] = useState(initialVote); - const prevVote = usePrevious(currentVote); +const AnswerVoteClient = ({ + answerId, + initialVotesAmt, + initialVote, +}: AnswerVoteClientProps) => { + const { loginToast } = useCustomToasts() + const [votesAmt, setVotesAmt] = useState<number>(initialVotesAmt) + const [currentVote, setCurrentVote] = useState(initialVote) + const prevVote = usePrevious(currentVote) - // ensure sync with server - useEffect(() => { - setCurrentVote(initialVote); - }, [initialVote]); + // ensure sync with server + useEffect(() => { + setCurrentVote(initialVote) + }, [initialVote]) - const { mutate: vote } = useMutation({ - mutationFn: async (type: VoteType) => { - const payload: AnswerVoteRequest = { - voteType: type, - answerId: answerId, - }; + const { mutate: vote } = useMutation({ + mutationFn: async (type: VoteType) => { + const payload: AnswerVoteRequest = { + voteType: type, + answerId: answerId, + } - await axios.patch("/api/subject/answer/vote", payload); - }, - onError: (err, voteType) => { - if (voteType === "UP") setVotesAmt((prev) => prev - 1); - else setVotesAmt((prev) => prev + 1); + await axios.patch("/api/subject/answer/vote", payload) + }, + onError: (err, voteType) => { + if (voteType === "UP") setVotesAmt((prev) => prev - 1) + else setVotesAmt((prev) => prev + 1) - // reset current vote - setCurrentVote(prevVote); + // reset current vote + setCurrentVote(prevVote) - if (err instanceof AxiosError) { - if (err.response?.status === 401) { - return loginToast(); - } - } + if (err instanceof AxiosError) { + if (err.response?.status === 401) { + return loginToast() + } + } - return toast({ - title: "Something went wrong.", - description: "Your vote was not registered. Please try again.", - variant: "destructive", - }); - }, - onMutate: (type: VoteType) => { - if (currentVote === type) { - // User is voting the same way again, so remove their vote - setCurrentVote(undefined); - if (type === "UP") setVotesAmt((prev) => prev - 1); - else if (type === "DOWN") setVotesAmt((prev) => prev + 1); - } else { - // User is voting in the opposite direction, so subtract 2 - setCurrentVote(type); - if (type === "UP") setVotesAmt((prev) => prev + (currentVote ? 2 : 1)); - else if (type === "DOWN") setVotesAmt((prev) => prev - (currentVote ? 2 : 1)); - } - }, - }); + return toast({ + title: "Something went wrong.", + description: "Your vote was not registered. Please try again.", + variant: "destructive", + }) + }, + onMutate: (type: VoteType) => { + if (currentVote === type) { + // User is voting the same way again, so remove their vote + setCurrentVote(undefined) + if (type === "UP") setVotesAmt((prev) => prev - 1) + else if (type === "DOWN") setVotesAmt((prev) => prev + 1) + } else { + // User is voting in the opposite direction, so subtract 2 + setCurrentVote(type) + if (type === "UP") setVotesAmt((prev) => prev + (currentVote ? 2 : 1)) + else if (type === "DOWN") + setVotesAmt((prev) => prev - (currentVote ? 2 : 1)) + } + }, + }) - const debouncedVote = debounce(vote, 1000, { leading: true, trailing: false }); + const debouncedVote = debounce(vote, 1000, { + leading: true, + trailing: false, + }) - return ( - <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> - {/* upvote */} - <Button - onClick={() => debouncedVote("UP")} - size="sm" - variant="ghost" - aria-label="upvote"> - <ArrowBigUp - className={cn("h-5 w-5 text-zinc-700", { - "text-emerald-500 fill-emerald-500": currentVote === "UP", - })} - /> - </Button> + return ( + <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> + {/* upvote */} + <Button + onClick={() => debouncedVote("UP")} + size="sm" + variant="ghost" + aria-label="upvote" + > + <ArrowBigUp + className={cn("h-5 w-5 text-zinc-700", { + "text-emerald-500 fill-emerald-500": currentVote === "UP", + })} + /> + </Button> - {/* score */} - <p className="text-center py-2 font-medium text-sm text-zinc-900">{votesAmt}</p> + {/* score */} + <p className="text-center py-2 font-medium text-sm text-zinc-900"> + {votesAmt} + </p> - {/* downvote */} - <Button - onClick={() => debouncedVote("DOWN")} - size="sm" - className={cn({ - "text-emerald-500": currentVote === "DOWN", - })} - variant="ghost" - aria-label="downvote"> - <ArrowBigDown - className={cn("h-5 w-5 text-zinc-700", { - "text-red-500 fill-red-500": currentVote === "DOWN", - })} - /> - </Button> - </div> - ); -}; + {/* downvote */} + <Button + onClick={() => debouncedVote("DOWN")} + size="sm" + className={cn({ + "text-emerald-500": currentVote === "DOWN", + })} + variant="ghost" + aria-label="downvote" + > + <ArrowBigDown + className={cn("h-5 w-5 text-zinc-700", { + "text-red-500 fill-red-500": currentVote === "DOWN", + })} + /> + </Button> + </div> + ) +} -export default AnswerVoteClient; +export default AnswerVoteClient diff --git a/src/components/votes/CommentVoteClient.tsx b/src/components/votes/CommentVoteClient.tsx index 11c9b9a..0524154 100644 --- a/src/components/votes/CommentVoteClient.tsx +++ b/src/components/votes/CommentVoteClient.tsx @@ -1,115 +1,127 @@ -"use client"; +"use client" -import { useCustomToasts } from "@/hooks/use-custom-toasts"; -import { CommentVoteRequest } from "@/lib/validators/vote"; -import { usePrevious } from "@mantine/hooks"; -import { VoteType } from "@prisma/client"; -import { useMutation } from "@tanstack/react-query"; -import axios, { AxiosError } from "axios"; -import { useEffect, useState } from "react"; -import { toast } from "../../hooks/use-toast"; -import { Button } from "../ui/Button"; -import { ArrowBigDown, ArrowBigUp } from "lucide-react"; -import { cn } from "@/lib/utils"; -import debounce from "lodash.debounce"; +import { useCustomToasts } from "@/hooks/use-custom-toasts" +import { CommentVoteRequest } from "@/lib/validators/vote" +import { usePrevious } from "@mantine/hooks" +import { VoteType } from "@prisma/client" +import { useMutation } from "@tanstack/react-query" +import axios, { AxiosError } from "axios" +import { useEffect, useState } from "react" +import { toast } from "../../hooks/use-toast" +import { Button } from "../ui/Button" +import { ArrowBigDown, ArrowBigUp } from "lucide-react" +import { cn } from "@/lib/utils" +import debounce from "lodash.debounce" interface CommentVoteClientProps { - commentId: string; - initialVotesAmt: number; - initialVote?: VoteType | null; + commentId: string + initialVotesAmt: number + initialVote?: VoteType | null } -const CommentVoteClient = ({ commentId, initialVotesAmt, initialVote }: CommentVoteClientProps) => { - const { loginToast } = useCustomToasts(); - const [votesAmt, setVotesAmt] = useState<number>(initialVotesAmt); - const [currentVote, setCurrentVote] = useState(initialVote); - const prevVote = usePrevious(currentVote); +const CommentVoteClient = ({ + commentId, + initialVotesAmt, + initialVote, +}: CommentVoteClientProps) => { + const { loginToast } = useCustomToasts() + const [votesAmt, setVotesAmt] = useState<number>(initialVotesAmt) + const [currentVote, setCurrentVote] = useState(initialVote) + const prevVote = usePrevious(currentVote) - // ensure sync with server - useEffect(() => { - setCurrentVote(initialVote); - }, [initialVote]); + // ensure sync with server + useEffect(() => { + setCurrentVote(initialVote) + }, [initialVote]) - const { mutate: vote } = useMutation({ - mutationFn: async (type: VoteType) => { - const payload: CommentVoteRequest = { - voteType: type, - commentId: commentId, - }; + const { mutate: vote } = useMutation({ + mutationFn: async (type: VoteType) => { + const payload: CommentVoteRequest = { + voteType: type, + commentId: commentId, + } - await axios.patch("/api/subject/comment/vote", payload); - }, - onError: (err, voteType) => { - if (voteType === "UP") setVotesAmt((prev) => prev - 1); - else setVotesAmt((prev) => prev + 1); + await axios.patch("/api/subject/comment/vote", payload) + }, + onError: (err, voteType) => { + if (voteType === "UP") setVotesAmt((prev) => prev - 1) + else setVotesAmt((prev) => prev + 1) - // reset current vote - setCurrentVote(prevVote); + // reset current vote + setCurrentVote(prevVote) - if (err instanceof AxiosError) { - if (err.response?.status === 401) { - return loginToast(); - } - } + if (err instanceof AxiosError) { + if (err.response?.status === 401) { + return loginToast() + } + } - return toast({ - title: "Something went wrong.", - description: "Your vote was not registered. Please try again.", - variant: "destructive", - }); - }, - onMutate: (type: VoteType) => { - if (currentVote === type) { - // User is voting the same way again, so remove their vote - setCurrentVote(undefined); - if (type === "UP") setVotesAmt((prev) => prev - 1); - else if (type === "DOWN") setVotesAmt((prev) => prev + 1); - } else { - // User is voting in the opposite direction, so subtract 2 - setCurrentVote(type); - if (type === "UP") setVotesAmt((prev) => prev + (currentVote ? 2 : 1)); - else if (type === "DOWN") setVotesAmt((prev) => prev - (currentVote ? 2 : 1)); - } - }, - }); + return toast({ + title: "Something went wrong.", + description: "Your vote was not registered. Please try again.", + variant: "destructive", + }) + }, + onMutate: (type: VoteType) => { + if (currentVote === type) { + // User is voting the same way again, so remove their vote + setCurrentVote(undefined) + if (type === "UP") setVotesAmt((prev) => prev - 1) + else if (type === "DOWN") setVotesAmt((prev) => prev + 1) + } else { + // User is voting in the opposite direction, so subtract 2 + setCurrentVote(type) + if (type === "UP") setVotesAmt((prev) => prev + (currentVote ? 2 : 1)) + else if (type === "DOWN") + setVotesAmt((prev) => prev - (currentVote ? 2 : 1)) + } + }, + }) - const debouncedVote = debounce(vote, 1000, { leading: true, trailing: false }); + const debouncedVote = debounce(vote, 1000, { + leading: true, + trailing: false, + }) - return ( - <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> - {/* upvote */} - <Button - onClick={() => debouncedVote("UP")} - size="sm" - variant="ghost" - aria-label="upvote"> - <ArrowBigUp - className={cn("h-5 w-5 text-zinc-700", { - "text-emerald-500 fill-emerald-500": currentVote === "UP", - })} - /> - </Button> + return ( + <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> + {/* upvote */} + <Button + onClick={() => debouncedVote("UP")} + size="sm" + variant="ghost" + aria-label="upvote" + > + <ArrowBigUp + className={cn("h-5 w-5 text-zinc-700", { + "text-emerald-500 fill-emerald-500": currentVote === "UP", + })} + /> + </Button> - {/* score */} - <p className="text-center py-2 font-medium text-sm text-zinc-900">{votesAmt}</p> + {/* score */} + <p className="text-center py-2 font-medium text-sm text-zinc-900"> + {votesAmt} + </p> - {/* downvote */} - <Button - onClick={() => debouncedVote("DOWN")} - size="sm" - className={cn({ - "text-emerald-500": currentVote === "DOWN", - })} - variant="ghost" - aria-label="downvote"> - <ArrowBigDown - className={cn("h-5 w-5 text-zinc-700", { - "text-red-500 fill-red-500": currentVote === "DOWN", - })} - /> - </Button> - </div> - ); -}; + {/* downvote */} + <Button + onClick={() => debouncedVote("DOWN")} + size="sm" + className={cn({ + "text-emerald-500": currentVote === "DOWN", + })} + variant="ghost" + aria-label="downvote" + > + <ArrowBigDown + className={cn("h-5 w-5 text-zinc-700", { + "text-red-500 fill-red-500": currentVote === "DOWN", + })} + /> + </Button> + </div> + ) +} -export default CommentVoteClient; +export default CommentVoteClient diff --git a/src/components/votes/PostVoteClient.tsx b/src/components/votes/PostVoteClient.tsx index 8873ec6..42730ff 100644 --- a/src/components/votes/PostVoteClient.tsx +++ b/src/components/votes/PostVoteClient.tsx @@ -1,115 +1,127 @@ -"use client"; +"use client" -import { useCustomToasts } from "@/hooks/use-custom-toasts"; -import { PostVoteRequest } from "@/lib/validators/vote"; -import { usePrevious } from "@mantine/hooks"; -import { VoteType } from "@prisma/client"; -import { useMutation } from "@tanstack/react-query"; -import axios, { AxiosError } from "axios"; -import { useEffect, useState } from "react"; -import { toast } from "../../hooks/use-toast"; -import { Button } from "../ui/Button"; -import { ArrowBigDown, ArrowBigUp } from "lucide-react"; -import { cn } from "@/lib/utils"; -import debounce from "lodash.debounce"; +import { useCustomToasts } from "@/hooks/use-custom-toasts" +import { PostVoteRequest } from "@/lib/validators/vote" +import { usePrevious } from "@mantine/hooks" +import { VoteType } from "@prisma/client" +import { useMutation } from "@tanstack/react-query" +import axios, { AxiosError } from "axios" +import { useEffect, useState } from "react" +import { toast } from "../../hooks/use-toast" +import { Button } from "../ui/Button" +import { ArrowBigDown, ArrowBigUp } from "lucide-react" +import { cn } from "@/lib/utils" +import debounce from "lodash.debounce" interface PostVoteClientProps { - postId: string; - initialVotesAmt: number; - initialVote?: VoteType | null; + postId: string + initialVotesAmt: number + initialVote?: VoteType | null } -const PostVoteClient = ({ postId, initialVotesAmt, initialVote }: PostVoteClientProps) => { - const { loginToast } = useCustomToasts(); - const [votesAmt, setVotesAmt] = useState<number>(initialVotesAmt); - const [currentVote, setCurrentVote] = useState(initialVote); - const prevVote = usePrevious(currentVote); +const PostVoteClient = ({ + postId, + initialVotesAmt, + initialVote, +}: PostVoteClientProps) => { + const { loginToast } = useCustomToasts() + const [votesAmt, setVotesAmt] = useState<number>(initialVotesAmt) + const [currentVote, setCurrentVote] = useState(initialVote) + const prevVote = usePrevious(currentVote) - // ensure sync with server - useEffect(() => { - setCurrentVote(initialVote); - }, [initialVote]); + // ensure sync with server + useEffect(() => { + setCurrentVote(initialVote) + }, [initialVote]) - const { mutate: vote } = useMutation({ - mutationFn: async (type: VoteType) => { - const payload: PostVoteRequest = { - voteType: type, - postId: postId, - }; + const { mutate: vote } = useMutation({ + mutationFn: async (type: VoteType) => { + const payload: PostVoteRequest = { + voteType: type, + postId: postId, + } - await axios.patch("/api/subject/post/vote", payload); - }, - onError: (err, voteType) => { - if (voteType === "UP") setVotesAmt((prev) => prev - 1); - else setVotesAmt((prev) => prev + 1); + await axios.patch("/api/subject/post/vote", payload) + }, + onError: (err, voteType) => { + if (voteType === "UP") setVotesAmt((prev) => prev - 1) + else setVotesAmt((prev) => prev + 1) - // reset current vote - setCurrentVote(prevVote); + // reset current vote + setCurrentVote(prevVote) - if (err instanceof AxiosError) { - if (err.response?.status === 401) { - return loginToast(); - } - } + if (err instanceof AxiosError) { + if (err.response?.status === 401) { + return loginToast() + } + } - return toast({ - title: "Something went wrong.", - description: "Your vote was not registered. Please try again.", - variant: "destructive", - }); - }, - onMutate: (type: VoteType) => { - if (currentVote === type) { - // User is voting the same way again, so remove their vote - setCurrentVote(undefined); - if (type === "UP") setVotesAmt((prev) => prev - 1); - else if (type === "DOWN") setVotesAmt((prev) => prev + 1); - } else { - // User is voting in the opposite direction, so subtract 2 - setCurrentVote(type); - if (type === "UP") setVotesAmt((prev) => prev + (currentVote ? 2 : 1)); - else if (type === "DOWN") setVotesAmt((prev) => prev - (currentVote ? 2 : 1)); - } - }, - }); + return toast({ + title: "Something went wrong.", + description: "Your vote was not registered. Please try again.", + variant: "destructive", + }) + }, + onMutate: (type: VoteType) => { + if (currentVote === type) { + // User is voting the same way again, so remove their vote + setCurrentVote(undefined) + if (type === "UP") setVotesAmt((prev) => prev - 1) + else if (type === "DOWN") setVotesAmt((prev) => prev + 1) + } else { + // User is voting in the opposite direction, so subtract 2 + setCurrentVote(type) + if (type === "UP") setVotesAmt((prev) => prev + (currentVote ? 2 : 1)) + else if (type === "DOWN") + setVotesAmt((prev) => prev - (currentVote ? 2 : 1)) + } + }, + }) - const debouncedVote = debounce(vote, 1000, { leading: true, trailing: false }); + const debouncedVote = debounce(vote, 1000, { + leading: true, + trailing: false, + }) - return ( - <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> - {/* upvote */} - <Button - onClick={() => debouncedVote("UP")} - size="sm" - variant="ghost" - aria-label="upvote"> - <ArrowBigUp - className={cn("h-5 w-5 text-zinc-700", { - "text-emerald-500 fill-emerald-500": currentVote === "UP", - })} - /> - </Button> + return ( + <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> + {/* upvote */} + <Button + onClick={() => debouncedVote("UP")} + size="sm" + variant="ghost" + aria-label="upvote" + > + <ArrowBigUp + className={cn("h-5 w-5 text-zinc-700", { + "text-emerald-500 fill-emerald-500": currentVote === "UP", + })} + /> + </Button> - {/* score */} - <p className="text-center py-2 font-medium text-sm text-zinc-900">{votesAmt}</p> + {/* score */} + <p className="text-center py-2 font-medium text-sm text-zinc-900"> + {votesAmt} + </p> - {/* downvote */} - <Button - onClick={() => debouncedVote("DOWN")} - size="sm" - className={cn({ - "text-emerald-500": currentVote === "DOWN", - })} - variant="ghost" - aria-label="downvote"> - <ArrowBigDown - className={cn("h-5 w-5 text-zinc-700", { - "text-red-500 fill-red-500": currentVote === "DOWN", - })} - /> - </Button> - </div> - ); -}; + {/* downvote */} + <Button + onClick={() => debouncedVote("DOWN")} + size="sm" + className={cn({ + "text-emerald-500": currentVote === "DOWN", + })} + variant="ghost" + aria-label="downvote" + > + <ArrowBigDown + className={cn("h-5 w-5 text-zinc-700", { + "text-red-500 fill-red-500": currentVote === "DOWN", + })} + /> + </Button> + </div> + ) +} -export default PostVoteClient; +export default PostVoteClient diff --git a/src/components/votes/PostVoteServer.tsx b/src/components/votes/PostVoteServer.tsx index 340c4f6..fb64f9b 100644 --- a/src/components/votes/PostVoteServer.tsx +++ b/src/components/votes/PostVoteServer.tsx @@ -1,13 +1,13 @@ -import { getAuthSession } from "@/lib/auth"; -import type { Post, PostVote } from "@prisma/client"; -import { notFound } from "next/navigation"; -import PostVoteClient from "./PostVoteClient"; +import { getAuthSession } from "@/lib/auth" +import type { Post, PostVote } from "@prisma/client" +import { notFound } from "next/navigation" +import PostVoteClient from "./PostVoteClient" interface PostVoteServerProps { - postId: string; - initialVotesAmt?: number; - initialVote?: PostVote["type"] | null; - getData?: () => Promise<(Post & { votes: PostVote[] }) | null>; + postId: string + initialVotesAmt?: number + initialVote?: PostVote["type"] | null + getData?: () => Promise<(Post & { votes: PostVote[] }) | null> } /** @@ -17,37 +17,44 @@ interface PostVoteServerProps { * */ -const PostVoteServer = async ({ postId, initialVotesAmt, initialVote, getData }: PostVoteServerProps) => { - const session = await getAuthSession(); - - let _votesAmt: number = 0; - let _currentVote: PostVote["type"] | null | undefined = undefined; - - if (getData) { - // fetch data in component - const post = await getData(); - if (!post) return notFound(); - - _votesAmt = post.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - _currentVote = post.votes.find((vote) => vote.userId === session?.user?.id)?.type; - } else { - // passed as props - _votesAmt = initialVotesAmt!; - _currentVote = initialVote; - } - - return ( - <PostVoteClient - postId={postId} - initialVotesAmt={_votesAmt} - initialVote={_currentVote} - /> - ); -}; - -export default PostVoteServer; +const PostVoteServer = async ({ + postId, + initialVotesAmt, + initialVote, + getData, +}: PostVoteServerProps) => { + const session = await getAuthSession() + + let _votesAmt: number = 0 + let _currentVote: PostVote["type"] | null | undefined = undefined + + if (getData) { + // fetch data in component + const post = await getData() + if (!post) return notFound() + + _votesAmt = post.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + _currentVote = post.votes.find( + (vote) => vote.userId === session?.user?.id, + )?.type + } else { + // passed as props + _votesAmt = initialVotesAmt! + _currentVote = initialVote + } + + return ( + <PostVoteClient + postId={postId} + initialVotesAmt={_votesAmt} + initialVote={_currentVote} + /> + ) +} + +export default PostVoteServer diff --git a/src/components/votes/QuestionVoteClient.tsx b/src/components/votes/QuestionVoteClient.tsx index 8610045..b46f61a 100644 --- a/src/components/votes/QuestionVoteClient.tsx +++ b/src/components/votes/QuestionVoteClient.tsx @@ -1,115 +1,127 @@ -"use client"; +"use client" -import { useCustomToasts } from "@/hooks/use-custom-toasts"; -import { QuestionVoteRequest } from "@/lib/validators/vote"; -import { usePrevious } from "@mantine/hooks"; -import { VoteType } from "@prisma/client"; -import { useMutation } from "@tanstack/react-query"; -import axios, { AxiosError } from "axios"; -import { useEffect, useState } from "react"; -import { toast } from "../../hooks/use-toast"; -import { Button } from "../ui/Button"; -import { ArrowBigDown, ArrowBigUp } from "lucide-react"; -import { cn } from "@/lib/utils"; -import debounce from "lodash.debounce"; +import { useCustomToasts } from "@/hooks/use-custom-toasts" +import { QuestionVoteRequest } from "@/lib/validators/vote" +import { usePrevious } from "@mantine/hooks" +import { VoteType } from "@prisma/client" +import { useMutation } from "@tanstack/react-query" +import axios, { AxiosError } from "axios" +import { useEffect, useState } from "react" +import { toast } from "../../hooks/use-toast" +import { Button } from "../ui/Button" +import { ArrowBigDown, ArrowBigUp } from "lucide-react" +import { cn } from "@/lib/utils" +import debounce from "lodash.debounce" interface QuestionVoteClientProps { - questionId: string; - initialVotesAmt: number; - initialVote?: VoteType | null; + questionId: string + initialVotesAmt: number + initialVote?: VoteType | null } -const QuestionVoteClient = ({ questionId, initialVotesAmt, initialVote }: QuestionVoteClientProps) => { - const { loginToast } = useCustomToasts(); - const [votesAmt, setVotesAmt] = useState<number>(initialVotesAmt); - const [currentVote, setCurrentVote] = useState(initialVote); - const prevVote = usePrevious(currentVote); +const QuestionVoteClient = ({ + questionId, + initialVotesAmt, + initialVote, +}: QuestionVoteClientProps) => { + const { loginToast } = useCustomToasts() + const [votesAmt, setVotesAmt] = useState<number>(initialVotesAmt) + const [currentVote, setCurrentVote] = useState(initialVote) + const prevVote = usePrevious(currentVote) - // ensure sync with server - useEffect(() => { - setCurrentVote(initialVote); - }, [initialVote]); + // ensure sync with server + useEffect(() => { + setCurrentVote(initialVote) + }, [initialVote]) - const { mutate: vote } = useMutation({ - mutationFn: async (type: VoteType) => { - const payload: QuestionVoteRequest = { - voteType: type, - questionId: questionId, - }; + const { mutate: vote } = useMutation({ + mutationFn: async (type: VoteType) => { + const payload: QuestionVoteRequest = { + voteType: type, + questionId: questionId, + } - await axios.patch("/api/subject/question/vote", payload); - }, - onError: (err, voteType) => { - if (voteType === "UP") setVotesAmt((prev) => prev - 1); - else setVotesAmt((prev) => prev + 1); + await axios.patch("/api/subject/question/vote", payload) + }, + onError: (err, voteType) => { + if (voteType === "UP") setVotesAmt((prev) => prev - 1) + else setVotesAmt((prev) => prev + 1) - // reset current vote - setCurrentVote(prevVote); + // reset current vote + setCurrentVote(prevVote) - if (err instanceof AxiosError) { - if (err.response?.status === 401) { - return loginToast(); - } - } + if (err instanceof AxiosError) { + if (err.response?.status === 401) { + return loginToast() + } + } - return toast({ - title: "Something went wrong.", - description: "Your vote was not registered. Please try again.", - variant: "destructive", - }); - }, - onMutate: (type: VoteType) => { - if (currentVote === type) { - // User is voting the same way again, so remove their vote - setCurrentVote(undefined); - if (type === "UP") setVotesAmt((prev) => prev - 1); - else if (type === "DOWN") setVotesAmt((prev) => prev + 1); - } else { - // User is voting in the opposite direction, so subtract 2 - setCurrentVote(type); - if (type === "UP") setVotesAmt((prev) => prev + (currentVote ? 2 : 1)); - else if (type === "DOWN") setVotesAmt((prev) => prev - (currentVote ? 2 : 1)); - } - }, - }); + return toast({ + title: "Something went wrong.", + description: "Your vote was not registered. Please try again.", + variant: "destructive", + }) + }, + onMutate: (type: VoteType) => { + if (currentVote === type) { + // User is voting the same way again, so remove their vote + setCurrentVote(undefined) + if (type === "UP") setVotesAmt((prev) => prev - 1) + else if (type === "DOWN") setVotesAmt((prev) => prev + 1) + } else { + // User is voting in the opposite direction, so subtract 2 + setCurrentVote(type) + if (type === "UP") setVotesAmt((prev) => prev + (currentVote ? 2 : 1)) + else if (type === "DOWN") + setVotesAmt((prev) => prev - (currentVote ? 2 : 1)) + } + }, + }) - const debouncedVote = debounce(vote, 1000, { leading: true, trailing: false }); + const debouncedVote = debounce(vote, 1000, { + leading: true, + trailing: false, + }) - return ( - <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> - {/* upvote */} - <Button - onClick={() => debouncedVote("UP")} - size="sm" - variant="ghost" - aria-label="upvote"> - <ArrowBigUp - className={cn("h-5 w-5 text-zinc-700", { - "text-emerald-500 fill-emerald-500": currentVote === "UP", - })} - /> - </Button> + return ( + <div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> + {/* upvote */} + <Button + onClick={() => debouncedVote("UP")} + size="sm" + variant="ghost" + aria-label="upvote" + > + <ArrowBigUp + className={cn("h-5 w-5 text-zinc-700", { + "text-emerald-500 fill-emerald-500": currentVote === "UP", + })} + /> + </Button> - {/* score */} - <p className="text-center py-2 font-medium text-sm text-zinc-900">{votesAmt}</p> + {/* score */} + <p className="text-center py-2 font-medium text-sm text-zinc-900"> + {votesAmt} + </p> - {/* downvote */} - <Button - onClick={() => debouncedVote("DOWN")} - size="sm" - className={cn({ - "text-emerald-500": currentVote === "DOWN", - })} - variant="ghost" - aria-label="downvote"> - <ArrowBigDown - className={cn("h-5 w-5 text-zinc-700", { - "text-red-500 fill-red-500": currentVote === "DOWN", - })} - /> - </Button> - </div> - ); -}; + {/* downvote */} + <Button + onClick={() => debouncedVote("DOWN")} + size="sm" + className={cn({ + "text-emerald-500": currentVote === "DOWN", + })} + variant="ghost" + aria-label="downvote" + > + <ArrowBigDown + className={cn("h-5 w-5 text-zinc-700", { + "text-red-500 fill-red-500": currentVote === "DOWN", + })} + /> + </Button> + </div> + ) +} -export default QuestionVoteClient; +export default QuestionVoteClient diff --git a/src/components/votes/QuestionVoteServer.tsx b/src/components/votes/QuestionVoteServer.tsx index 87b9d8f..ca85de0 100644 --- a/src/components/votes/QuestionVoteServer.tsx +++ b/src/components/votes/QuestionVoteServer.tsx @@ -1,13 +1,13 @@ -import { getAuthSession } from "@/lib/auth"; -import type { Question, QuestionVote } from "@prisma/client"; -import { notFound } from "next/navigation"; -import QuestionVoteClient from "./QuestionVoteClient"; +import { getAuthSession } from "@/lib/auth" +import type { Question, QuestionVote } from "@prisma/client" +import { notFound } from "next/navigation" +import QuestionVoteClient from "./QuestionVoteClient" interface QuestionVoteServerProps { - questionId: string; - initialVotesAmt?: number; - initialVote?: QuestionVote["type"] | null; - getData?: () => Promise<(Question & { votes: QuestionVote[] }) | null>; + questionId: string + initialVotesAmt?: number + initialVote?: QuestionVote["type"] | null + getData?: () => Promise<(Question & { votes: QuestionVote[] }) | null> } /** @@ -17,37 +17,44 @@ interface QuestionVoteServerProps { * */ -const QuestionVoteServer = async ({ questionId, initialVotesAmt, initialVote, getData }: QuestionVoteServerProps) => { - const session = await getAuthSession(); - - let _votesAmt: number = 0; - let _currentVote: QuestionVote["type"] | null | undefined = undefined; - - if (getData) { - // fetch data in component - const question = await getData(); - if (!question) return notFound(); - - _votesAmt = question.votes.reduce((acc, vote) => { - if (vote.type === "UP") return acc + 1; - if (vote.type === "DOWN") return acc - 1; - return acc; - }, 0); - - _currentVote = question.votes.find((vote) => vote.userId === session?.user?.id)?.type; - } else { - // passed as props - _votesAmt = initialVotesAmt!; - _currentVote = initialVote; - } - - return ( - <QuestionVoteClient - questionId={questionId} - initialVotesAmt={_votesAmt} - initialVote={_currentVote} - /> - ); -}; - -export default QuestionVoteServer; +const QuestionVoteServer = async ({ + questionId, + initialVotesAmt, + initialVote, + getData, +}: QuestionVoteServerProps) => { + const session = await getAuthSession() + + let _votesAmt: number = 0 + let _currentVote: QuestionVote["type"] | null | undefined = undefined + + if (getData) { + // fetch data in component + const question = await getData() + if (!question) return notFound() + + _votesAmt = question.votes.reduce((acc, vote) => { + if (vote.type === "UP") return acc + 1 + if (vote.type === "DOWN") return acc - 1 + return acc + }, 0) + + _currentVote = question.votes.find( + (vote) => vote.userId === session?.user?.id, + )?.type + } else { + // passed as props + _votesAmt = initialVotesAmt! + _currentVote = initialVote + } + + return ( + <QuestionVoteClient + questionId={questionId} + initialVotesAmt={_votesAmt} + initialVote={_currentVote} + /> + ) +} + +export default QuestionVoteServer diff --git a/src/config.ts b/src/config.ts index def9267..53cbb5a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1 +1 @@ -export const INFINITE_SCROLL_PAGINATION_RESULTS = 4; +export const INFINITE_SCROLL_PAGINATION_RESULTS = 4 diff --git a/src/hooks/use-custom-toasts.tsx b/src/hooks/use-custom-toasts.tsx index 04ffdc0..f10f486 100644 --- a/src/hooks/use-custom-toasts.tsx +++ b/src/hooks/use-custom-toasts.tsx @@ -1,23 +1,24 @@ -import Link from "next/link"; -import { toast } from "./use-toast"; -import { buttonVariants } from "@/components/ui/Button"; +import Link from "next/link" +import { toast } from "./use-toast" +import { buttonVariants } from "@/components/ui/Button" export const useCustomToasts = () => { - const loginToast = () => { - const { dismiss } = toast({ - title: "Inici de sessió necessari.", - description: "Necessites iniciar sessió per a accedir a aquesta pàgina.", - variant: "destructive", - action: ( - <Link - href="/sign-in" - onClick={() => dismiss()} - className={buttonVariants({ variant: "outline" })}> - Inicia sessió - </Link> - ), - }); - }; + const loginToast = () => { + const { dismiss } = toast({ + title: "Inici de sessió necessari.", + description: "Necessites iniciar sessió per a accedir a aquesta pàgina.", + variant: "destructive", + action: ( + <Link + href="/sign-in" + onClick={() => dismiss()} + className={buttonVariants({ variant: "outline" })} + > + Inicia sessió + </Link> + ), + }) + } - return { loginToast }; -}; + return { loginToast } +} diff --git a/src/hooks/use-on-click-outside.ts b/src/hooks/use-on-click-outside.ts index 3364023..e0aa32d 100644 --- a/src/hooks/use-on-click-outside.ts +++ b/src/hooks/use-on-click-outside.ts @@ -1,10 +1,10 @@ -import { RefObject, useEffect } from 'react' +import { RefObject, useEffect } from "react" type Event = MouseEvent | TouchEvent export const useOnClickOutside = <T extends HTMLElement = HTMLElement>( ref: RefObject<T>, - handler: (event: Event) => void + handler: (event: Event) => void, ) => { useEffect(() => { const listener = (event: Event) => { @@ -16,12 +16,12 @@ export const useOnClickOutside = <T extends HTMLElement = HTMLElement>( handler(event) // Call the handler only if the click is outside of the element passed. } - document.addEventListener('mousedown', listener) - document.addEventListener('touchstart', listener) + document.addEventListener("mousedown", listener) + document.addEventListener("touchstart", listener) return () => { - document.removeEventListener('mousedown', listener) - document.removeEventListener('touchstart', listener) + document.removeEventListener("mousedown", listener) + document.removeEventListener("touchstart", listener) } }, [ref, handler]) // Reload only if ref or handler changes } diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts index 4274db7..7ccf21b 100644 --- a/src/hooks/use-toast.ts +++ b/src/hooks/use-toast.ts @@ -1,10 +1,7 @@ // Inspired by react-hot-toast library import * as React from "react" -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/Toast" +import type { ToastActionElement, ToastProps } from "@/components/ui/Toast" const TOAST_LIMIT = 1 const TOAST_REMOVE_DELAY = 1000000 @@ -84,7 +81,7 @@ export const reducer = (state: State, action: Action): State => { return { ...state, toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t + t.id === action.toast.id ? { ...t, ...action.toast } : t, ), } @@ -109,7 +106,7 @@ export const reducer = (state: State, action: Action): State => { ...t, open: false, } - : t + : t, ), } } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index bee4f75..f6e1b1d 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,86 +1,86 @@ -import { db } from "@/lib/db"; -import { PrismaAdapter } from "@next-auth/prisma-adapter"; -import { nanoid } from "nanoid"; -import { NextAuthOptions, getServerSession } from "next-auth"; -import GoogleProvider from "next-auth/providers/google"; +import { db } from "@/lib/db" +import { PrismaAdapter } from "@next-auth/prisma-adapter" +import { nanoid } from "nanoid" +import { NextAuthOptions, getServerSession } from "next-auth" +import GoogleProvider from "next-auth/providers/google" export const authOptions: NextAuthOptions = { - adapter: PrismaAdapter(db), - session: { - strategy: "jwt", - }, - pages: { - signIn: "/sign-in", - }, - providers: [ - GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }), - ], - callbacks: { - async signIn(params) { - const { profile } = params; - const allowedEmail = await db.authorizedUsers.findFirst({ - where: { - email: profile?.email, - }, - }); - if (!allowedEmail) { - return false; - } - return true; - }, + adapter: PrismaAdapter(db), + session: { + strategy: "jwt", + }, + pages: { + signIn: "/sign-in", + }, + providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), + ], + callbacks: { + async signIn(params) { + const { profile } = params + const allowedEmail = await db.authorizedUsers.findFirst({ + where: { + email: profile?.email, + }, + }) + if (!allowedEmail) { + return false + } + return true + }, - async session({ token, session }) { - if (token) { - session.user.id = token.id; - session.user.name = token.name; - session.user.email = token.email; - session.user.image = token.picture; - session.user.username = token.username; - session.user.generacio = token.generacio as string; - } + async session({ token, session }) { + if (token) { + session.user.id = token.id + session.user.name = token.name + session.user.email = token.email + session.user.image = token.picture + session.user.username = token.username + session.user.generacio = token.generacio as string + } - return session; - }, + return session + }, - async jwt({ token, user }) { - const dbUser = await db.user.findFirst({ - where: { - email: token.email, - }, - }); + async jwt({ token, user }) { + const dbUser = await db.user.findFirst({ + where: { + email: token.email, + }, + }) - if (!dbUser) { - token.id = user!.id; - return token; - } + if (!dbUser) { + token.id = user!.id + return token + } - if (!dbUser.username) { - await db.user.update({ - where: { - id: dbUser.id, - }, - data: { - username: nanoid(10), - }, - }); - } + if (!dbUser.username) { + await db.user.update({ + where: { + id: dbUser.id, + }, + data: { + username: nanoid(10), + }, + }) + } - return { - id: dbUser.id, - name: dbUser.name, - email: dbUser.email, - picture: dbUser.image, - username: dbUser.username, - generacio: dbUser.generacio, - }; - }, - redirect() { - return "/"; - }, - }, -}; + return { + id: dbUser.id, + name: dbUser.name, + email: dbUser.email, + picture: dbUser.image, + username: dbUser.username, + generacio: dbUser.generacio, + } + }, + redirect() { + return "/" + }, + }, +} -export const getAuthSession = () => getServerSession(authOptions); +export const getAuthSession = () => getServerSession(authOptions) diff --git a/src/lib/db.ts b/src/lib/db.ts index 3dbeec3..2f2cae5 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from '@prisma/client' +import { PrismaClient } from "@prisma/client" import "server-only" declare global { @@ -7,7 +7,7 @@ declare global { } let prisma: PrismaClient -if (process.env.NODE_ENV === 'production') { +if (process.env.NODE_ENV === "production") { prisma = new PrismaClient() } else { if (!global.cachedPrisma) { @@ -16,4 +16,4 @@ if (process.env.NODE_ENV === 'production') { prisma = global.cachedPrisma } -export const db = prisma \ No newline at end of file +export const db = prisma diff --git a/src/lib/redis.ts b/src/lib/redis.ts index 52582d1..1f4c9f7 100644 --- a/src/lib/redis.ts +++ b/src/lib/redis.ts @@ -1,3 +1,6 @@ -import { Redis } from "@upstash/redis"; +import { Redis } from "@upstash/redis" -export const redis = new Redis({ url: process.env.REDIS_URL!, token: process.env.REDIS_SECRET! }); +export const redis = new Redis({ + url: process.env.REDIS_URL!, + token: process.env.REDIS_SECRET!, +}) diff --git a/src/lib/uploadthing.ts b/src/lib/uploadthing.ts index 2604b14..e2a1d98 100644 --- a/src/lib/uploadthing.ts +++ b/src/lib/uploadthing.ts @@ -1,5 +1,5 @@ -import { generateReactHelpers } from "@uploadthing/react/hooks"; +import { generateReactHelpers } from "@uploadthing/react/hooks" -import type { OurFileRouter } from "@/app//api/uploadthing/core"; +import type { OurFileRouter } from "@/app//api/uploadthing/core" -export const { uploadFiles } = generateReactHelpers<OurFileRouter>(); +export const { uploadFiles } = generateReactHelpers<OurFileRouter>() diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ff12c14..a199a3f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,57 +1,56 @@ -import { ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; -import { formatDistanceToNowStrict } from "date-fns"; -import locale from "date-fns/locale/en-US"; +import { ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" +import { formatDistanceToNowStrict } from "date-fns" +import locale from "date-fns/locale/en-US" export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)) } const formatDistanceLocale = { - lessThanXSeconds: "just now", - xSeconds: "just now", - halfAMinute: "just now", - lessThanXMinutes: "{{count}}m", - xMinutes: "{{count}}m", - aboutXHours: "{{count}}h", - xHours: "{{count}}h", - xDays: "{{count}}d", - aboutXWeeks: "{{count}}w", - xWeeks: "{{count}}w", - aboutXMonths: "{{count}}m", - xMonths: "{{count}}m", - aboutXYears: "{{count}}y", - xYears: "{{count}}y", - overXYears: "{{count}}y", - almostXYears: "{{count}}y", -}; + lessThanXSeconds: "just now", + xSeconds: "just now", + halfAMinute: "just now", + lessThanXMinutes: "{{count}}m", + xMinutes: "{{count}}m", + aboutXHours: "{{count}}h", + xHours: "{{count}}h", + xDays: "{{count}}d", + aboutXWeeks: "{{count}}w", + xWeeks: "{{count}}w", + aboutXMonths: "{{count}}m", + xMonths: "{{count}}m", + aboutXYears: "{{count}}y", + xYears: "{{count}}y", + overXYears: "{{count}}y", + almostXYears: "{{count}}y", +} function formatDistance(token: string, count: number, options?: any): string { - options = options || {}; + options = options || {} - const result = formatDistanceLocale[token as keyof typeof formatDistanceLocale].replace( - "{{count}}", - count.toString() - ); + const result = formatDistanceLocale[ + token as keyof typeof formatDistanceLocale + ].replace("{{count}}", count.toString()) - if (options.addSuffix) { - if (options.comparison > 0) { - return "in " + result; - } else { - if (result === "just now") return result; - return result + " ago"; - } - } + if (options.addSuffix) { + if (options.comparison > 0) { + return "in " + result + } else { + if (result === "just now") return result + return result + " ago" + } + } - return result; + return result } export function formatTimeToNow(date: Date): string { - return formatDistanceToNowStrict(date, { - addSuffix: true, - locale: { - ...locale, - formatDistance, - }, - }); + return formatDistanceToNowStrict(date, { + addSuffix: true, + locale: { + ...locale, + formatDistance, + }, + }) } diff --git a/src/lib/validators/post.ts b/src/lib/validators/post.ts index 34e0ae5..c995ee8 100644 --- a/src/lib/validators/post.ts +++ b/src/lib/validators/post.ts @@ -1,27 +1,28 @@ -import { z } from "zod"; +import { z } from "zod" export const PostValidator = z.object({ - title: z - .string() - .min(3, { message: "Title must be at least 3 characters long" }) - .max(128, { message: "Title must be at most 128 characters long" }), - subjectId: z.string(), - content: z.any(), - tipus: z.string(), - year: z.number(), -}); + title: z + .string() + .min(3, { message: "Title must be at least 3 characters long" }) + .max(128, { message: "Title must be at most 128 characters long" }), + subjectId: z.string(), + content: z.any(), + tipus: z.string(), + year: z.number(), +}) export const CommentValidator = z.object({ - content: z.string().min(1).max(2048), - postId: z.string(), -}); + content: z.string().min(1).max(2048), + postId: z.string(), +}) export const ApuntsPostValidator = z.object({ - pdf: z.any(), - title: z.string(), - assignatura: z.string().min(2).max(5), - tipus: z.string(), -}); + pdf: z.any(), + title: z.string(), + assignatura: z.string().min(2).max(5), + tipus: z.string(), + anonim: z.boolean(), +}) -export type PostCreationRequest = z.infer<typeof PostValidator>; -export type ApuntsPostCreationRequest = z.infer<typeof ApuntsPostValidator>; +export type PostCreationRequest = z.infer<typeof PostValidator> +export type ApuntsPostCreationRequest = z.infer<typeof ApuntsPostValidator> diff --git a/src/lib/validators/question.ts b/src/lib/validators/question.ts index 284694d..835df5b 100644 --- a/src/lib/validators/question.ts +++ b/src/lib/validators/question.ts @@ -1,20 +1,21 @@ -import { z } from "zod"; +import { z } from "zod" export const QuestionValidator = z.object({ - title: z - .string() - .min(3, { message: "Title must be at least 3 characters long" }) - .max(128, { message: "Title must be at most 128 characters long" }), - subjectId: z.string(), - content: z.any(), -}); + title: z + .string() + .min(3, { message: "Title must be at least 3 characters long" }) + .max(128, { message: "Title must be at most 128 characters long" }), + subjectId: z.string(), + content: z.any(), +}) export const AnswerValidator = z.object({ - title : z.string() - .min(3, { message: "Content must be at least 3 characters long" }) - .max(128, { message: "Content must be at most 2048 characters long" }), - subjectId: z.string(), - content: z.any(), - questionId: z.string(), -}); -export type QuestionCreationRequest = z.infer<typeof QuestionValidator>; -export type AnswerCreationRequest = z.infer<typeof AnswerValidator>; \ No newline at end of file + title: z + .string() + .min(3, { message: "Content must be at least 3 characters long" }) + .max(128, { message: "Content must be at most 2048 characters long" }), + subjectId: z.string(), + content: z.any(), + questionId: z.string(), +}) +export type QuestionCreationRequest = z.infer<typeof QuestionValidator> +export type AnswerCreationRequest = z.infer<typeof AnswerValidator> diff --git a/src/lib/validators/subject.ts b/src/lib/validators/subject.ts index a38e21a..7b81512 100644 --- a/src/lib/validators/subject.ts +++ b/src/lib/validators/subject.ts @@ -1,14 +1,16 @@ -import { z } from "zod"; +import { z } from "zod" export const SubjectValidator = z.object({ - name: z.string().min(3).max(128), - acronym: z.string().min(2).max(5), - semester: z.string().min(1).max(2), -}); + name: z.string().min(3).max(128), + acronym: z.string().min(2).max(5), + semester: z.string().min(1).max(2), +}) export const SubjectSubscriptionValidator = z.object({ - subjectId: z.string(), -}); + subjectId: z.string(), +}) -export type CreateSubjectPayload = z.infer<typeof SubjectValidator>; -export type SubscribeToSubjectPayload = z.infer<typeof SubjectSubscriptionValidator>; +export type CreateSubjectPayload = z.infer<typeof SubjectValidator> +export type SubscribeToSubjectPayload = z.infer< + typeof SubjectSubscriptionValidator +> diff --git a/src/lib/validators/vote.ts b/src/lib/validators/vote.ts index 330710f..0fc7adb 100644 --- a/src/lib/validators/vote.ts +++ b/src/lib/validators/vote.ts @@ -1,36 +1,36 @@ -import { z } from "zod"; +import { z } from "zod" export const PostVoteValidator = z.object({ - postId: z.string(), - voteType: z.enum(["UP", "DOWN"]), -}); + postId: z.string(), + voteType: z.enum(["UP", "DOWN"]), +}) -export type PostVoteRequest = z.infer<typeof PostVoteValidator>; +export type PostVoteRequest = z.infer<typeof PostVoteValidator> export const CommentVoteValidator = z.object({ - commentId: z.string(), - voteType: z.enum(["UP", "DOWN"]), -}); + commentId: z.string(), + voteType: z.enum(["UP", "DOWN"]), +}) -export type CommentVoteRequest = z.infer<typeof CommentVoteValidator>; +export type CommentVoteRequest = z.infer<typeof CommentVoteValidator> export const QuestionVoteValidator = z.object({ - questionId: z.string(), - voteType: z.enum(["UP", "DOWN"]), -}); + questionId: z.string(), + voteType: z.enum(["UP", "DOWN"]), +}) -export type QuestionVoteRequest = z.infer<typeof QuestionVoteValidator>; +export type QuestionVoteRequest = z.infer<typeof QuestionVoteValidator> export const AnswerVoteValidator = z.object({ - answerId: z.string(), - voteType: z.enum(["UP", "DOWN"]), -}); + answerId: z.string(), + voteType: z.enum(["UP", "DOWN"]), +}) -export type AnswerVoteRequest = z.infer<typeof AnswerVoteValidator>; +export type AnswerVoteRequest = z.infer<typeof AnswerVoteValidator> export const AnswerAcceptedValidator = z.object({ - answerId: z.string(), - accepted: z.boolean(), -}); + answerId: z.string(), + accepted: z.boolean(), +}) -export type AnswerAcceptedRequest = z.infer<typeof AnswerAcceptedValidator>; +export type AnswerAcceptedRequest = z.infer<typeof AnswerAcceptedValidator> diff --git a/src/middleware.ts b/src/middleware.ts index ba6ebeb..dc1aeff 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,21 +1,17 @@ import { withAuth } from "next-auth/middleware" -export default withAuth( - function middleware (req) { - }, - { - callbacks: { - authorized: ({ req, token }) => { - if ( - req.nextUrl.pathname && - req.nextUrl.pathname.startsWith('/') && - !req.nextUrl.pathname.startsWith('/sign-in') && - token === null - ) { - return false - } - return true +export default withAuth(function middleware(req) {}, { + callbacks: { + authorized: ({ req, token }) => { + if ( + req.nextUrl.pathname && + req.nextUrl.pathname.startsWith("/") && + !req.nextUrl.pathname.startsWith("/sign-in") && + token === null + ) { + return false } - } - } -) \ No newline at end of file + return true + }, + }, +}) diff --git a/src/styles/editor.css b/src/styles/editor.css index cf6c4b0..d7e0bd3 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -8,9 +8,9 @@ .dark .cdx-button, .dark .ce-popover, .dark .ce-toolbar__plus:hover { - background: theme('colors.popover.DEFAULT'); + background: theme("colors.popover.DEFAULT"); color: inherit; - border-color: theme('colors.border'); + border-color: theme("colors.border"); } .dark .ce-inline-tool, @@ -23,18 +23,18 @@ .dark .ce-popover__item-icon, .dark .ce-conversion-tool__icon { - background-color: theme('colors.muted.DEFAULT'); + background-color: theme("colors.muted.DEFAULT"); box-shadow: none; } .dark .cdx-search-field { - border-color: theme('colors.border'); - background: theme('colors.input'); + border-color: theme("colors.border"); + background: theme("colors.input"); color: inherit; } .dark ::selection { - background: theme('colors.accent.DEFAULT'); + background: theme("colors.accent.DEFAULT"); } .dark .cdx-settings-button:hover, @@ -47,12 +47,12 @@ .dark .ce-popover__item:hover, .dark .ce-conversion-tool:hover, .dark .ce-toolbar__settings-btn:hover { - background-color: theme('colors.accent.DEFAULT'); - color: theme('colors.accent.foreground'); + background-color: theme("colors.accent.DEFAULT"); + color: theme("colors.accent.foreground"); } .dark .cdx-notify--error { - background: theme('colors.destructive.DEFAULT') !important; + background: theme("colors.destructive.DEFAULT") !important; } .dark .cdx-notify__cross::after, diff --git a/src/styles/globals.css b/src/styles/globals.css index 0b46ea1..0f0ff2a 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,7 +1,7 @@ @tailwind base; @tailwind components; @tailwind utilities; - + @layer base { :root { --background: 0 0% 100%; @@ -9,63 +9,63 @@ --card: 0 0% 100%; --card-foreground: 240 10% 3.9%; - + --popover: 0 0% 100%; --popover-foreground: 240 10% 3.9%; - + --primary: 240 5.9% 10%; --primary-foreground: 0 0% 98%; - + --secondary: 240 4.8% 95.9%; --secondary-foreground: 240 5.9% 10%; - + --muted: 240 4.8% 95.9%; --muted-foreground: 240 3.8% 46.1%; - + --accent: 240 4.8% 95.9%; --accent-foreground: 240 5.9% 10%; - + --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; --border: 240 5.9% 90%; --input: 240 5.9% 90%; --ring: 240 10% 3.9%; - + --radius: 0.5rem; } - + .dark { --background: 240 10% 3.9%; --foreground: 0 0% 98%; - + --card: 240 10% 3.9%; --card-foreground: 0 0% 98%; - + --popover: 240 10% 3.9%; --popover-foreground: 0 0% 98%; - + --primary: 0 0% 98%; --primary-foreground: 240 5.9% 10%; - + --secondary: 240 3.7% 15.9%; --secondary-foreground: 0 0% 98%; - + --muted: 240 3.7% 15.9%; --muted-foreground: 240 5% 64.9%; - + --accent: 240 3.7% 15.9%; --accent-foreground: 0 0% 98%; - + --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; - + --border: 240 3.7% 15.9%; --input: 240 3.7% 15.9%; --ring: 240 4.9% 83.9%; } } - + @layer base { * { @apply border-border; @@ -73,4 +73,4 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} diff --git a/src/types/db.d.ts b/src/types/db.d.ts index a4c8467..85fa0d9 100644 --- a/src/types/db.d.ts +++ b/src/types/db.d.ts @@ -1,26 +1,35 @@ -import { Post, Subject, PostVote, QuestionVote, User, Comment, Question, Answer } from "@prisma/client"; +import { + Post, + Subject, + PostVote, + QuestionVote, + User, + Comment, + Question, + Answer, +} from "@prisma/client" export type ExtendedPost = Post & { - subject: Subject; - votes: PostVote[]; - author: User; - comments: Comment[]; -}; + subject: Subject + votes: PostVote[] + author: User + comments: Comment[] +} export type ExtendedQuestion = Question & { - subject: Subject; - votes: QuestionVote[]; - author: User; - answers: Answer[]; -}; + subject: Subject + votes: QuestionVote[] + author: User + answers: Answer[] +} export type ExtendedAnswer = Answer & { - question: Question; - votes: Vote[]; - author: User; -}; + question: Question + votes: Vote[] + author: User +} export type ExtendedComment = Comment & { - votes: Vote[]; - author: User; -}; \ No newline at end of file + votes: Vote[] + author: User +} diff --git a/src/types/editor.d.ts b/src/types/editor.d.ts index 45abd7d..f9a7293 100644 --- a/src/types/editor.d.ts +++ b/src/types/editor.d.ts @@ -4,4 +4,4 @@ declare module "@editorjs/list" declare module "@editorjs/code" declare module "@editorjs/link" declare module "@editorjs/inline-code" -declare module "@editorjs/image" \ No newline at end of file +declare module "@editorjs/image" diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index 8396547..7a9bdf8 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -1,21 +1,21 @@ -import type { Session, User } from "next-auth"; -import type { JWT } from "next-auth/jwt"; +import type { Session, User } from "next-auth" +import type { JWT } from "next-auth/jwt" -type UserId = string; +type UserId = string declare module "next-auth/jwt" { - interface JWT { - id: UserId; - username?: string | null; - } + interface JWT { + id: UserId + username?: string | null + } } declare module "next-auth" { - interface Session { - user: User & { - id: UserId; - username?: string | null; - generacio?: string | null; - }; - } + interface Session { + user: User & { + id: UserId + username?: string | null + generacio?: string | null + } + } } diff --git a/src/types/redis.d.ts b/src/types/redis.d.ts index eab1dea..0afe0f4 100644 --- a/src/types/redis.d.ts +++ b/src/types/redis.d.ts @@ -1,36 +1,36 @@ -import { PostVote, QuestionVote, AnswerVote, CommentVote } from "@prisma/client"; +import { PostVote, QuestionVote, AnswerVote, CommentVote } from "@prisma/client" export type CachedPost = { - id: string; - title: string; - authorName: string; - content: string; - currentVote: PostVote["type"] | null; - createdAt: Date; -}; + id: string + title: string + authorName: string + content: string + currentVote: PostVote["type"] | null + createdAt: Date +} export type CachedComment = { - id: string; - authorName: string; - content: string; - currentVote: CommentVote["type"] | null; - createdAt: Date; -}; + id: string + authorName: string + content: string + currentVote: CommentVote["type"] | null + createdAt: Date +} export type CachedQuestion = { - id: string; - title: string; - authorName: string; - content: string; - currentVote: QuestionVote["type"] | null; - createdAt: Date; -}; + id: string + title: string + authorName: string + content: string + currentVote: QuestionVote["type"] | null + createdAt: Date +} export type CachedAnswer = { - id: string; - title: string; - authorName: string; - content: string; - currentVote: AnswerVote["type"] | null; - createdAt: Date; -}; + id: string + title: string + authorName: string + content: string + currentVote: AnswerVote["type"] | null + createdAt: Date +} diff --git a/tailwind.config.js b/tailwind.config.js index 9e800bd..20df21d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,81 +1,81 @@ -const { fontFamily } = require('tailwindcss/defaultTheme') +const { fontFamily } = require("tailwindcss/defaultTheme") /** @type {import('tailwindcss').Config} */ module.exports = { - darkMode: ['class'], - content: ['./src/app/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'], + darkMode: ["class"], + content: ["./src/app/**/*.{ts,tsx}", "./src/components/**/*.{ts,tsx}"], theme: { container: { center: true, - padding: '2rem', + padding: "2rem", screens: { - '2xl': '1400px', + "2xl": "1400px", }, }, extend: { backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': - 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", }, colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", }, secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", }, destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", }, muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", }, accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", }, popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", }, card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: `var(--radius)`, md: `calc(var(--radius) - 2px)`, - sm: 'calc(var(--radius) - 4px)', + sm: "calc(var(--radius) - 4px)", }, fontFamily: { - sans: ['var(--font-sans)', ...fontFamily.sans], + sans: ["var(--font-sans)", ...fontFamily.sans], }, keyframes: { - 'accordion-down': { + "accordion-down": { from: { height: 0 }, - to: { height: 'var(--radix-accordion-content-height)' }, + to: { height: "var(--radix-accordion-content-height)" }, }, - 'accordion-up': { - from: { height: 'var(--radix-accordion-content-height)' }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, to: { height: 0 }, }, }, animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out', + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", }, }, }, - plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')], + plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], } diff --git a/tailwind.config.ts b/tailwind.config.ts index 84287e8..1ce0389 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -3,11 +3,11 @@ import type { Config } from "tailwindcss" const config = { darkMode: ["class"], content: [ - './pages/**/*.{ts,tsx}', - './components/**/*.{ts,tsx}', - './app/**/*.{ts,tsx}', - './src/**/*.{ts,tsx}', - ], + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], prefix: "", theme: { container: { @@ -77,4 +77,4 @@ const config = { plugins: [require("tailwindcss-animate")], } satisfies Config -export default config \ No newline at end of file +export default config diff --git a/tsconfig.json b/tsconfig.json index d53a6ed..e128fc0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,12 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "prisma/accountVerify.js"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "prisma/accountVerify.js" + ], "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index 12743c1..ef89138 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1274,6 +1274,13 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-escapes@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.0.tgz#8a13ce75286f417f1963487d86ba9f90dccf9947" + integrity sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw== + dependencies: + type-fest "^3.0.0" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" @@ -1291,7 +1298,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.1.0: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -1658,6 +1665,11 @@ canvas@^2.11.2: nan "^2.17.0" simple-get "^3.0.3" +chalk@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + chalk@^4.0.0: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" @@ -1698,6 +1710,21 @@ class-variance-authority@^0.6.0: dependencies: clsx "1.2.1" +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + dependencies: + slice-ansi "^5.0.0" + string-width "^7.0.0" + client-only@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" @@ -1758,6 +1785,11 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1765,6 +1797,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== + commander@^4.0.0: version "4.1.1" resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" @@ -1833,7 +1870,7 @@ date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" -debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2388,11 +2425,31 @@ esutils@^2.0.2: resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +execa@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + execa@^5.0.0: version "5.1.1" resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" @@ -2614,6 +2671,11 @@ gauge@^3.0.0: strip-ansi "^6.0.1" wide-align "^1.1.2" +get-east-asian-width@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" + integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" @@ -2635,6 +2697,11 @@ get-stream@^6.0.0, get-stream@^6.0.1: resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz" @@ -2863,6 +2930,16 @@ human-signals@^4.3.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + +husky@^9.0.11: + version "9.0.11" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.11.tgz#fc91df4c756050de41b3e478b2158b87c1e79af9" + integrity sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw== + hyphen@^1.6.4: version "1.10.4" resolved "https://registry.npmjs.org/hyphen/-/hyphen-1.10.4.tgz" @@ -3018,6 +3095,18 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-fullwidth-code-point@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" + integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + dependencies: + get-east-asian-width "^1.0.0" + is-generator-function@^1.0.10: version "1.0.10" resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz" @@ -3262,6 +3351,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lilconfig@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" + integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== + lilconfig@^2.0.5, lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" @@ -3272,6 +3366,34 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +lint-staged@^15.2.2: + version "15.2.2" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.2.tgz#ad7cbb5b3ab70e043fa05bff82a09ed286bc4c5f" + integrity sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw== + dependencies: + chalk "5.3.0" + commander "11.1.0" + debug "4.3.4" + execa "8.0.1" + lilconfig "3.0.0" + listr2 "8.0.1" + micromatch "4.0.5" + pidtree "0.6.0" + string-argv "0.3.2" + yaml "2.3.4" + +listr2@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.0.1.tgz#4d3f50ae6cec3c62bdf0e94f5c2c9edebd4b9c34" + integrity sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA== + dependencies: + cli-truncate "^4.0.0" + colorette "^2.0.20" + eventemitter3 "^5.0.1" + log-update "^6.0.0" + rfdc "^1.3.0" + wrap-ansi "^9.0.0" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" @@ -3299,6 +3421,17 @@ lodash.merge@^4.6.2: resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +log-update@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" + integrity sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw== + dependencies: + ansi-escapes "^6.2.0" + cli-cursor "^4.0.0" + slice-ansi "^7.0.0" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" @@ -3365,7 +3498,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.4, micromatch@^4.0.5: +micromatch@4.0.5, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -3711,7 +3844,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -3859,6 +3992,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pidtree@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + pify@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" @@ -3974,6 +4112,11 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + pretty-format@^3.8.0: version "3.8.0" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz" @@ -4240,6 +4383,14 @@ resolve@^2.0.0-next.4: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + restructure@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/restructure/-/restructure-3.0.0.tgz" @@ -4250,6 +4401,11 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" + integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" @@ -4388,12 +4544,12 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.0, signal-exit@^3.0.3, signal-exit@^3.0.7: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -4438,6 +4594,22 @@ slash@^4.0.0: resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +slice-ansi@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" + integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^5.0.0" + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" @@ -4469,6 +4641,11 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" +string-argv@0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" + integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== + "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.3: name string-width-cjs version "4.2.3" @@ -4488,6 +4665,15 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a" + integrity sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + string.prototype.matchall@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz" @@ -4543,7 +4729,7 @@ string_decoder@^1.1.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -4831,6 +5017,11 @@ type-fest@^0.20.2: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^3.0.0: + version "3.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" + integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== + typed-array-buffer@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz" @@ -5087,6 +5278,15 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" @@ -5097,6 +5297,11 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + yaml@^2.1.1: version "2.3.1" resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz"