Skip to content
This repository has been archived by the owner on Dec 2, 2022. It is now read-only.

Commit

Permalink
feat: add dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
Ovyerus committed Jun 6, 2021
1 parent 0001954 commit 92d6d0d
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": ["superjson-next"]
}
53 changes: 53 additions & 0 deletions assets/style/dash.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
154 changes: 154 additions & 0 deletions pages/dash.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="p-2 flex items-center justify-center min-h-screen bg">
<Toasts />

<main className="max-w-6xl w-full flex flex-col">
<header className="m-4 mt-2 flex items-center sm:mt-0 dark:text-white">
<div>
<span className="font-bold">{redirects.length}</span> redirects
</div>

<div className="flex-grow" />

<div className="flex items-center justify-end">
<button className="rounded-button">
<Icon icon={plus} height={24} />
</button>
</div>
</header>

<table className={styles.table}>
<thead className="contents">
{/* TODO: clicking on `th` to sort by column */}
<tr className={styles.row}>
<th className={styles.cell}>Path</th>
<th className={styles.cell}>URL</th>
<th className={styles.cell}>Unique Visitors</th>
<th className={styles.cell} />
</tr>
</thead>

<tbody className="contents">
{rows.map((row) => (
<tr key={row.id} className={styles.row}>
<td className={styles.cell}>
<span
className="cursor-pointer"
onClick={() => copyHash(row.hash)}
>
/{row.hash}
</span>
</td>
<td className={styles.cell}>
<a
className="block w-full break-words text-indigo-500 hover:underline"
href={row.url}
>
{/* TODO: strip protocol? */}
{row.url}
</a>
</td>
<td className={styles.cell}>
<span>{row.visitors}</span>
</td>
<td className={cc([styles.cell, "justify-end"])}>
<button className="mr-3">
<Icon icon={pen} height={24} />
</button>
<button className="text-red-500">
<Icon icon={trash} height={24} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</main>
</div>
);
};

interface Props {
redirects: Redirect[];
}

export const getServerSideProps: GetServerSideProps<Props> = 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;

0 comments on commit 92d6d0d

Please sign in to comment.