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