diff --git a/package-lock.json b/package-lock.json index e46a1ad..c466dcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "atmosphere-showcase", "version": "0.1.0", "dependencies": { + "@prisma/client": "^5.17.0", "axios": "^1.7.2", "classnames": "^2.5.1", "framer-motion": "^11.3.8", @@ -24,6 +25,7 @@ "eslint": "^8", "eslint-config-next": "14.2.5", "postcss": "^8", + "prisma": "^5.17.0", "tailwindcss": "^3.4.1", "typescript": "^5" } @@ -427,6 +429,68 @@ "node": ">=14" } }, + "node_modules/@prisma/client": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.17.0.tgz", + "integrity": "sha512-N2tnyKayT0Zf7mHjwEyE8iG7FwTmXDHFZ1GnNhQp0pJUObsuel4ZZ1XwfuAYkq5mRIiC/Kot0kt0tGCfLJ70Jw==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.17.0.tgz", + "integrity": "sha512-l7+AteR3P8FXiYyo496zkuoiJ5r9jLQEdUuxIxNCN1ud8rdbH3GTxm+f+dCyaSv9l9WY+29L9czaVRXz9mULfg==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.17.0.tgz", + "integrity": "sha512-+r+Nf+JP210Jur+/X8SIPLtz+uW9YA4QO5IXA+KcSOBe/shT47bCcRMTYCbOESw3FFYFTwe7vU6KTWHKPiwvtg==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.17.0", + "@prisma/engines-version": "5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053", + "@prisma/fetch-engine": "5.17.0", + "@prisma/get-platform": "5.17.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053.tgz", + "integrity": "sha512-tUuxZZysZDcrk5oaNOdrBnnkoTtmNQPkzINFDjz7eG6vcs9AVDmA/F6K5Plsb2aQc/l5M2EnFqn3htng9FA4hg==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.17.0.tgz", + "integrity": "sha512-ESxiOaHuC488ilLPnrv/tM2KrPhQB5TRris/IeIV4ZvUuKeaicCl4Xj/JCQeG9IlxqOgf1cCg5h5vAzlewN91Q==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.17.0", + "@prisma/engines-version": "5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053", + "@prisma/get-platform": "5.17.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.17.0.tgz", + "integrity": "sha512-UlDgbRozCP1rfJ5Tlkf3Cnftb6srGrEQ4Nm3og+1Se2gWmCZ0hmPIi+tQikGDUVLlvOWx3Gyi9LzgRP+HTXV9w==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.17.0" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", @@ -3929,6 +3993,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.17.0.tgz", + "integrity": "sha512-m4UWkN5lBE6yevqeOxEvmepnL5cNPEjzMw2IqDB59AcEV6w7D8vGljDLd1gPFH+W6gUxw9x7/RmN5dCS/WTPxA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.17.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 621fb6e..3f7b71c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@prisma/client": "^5.17.0", "axios": "^1.7.2", "classnames": "^2.5.1", "framer-motion": "^11.3.8", @@ -25,6 +26,7 @@ "eslint": "^8", "eslint-config-next": "14.2.5", "postcss": "^8", + "prisma": "^5.17.0", "tailwindcss": "^3.4.1", "typescript": "^5" } diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..702b583 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,48 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Book { + id Int @id @default(autoincrement()) + slug String @unique + title String + author String + description String + cover String + datePublished DateTime + length Int + genre String + accentColor String + chapters Chapter[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Chapter { + id Int @id @default(autoincrement()) + number Int + audioUrl String + title String + content String + bookId Int + book Book @relation(fields: [bookId], references: [id]) + ambientSections AmbientSection[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model AmbientSection { + id Int @id @default(autoincrement()) + start Int + end Int + description String + chapterId Int + chapter Chapter @relation(fields: [chapterId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..bfda54b --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1 @@ +// seed script diff --git a/src/components/books/book-card.tsx b/src/components/books/book-card.tsx index fec681e..355f74e 100644 --- a/src/components/books/book-card.tsx +++ b/src/components/books/book-card.tsx @@ -9,7 +9,9 @@ export const BookCard: React.FC = ({ slug, title, cover, author, date, cha {title}

{title}

