diff --git a/prisma/migrations/20230221011519_add_links/migration.sql b/prisma/migrations/20230221011519_add_links/migration.sql new file mode 100644 index 0000000..860e2cd --- /dev/null +++ b/prisma/migrations/20230221011519_add_links/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "Link" ( + "id" SERIAL NOT NULL, + "remoteAddress" TEXT NOT NULL, + "location" TEXT NOT NULL, + "authorId" INTEGER NOT NULL, + + CONSTRAINT "Link_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Link" ADD CONSTRAINT "Link_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ded4add..0312f23 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,6 +17,7 @@ model User { password String name String posts Post[] + links Link[] } model Post { @@ -30,3 +31,12 @@ model Post { author User @relation(fields: [authorId], references: [id]) authorId Int } + +model Link { + id Int @id @default(autoincrement()) + remoteAddress String + location String + + author User @relation(fields: [authorId], references: [id]) + authorId Int +} diff --git a/src/components/EmptyPage.tsx b/src/components/EmptyPage.tsx new file mode 100644 index 0000000..729d4aa --- /dev/null +++ b/src/components/EmptyPage.tsx @@ -0,0 +1,13 @@ +import styles from "./emptyPage.module.scss"; + +import { FC } from "react"; + +const EmptyPage: FC = ({ children }) => { + return ( +
+
{children}
+
+ ); +}; + +export default EmptyPage; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index bc9f26b..3d3e92a 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -12,9 +12,11 @@ const Footer: FC = () => { diff --git a/src/components/LinkComponent.tsx b/src/components/LinkComponent.tsx new file mode 100644 index 0000000..8cf7f29 --- /dev/null +++ b/src/components/LinkComponent.tsx @@ -0,0 +1,67 @@ +import Link from "next/link"; +import z from "zod"; + +import styles from "./linkComponent.module.scss"; + +import { FC, useCallback, useState } from "react"; + +import axios from "axios"; + +import { Link as LinkType } from "@prisma/client"; + +import { parseAxiosError, parseAxiosResponse } from "@utils/fetch"; +import useUser from "@utils/hooks/useUser"; +import multipleClassNames from "@utils/multipleClassNames"; + +const LinkComponent: FC<{ link: LinkType }> = ({ link }) => { + const user = useUser(); + const [error, setError] = useState(null); + const [deleted, setDeleted] = useState(false); + + const deleteLink = useCallback(async (id) => { + const parseUserInputResult = z.number().safeParse(id); + + if (!parseUserInputResult.success) { + setError(parseUserInputResult.error.message); + return; + } + + const response = await axios + .post("/api/link/delete", { id }) + .then(parseAxiosResponse) + .catch(parseAxiosError); + + if (!response.ok) { + setError(JSON.stringify(response.error)); + return; + } + + setDeleted(true); + }, []); + + if (deleted) return <>; + + return ( +
+
+
+
+ + {link.location} + +
+ {user !== null && ( +
+ + edit + + deleteLink(link.id)}>delete +
+ )} +
+
+
+ ); +}; + +export default LinkComponent; diff --git a/src/components/emptyPage.module.scss b/src/components/emptyPage.module.scss new file mode 100644 index 0000000..f685f50 --- /dev/null +++ b/src/components/emptyPage.module.scss @@ -0,0 +1,3 @@ +.body { + padding: 10rem 0; +} \ No newline at end of file diff --git a/src/components/linkComponent.module.scss b/src/components/linkComponent.module.scss new file mode 100644 index 0000000..bd8a65c --- /dev/null +++ b/src/components/linkComponent.module.scss @@ -0,0 +1,4 @@ +.body { + padding: 1rem; + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/src/models/link.ts b/src/models/link.ts new file mode 100644 index 0000000..36e9c33 --- /dev/null +++ b/src/models/link.ts @@ -0,0 +1,8 @@ +import z from "zod"; + +const Link = z.object({ + remoteAddress: z.string().url(), + location: z.string() +}); + +export default Link; diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..a049ecd --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +import { SignupCredentials } from "./signup"; + +export const User = SignupCredentials.extend({ + id: z.number(), + admin: z.boolean() +}); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 5860131..798941c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -7,7 +7,9 @@ import SEO from "../next-seo.config"; import "@styles/globals.scss"; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { queries: { refetchOnWindowFocus: false } } +}); const App = ({ Component, pageProps }: AppProps): JSX.Element => ( <> diff --git a/src/pages/api/link/new.ts b/src/pages/api/link/new.ts new file mode 100644 index 0000000..172fa64 --- /dev/null +++ b/src/pages/api/link/new.ts @@ -0,0 +1,43 @@ +import { NextApiHandler } from "next"; + +import Link from "@models/link"; +import { Response } from "@models/response"; + +import { methodNotAllowed, unauthorized } from "@utils/errors"; +import prisma from "@utils/prisma"; +import { withIronSession } from "@utils/session"; + +const handler: NextApiHandler = async (req, res) => { + if (req.method?.toUpperCase() !== "POST") { + res.status(405).json(methodNotAllowed); + return; + } + + const user = req.session.user; + + if (user === undefined) { + res.status(401).json(unauthorized); + return; + } + + const parsedInputResult = Link.safeParse(req.body); + + if (!parsedInputResult.success) { + res.status(400).json({ ok: false, error: parsedInputResult.error.message }); + return; + } + + await prisma.user + .update({ + where: { id: user.id }, + data: { links: { create: parsedInputResult.data } } + }) + .then(() => res.json({ ok: true, data: "Successfully created link" })) + .catch((error) => { + console.log(error); + + return res.status(500).json({ ok: false, error }); + }); +}; + +export default withIronSession(handler); diff --git a/src/pages/api/signup.ts b/src/pages/api/signup.ts index fe97c32..89904d8 100644 --- a/src/pages/api/signup.ts +++ b/src/pages/api/signup.ts @@ -40,7 +40,7 @@ const handle: NextApiHandler = async (req, res) => { ) ); - prisma.user + await prisma.user .create({ data: { email: signupCredentials.data.email, diff --git a/src/pages/api/user.ts b/src/pages/api/user.ts new file mode 100644 index 0000000..2d14e8e --- /dev/null +++ b/src/pages/api/user.ts @@ -0,0 +1,24 @@ +import { NextApiHandler } from "next"; + +import { Response } from "@models/response"; + +import { methodNotAllowed, unauthorized } from "@utils/errors"; +import { withIronSession } from "@utils/session"; + +const handler: NextApiHandler = (req, res) => { + if (req.method?.toUpperCase() !== "GET") { + res.status(405).json(methodNotAllowed); + return; + } + + const user = req.session.user; + + if (user === undefined) { + res.status(401).json(unauthorized); + return; + } + + res.json({ ok: true, data: user }); +}; + +export default withIronSession(handler); diff --git a/src/pages/blog/new.module.scss b/src/pages/blog/new.module.scss deleted file mode 100644 index e0083a4..0000000 --- a/src/pages/blog/new.module.scss +++ /dev/null @@ -1,4 +0,0 @@ -.body { - height: 100vh; - padding: 10rem 0; -} diff --git a/src/pages/blog/new.tsx b/src/pages/blog/new.tsx index aa0a952..1e962f7 100644 --- a/src/pages/blog/new.tsx +++ b/src/pages/blog/new.tsx @@ -2,8 +2,6 @@ import { NextPage } from "next"; import { NextSeo } from "next-seo"; import { useRouter } from "next/router"; -import styles from "./new.module.scss"; - import { FormEvent, useCallback, useState } from "react"; import axios from "axios"; @@ -17,6 +15,7 @@ import { parseUserInputError } from "@utils/errors"; import { parseAxiosError, parseAxiosResponse } from "@utils/fetch"; import { withSessionSsr } from "@utils/session"; +import EmptyPage from "@components/EmptyPage"; import Layout from "@components/Layout"; const NewPostPage: NextPage<{ user: User }> = ({ user }) => { @@ -62,90 +61,88 @@ const NewPostPage: NextPage<{ user: User }> = ({ user }) => { return ( -
-
-
-
-

Create new post

-
Logged in as {user.name}
-
-
- + +
+
+

Create new post

+
Logged in as {user.name}
+ +
+ + setTitle(e.target.value)} + required + className="form-input" + name="title" + type="text" + id="title" + placeholder="Title" + /> + + + setTags(e.target.value)} + required + className="form-input" + name="tags" + type="text" + id="tags" + placeholder="A space seperated list of tags" + /> + + +