From 92d6d0d267dd0345a0ddc34684c7fe788abd0a58 Mon Sep 17 00:00:00 2001 From: Ovyerus Date: Sun, 6 Jun 2021 21:06:35 +1000 Subject: [PATCH] feat: add dashboard --- .babelrc | 4 + assets/style/dash.module.css | 53 ++++++++++++ pages/dash.tsx | 154 +++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 .babelrc create mode 100644 assets/style/dash.module.css create mode 100644 pages/dash.tsx diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..e573860 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["superjson-next"] +} diff --git a/assets/style/dash.module.css b/assets/style/dash.module.css new file mode 100644 index 0000000..4b71f37 --- /dev/null +++ b/assets/style/dash.module.css @@ -0,0 +1,53 @@ +.table { + @apply max-h-full sm:max-h-[500px]; + @apply bg-white dark:bg-black dark:text-white; + @apply grid overflow-y-auto; + @apply shadow-md rounded-xl border-collapse; + + grid-template-columns: max-content minmax(110px, auto) minmax(auto, 200px) 90px; +} + +th.cell { + @apply relative flex items-center px-3 py-2; + min-width: 24px; +} + +td.cell { + @apply flex items-center p-3 bg-gray-50 dark:bg-gray-800; +} + +.row { + @apply contents; + + &:first-child { + @apply rounded-t-xl; + + & > th.cell:first-child { + @apply rounded-tl-xl; + } + + & > th.cell:last-child { + @apply rounded-tr-xl; + } + } + + &:last-child { + @apply rounded-b-xl; + + & > th.cell { + @apply border-b-2 dark:border-gray-600; + } + + & > td.cell:first-child { + @apply rounded-bl-xl; + } + + & > td.cell:last-child { + @apply rounded-br-xl; + } + } +} + +tbody > .row:hover > td.cell { + @apply bg-indigo-50 dark:bg-opacity-10; +} diff --git a/pages/dash.tsx b/pages/dash.tsx new file mode 100644 index 0000000..3196fd1 --- /dev/null +++ b/pages/dash.tsx @@ -0,0 +1,154 @@ +import plus from "@iconify/icons-gg/math-plus"; +import pen from "@iconify/icons-gg/pen"; +import trash from "@iconify/icons-gg/trash"; +import { Icon } from "@iconify/react"; +import { Redirect } from "@prisma/client"; +import cc from "classcat"; +import { GetServerSideProps } from "next"; +import React from "react"; + +import { getRedirectsForUser } from "./api/redirects"; + +import styles from "~/assets/style/dash.module.css"; +import Toasts from "~/components/Toasts"; +import * as jwt from "~/util/jwt"; +import { useStore } from "~/util/store"; + +const rows = [ + { + id: "1", + hash: "google-but-its-actually-super-fucking-epic", + url: "https://google.com", + visitors: 0, + }, + { id: "2", hash: "bing", url: "https://bing.com", visitors: 69 }, + { id: "3", hash: "blog", url: "https://ovyerus.com/blog", visitors: 420 }, + { id: "4", hash: "43f78c02", url: "https://github.com", visitors: -1 }, + { id: "5", hash: "google", url: "https://google.com", visitors: 0 }, + { id: "6", hash: "google", url: "https://google.com", visitors: 0 }, + { id: "7", hash: "google", url: "https://google.com", visitors: 0 }, + { id: "8", hash: "google", url: "https://google.com", visitors: 0 }, + { id: "9", hash: "google", url: "https://google.com", visitors: 0 }, + { id: "10", hash: "google", url: "https://google.com", visitors: 0 }, + { id: "11", hash: "google", url: "https://google.com", visitors: 0 }, +]; + +const DashPage = ({ redirects }: Props) => { + const pushToast = useStore(({ pushToast }) => pushToast); + + const copyHash = async (hash: string) => { + const toCopy = new URL(location.href); + toCopy.pathname = `/${hash}`; + + try { + await navigator.clipboard.writeText(toCopy.toString()); + pushToast({ + duration: 3000, + children: "Copied to clipboard.", + className: "!bg-green-100 dark:!bg-green-900", + }); + } catch (err) { + console.error("failed to write to clipboard", err); + pushToast({ + duration: 5000, + children: "Failed to copy to clipboard.", + className: "!bg-red-100 dark:!bg-red-900", + }); + } + }; + + // TODO: graphs? + return ( +
+ + +
+
+
+ {redirects.length} redirects +
+ +
+ +
+ +
+
+ + + + {/* TODO: clicking on `th` to sort by column */} + + + + + + + + + {rows.map((row) => ( + + + + + + + ))} + +
PathURLUnique Visitors +
+ copyHash(row.hash)} + > + /{row.hash} + + + + {/* TODO: strip protocol? */} + {row.url} + + + {row.visitors} + + + +
+
+
+ ); +}; + +interface Props { + redirects: Redirect[]; +} + +export const getServerSideProps: GetServerSideProps = async ({ + req, +}) => { + const isAuthed = await jwt.verify(req.cookies.token); + + if (!isAuthed) + return { + redirect: { + destination: "/login", + statusCode: 307, + }, + }; + + const { sub: userId } = await jwt.getPayload(req.cookies.token); + const redirects = await getRedirectsForUser(userId); + + return { props: { redirects } }; +}; + +export default DashPage;