- {genre} +
+ {genre} +
{author} • {date} diff --git a/src/lib/server/index.ts b/src/lib/server/index.ts new file mode 100644 index 0000000..8e4ed50 --- /dev/null +++ b/src/lib/server/index.ts @@ -0,0 +1 @@ +export * from "./prisma"; diff --git a/src/lib/server/prisma.ts b/src/lib/server/prisma.ts new file mode 100644 index 0000000..83a6a1a --- /dev/null +++ b/src/lib/server/prisma.ts @@ -0,0 +1,13 @@ +import { PrismaClient } from "@prisma/client"; + +const prismaClientSingleton = () => { + return new PrismaClient(); +}; + +declare const globalThis: { + prismaGlobal: ReturnType; +} & typeof global; + +export const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); + +if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma; diff --git a/src/pages/api/books/[slug].endpoint.ts b/src/pages/api/books/[slug].endpoint.ts deleted file mode 100644 index 59848e5..0000000 --- a/src/pages/api/books/[slug].endpoint.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import type { ServerError } from "~/types"; -import { Book } from "~/types"; -import { books } from "./index.endpoint"; - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - const book = books.find((book) => book.slug === req.query.slug); - if (!book) { - return res.status(404).json({ message: "Book not found" }); - } - - res.status(200).json(book); -} diff --git a/src/pages/api/books/[slug]/[chapter].endpoint.ts b/src/pages/api/books/[slug]/[chapter].endpoint.ts new file mode 100644 index 0000000..b268f3a --- /dev/null +++ b/src/pages/api/books/[slug]/[chapter].endpoint.ts @@ -0,0 +1,28 @@ +import type { AmbientSection, Book, Chapter } from "@prisma/client"; +import type { Handler } from "~/types"; +import { prisma } from "~/lib/server"; + +export type GetBookChapterParams = { query: { slug: string; chapter: string } }; +export type GetBookChapterPayload = Chapter & { + ambientSections: AmbientSection[]; + book: Pick; +}; + +const handler: Handler = async (req, res) => { + try { + const chapter = await prisma.chapter.findFirst({ + where: { number: parseInt(req.query.chapter), book: { slug: req.query.slug } }, + include: { ambientSections: true, book: { select: { title: true, author: true, accentColor: true } } }, + }); + + if (!chapter) { + return res.status(404).json({ status: "error", message: "Chapter not found" }); + } + + return res.status(200).json({ status: "success", ...chapter }); + } catch (e) { + return res.status(500).json({ status: "error", message: "Internal Server Error" }); + } +}; + +export default handler; diff --git a/src/pages/api/books/[slug]/index.endpoint.ts b/src/pages/api/books/[slug]/index.endpoint.ts new file mode 100644 index 0000000..b67a66e --- /dev/null +++ b/src/pages/api/books/[slug]/index.endpoint.ts @@ -0,0 +1,25 @@ +import type { Book } from "@prisma/client"; +import type { Handler } from "~/types"; +import { prisma } from "~/lib/server"; + +export type GetBookParams = { query: { slug: string } }; +export type GetBookPayload = Book; + +const handler: Handler = async (req, res) => { + try { + const book = await prisma.book.findUnique({ + where: { slug: req.query.slug }, + include: { chapters: { include: { ambientSections: true } } }, + }); + + if (!book) { + return res.status(404).json({ status: "error", message: "Book not found" }); + } + + return res.status(200).json({ status: "success", ...book }); + } catch (error) { + return res.status(500).json({ status: "error", message: "Internal server error" }); + } +}; + +export default handler; diff --git a/src/pages/api/books/[slug]/preview.endpoint.ts b/src/pages/api/books/[slug]/preview.endpoint.ts new file mode 100644 index 0000000..c86eb43 --- /dev/null +++ b/src/pages/api/books/[slug]/preview.endpoint.ts @@ -0,0 +1,22 @@ +import type { Book } from "@prisma/client"; +import type { Handler } from "~/types"; +import { prisma } from "~/lib/server"; + +export type GetBookPreviewParams = { query: { slug: string } }; +export type GetBookPreviewPayload = Book; + +const handler: Handler = async (req, res) => { + try { + const book = await prisma.book.findUnique({ where: { slug: req.query.slug } }); + + if (!book) { + return res.status(404).json({ status: "error", message: "Book not found" }); + } + + return res.status(200).json({ status: "success", ...book }); + } catch (e) { + return res.status(500).json({ status: "error", message: "Internal Server Error" }); + } +}; + +export default handler; diff --git a/src/pages/api/books/featured.endpoint.ts b/src/pages/api/books/featured.endpoint.ts new file mode 100644 index 0000000..708aaaa --- /dev/null +++ b/src/pages/api/books/featured.endpoint.ts @@ -0,0 +1,17 @@ +import type { Book } from "@prisma/client"; +import type { Handler } from "~/types"; +import { prisma } from "~/lib/server"; + +export type GetFeaturedBookParams = {}; +export type GetFeaturedBookPayload = Pick; + +const handler: Handler = async (req, res) => { + try { + const latestBook = (await prisma.book.findMany({ orderBy: { id: "desc" }, take: 1, select: { slug: true } }))[0]; + return res.status(200).json({ status: "success", slug: latestBook.slug }); + } catch (e) { + return res.status(500).json({ status: "error", message: "Internal Server Error" }); + } +}; + +export default handler; diff --git a/src/pages/api/books/index.endpoint.ts b/src/pages/api/books/index.endpoint.ts index b9a4df3..db211b9 100644 --- a/src/pages/api/books/index.endpoint.ts +++ b/src/pages/api/books/index.endpoint.ts @@ -1,8 +1,9 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { Books } from "~/types"; +import type { Book } from "@prisma/client"; +import type { Handler } from "~/types"; +import { prisma } from "~/lib/server"; // replace with db -export const books: Books = [ +export const books = [ { slug: "the-great-gatsby", title: "The Great Gatsby", @@ -172,6 +173,16 @@ export const books: Books = [ }, ]; -export default function handler(req: NextApiRequest, res: NextApiResponse) { - res.status(200).json(books); -} +export type GetBooksParams = {}; +export type GetBooksPayload = { books: Book[] }; + +const handler: Handler = async (req, res) => { + try { + const books = await prisma.book.findMany(); + return res.status(200).json({ status: "success", books }); + } catch (e) { + return res.status(500).json({ status: "error", message: "Internal Server Error" }); + } +}; + +export default handler; diff --git a/src/pages/api/middleware.ts b/src/pages/api/middleware.ts new file mode 100644 index 0000000..edd57e1 --- /dev/null +++ b/src/pages/api/middleware.ts @@ -0,0 +1,15 @@ +import { NextRequest } from "next/server"; + +export const config = { + matcher: "/api/:function*", +}; + +export const isAuthenticated = (req: NextRequest) => { + return false; +}; + +export function middleware(request: NextRequest) { + if (!isAuthenticated(request)) { + return Response.json({ success: false, message: "authentication failed" }, { status: 401 }); + } +} diff --git a/src/types.ts b/src/types.ts index 3eec964..d86563d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,18 @@ -import type { NextPage } from "next"; +import type { NextPage, NextApiRequest, NextApiResponse } from "next"; import type { AppProps } from "next/app"; import type { LayoutOptions, SEOOptions } from "~/partials/layout"; +/* --------------------------------- SERVER --------------------------------- */ + +export type Handler = ( + req: NextApiRequest & R, + res: NextApiResponse> +) => Promise; + export type ServerError = { message: string }; +/* --------------------------------- CLIENT --------------------------------- */ + export type ExtendedAppProps = AppProps & { Component: NextPage>; pageProps: PropsWithConfig; @@ -29,30 +38,36 @@ export type QuerySuccessResponse = { export type QueryResponse = QueryErrorResponse | QuerySuccessResponse; -export type AmbientSection = { - start: number; - end: number; - description: string; -}; +/* --------------------------------- SHARED --------------------------------- */ -export type Chapter = { - title: string; - audio: string; - paragraphs: string[]; - ambientSections: AmbientSection[]; -}; +export type ServerResponse = ({ status: "success" } & T) | ({ status: "error" } & ServerError); -export type Book = { - slug: string; - title: string; - description: string; - cover: string; - author: string; - date: string; - length: number; // in minutes - genre: string; - accentColor: string; - chapters: Chapter[]; -}; +// todo: remove these unused types + +// export type AmbientSection = { +// start: number; +// end: number; +// description: string; +// }; + +// export type Chapter = { +// title: string; +// audio: string; +// paragraphs: string[]; +// ambientSections: AmbientSection[]; +// }; + +// export type Book = { +// slug: string; +// title: string; +// description: string; +// cover: string; +// author: string; +// date: string; +// length: number; // in minutes +// genre: string; +// accentColor: string; +// chapters: Chapter[]; +// }; -export type Books = Book[]; +// export type Books = Book[];