Skip to content

Commit

Permalink
Massive PR – built out most of the app, changed API route structure, …
Browse files Browse the repository at this point in the history
…added favicons, etc. Still got lots to do though
  • Loading branch information
steven-tey committed Sep 7, 2022
1 parent 8f42e92 commit fc537e5
Show file tree
Hide file tree
Showing 65 changed files with 1,482 additions and 163 deletions.
67 changes: 67 additions & 0 deletions components/app/link-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import BlurImage from "@/components/shared/blur-image";
import CopyButton from "@/components/shared/copy-button";
import { LoadingDots } from "@/components/shared/icons";
import { useRouter } from "next/router";
import useSWR from "swr";
import { fetcher, nFormatter, linkConstructor } from "@/lib/utils";
import Link from "next/link";

export default function LinkCard({
_key: key,
url,
}: {
_key: string;
url: string;
}) {
const urlHostname = new URL(url).hostname;

const router = useRouter();
const { slug } = router.query as {
slug: string;
};

const { data: clicks, isValidating } = useSWR<string>(
`/api/projects/${slug}/links/${key}/clicks`,
fetcher
);

return (
<div className="flex items-center border border-gray-200 dark:border-gray-600 hover:border-black dark:hover:border-white p-3 max-w-md rounded-md transition-all">
<BlurImage
src={`https://logo.clearbit.com/${urlHostname}`}
alt={urlHostname}
className="w-10 h-10 rounded-full mr-2 border border-gray-200 dark:border-gray-600"
width={20}
height={20}
/>
<div>
<div className="flex items-center space-x-2 mb-1">
<a
className="text-blue-800 dark:text-blue-400 font-semibold"
href={linkConstructor(key)}
target="_blank"
rel="noreferrer"
>
{linkConstructor(key, true)}
</a>
<CopyButton url={linkConstructor(key)} />
<Link href={`${router.asPath}/${encodeURI(key)}`}>
<a className="rounded-md bg-gray-100 dark:bg-gray-800 px-2 py-0.5 hover:scale-105 active:scale-95 transition-all duration-75">
<p className="text-sm text-gray-500 dark:text-white">
{isValidating || !clicks ? (
<LoadingDots color="#71717A" />
) : (
nFormatter(parseInt(clicks))
)}{" "}
clicks
</p>
</a>
</Link>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 truncate w-72">
{url}
</p>
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions components/home/link-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function LinkCard({
const urlHostname = new URL(url).hostname;

const { data: clicks, isValidating } = useSWR<string>(
`/api/links/${key}/clicks`,
`/api/edge/links/${key}/clicks`,
fetcher
);

Expand Down Expand Up @@ -50,7 +50,7 @@ export default function LinkCard({
return (
<motion.li
variants={FRAMER_MOTION_LIST_ITEM_VARIANTS}
className="flex items-center border border-gray-200 dark:border-gray-600 hover:border-black dark:hover:border-white p-3 rounded-md transition-all"
className="flex items-center border border-gray-200 dark:border-gray-600 hover:border-black dark:hover:border-white p-3 max-w-md rounded-md transition-all"
>
<BlurImage
src={`https://logo.clearbit.com/${urlHostname}`}
Expand Down
2 changes: 1 addition & 1 deletion components/home/placeholder-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function PlaceholderCard() {
return (
<motion.li
variants={FRAMER_MOTION_LIST_ITEM_VARIANTS}
className="flex items-center border border-gray-200 dark:border-gray-600 hover:border-black dark:hover:border-white p-3 rounded-md transition-all"
className="flex items-center border border-gray-200 dark:border-gray-600 hover:border-black dark:hover:border-white p-3 max-w-md rounded-md transition-all"
>
<div className="w-10 h-10 rounded-full mr-2 bg-gray-200 dark:bg-gray-600 animate-pulse" />
<div>
Expand Down
37 changes: 36 additions & 1 deletion components/layout/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
import { ReactNode } from "react";
import dynamic from "next/dynamic";
import Link from "next/link";
import Meta from "../meta";
import { useSession, signOut } from "next-auth/react";
import { Logo, Divider } from "@/components/shared/icons";
import ListBox from "./list-box";

const NavTabs = dynamic(() => import("./nav-tabs"), {
ssr: false,
loading: () => <div className="w-full h-12 -mb-0.5" />,
}); // dynamic import to avoid react hydration mismatch error

export default function AppLayout({ children }: { children: ReactNode }) {
const { data: session } = useSession();

return (
<div>
<Meta />
{children}
<div className="min-h-screen w-full bg-gray-50">
<div className="sticky top-0 left-0 right-0 border-b bg-white border-gray-200 z-10">
<div className="max-w-screen-xl mx-auto px-5 sm:px-20">
<div className="h-10 flex justify-between items-center my-3">
<div className="flex items-center">
<Link href="/">
<a>
<Logo className="w-8 h-8 active:scale-95 transition-all duration-75" />
</a>
</Link>
<Divider className="h-8 w-8 ml-3 text-gray-200" />
<ListBox />
</div>
<button className="rounded-full overflow-hidden border border-gray-300 w-10 h-10 flex justify-center items-center active:scale-95 focus:outline-none transition-all duration-75">
{session && (
<img
alt={session?.user?.email || "Avatar for logged in user"}
src={`https://avatars.dicebear.com/api/micah/${session?.user?.email}.svg`}
/>
)}
</button>
</div>
<NavTabs />
</div>
</div>
<div className="max-w-screen-xl mx-auto px-5 sm:px-20">{children}</div>
</div>
</div>
);
}
102 changes: 102 additions & 0 deletions components/layout/app/list-box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Fragment, useMemo } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { Tick, ChevronUpDown } from "@/components/shared/icons";
import { useRouter } from "next/router";
import BlurImage from "@/components/shared/blur-image";
import useSWR from "swr";
import { fetcher } from "@/lib/utils";
import { ProjectProps } from "@/lib/api/types";

export default function ListBox() {
const { data: projects } = useSWR<ProjectProps[]>("/api/projects", fetcher);

const router = useRouter();
const selected = useMemo(() => {
const { teamSlug } = router.query;
return (
projects?.find(({ slug }) => slug === teamSlug) || {
name: "Dub.sh",
slug: "dub",
}
);
}, [router, projects]);

if (!projects)
return (
<div className="w-52 h-9 px-2 rounded-lg bg-gray-100 animate-pulse flex justify-end items-center">
<ChevronUpDown className="h-4 w-4 text-gray-400" aria-hidden="true" />
</div>
);

return (
<div className="w-52 -mt-1">
<Listbox
value={selected}
onChange={(e) => {
router.push(`/${e.slug}`);
}}
>
<div className="relative mt-1">
<Listbox.Button className="relative w-full rounded-lg bg-white hover:bg-gray-100 py-1.5 pl-3 pr-10 text-left focus:outline-none text-sm active:scale-95 transition-all duration-75">
<div className="flex justify-start items-center space-x-3">
<BlurImage
src={`https://avatar.tobi.sh/${selected.slug}`}
alt={selected.name}
className="w-8 h-8 flex-shrink-0 rounded-full overflow-hidden border border-gray-300"
width={48}
height={48}
/>
<span className="block truncate font-medium">
{selected.name}
</span>
</div>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDown
className="h-4 w-4 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute mt-1 max-h-60 w-60 overflow-auto rounded-md bg-white p-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{projects.map(({ name, slug }) => (
<Listbox.Option
key={slug}
className={`relative flex items-center space-x-2 cursor-pointer p-2 rounded-md hover:bg-gray-100 active:scale-95 ${
selected.slug === slug ? "font-medium" : ""
} transition-all duration-75`}
value={{ name, slug }}
>
<BlurImage
src={`https://avatar.tobi.sh/${slug}`}
alt={name}
className="w-7 h-7 flex-shrink-0 rounded-full overflow-hidden border border-gray-300"
width={48}
height={48}
/>
<span
className={`block truncate ${
selected.slug === slug ? "font-medium" : "font-normal"
}`}
>
{name}
</span>
{selected.slug === slug ? (
<span className="absolute inset-y-0 right-0 flex items-center pr-3 text-black">
<Tick className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
);
}
38 changes: 38 additions & 0 deletions components/layout/app/nav-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useMemo } from "react";
import Link from "next/link";
import { NextRouter, useRouter } from "next/router";

const TabsHelper = (router: NextRouter): { name: string; href: string }[] => {
const { slug } = router.query;
if (slug) {
return [{ name: "Overview", href: `/${slug}` }];
}
return [{ name: "Overview", href: `/` }];
};

export default function NavTabs() {
const router = useRouter();
const tabs = useMemo(() => {
return TabsHelper(router);
}, [router.query]);

return (
<div className="flex justify-start space-x-8 items-center h-12 -mb-0.5">
{tabs.map(({ name, href }) => (
<Link key={href} href={href}>
<a
className={`px-1 py-3 border-b-2 ${
router.asPath === href
? "border-black font-semibold"
: "border-transparent text-gray-700 hover:text-black"
} transition-all`}
>
<p className="text-sm active:scale-95 transition-all duration-75">
{name}
</p>
</a>
</Link>
))}
</div>
);
}
45 changes: 37 additions & 8 deletions components/layout/meta.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Head from "next/head";

const faviconFolder = "/static/favicons";

export default function Meta() {
return (
<Head>
Expand All @@ -8,27 +10,54 @@ export default function Meta() {
name="description"
content="An open-source link shortener with built-in analytics and free custom domains."
/>
<link rel="icon" href="/favicon.ico" />
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico" />
<meta name="theme-color" content="#7b46f6" />
<link
rel="apple-touch-icon"
sizes="180x180"
href={`${faviconFolder}/apple-touch-icon.png`}
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href={`${faviconFolder}/favicon-32x32.png`}
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href={`${faviconFolder}/favicon-16x16.png`}
/>
<link rel="manifest" href="/site.webmanifest" />
<link
rel="mask-icon"
href={`${faviconFolder}/safari-pinned-tab.svg`}
color="#5bbad5"
/>
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="theme-color" content="#ffffff" />

<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="keywords" content="hacker news, slack, bot" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta itemProp="image" content="https://dub.sh/thumbnail.png" />
<meta property="og:image" content="https://dub.sh/thumbnail.png" />
<meta itemProp="image" content="https://dub.sh/static/thumbnail.png" />
<meta property="og:image" content="https://dub.sh/static/thumbnail.png" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@vercel" />
<meta name="twitter:creator" content="@steventey" />
<meta name="twitter:title" content="Dub - Open-Source Link Shortener" />
<meta
name="twitter:title"
content="Dub - Open-Source Bitly Alternative"
/>
<meta
name="twitter:description"
content="An open-source link shortener with built-in analytics and free custom domains."
/>
<meta name="twitter:image" content="https://dub.sh/thumbnail.png" />
<meta
name="twitter:image"
content="https://dub.sh/static/thumbnail.png"
/>
</Head>
);
}
18 changes: 18 additions & 0 deletions components/shared/icons/chevron-down.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function ChevronDown({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M6 9l6 6 6-6" />
</svg>
);
}
18 changes: 18 additions & 0 deletions components/shared/icons/chevron-left.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function ChevronLeft({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M15 18l-6-6 6-6" />
</svg>
);
}
Loading

0 comments on commit fc537e5

Please sign in to comment.