From b107af9bd98a4d565a521bb0ef82494c3a1b496b Mon Sep 17 00:00:00 2001 From: Luke Bunselmeyer Date: Sun, 7 May 2023 20:46:19 -0400 Subject: [PATCH] - UI - Moved PageLayout to /listen routes - Enabled persistent player across navgiation --- app/components/page-layout.tsx | 171 +++++++---- app/components/station-player.tsx | 5 +- app/components/stations-gallery.tsx | 11 +- app/root.tsx | 19 +- app/routes/_index.tsx | 3 - .../{listen.home.tsx => listen._index.tsx} | 0 .../listen.channel.$channel.$station.tsx | 27 -- app/routes/listen.home.$station.tsx | 4 - app/routes/listen.tag.$tag.$station.tsx | 7 - app/routes/listen.tsx | 25 +- app/routes/login.tsx | 288 +++++++++--------- app/routes/logout.tsx | 9 +- app/session.server.ts | 2 +- 13 files changed, 294 insertions(+), 277 deletions(-) rename app/routes/{listen.home.tsx => listen._index.tsx} (100%) delete mode 100644 app/routes/listen.channel.$channel.$station.tsx delete mode 100644 app/routes/listen.home.$station.tsx delete mode 100644 app/routes/listen.tag.$tag.$station.tsx diff --git a/app/components/page-layout.tsx b/app/components/page-layout.tsx index c8c01b1..6e9f0f4 100644 --- a/app/components/page-layout.tsx +++ b/app/components/page-layout.tsx @@ -1,79 +1,128 @@ import { RadioIcon } from "@heroicons/react/24/solid"; -import { NavLink } from "@remix-run/react"; +import { Link, NavLink } from "@remix-run/react"; +import type { RemixLinkProps } from "@remix-run/react/dist/components"; +import { RemixNavLinkProps } from "@remix-run/react/dist/components"; import type { ReactNode } from "react"; +import * as React from "react"; +import { createContext, useContext } from "react"; +import type { StationWithTagsClientSide } from "~/models/station.server"; import type { TagWithStationsClientSide } from "~/models/tag.server"; import type { UserWithFavoriteStationsClientSide } from "~/models/user.server"; export type PageLayoutProps = { children: ReactNode; tags: TagWithStationsClientSide[]; - user?: UserWithFavoriteStationsClientSide + user?: UserWithFavoriteStationsClientSide; + station: StationWithTagsClientSide | null; } -export function PageLayout({ children, tags, user }: PageLayoutProps) { +export type StationContextType = { + station: StationWithTagsClientSide | null +} + +const StationContext = createContext({ station: null }); + +export function useStationContext() { + return useContext(StationContext); +} + + +export function ListenLink(props: RemixLinkProps & React.RefAttributes) { + const { station } = useStationContext(); + const url = props.to + (station ? `?station=${station.id}` : ""); + return {props.children}; +} + +export function ListenNavLink(props: RemixNavLinkProps & React.RefAttributes) { + const { station } = useStationContext(); + const url = props.to + (station ? `?station=${station.id}` : ""); + return {props.children}; +} + +export function PageLayout({ children, tags, user, station }: PageLayoutProps) { + return ( -
- -
-
-
- + +
+ +
+
+
+ +
+
+ +

Awesome Radio

+
+
+ {user ? +
+ +
    +
  • + Logout +
  • +
+
: + Join + } + +
-
- -

Awesome Radio

-
-
+
+ {children}
-
- {children} +
+ +
    +
  • + +

    Awesome Radio

    +
  • +
  • + Listen +
  • +
  • + Home +
  • +
  • + Tags +
  • + {tags + .filter(tag => tag.stations.length > 0) + .map((tag) => { + return ( +
  • + + {tag.name} + {tag.stations?.length ?? 0} + +
  • + ); + })} +
  • + Manage Content +
  • +
  • + Sources +
  • + +
+
-
- -
    -
  • - -

    Awesome Radio

    -
  • -
  • - Listen -
  • -
  • - Home -
  • -
  • - Tags -
  • - {tags - .filter(tag => tag.stations.length > 0) - .map((tag) => { - return ( -
  • - - {tag.name} - {tag.stations?.length ?? 0} - -
  • - ); - })} -
  • - Manage Content -
  • -
  • - Sources -
  • - -
- -
-
+ ); } diff --git a/app/components/station-player.tsx b/app/components/station-player.tsx index fb095b6..d69834f 100644 --- a/app/components/station-player.tsx +++ b/app/components/station-player.tsx @@ -1,10 +1,13 @@ import type { StationWithTagsClientSide } from "~/models/station.server"; export type StationPlayerProps = { - station: StationWithTagsClientSide + station: StationWithTagsClientSide | null }; export function StationPlayer({ station }: StationPlayerProps) { + if (!station) { + return <>; + } return (

{station.tags.map((t, id) => { - return {t.tag.name}; + return {t.tag.name}; })}

{station.description}

diff --git a/app/root.tsx b/app/root.tsx index bfff8c1..6fe7f82 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,6 +1,5 @@ import { cssBundleHref } from "@remix-run/css-bundle"; -import type { LinksFunction, LoaderArgs } from "@remix-run/node"; -import { json } from "@remix-run/node"; +import type { LinksFunction } from "@remix-run/node"; import { isRouteErrorResponse, Links, @@ -9,14 +8,9 @@ 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"; export const links: LinksFunction = () => [ @@ -24,12 +18,6 @@ export const links: LinksFunction = () => [ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []) ]; -export async function loader({ request }: LoaderArgs) { - const tags: TagWithStations[] = await getTags(); - const user = await getUser(request); - return json({ user, tags }); -}; - export type DocumentProps = { children: ReactNode; title?: string; @@ -57,12 +45,9 @@ export function Document({ title, children }: DocumentProps) { } export default function App() { - const { tags, user } = useLoaderData(); return ( - - - + ); } diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index edfebff..ace65ab 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,11 +1,8 @@ import type { V2_MetaFunction } from "@remix-run/node"; -import { useOptionalUser } from "~/utils"; - export const meta: V2_MetaFunction = () => [{ title: "Awesome Radio" }]; export default function Index() { - const user = useOptionalUser(); return (
diff --git a/app/routes/listen.home.tsx b/app/routes/listen._index.tsx similarity index 100% rename from app/routes/listen.home.tsx rename to app/routes/listen._index.tsx diff --git a/app/routes/listen.channel.$channel.$station.tsx b/app/routes/listen.channel.$channel.$station.tsx deleted file mode 100644 index c4b3059..0000000 --- a/app/routes/listen.channel.$channel.$station.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { LoaderArgs } from "@remix-run/node"; -import { json } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; -import { StationPlayer } from "~/components/station-player"; -import { getStationById } from "~/models/station.server"; -import { notFound } from "~/utils"; - - -export async function loader({ params }: LoaderArgs) { - if (!params.station) { - throw notFound(); - } - - const station = await getStationById(params.station); - if (!station) { - throw notFound(); - } - return json({ station }); -} - -export default function ListenChanelStation() { - const { station } = useLoaderData(); - return ( - - ); - -} diff --git a/app/routes/listen.home.$station.tsx b/app/routes/listen.home.$station.tsx deleted file mode 100644 index cd545d9..0000000 --- a/app/routes/listen.home.$station.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import ListenChanelStation from "~/routes/listen.channel.$channel.$station"; - -export { loader } from "~/routes/listen.channel.$channel.$station"; -export default ListenChanelStation; diff --git a/app/routes/listen.tag.$tag.$station.tsx b/app/routes/listen.tag.$tag.$station.tsx deleted file mode 100644 index 3a0155b..0000000 --- a/app/routes/listen.tag.$tag.$station.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import ListenChanelStation from "~/routes/listen.channel.$channel.$station"; - -export { loader } from "~/routes/listen.channel.$channel.$station"; -export default ListenChanelStation; - - - diff --git a/app/routes/listen.tsx b/app/routes/listen.tsx index bce6b09..b397e02 100644 --- a/app/routes/listen.tsx +++ b/app/routes/listen.tsx @@ -1,8 +1,29 @@ -import { Outlet } from "@remix-run/react"; +import type { LoaderArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { Outlet, useLoaderData } from "@remix-run/react"; +import { PageLayout } from "~/components/page-layout"; +import { StationPlayer } from "~/components/station-player"; +import type { StationWithTags } from "~/models/station.server"; +import { getStationById } from "~/models/station.server"; +import type { TagWithStations } from "~/models/tag.server"; +import { getTags } from "~/models/tag.server"; +import { getUser } from "~/session.server"; +export async function loader({ request }: LoaderArgs) { + const tags: TagWithStations[] = await getTags(); + const user = await getUser(request); + const url = new URL(request.url); + const stationId = url.searchParams.get("station"); + const station: StationWithTags | null = stationId ? await getStationById(stationId) : null; + return json({ user, tags, station }); +} export default function ListenLayout() { + const { tags, user, station } = useLoaderData(); return ( - + + + + ); } diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 59fa21d..a4597f1 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -8,168 +8,168 @@ import { createUserSession, getUserId } from "~/session.server"; import { safeRedirect, validateEmail } from "~/utils"; export const loader = async ({ request }: LoaderArgs) => { - const userId = await getUserId(request); - if (userId) return redirect("/"); - return json({}); + const userId = await getUserId(request); + if (userId) return redirect("/"); + return json({}); }; export const action = async ({ request }: ActionArgs) => { - const formData = await request.formData(); - const email = formData.get("email"); - const password = formData.get("password"); - const redirectTo = safeRedirect(formData.get("redirectTo"), "/"); - const remember = formData.get("remember"); + const formData = await request.formData(); + const email = formData.get("email"); + const password = formData.get("password"); + const redirectTo = safeRedirect(formData.get("redirectTo"), "/"); + const remember = formData.get("remember"); - if (!validateEmail(email)) { - return json( - { errors: { email: "Email is invalid", password: null } }, - { status: 400 } - ); - } + if (!validateEmail(email)) { + return json( + { errors: { email: "Email is invalid", password: null } }, + { status: 400 } + ); + } - if (typeof password !== "string" || password.length === 0) { - return json( - { errors: { email: null, password: "Password is required" } }, - { status: 400 } - ); - } + if (typeof password !== "string" || password.length === 0) { + return json( + { errors: { email: null, password: "Password is required" } }, + { status: 400 } + ); + } - if (password.length < 8) { - return json( - { errors: { email: null, password: "Password is too short" } }, - { status: 400 } - ); - } + if (password.length < 8) { + return json( + { errors: { email: null, password: "Password is too short" } }, + { status: 400 } + ); + } - const user = await verifyLogin(email, password); + const user = await verifyLogin(email, password); - if (!user) { - return json( - { errors: { email: "Invalid email or password", password: null } }, - { status: 400 } - ); - } + if (!user) { + return json( + { errors: { email: "Invalid email or password", password: null } }, + { status: 400 } + ); + } - return createUserSession({ - redirectTo, - remember: remember === "on" ? true : false, - request, - userId: user.id, - }); + return createUserSession({ + redirectTo, + remember: remember === "on", + request, + userId: user.id + }); }; export const meta: V2_MetaFunction = () => [{ title: "Login" }]; export default function LoginPage() { - const [searchParams] = useSearchParams(); - const redirectTo = searchParams.get("redirectTo") || "/"; + const [searchParams] = useSearchParams(); + const redirectTo = searchParams.get("redirectTo") || "/listen"; const actionData = useActionData(); - const emailRef = useRef(null); - const passwordRef = useRef(null); + const emailRef = useRef(null); + const passwordRef = useRef(null); - useEffect(() => { - if (actionData?.errors?.email) { - emailRef.current?.focus(); - } else if (actionData?.errors?.password) { - passwordRef.current?.focus(); - } - }, [actionData]); + useEffect(() => { + if (actionData?.errors?.email) { + emailRef.current?.focus(); + } else if (actionData?.errors?.password) { + passwordRef.current?.focus(); + } + }, [actionData]); - return ( -
-
-
-
- -
- - {actionData?.errors?.email ? ( -
- {actionData.errors.email} -
- ) : null} -
-
+ return ( +
+
+ +
+ +
+ + {actionData?.errors?.email ? ( +
+ {actionData.errors.email} +
+ ) : null} +
+
-
- -
- - {actionData?.errors?.password ? ( -
- {actionData.errors.password} -
- ) : null} -
-
+
+ +
+ + {actionData?.errors?.password ? ( +
+ {actionData.errors.password} +
+ ) : null} +
+
- - -
-
- - + + +
+
+ + +
+
+ Don't have an account?{" "} + + Sign up + +
+
+
-
- Don't have an account?{" "} - - Sign up - -
-
- -
-
- ); +
+ ); } diff --git a/app/routes/logout.tsx b/app/routes/logout.tsx index 541794d..255b5c2 100644 --- a/app/routes/logout.tsx +++ b/app/routes/logout.tsx @@ -1,8 +1,7 @@ -import type { ActionArgs } from "@remix-run/node"; -import { redirect } from "@remix-run/node"; +import type { LoaderArgs } from "@remix-run/node"; import { logout } from "~/session.server"; -export const action = async ({ request }: ActionArgs) => logout(request); - -export const loader = async () => redirect("/"); +export async function loader({ request }: LoaderArgs) { + return logout(request); +}; diff --git a/app/session.server.ts b/app/session.server.ts index 9d5a10e..6242060 100644 --- a/app/session.server.ts +++ b/app/session.server.ts @@ -90,7 +90,7 @@ export async function createUserSession({ export async function logout(request: Request) { const session = await getSession(request); - return redirect("/", { + return redirect("/listen", { headers: { "Set-Cookie": await sessionStorage.destroySession(session) }