- Updated side nav to list tags
This commit is contained in:
Luke Bunselmeyer 2023-05-07 16:21:15 -04:00
parent 5d0a7623e3
commit 27cc0ddeb8
7 changed files with 188 additions and 113 deletions

View File

@ -1,12 +1,16 @@
import { RadioIcon } from "@heroicons/react/24/solid"; import { RadioIcon } from "@heroicons/react/24/solid";
import { NavLink } from "@remix-run/react"; import { NavLink } from "@remix-run/react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { TagWithStationsClientSide } from "~/models/tag.server";
import type { UserWithFavoriteStationsClientSide } from "~/models/user.server";
export type PageLayoutProps = { export type PageLayoutProps = {
children: ReactNode; children: ReactNode;
tags: TagWithStationsClientSide[];
user?: UserWithFavoriteStationsClientSide
} }
export function PageLayout({ children }: PageLayoutProps) { export function PageLayout({ children, tags, user }: PageLayoutProps) {
return ( return (
<div className="drawer drawer-mobile"> <div className="drawer drawer-mobile">
<input id="primary-drawer" type="checkbox" className="drawer-toggle" /> <input id="primary-drawer" type="checkbox" className="drawer-toggle" />
@ -26,10 +30,6 @@ export function PageLayout({ children }: PageLayoutProps) {
<h1 className="text-2xl p-0">Awesome Radio</h1> <h1 className="text-2xl p-0">Awesome Radio</h1>
</div> </div>
<div className="flex-none hidden lg:block"> <div className="flex-none hidden lg:block">
<ul className="menu menu-horizontal">
<li><a>Topnav Item 1</a></li>
<li><a>Topnav Item 2</a></li>
</ul>
</div> </div>
</div> </div>
<div className="py-2 px-6"> <div className="py-2 px-6">
@ -49,12 +49,21 @@ export function PageLayout({ children }: PageLayoutProps) {
<li> <li>
<NavLink to="/listen/home">Home</NavLink> <NavLink to="/listen/home">Home</NavLink>
</li> </li>
<li> <li className="menu-title">
<NavLink to="/listen/channel/music">Music</NavLink> <span>Tags</span>
</li> </li>
<li> {tags
<NavLink to="/listen/channel/news">News & Talk</NavLink> .filter(tag => tag.stations.length > 0)
.map((tag) => {
return (
<li key={tag.slug}>
<NavLink to={`/listen/tag/${tag.slug}`} className="capitalize">
{tag.name}
<span className="badge badge-outline">{tag.stations?.length ?? 0}</span>
</NavLink>
</li> </li>
);
})}
<li className="menu-title"> <li className="menu-title">
<span>Manage Content</span> <span>Manage Content</span>
</li> </li>

View File

@ -1,3 +1,4 @@
import { PlayIcon } from "@heroicons/react/24/solid";
import type { Tag } from "@prisma/client"; import type { Tag } from "@prisma/client";
import { Link } from "@remix-run/react"; import { Link } from "@remix-run/react";
import type { StationWithTagsClientSide } from "~/models/station.server"; import type { StationWithTagsClientSide } from "~/models/station.server";
@ -40,7 +41,10 @@ export function StationsGallery({ stations, tag, channel }: StationsGalleryProps
</h2> </h2>
<p>{station.description}</p> <p>{station.description}</p>
<div className="card-actions justify-end"> <div className="card-actions justify-end">
<Link to={getStationUrl(station.id)} className="btn btn-primary">Listen Now</Link> <Link to={getStationUrl(station.id)} className="btn btn-primary gap-2">
<PlayIcon className="h-6 w-6" />
Listen Now
</Link>
</div> </div>
</div> </div>
</div> </div>

View File

@ -28,6 +28,7 @@ export function getStations(reliability: number = 80) {
} }
}, },
orderBy: [ orderBy: [
{ reliability: "desc" },
{ popularity: "desc" } { popularity: "desc" }
] ]
}); });
@ -54,6 +55,7 @@ export function findStationsByTags(tags: string[], reliability: number = 80) {
} }
}, },
orderBy: [ orderBy: [
{ reliability: "desc" },
{ popularity: "desc" } { popularity: "desc" }
] ]
}); });

View File

