From 8d06e911d940d37bfa12676f77dcbd82a0b3dfb1 Mon Sep 17 00:00:00 2001
From: Luke Bunselmeyer <luke@bunselmeyer.net>
Date: Sun, 7 May 2023 12:31:18 -0400
Subject: [PATCH] - Models   - Created query to find stations by tag - UI   -
 Created pages to list station gallery for channels and tags   - Created
 <StationsGallery> component

---
 app/components/page-layout.tsx         |  7 +---
 app/components/stations-gallery.tsx    | 37 ++++++++++++++++
 app/models/station.server.ts           | 39 ++++++++++++++++-
 app/models/tag.server.ts               |  4 ++
 app/routes/listen._index.tsx           | 13 ++++++
 app/routes/listen.channel.$channel.tsx | 58 ++++++++++++++++++++++++++
 app/routes/listen.tag.$tag.tsx         | 40 ++++++++++++++++++
 app/routes/listen.tsx                  |  8 ++++
 8 files changed, 200 insertions(+), 6 deletions(-)
 create mode 100644 app/components/stations-gallery.tsx
 create mode 100644 app/routes/listen._index.tsx
 create mode 100644 app/routes/listen.channel.$channel.tsx
 create mode 100644 app/routes/listen.tag.$tag.tsx
 create mode 100644 app/routes/listen.tsx

