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 /> + ); +}