@ -1,9 +1,53 @@
import type { Prisma } from "@prisma/client";
import { prisma } from "~/db.server"; import { prisma } from "~/db.server";
import type { PrismaTxClient } from "~/models/station.server"; import type { PrismaTxClient } from "~/models/station.server";
import type { ConvertDatesToStrings } from "~/utils";
import { slugify } from "~/utils"; import { slugify } from "~/utils";
export function findTagBySlug(slug: string) { export type TagWithStations = NonNullable<Prisma.PromiseReturnType<typeof findTagBySlug>>;
return prisma.tag.findUnique({ where: { slug } }); export type TagWithStationsClientSide = ConvertDatesToStrings<TagWithStations>;
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) { export function upsertTagOnName(tag: string, p: PrismaTxClient = prisma) {

View File

@ -1,12 +1,22 @@
import type { Password, User } from "@prisma/client"; import type { Password, Prisma, User } from "@prisma/client";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { prisma } from "~/db.server"; import { prisma } from "~/db.server";
import type { ConvertDatesToStrings } from "~/utils";
export type { User } from "@prisma/client"; export type { User } from "@prisma/client";
export type UserWithFavoriteStations = NonNullable<Prisma.PromiseReturnType<typeof getUserById>>;
export type UserWithFavoriteStationsClientSide = ConvertDatesToStrings<UserWithFavoriteStations>;
export async function getUserById(id: User["id"]) { 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"]) { export async function getUserByEmail(email: User["email"]) {
@ -21,10 +31,10 @@ export async function createUser(email: User["email"], password: string) {
email, email,
password: { password: {
create: { create: {
hash: hashedPassword, hash: hashedPassword
}, }
}, }
}, }
}); });
} }
@ -39,8 +49,8 @@ export async function verifyLogin(
const userWithPassword = await prisma.user.findUnique({ const userWithPassword = await prisma.user.findUnique({
where: { email }, where: { email },
include: { include: {
password: true, password: true
}, }
}); });
if (!userWithPassword || !userWithPassword.password) { if (!userWithPassword || !userWithPassword.password) {

View File

@ -9,10 +9,12 @@ import {
Outlet, Outlet,
Scripts, Scripts,
ScrollRestoration, ScrollRestoration,
useLoaderData,
useRouteError useRouteError
} from "@remix-run/react"; } from "@remix-run/react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { PageLayout } from "~/components/page-layout"; import { PageLayout } from "~/components/page-layout";
import { getTags, TagWithStations } from "~/models/tag.server";
import { getUser } from "~/session.server"; import { getUser } from "~/session.server";
import stylesheet from "~/tailwind.css"; import stylesheet from "~/tailwind.css";
@ -22,8 +24,10 @@ export const links: LinksFunction = () => [
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []) ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : [])
]; ];
export const loader = async ({ request }: LoaderArgs) => { export async function loader({ request }: LoaderArgs) {
return json({ user: await getUser(request) }); const tags: TagWithStations[] = await getTags();
const user = await getUser(request);
return json({ user, tags });
}; };
export type DocumentProps = { export type DocumentProps = {
@ -32,7 +36,7 @@ export type DocumentProps = {
} }
function Document({ title, children }: DocumentProps) { export function Document({ title, children }: DocumentProps) {
return ( return (
<html lang="en" className="h-full"> <html lang="en" className="h-full">
<head> <head>
@ -53,9 +57,10 @@ function Document({ title, children }: DocumentProps) {
} }
export default function App() { export default function App() {
const { tags, user } = useLoaderData<typeof loader>();
return ( return (
<Document> <Document>
<PageLayout> <PageLayout tags={tags} user={user}>
<Outlet /> <Outlet />
</PageLayout> </PageLayout>
</Document> </Document>

View File

@ -1,7 +1,7 @@
import { createCookieSessionStorage, redirect } from "@remix-run/node"; import { createCookieSessionStorage, redirect } from "@remix-run/node";
import invariant from "tiny-invariant"; 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"; import { getUserById } from "~/models/user.server";
invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set"); invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");
@ -13,8 +13,8 @@ export const sessionStorage = createCookieSessionStorage({
path: "/", path: "/",
sameSite: "lax", sameSite: "lax",
secrets: [process.env.SESSION_SECRET], secrets: [process.env.SESSION_SECRET],
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production"
}, }
}); });
const USER_SESSION_KEY = "userId"; const USER_SESSION_KEY = "userId";
@ -28,19 +28,20 @@ export async function getUserId(
request: Request request: Request
): Promise<User["id"] | undefined> { ): Promise<User["id"] | undefined> {
const session = await getSession(request); const session = await getSession(request);
const userId = session.get(USER_SESSION_KEY); return session.get(USER_SESSION_KEY);
return userId;
} }
export async function getUser(request: Request) { export async function getUser(request: Request) {
const userId = await getUserId(request); const userId = await getUserId(request);
if (userId === undefined) return null; if (!userId) {
return;
const user = await getUserById(userId); }
if (user) return user; const user: UserWithFavoriteStations | null = await getUserById(userId);
if (!user) {
throw await logout(request); throw await logout(request);
} }
return user;
}
export async function requireUserId( export async function requireUserId(
request: Request, request: Request,
@ -67,7 +68,7 @@ export async function createUserSession({
request, request,
userId, userId,
remember, remember,
redirectTo, redirectTo
}: { }: {
request: Request; request: Request;
userId: string; userId: string;
@ -81,9 +82,9 @@ export async function createUserSession({
"Set-Cookie": await sessionStorage.commitSession(session, { "Set-Cookie": await sessionStorage.commitSession(session, {
maxAge: remember maxAge: remember
? 60 * 60 * 24 * 7 // 7 days ? 60 * 60 * 24 * 7 // 7 days
: undefined, : undefined
}), })
}, }
}); });
} }
@ -91,7 +92,7 @@ export async function logout(request: Request) {
const session = await getSession(request); const session = await getSession(request);
return redirect("/", { return redirect("/", {
headers: { headers: {
"Set-Cookie": await sessionStorage.destroySession(session), "Set-Cookie": await sessionStorage.destroySession(session)
}, }
}); });
} }