Started on links page
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
fe425d65cc
commit
ae4a38d1b9
@ -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;
|
@ -0,0 +1,13 @@
|
||||
import styles from "./emptyPage.module.scss";
|
||||
|
||||
import { FC } from "react";
|
||||
|
||||
const EmptyPage: FC = ({ children }) => {
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<div className="container">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyPage;
|
@ -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<string | null>(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 (
|
||||
<div className={multipleClassNames("bg-gray", "s-rounded", styles.body)}>
|
||||
<div className="container">
|
||||
<div className="columns">
|
||||
<div className="col col-11">
|
||||
<Link href={link.remoteAddress}>
|
||||
<a>{link.location}</a>
|
||||
</Link>
|
||||
</div>
|
||||
{user !== null && (
|
||||
<div className="col col-1">
|
||||
<Link href="/link/edit">
|
||||
<a className="mr-2">edit</a>
|
||||
</Link>
|
||||
<a onClick={() => deleteLink(link.id)}>delete</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkComponent;
|
@ -0,0 +1,3 @@
|
||||
.body {
|
||||
padding: 10rem 0;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
.body {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import z from "zod";
|
||||
|
||||
const Link = z.object({
|
||||
remoteAddress: z.string().url(),
|
||||
location: z.string()
|
||||
});
|
||||
|
||||
export default Link;
|
@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SignupCredentials } from "./signup";
|
||||
|
||||
export const User = SignupCredentials.extend({
|
||||
id: z.number(),
|
||||
admin: z.boolean()
|
||||
});
|
@ -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<Response> = 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);
|
@ -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<Response> = (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);
|
@ -1,4 +0,0 @@
|
||||
.body {
|
||||
height: 100vh;
|
||||
padding: 10rem 0;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { GetServerSideProps, NextPage } from "next";
|
||||
|
||||
import prisma from "@utils/prisma";
|
||||
|
||||
const LinkRedirectPage: NextPage = () => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
|
||||
const location = params?.location;
|
||||
|
||||
if (location === undefined || typeof location !== "string")
|
||||
return { notFound: true };
|
||||
|
||||
const link = await prisma.link.findFirst({ where: { location } });
|
||||
|
||||
if (link === null) return { notFound: true };
|
||||
|
||||
return { redirect: { destination: link.remoteAddress, permanent: false } };
|
||||
};
|
||||
|
||||
export default LinkRedirectPage;
|
@ -0,0 +1,62 @@
|
||||
import { GetStaticProps, NextPage } from "next";
|
||||
import { NextSeo } from "next-seo";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Link as LinkType } from "@prisma/client";
|
||||
|
||||
import useUser from "@utils/hooks/useUser";
|
||||
import prisma from "@utils/prisma";
|
||||
|
||||
import EmptyPage from "@components/EmptyPage";
|
||||
import Layout from "@components/Layout";
|
||||
import LinkComponent from "@components/LinkComponent";
|
||||
|
||||
const LinksPage: NextPage<{ links: LinkType[] }> = ({ links }) => {
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<NextSeo title="Links" />
|
||||
<EmptyPage>
|
||||
<div className="columns">
|
||||
<div className="col col-8 col-mx-auto">
|
||||
<h1>Links</h1>
|
||||
<div className="container">
|
||||
<div className="columns">
|
||||
<div className="col col-11">
|
||||
<h5>This is a collection of quick usefull links</h5>
|
||||
</div>
|
||||
{user !== null && (
|
||||
<div className="col col-1">
|
||||
<Link href="/link/new">
|
||||
<a>new</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col-8 col-mx-auto">
|
||||
{links.length < 1 && <h2>No links yet</h2>}
|
||||
{links.map((link) => (
|
||||
<LinkComponent key={link.id} link={link} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</EmptyPage>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
const links = await prisma.link
|
||||
.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 5
|
||||
})
|
||||
.catch(() => []);
|
||||
|
||||
return { props: { links }, revalidate: 1 * 60 };
|
||||
};
|
||||
|
||||
export default LinksPage;
|
@ -0,0 +1,132 @@
|
||||
import { GetServerSideProps, NextPage } from "next";
|
||||
import { NextSeo } from "next-seo";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { FormEvent, useCallback, useState } from "react";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import Link from "@models/link";
|
||||
|
||||
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
|
||||
import { withSessionSsr } from "@utils/session";
|
||||
|
||||
import EmptyPage from "@components/EmptyPage";
|
||||
import Layout from "@components/Layout";
|
||||
|
||||
const NewLinkPage: NextPage<{ user: User }> = ({ user }) => {
|
||||
const [remoteAddress, setRemoteAddress] = useState("");
|
||||
const [localAddress, setLocalAddress] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const host =
|
||||
typeof window !== "undefined" && "location" in window
|
||||
? location.host
|
||||
: "guusvanmeerveld.dev";
|
||||
|
||||
const createLink = useCallback(
|
||||
async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const parsedLinkResult = Link.safeParse({
|
||||
remoteAddress,
|
||||
location: localAddress
|
||||
});
|
||||
|
||||
if (!parsedLinkResult.success) {
|
||||
setError(parsedLinkResult.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios
|
||||
.post("/api/link/new", parsedLinkResult.data)
|
||||
.then(parseAxiosResponse)
|
||||
.catch(parseAxiosError);
|
||||
|
||||
if (!response.ok) {
|
||||
setError(JSON.stringify(response.error));
|
||||
return;
|
||||
}
|
||||
|
||||
router.push("/link");
|
||||
},
|
||||
[remoteAddress, localAddress, router]
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<NextSeo title="New link" />
|
||||
<EmptyPage>
|
||||
<div className="columns">
|
||||
<div className="col col-6 col-mx-auto">
|
||||
<h2>Create a new link</h2>
|
||||
<form onSubmit={createLink}>
|
||||
<label className="form-label" htmlFor="remote-address">
|
||||
Remote address
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
onChange={(e) => setRemoteAddress(e.target.value)}
|
||||
name="remoteAddress"
|
||||
type="text"
|
||||
id="remote-address"
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
<label className="form-label" htmlFor="local-address">
|
||||
Address on the server
|
||||
</label>
|
||||
<div className="input-group">
|
||||
<span className="input-group-addon">{host}/link/</span>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
onChange={(e) => setLocalAddress(e.target.value)}
|
||||
name="local-address"
|
||||
type="text"
|
||||
id="local-address"
|
||||
placeholder="example"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error !== null && (
|
||||
<div className="toast toast-error mt-2">
|
||||
<button
|
||||
className="btn btn-clear float-right"
|
||||
onClick={() => setError(null)}
|
||||
/>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary mt-2"
|
||||
value="Create new link"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</EmptyPage>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
||||
async ({ req }) => {
|
||||
const user = req.session.user;
|
||||
|
||||
if (user === undefined)
|
||||
return {
|
||||
notFound: true
|
||||
};
|
||||
|
||||
return { props: { user } };
|
||||
}
|
||||
);
|
||||
|
||||
export default NewLinkPage;
|
@ -0,0 +1,35 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { User as UserModel } from "@models/user";
|
||||
|
||||
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
|
||||
|
||||
const useUser = (): User | null => {
|
||||
const { data } = useQuery(
|
||||
["user"],
|
||||
async () => {
|
||||
const response = await axios
|
||||
.get("/api/user")
|
||||
.then(parseAxiosResponse)
|
||||
.catch(parseAxiosError);
|
||||
|
||||
if (response.ok) return response.data;
|
||||
else throw response.error;
|
||||
},
|
||||
{ retry: () => false, enabled: typeof window !== "undefined" }
|
||||
);
|
||||
|
||||
if (typeof window === "undefined") return null;
|
||||
|
||||
const parseUserResult = UserModel.safeParse(data);
|
||||
|
||||
if (parseUserResult.success) {
|
||||
return parseUserResult.data;
|
||||
} else return null;
|
||||
};
|
||||
|
||||
export default useUser;
|
Loading…
Reference in new issue