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