From c4b1c4842ae4cca5ca6a8416c5011d46f7f3a536 Mon Sep 17 00:00:00 2001
From: Luke Bunselmeyer <luke@bunselmeyer.net>
Date: Mon, 8 May 2023 14:47:00 -0400
Subject: [PATCH] Minor refactoring to breakup large components into smaller
 ones.

---
 README.md                              |  10 +-
 app/components/page-layout.tsx         | 146 +++++++------------------
 app/components/side-nav.tsx            |  63 +++++++++++
 app/components/station-player.tsx      |   2 +-
 app/components/top-navbar.tsx          |  63 +++++++++++
 app/routes/listen.channel.$channel.tsx |  59 ----------
 6 files changed, 174 insertions(+), 169 deletions(-)
 create mode 100644 app/components/side-nav.tsx
 create mode 100644 app/components/top-navbar.tsx
 delete mode 100644 app/routes/listen.channel.$channel.tsx

diff --git a/README.md b/README.md
index a90cdff..856fbfe 100644
--- a/README.md
+++ b/README.md
@@ -64,13 +64,21 @@ Awesome Radio is a personal internet radio station aggregator. See the [demo](ht
 cp .env.example .env
 ```
 
-2. Migrate & Seed the SQLite DB
+2. Install dependencies
+
+```shell
+npm install
+```
+
+3. Migrate & Seed the SQLite DB
 
 ```shell
 npx prisma migrate deploy
 npx prisma db seed
 ```
 
+Note: The db schema, migrations, and seed steps are located in the [prisma](prisma) folder.
+
 ### Running
 
 ```shell
diff --git a/app/components/page-layout.tsx b/app/components/page-layout.tsx
index d58fa1b..4d2da88 100644
--- a/app/components/page-layout.tsx
+++ b/app/components/page-layout.tsx
@@ -1,13 +1,46 @@
-import { RadioIcon, UserCircleIcon } from "@heroicons/react/24/solid";
 import { Link, NavLink } from "@remix-run/react";
 import type { RemixLinkProps, RemixNavLinkProps } from "@remix-run/react/dist/components";
-import type { ReactNode } from "react";
-import * as React from "react";
+import type { ReactNode, RefAttributes } from "react";
 import { createContext, useContext } from "react";
+import { SideNav } from "~/components/side-nav";
+import { TopNavbar } from "~/components/top-navbar";
 import type { StationWithTagsClientSide } from "~/models/station.server";
 import type { TagWithStationsClientSide } from "~/models/tag.server";
 import type { UserWithFavoriteStationsClientSide } from "~/models/user.server";
 
+export type StationContextType = {
+    station: StationWithTagsClientSide | null
+}
+
+export const StationContext = createContext<StationContextType>({ station: null });
+
+/**
+ * Hook that provides the active radio station.  Station is null if one is not selected.
+ */
+export function useStationContext() {
+    return useContext(StationContext);
+}
+
+/**
+ * Helper component that wraps <Link> and appends `?station={station.id}` to the link `to` prop if
+ * there's an active station playing
+ */
+export function ListenLink(props: RemixLinkProps & RefAttributes<HTMLAnchorElement>) {
+    const { station } = useStationContext();
+    const url = props.to + (station ? `?station=${station.id}` : "");
+    return <Link {...props} to={url}>{props.children}</Link>;
+}
+
+/**
+ * Helper component that wraps <NavLink> and appends `?station={station.id}` to the link `to` prop if
+ * there's an active station playing
+ */
+export function ListenNavLink(props: RemixNavLinkProps & RefAttributes<HTMLAnchorElement>) {
+    const { station } = useStationContext();
+    const url = props.to + (station ? `?station=${station.id}` : "");
+    return <NavLink {...props} to={url}>{props.children}</NavLink>;
+}
+
 export type PageLayoutProps = {
     children: ReactNode;
     tags: TagWithStationsClientSide[];
@@ -15,123 +48,20 @@ export type PageLayoutProps = {
     station: StationWithTagsClientSide | null;
 }
 
-export type StationContextType = {
-    station: StationWithTagsClientSide | null
-}
-
-const StationContext = createContext<StationContextType>({ station: null });
-
-export function useStationContext() {
-    return useContext(StationContext);
-}
-
-
-export function ListenLink(props: RemixLinkProps & React.RefAttributes<HTMLAnchorElement>) {
-    const { station } = useStationContext();
-    const url = props.to + (station ? `?station=${station.id}` : "");
-    return <Link {...props} to={url}>{props.children}</Link>;
-}
-
-export function ListenNavLink(props: RemixNavLinkProps & React.RefAttributes<HTMLAnchorElement>) {
-    const { station } = useStationContext();
-    const url = props.to + (station ? `?station=${station.id}` : "");
-    return <NavLink {...props} to={url}>{props.children}</NavLink>;
-}
-
-export type ManageContentNavProps = {
-    user?: UserWithFavoriteStationsClientSide;
-};
-
-export function ManageContentNav({ user }: ManageContentNavProps) {
-    if (!user) {
-        return <></>;
-    }
-    return (
-        <>
-            <li className="menu-title">
-                <span>Manage Content</span>
-            </li>
-            <li>
-                <NavLink to="/listen/sources">Sources</NavLink>
-            </li>
-        </>
-    );
-}
-
 export function PageLayout({ children, tags, user, station }: PageLayoutProps) {
-
     return (
         <StationContext.Provider value={{ station }}>
             <div className="drawer drawer-mobile">
                 <input id="primary-drawer" type="checkbox" className="drawer-toggle" />
                 <div className="drawer-content flex flex-col">
-                    <div className="w-full navbar bg-base-300 lg:justify-end">
-                        <div className="flex-none lg:hidden">
-                            <label htmlFor="primary-drawer" className="btn btn-square btn-ghost">
-                                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
-                                     className="inline-block w-6 h-6 stroke-current">
-                                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
-                                          d="M4 6h16M4 12h16M4 18h16"></path>
-                                </svg>
-                            </label>
-                        </div>
-                        <div className="flex-1 px-2 mx-2 gap-2 lg:hidden">
-                            <RadioIcon className="h-8 w-8 p-0" />
-                            <h1 className="text-2xl p-0">Awesome Radio</h1>
-                        </div>
-                        <div className="flex-none">
-                            {user ?
-                                <div className="dropdown dropdown-end">
-                                    <label tabIndex={0} className="btn btn-ghost btn-circle avatar placeholder">
-                                        <UserCircleIcon className="w-8 h-8" />
-                                    </label>
-                                    <ul tabIndex={0}
-                                        className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-200 rounded-box w-30">
-                                        <li>
-                                            <Link to="/logout">Logout</Link>
-                                        </li>
-                                    </ul>
-                                </div> :
-                                <Link to="/join" className="btn">Join</Link>
-                            }
-
-                        </div>
-                    </div>
+                    <TopNavbar user={user} />
                     <div className="py-2 px-6">
                         {children}
                     </div>
                 </div>
                 <div className="drawer-side">
                     <label htmlFor="primary-drawer" className="drawer-overlay"></label>
-                    <ul className="menu menu-compact flex w-80 flex-col bg-base-200 text-base-content p-0 px-4 pt-4">
-                        <li className="flex flex-row justify-start gap-2 mb-4">
-                            <RadioIcon className="h-8 w-8 p-0" />
-                            <h1 className="text-2xl p-0">Awesome Radio</h1>
-                        </li>
-                        <li className="menu-title">
-                            <span>Listen</span>
-                        </li>
-                        <li>
-                            <ListenNavLink to="/listen" end>Home</ListenNavLink>
-                        </li>
-                        <li className="menu-title">
-                            <span>Tags</span>
-                        </li>
-                        {tags
-                            .filter(tag => tag.stations.length > 0)
-                            .map((tag) => {
-                                return (
-                                    <li key={tag.slug}>
-                                        <ListenNavLink to={`/listen/tag/${tag.slug}`} className="capitalize">
-                                            {tag.name}
-                                            <span className="badge badge-outline">{tag.stations?.length ?? 0}</span>
-                                        </ListenNavLink>
-                                    </li>
-                                );
-                            })}
-                        <ManageContentNav user={user} />
-                    </ul>
-
+                    <SideNav tags={tags} user={user} />
                 </div>
             </div>
         </StationContext.Provider>
diff --git a/app/components/side-nav.tsx b/app/components/side-nav.tsx
new file mode 100644
index 0000000..3dd9186
--- /dev/null
+++ b/app/components/side-nav.tsx
@@ -0,0 +1,63 @@
+import { RadioIcon } from "@heroicons/react/24/solid";
+import { NavLink } from "@remix-run/react";
+import { ListenNavLink } from "~/components/page-layout";
+import type { TagWithStationsClientSide } from "~/models/tag.server";
+import type { UserWithFavoriteStationsClientSide } from "~/models/user.server";
+
+export type ManageContentNavProps = {
+    user?: UserWithFavoriteStationsClientSide;
+};
+
+export function ManageContentNav({ user }: ManageContentNavProps) {
+    if (!user) {
+        return <></>;
+    }
+    return (
+        <>
+            <li className="menu-title">
+                <span>Manage Content</span>
+            </li>
+            <li>
+                <NavLink to="/listen/sources">Sources</NavLink>
+            </li>
+        </>
+    );
+}
+
+export type SideNavProps = {
+    tags: TagWithStationsClientSide[];
+    user?: UserWithFavoriteStationsClientSide;
+};
+
+export function SideNav({ tags, user }: SideNavProps) {
+    return (
+        <ul className="menu menu-compact flex w-80 flex-col bg-base-200 text-base-content p-0 px-4 pt-4">
+            <li className="flex flex-row justify-start gap-2 mb-4">
+                <RadioIcon className="h-8 w-8 p-0" />
+                <h1 className="text-2xl p-0">Awesome Radio</h1>
+            </li>
+            <li className="menu-title">
+                <span>Listen</span>
+            </li>
+            <li>
+                <ListenNavLink to="/listen" end>Home</ListenNavLink>
+            </li>
+            <li className="menu-title">
+                <span>Tags</span>
+            </li>
+            {tags
+                .filter(tag => tag.stations.length > 0)
+                .map((tag) => {
+                    return (
+                        <li key={tag.slug}>
+                            <ListenNavLink to={`/listen/tag/${tag.slug}`} className="capitalize">
+                                {tag.name}
+                                <span className="badge badge-outline">{tag.stations?.length ?? 0}</span>
+                            </ListenNavLink>
+                        </li>
+                    );
+                })}
+            <ManageContentNav user={user} />
+        </ul>
+    );
+}
diff --git a/app/components/station-player.tsx b/app/components/station-player.tsx
index 8f8b534..b11731d 100644
--- a/app/components/station-player.tsx
+++ b/app/components/station-player.tsx
@@ -13,7 +13,7 @@ export function StationPlayer({ station }: StationPlayerProps) {
             className="fixed bottom-0 left-0 w-full h-[70px] px-4 py-2 z-50 flex justify-end content-center items-center gap-2 bg-accent text-accent-content">
             <h3 className="text-xl">Now Playing: <strong>{station.name}</strong></h3>
 
-            <audio controls autoPlay src={station.streamUrl}>
+            <audio controls autoPlay src={station.streamUrl} title={station.name}>
                 Your browser does not support the audio element.
             </audio>
         </div>
diff --git a/app/components/top-navbar.tsx b/app/components/top-navbar.tsx
new file mode 100644
index 0000000..2ec4494
--- /dev/null
+++ b/app/components/top-navbar.tsx
@@ -0,0 +1,63 @@
+import { RadioIcon, UserCircleIcon } from "@heroicons/react/24/solid";
+import { Link } from "@remix-run/react";
+import type { UserWithFavoriteStationsClientSide } from "~/models/user.server";
+
+export function GithubIconLink() {
+    return (
+        <a aria-label="Github | Awesome Radio" target="_blank" href="https://github.com/wmluke/awesome-radio"
+           rel="noopener noreferrer"
+           className="btn btn-ghost drawer-button btn-square normal-case">
+            <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
+                 className="inline-block h-8 w-8 fill-current">
+                <path
+                    d="M256,32C132.3,32,32,134.9,32,261.7c0,101.5,64.2,187.5,153.2,217.9a17.56,17.56,0,0,0,3.8.4c8.3,0,11.5-6.1,11.5-11.4,0-5.5-.2-19.9-.3-39.1a102.4,102.4,0,0,1-22.6,2.7c-43.1,0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1,1.4-14.1h.1c22.5,2,34.3,23.8,34.3,23.8,11.2,19.6,26.2,25.1,39.6,25.1a63,63,0,0,0,25.6-6c2-14.8,7.8-24.9,14.2-30.7-49.7-5.8-102-25.5-102-113.5,0-25.1,8.7-45.6,23-61.6-2.3-5.8-10-29.2,2.2-60.8a18.64,18.64,0,0,1,5-.5c8.1,0,26.4,3.1,56.6,24.1a208.21,208.21,0,0,1,112.2,0c30.2-21,48.5-24.1,56.6-24.1a18.64,18.64,0,0,1,5,.5c12.2,31.6,4.5,55,2.2,60.8,14.3,16.1,23,36.6,23,61.6,0,88.2-52.4,107.6-102.3,113.3,8,7.1,15.2,21.1,15.2,42.5,0,30.7-.3,55.5-.3,63,0,5.4,3.1,11.5,11.4,11.5a19.35,19.35,0,0,0,4-.4C415.9,449.2,480,363.1,480,261.7,480,134.9,379.7,32,256,32Z"></path>
+            </svg>
+        </a>
+    );
+}
+
+export function UserDropdown() {
+    return (
+        <div className="dropdown dropdown-end">
+            <label tabIndex={0} className="btn btn-ghost btn-circle avatar placeholder">
+                <UserCircleIcon className="w-8 h-8" />
+            </label>
+            <ul tabIndex={0}
+                className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-200 rounded-box w-30">
+                <li>
+                    <Link to="/logout">Logout</Link>
+                </li>
+            </ul>
+        </div>
+    );
+}
+
+export type TopNavbarProps = { user?: UserWithFavoriteStationsClientSide; };
+
+export function TopNavbar({ user }: TopNavbarProps) {
+    return (
+        <div className="w-full navbar bg-base-300 lg:justify-end">
+            <div className="flex-none lg:hidden">
+                <label htmlFor="primary-drawer" className="btn btn-square btn-ghost">
+                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
+                         className="inline-block w-6 h-6 stroke-current">
+                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
+                              d="M4 6h16M4 12h16M4 18h16"></path>
+                    </svg>
+                </label>
+            </div>
+            <div className="flex-1 px-2 mx-2 gap-2 lg:hidden">
+                <RadioIcon className="h-8 w-8 p-0" />
+                <h1 className="text-2xl p-0">Awesome Radio</h1>
+            </div>
+            <div className="flex-none gap-4">
+                <GithubIconLink />
+                {user ?
+                    <UserDropdown /> :
+                    <Link to="/join" className="btn btn-accent btn-sm">Join</Link>
+                }
+
+            </div>
+        </div>
+    );
+}
diff --git a/app/routes/listen.channel.$channel.tsx b/app/routes/listen.channel.$channel.tsx
deleted file mode 100644
index 42d83ce..0000000
--- a/app/routes/listen.channel.$channel.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import type { LoaderArgs } from "@remix-run/node";
-import { json } from "@remix-run/node";
-import { Link, Outlet, useLoaderData } from "@remix-run/react";
-import { Breadcrumbs } from "~/components/breadcrumbs";
-import { StationsGallery } from "~/components/stations-gallery";
-import type { StationWithTags } from "~/models/station.server";
-import { findStationsByTags } from "~/models/station.server";
-import { notFound } from "~/utils";
-
-
-export type Channel = {
-    tags: string[];
-    slug: string;
-    name: string;
-}
-
-const channels: { [channel: string]: Channel } = {
-    "music": {
-        tags: ["music"],
-        slug: "music",
-        name: "Music"
-    },
-    "news": {
-        tags: ["news"],
-        slug: "news",
-        name: "News"
-    }
-};
-
-
-export async function loader({ params }: LoaderArgs) {
-    if (!params.channel) {
-        throw notFound();
-    }
-
-    const channel = channels[params.channel];
-    if (!channel) {
-        throw notFound();
-    }
-
-    const stations: StationWithTags[] = await findStationsByTags(channel.tags);
-    return json({ channel, stations });
-}
-
-export default function ListenChannel() {
-    const { channel, stations } = useLoaderData<typeof loader>();
-
-    return (
-        <>
-            <Breadcrumbs>
-                <Link to="/listen">Home</Link>
-                <Link to={`/listen/${channel.slug}`}>{channel.name}</Link>
-            </Breadcrumbs>
-            <StationsGallery stations={stations} channel={channel} />
-            <Outlet />
-        </>
-    );
-
-}