diff --git a/app/components/page-layout.tsx b/app/components/page-layout.tsx
index 50d3048..6eaa191 100644
--- a/app/components/page-layout.tsx
+++ b/app/components/page-layout.tsx
@@ -47,13 +47,10 @@ export function PageLayout({ children }: PageLayoutProps) {
                         <span>Listen</span>
                     </li>
                     <li>
-                        <NavLink to="/listen/music">Music</NavLink>
+                        <NavLink to="/listen/channel/music">Music</NavLink>
                     </li>
                     <li>
-                        <NavLink to="/listen/sports">Sports</NavLink>
-                    </li>
-                    <li>
-                        <NavLink to="/listen/news">News & Talk</NavLink>
+                        <NavLink to="/listen/channel/news">News & Talk</NavLink>
                     </li>
                     <li className="menu-title">
                         <span>Manage Content</span>
diff --git a/app/components/stations-gallery.tsx b/app/components/stations-gallery.tsx
new file mode 100644
index 0000000..29103ea
--- /dev/null
+++ b/app/components/stations-gallery.tsx
@@ -0,0 +1,37 @@
+import { Link } from "@remix-run/react";
+import type { ConvertDatesToStrings } from "@remix-run/router/utils";
+import type { StationWithTags } from "~/models/station.server";
+
+export type StationsGalleryProps = {
+    stations: ConvertDatesToStrings<NonNullable<StationWithTags>>[]
+};
+
+export function StationsGallery({ stations }: StationsGalleryProps) {
+    return (
+        <div className="grid grid-cols-3 gap-4">
+            {stations.map((station) => {
+                return (
+                    <div key={station.id} className="card card-compact bg-base-100 shadow-xl">
+                        <figure><img src={station.imgUrl} alt="Radio Station" /></figure>
+                        <div className="card-body">
+                            <h2 className="card-title">
+                                {station.name}
+                            </h2>
+                            <h2 className="flex gap-1">
+                                {station.tags.map((t, id) => {
+                                    return <Link key={id} to={`/listen/tag/${t.tag.slug}`}
+                                                 className="badge badge-secondary">{t.tag.name}</Link>;
+                                })}
+                            </h2>
+                            <p>{station.description}</p>
+                            <div className="card-actions justify-end">
+                                <button className="btn btn-primary">Listen Now</button>
+                            </div>
+                        </div>
+                    </div>
+                );
+
+            })}
+        </div>
+    );
+}
diff --git a/app/models/station.server.ts b/app/models/station.server.ts
index a699950..219bdd5 100644
--- a/app/models/station.server.ts
+++ b/app/models/station.server.ts
@@ -1,4 +1,4 @@
-import type { PrismaClient, Station, Tag } from "@prisma/client";
+import type { Prisma, PrismaClient, Station, Tag } from "@prisma/client";
 import { prisma } from "~/db.server";
 import { upsertTagOnName } from "~/models/tag.server";
 import { slugify } from "~/utils";
@@ -16,6 +16,43 @@ export type StationInput = {
 
 export type PrismaTxClient = Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use">;
 
+export function findStationsByTags(tags: string[]) {
+    return prisma.station.findMany({
+        where: {
+            tags: {
+                some: {
+                    tag: {
+                        name: { in: tags }
+                    }
+                }
+            }
+        },
+        include: {
+            tags: {
+                include: {
+                    tag: true
+                }
+            }
+        }
+    });
+}
+
+export type StationWithTags = Prisma.PromiseReturnType<typeof getStationById>;
+
+export function getStationById(id: string) {
+    return prisma.station.findUnique({
+        where: { id },
+        include: {
+            tags: {
+                include: {
+                    tag: true
+                }
+            }
+        }
+    });
+
+}
+
 export function upsertStationOnStreamUrl(input: StationInput, p: PrismaTxClient = prisma) {
     return p.station.upsert({
         where: { streamUrl: input.streamUrl },
diff --git a/app/models/tag.server.ts b/app/models/tag.server.ts
index b95142a..cb58cd6 100644
--- a/app/models/tag.server.ts
+++ b/app/models/tag.server.ts
@@ -2,6 +2,10 @@ import { prisma } from "~/db.server";
 import type { PrismaTxClient } from "~/models/station.server";
 import { slugify } from "~/utils";
 
+export function findTagBySlug(slug: string) {
+    return prisma.tag.findUnique({ where: { slug } });
+}
+
 export function upsertTagOnName(tag: string, p: PrismaTxClient = prisma) {
     const slug = slugify(tag);
     return p.tag.upsert({
diff --git a/app/routes/listen._index.tsx b/app/routes/listen._index.tsx
new file mode 100644
index 0000000..63509f6
--- /dev/null
+++ b/app/routes/listen._index.tsx
@@ -0,0 +1,13 @@
+import { Link } from "@remix-run/react";
+import { Breadcrumbs } from "~/components/breadcrumbs";
+
+export default function ListenHome() {
+    return (
+        <>
+            <Breadcrumbs>
+                <Link to="/listen">Home</Link>
+            </Breadcrumbs>
+        </>
+    );
+
+}
diff --git a/app/routes/listen.channel.$channel.tsx b/app/routes/listen.channel.$channel.tsx
new file mode 100644
index 0000000..fb35c8b
--- /dev/null
+++ b/app/routes/listen.channel.$channel.tsx
@@ -0,0 +1,58 @@
+import type { LoaderArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { Link, 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: NonNullable<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} />
+        </>
+    );
+
+}
diff --git a/app/routes/listen.tag.$tag.tsx b/app/routes/listen.tag.$tag.tsx
new file mode 100644
index 0000000..13ae53b
--- /dev/null
+++ b/app/routes/listen.tag.$tag.tsx
@@ -0,0 +1,40 @@
+import type { LoaderArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { Link, 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 { findTagBySlug } from "~/models/tag.server";
+import { notFound } from "~/utils";
+
+
+export async function loader({ params }: LoaderArgs) {
+    if (!params.tag) {
+        throw notFound();
+    }
+
+    const tag = await findTagBySlug(params.tag);
+    if (!tag) {
+        throw notFound();
+    }
+
+    const stations: NonNullable<StationWithTags>[] = await findStationsByTags([tag.name]);
+
+    return json({ tag, stations });
+
+
+}
+
+export default function ListenTag() {
+    const { tag, stations } = useLoaderData<typeof loader>();
+    return (
+        <>
+            <Breadcrumbs>
+                <Link to="/listen">Home</Link>
+                <Link to={`/listen/${tag.slug}`} className="capitalize">{tag.name}</Link>
+            </Breadcrumbs>
+            <StationsGallery stations={stations} />
+        </>
+    );
+}
diff --git a/app/routes/listen.tsx b/app/routes/listen.tsx
new file mode 100644
index 0000000..bce6b09
--- /dev/null
+++ b/app/routes/listen.tsx
@@ -0,0 +1,8 @@
+import { Outlet } from "@remix-run/react";
+
+
+export default function ListenLayout() {
+    return (
+        <Outlet />
+    );
+}