From 27cc0ddeb8d6e479bfa683c40647af3925a96ed9 Mon Sep 17 00:00:00 2001 From: Luke Bunselmeyer Date: Sun, 7 May 2023 16:21:15 -0400 Subject: [PATCH] - UI - Updated side nav to list tags --- app/components/page-layout.tsx | 29 ++++--- app/components/stations-gallery.tsx | 6 +- app/models/station.server.ts | 2 + app/models/tag.server.ts | 48 ++++++++++- app/models/user.server.ts | 82 ++++++++++--------- app/root.tsx | 13 ++- app/session.server.ts | 121 ++++++++++++++-------------- 7 files changed, 188 insertions(+), 113 deletions(-) diff --git a/app/components/page-layout.tsx b/app/components/page-layout.tsx index 5076166..c8c01b1 100644 --- a/app/components/page-layout.tsx +++ b/app/components/page-layout.tsx @@ -1,12 +1,16 @@ import { RadioIcon } from "@heroicons/react/24/solid"; import { NavLink } from "@remix-run/react"; import type { ReactNode } from "react"; +import type { TagWithStationsClientSide } from "~/models/tag.server"; +import type { UserWithFavoriteStationsClientSide } from "~/models/user.server"; export type PageLayoutProps = { children: ReactNode; + tags: TagWithStationsClientSide[]; + user?: UserWithFavoriteStationsClientSide } -export function PageLayout({ children }: PageLayoutProps) { +export function PageLayout({ children, tags, user }: PageLayoutProps) { return (
@@ -26,10 +30,6 @@ export function PageLayout({ children }: PageLayoutProps) {

Awesome Radio

-
@@ -49,12 +49,21 @@ export function PageLayout({ children }: PageLayoutProps) {
  • Home
  • -
  • - Music -
  • -
  • - News & Talk +
  • + Tags
  • + {tags + .filter(tag => tag.stations.length > 0) + .map((tag) => { + return ( +
  • + + {tag.name} + {tag.stations?.length ?? 0} + +
  • + ); + })}
  • Manage Content
  • diff --git a/app/components/stations-gallery.tsx b/app/components/stations-gallery.tsx index 40ea4da..e39198d 100644 --- a/app/components/stations-gallery.tsx +++ b/app/components/stations-gallery.tsx @@ -1,3 +1,4 @@ +import { PlayIcon } from "@heroicons/react/24/solid"; import type { Tag } from "@prisma/client"; import { Link } from "@remix-run/react"; import type { StationWithTagsClientSide } from "~/models/station.server"; @@ -40,7 +41,10 @@ export function StationsGallery({ stations, tag, channel }: StationsGalleryProps

    {station.description}

    - Listen Now + + + Listen Now +
    diff --git a/app/models/station.server.ts b/app/models/station.server.ts index a1937ef..aef8c15 100644 --- a/app/models/station.server.ts +++ b/app/models/station.server.ts @@ -28,6 +28,7 @@ export function getStations(reliability: number = 80) { } }, orderBy: [ + { reliability: "desc" }, { popularity: "desc" } ] }); @@ -54,6 +55,7 @@ export function findStationsByTags(tags: string[], reliability: number = 80) { } }, orderBy: [ + { reliability: "desc" }, { popularity: "desc" } ] }); diff --git a/app/models/tag.server.ts b/app/models/tag.server.ts index cb58cd6..361c66d 100644 --- a/app/models/tag.server.ts +++ b/app/models/tag.server.ts @@ -1,9 +1,53 @@ +import type { Prisma } from "@prisma/client"; import { prisma } from "~/db.server"; import type { PrismaTxClient } from "~/models/station.server"; +import type { ConvertDatesToStrings } from "~/utils"; import { slugify } from "~/utils"; -export function findTagBySlug(slug: string) { - return prisma.tag.findUnique({ where: { slug } }); +export type TagWithStations = NonNullable>; +export type TagWithStationsClientSide = ConvertDatesToStrings; + + +export function findTagBySlug(slug: string, reliability = 80) { + return prisma.tag.findUnique({ + where: { slug }, + include: { + stations: { + where: { + station: { + reliability: { + gte: reliability + } + } + }, + include: { + station: true + } + } + } + }); +} + +export function getTags(reliability = 80) { + return prisma.tag.findMany({ + include: { + stations: { + where: { + station: { + reliability: { + gte: reliability + } + } + }, + include: { + station: true + } + } + }, + orderBy: { + name: "asc" + } + }); } export function upsertTagOnName(tag: string, p: PrismaTxClient = prisma) { diff --git a/app/models/user.server.ts b/app/models/user.server.ts index cce401e..ee1629b 100644 --- a/app/models/user.server.ts +++ b/app/models/user.server.ts @@ -1,62 +1,72 @@ -import type { Password, User } from "@prisma/client"; +import type { Password, Prisma, User } from "@prisma/client"; import bcrypt from "bcryptjs"; - import { prisma } from "~/db.server"; +import type { ConvertDatesToStrings } from "~/utils"; export type { User } from "@prisma/client"; +export type UserWithFavoriteStations = NonNullable>; +export type UserWithFavoriteStationsClientSide = ConvertDatesToStrings; + export async function getUserById(id: User["id"]) { - return prisma.user.findUnique({ where: { id } }); + return prisma.user.findUnique({ + where: { id }, + include: { + favoriteStations: { + include: { station: true } + } + } + }); } export async function getUserByEmail(email: User["email"]) { - return prisma.user.findUnique({ where: { email } }); + return prisma.user.findUnique({ where: { email } }); } export async function createUser(email: User["email"], password: string) { - const hashedPassword = await bcrypt.hash(password, 10); + const hashedPassword = await bcrypt.hash(password, 10); - return prisma.user.create({ - data: { - email, - password: { - create: { - hash: hashedPassword, - }, - }, - }, - }); + return prisma.user.create({ + data: { + email, + password: { + create: { + hash: hashedPassword + } + } + } + }); } export async function deleteUserByEmail(email: User["email"]) { - return prisma.user.delete({ where: { email } }); + return prisma.user.delete({ where: { email } }); } export async function verifyLogin( - email: User["email"], - password: Password["hash"] + email: User["email"], + password: Password["hash"] ) { - const userWithPassword = await prisma.user.findUnique({ - where: { email }, - include: { - password: true, - }, - }); + const userWithPassword = await prisma.user.findUnique({ + where: { email }, + include: { + password: true + } + }); - if (!userWithPassword || !userWithPassword.password) { - return null; - } + if (!userWithPassword || !userWithPassword.password) { + return null; + } - const isValid = await bcrypt.compare( - password, - userWithPassword.password.hash - ); + const isValid = await bcrypt.compare( + password, + userWithPassword.password.hash + ); - if (!isValid) { - return null; - } + if (!isValid) { + return null; + } - const { password: _password, ...userWithoutPassword } = userWithPassword; + const { password: _password, ...userWithoutPassword } = userWithPassword; - return userWithoutPassword; + return userWithoutPassword; } diff --git a/app/root.tsx b/app/root.tsx index 5a5bba8..bfff8c1 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -9,10 +9,12 @@ import { Outlet, Scripts, ScrollRestoration, + useLoaderData, useRouteError } from "@remix-run/react"; import type { ReactNode } from "react"; import { PageLayout } from "~/components/page-layout"; +import { getTags, TagWithStations } from "~/models/tag.server"; import { getUser } from "~/session.server"; import stylesheet from "~/tailwind.css"; @@ -22,8 +24,10 @@ export const links: LinksFunction = () => [ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []) ]; -export const loader = async ({ request }: LoaderArgs) => { - return json({ user: await getUser(request) }); +export async function loader({ request }: LoaderArgs) { + const tags: TagWithStations[] = await getTags(); + const user = await getUser(request); + return json({ user, tags }); }; export type DocumentProps = { @@ -32,7 +36,7 @@ export type DocumentProps = { } -function Document({ title, children }: DocumentProps) { +export function Document({ title, children }: DocumentProps) { return ( @@ -53,9 +57,10 @@ function Document({ title, children }: DocumentProps) { } export default function App() { + const { tags, user } = useLoaderData(); return ( - + diff --git a/app/session.server.ts b/app/session.server.ts index 31a861e..9d5a10e 100644 --- a/app/session.server.ts +++ b/app/session.server.ts @@ -1,97 +1,98 @@ import { createCookieSessionStorage, redirect } from "@remix-run/node"; import invariant from "tiny-invariant"; -import type { User } from "~/models/user.server"; +import type { User, UserWithFavoriteStations } from "~/models/user.server"; import { getUserById } from "~/models/user.server"; invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set"); export const sessionStorage = createCookieSessionStorage({ - cookie: { - name: "__session", - httpOnly: true, - path: "/", - sameSite: "lax", - secrets: [process.env.SESSION_SECRET], - secure: process.env.NODE_ENV === "production", - }, + cookie: { + name: "__session", + httpOnly: true, + path: "/", + sameSite: "lax", + secrets: [process.env.SESSION_SECRET], + secure: process.env.NODE_ENV === "production" + } }); const USER_SESSION_KEY = "userId"; export async function getSession(request: Request) { - const cookie = request.headers.get("Cookie"); - return sessionStorage.getSession(cookie); + const cookie = request.headers.get("Cookie"); + return sessionStorage.getSession(cookie); } export async function getUserId( - request: Request + request: Request ): Promise { - const session = await getSession(request); - const userId = session.get(USER_SESSION_KEY); - return userId; + const session = await getSession(request); + return session.get(USER_SESSION_KEY); } export async function getUser(request: Request) { - const userId = await getUserId(request); - if (userId === undefined) return null; - - const user = await getUserById(userId); - if (user) return user; - - throw await logout(request); + const userId = await getUserId(request); + if (!userId) { + return; + } + const user: UserWithFavoriteStations | null = await getUserById(userId); + if (!user) { + throw await logout(request); + } + return user; } export async function requireUserId( - request: Request, - redirectTo: string = new URL(request.url).pathname + request: Request, + redirectTo: string = new URL(request.url).pathname ) { - const userId = await getUserId(request); - if (!userId) { - const searchParams = new URLSearchParams([["redirectTo", redirectTo]]); - throw redirect(`/login?${searchParams}`); - } - return userId; + const userId = await getUserId(request); + if (!userId) { + const searchParams = new URLSearchParams([["redirectTo", redirectTo]]); + throw redirect(`/login?${searchParams}`); + } + return userId; } export async function requireUser(request: Request) { - const userId = await requireUserId(request); + const userId = await requireUserId(request); - const user = await getUserById(userId); - if (user) return user; + const user = await getUserById(userId); + if (user) return user; - throw await logout(request); + throw await logout(request); } export async function createUserSession({ - request, - userId, - remember, - redirectTo, -}: { - request: Request; - userId: string; - remember: boolean; - redirectTo: string; + request, + userId, + remember, + redirectTo + }: { + request: Request; + userId: string; + remember: boolean; + redirectTo: string; }) { - const session = await getSession(request); - session.set(USER_SESSION_KEY, userId); - return redirect(redirectTo, { - headers: { - "Set-Cookie": await sessionStorage.commitSession(session, { - maxAge: remember - ? 60 * 60 * 24 * 7 // 7 days - : undefined, - }), - }, - }); + const session = await getSession(request); + session.set(USER_SESSION_KEY, userId); + return redirect(redirectTo, { + headers: { + "Set-Cookie": await sessionStorage.commitSession(session, { + maxAge: remember + ? 60 * 60 * 24 * 7 // 7 days + : undefined + }) + } + }); } export async function logout(request: Request) { - const session = await getSession(request); - return redirect("/", { - headers: { - "Set-Cookie": await sessionStorage.destroySession(session), - }, - }); + const session = await getSession(request); + return redirect("/", { + headers: { + "Set-Cookie": await sessionStorage.destroySession(session) + } + }); }