- UI
- Moved PageLayout to /listen routes - Enabled persistent player across navgiation
This commit is contained in:
parent
27cc0ddeb8
commit
b107af9bd9
@ -1,79 +1,128 @@
|
|||||||
import { RadioIcon } from "@heroicons/react/24/solid";
|
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 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 { TagWithStationsClientSide } from "~/models/tag.server";
|
||||||
import type { UserWithFavoriteStationsClientSide } from "~/models/user.server";
|
import type { UserWithFavoriteStationsClientSide } from "~/models/user.server";
|
||||||
|
|
||||||
export type PageLayoutProps = {
|
export type PageLayoutProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
tags: TagWithStationsClientSide[];
|
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<StationContextType>({ station: null });
|
||||||
|
|
||||||
|
export function useStationContext() {
|
||||||
|
return useContext(StationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function ListenLink(props: RemixLinkProps & React.RefAttributes<HTMLAnchorElement>) {
|
||||||
|
const { station } = useStationContext();
|
||||||
|
const url = props.to + (station ? `?station=${station.id}` : "");
|
||||||
|
return <Link {...props} to={url}>{props.children}</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListenNavLink(props: RemixNavLinkProps & React.RefAttributes<HTMLAnchorElement>) {
|
||||||
|
const { station } = useStationContext();
|
||||||
|
const url = props.to + (station ? `?station=${station.id}` : "");
|
||||||
|
return <NavLink {...props} to={url}>{props.children}</NavLink>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageLayout({ children, tags, user, station }: PageLayoutProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="drawer drawer-mobile">
|
<StationContext.Provider value={{ station }}>
|
||||||
<input id="primary-drawer" type="checkbox" className="drawer-toggle" />
|
<div className="drawer drawer-mobile">
|
||||||
<div className="drawer-content flex flex-col">
|
<input id="primary-drawer" type="checkbox" className="drawer-toggle" />
|
||||||
<div className="w-full navbar bg-base-300">
|
<div className="drawer-content flex flex-col">
|
||||||
<div className="flex-none lg:hidden">
|
<div className="w-full navbar bg-base-300 lg:justify-end">
|
||||||
<label htmlFor="primary-drawer" className="btn btn-square btn-ghost">
|
<div className="flex-none lg:hidden">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
<label htmlFor="primary-drawer" className="btn btn-square btn-ghost">
|
||||||
className="inline-block w-6 h-6 stroke-current">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
className="inline-block w-6 h-6 stroke-current">
|
||||||
d="M4 6h16M4 12h16M4 18h16"></path>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||||
</svg>
|
d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
</label>
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 px-2 mx-2 gap-2 lg:hidden">
|
||||||
|
<RadioIcon className="h-8 w-8 p-0" />
|
||||||
|
<h1 className="text-2xl p-0">Awesome Radio</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex-none">
|
||||||
|
{user ?
|
||||||
|
<div className="dropdown dropdown-end">
|
||||||
|
<label tabIndex={0} className="btn btn-ghost btn-circle avatar placeholder">
|
||||||
|
<div className="bg-neutral-focus text-neutral-content rounded-full w-12">
|
||||||
|
<span>LB</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<ul tabIndex={0}
|
||||||
|
className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
|
<li>
|
||||||
|
<Link to="/logout">Logout</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div> :
|
||||||
|
<Link to="/join" className="btn">Join</Link>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 px-2 mx-2 gap-2 lg:hidden">
|
<div className="py-2 px-6">
|
||||||
<RadioIcon className="h-8 w-8 p-0" />
|
{children}
|
||||||
<h1 className="text-2xl p-0">Awesome Radio</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex-none hidden lg:block">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-2 px-6">
|
<div className="drawer-side">
|
||||||
{children}
|
<label htmlFor="primary-drawer" className="drawer-overlay"></label>
|
||||||
|
<ul className="menu menu-compact flex w-80 flex-col bg-base-200 text-base-content p-0 px-4 pt-4">
|
||||||
|
<li className="flex flex-row justify-start gap-2 mb-4">
|
||||||
|
<RadioIcon className="h-8 w-8 p-0" />
|
||||||
|
<h1 className="text-2xl p-0">Awesome Radio</h1>
|
||||||
|
</li>
|
||||||
|
<li className="menu-title">
|
||||||
|
<span>Listen</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ListenNavLink to="/listen" end>Home</ListenNavLink>
|
||||||
|
</li>
|
||||||
|
<li className="menu-title">
|
||||||
|
<span>Tags</span>
|
||||||
|
</li>
|
||||||
|
{tags
|
||||||
|
.filter(tag => tag.stations.length > 0)
|
||||||
|
.map((tag) => {
|
||||||
|
return (
|
||||||
|
<li key={tag.slug}>
|
||||||
|
<ListenNavLink to={`/listen/tag/${tag.slug}`} className="capitalize">
|
||||||
|
{tag.name}
|
||||||
|
<span className="badge badge-outline">{tag.stations?.length ?? 0}</span>
|
||||||
|
</ListenNavLink>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<li className="menu-title">
|
||||||
|
<span>Manage Content</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink to="/sources">Sources</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="drawer-side">
|
</StationContext.Provider>
|
||||||
<label htmlFor="primary-drawer" className="drawer-overlay"></label>
|
|
||||||
<ul className="menu menu-compact flex w-80 flex-col bg-base-200 text-base-content p-0 px-4 pt-4">
|
|
||||||
<li className="flex flex-row justify-start gap-2 mb-4">
|
|
||||||
<RadioIcon className="h-8 w-8 p-0" />
|
|
||||||
<h1 className="text-2xl p-0">Awesome Radio</h1>
|
|
||||||
</li>
|
|
||||||
<li className="menu-title">
|
|
||||||
<span>Listen</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NavLink to="/listen/home">Home</NavLink>
|
|
||||||
</li>
|
|
||||||
<li className="menu-title">
|
|
||||||
<span>Tags</span>
|
|
||||||
</li>
|
|
||||||
{tags
|
|
||||||
.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 className="menu-title">
|
|
||||||
<span>Manage Content</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NavLink to="/sources">Sources</NavLink>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import type { StationWithTagsClientSide } from "~/models/station.server";
|
import type { StationWithTagsClientSide } from "~/models/station.server";
|
||||||
|
|
||||||
export type StationPlayerProps = {
|
export type StationPlayerProps = {
|
||||||
station: StationWithTagsClientSide
|
station: StationWithTagsClientSide | null
|
||||||
};
|
};
|
||||||
|
|
||||||
export function StationPlayer({ station }: StationPlayerProps) {
|
export function StationPlayer({ station }: StationPlayerProps) {
|
||||||
|
if (!station) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed bottom-0 left-0 w-full h-[70px] px-4 py-2 z-50 flex justify-end content-center items-center gap-2"
|
className="fixed bottom-0 left-0 w-full h-[70px] px-4 py-2 z-50 flex justify-end content-center items-center gap-2"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { PlayIcon } from "@heroicons/react/24/solid";
|
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 { ListenLink } from "~/components/page-layout";
|
||||||
import type { StationWithTagsClientSide } from "~/models/station.server";
|
import type { StationWithTagsClientSide } from "~/models/station.server";
|
||||||
import type { Channel } from "~/routes/listen.channel.$channel";
|
import type { Channel } from "~/routes/listen.channel.$channel";
|
||||||
import type { ConvertDatesToStrings } from "~/utils";
|
import type { ConvertDatesToStrings } from "~/utils";
|
||||||
@ -15,12 +16,12 @@ export function StationsGallery({ stations, tag, channel }: StationsGalleryProps
|
|||||||
|
|
||||||
function getStationUrl(id: string): string {
|
function getStationUrl(id: string): string {
|
||||||
if (channel) {
|
if (channel) {
|
||||||
return `/listen/channel/${channel.slug}/${id}`;
|
return `/listen/channel/${channel.slug}?station=${id}`;
|
||||||
}
|
}
|
||||||
if (tag) {
|
if (tag) {
|
||||||
return `/listen/tag/${tag?.slug}/${id}`;
|
return `/listen/tag/${tag?.slug}?station=${id}`;
|
||||||
}
|
}
|
||||||
return `/listen/home/${id}`;
|
return `/listen?station=${id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -35,8 +36,8 @@ export function StationsGallery({ stations, tag, channel }: StationsGalleryProps
|
|||||||
</h2>
|
</h2>
|
||||||
<h2 className="flex gap-1">
|
<h2 className="flex gap-1">
|
||||||
{station.tags.map((t, id) => {
|
{station.tags.map((t, id) => {
|
||||||
return <Link key={id} to={`/listen/tag/${t.tag.slug}`}
|
return <ListenLink key={id} to={`/listen/tag/${t.tag.slug}`}
|
||||||
className="badge badge-secondary">{t.tag.name}</Link>;
|
className="badge badge-secondary">{t.tag.name}</ListenLink>;
|
||||||
})}
|
})}
|
||||||
</h2>
|
</h2>
|
||||||
<p>{station.description}</p>
|
<p>{station.description}</p>
|
||||||
|
19
app/root.tsx
19
app/root.tsx
@ -1,6 +1,5 @@
|
|||||||
import { cssBundleHref } from "@remix-run/css-bundle";
|
import { cssBundleHref } from "@remix-run/css-bundle";
|
||||||
import type { LinksFunction, LoaderArgs } from "@remix-run/node";
|
import type { LinksFunction } from "@remix-run/node";
|
||||||
import { json } from "@remix-run/node";
|
|
||||||
import {
|
import {
|
||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
Links,
|
Links,
|
||||||
@ -9,14 +8,9 @@ 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 { getTags, TagWithStations } from "~/models/tag.server";
|
|
||||||
|
|
||||||
import { getUser } from "~/session.server";
|
|
||||||
import stylesheet from "~/tailwind.css";
|
import stylesheet from "~/tailwind.css";
|
||||||
|
|
||||||
export const links: LinksFunction = () => [
|
export const links: LinksFunction = () => [
|
||||||
@ -24,12 +18,6 @@ export const links: LinksFunction = () => [
|
|||||||
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : [])
|
...(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 = {
|
export type DocumentProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -57,12 +45,9 @@ export function Document({ title, children }: DocumentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { tags, user } = useLoaderData<typeof loader>();
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<PageLayout tags={tags} user={user}>
|
<Outlet />
|
||||||
<Outlet />
|
|
||||||
</PageLayout>
|
|
||||||
</Document>
|
</Document>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import type { V2_MetaFunction } from "@remix-run/node";
|
import type { V2_MetaFunction } from "@remix-run/node";
|
||||||
|
|
||||||
import { useOptionalUser } from "~/utils";
|
|
||||||
|
|
||||||
export const meta: V2_MetaFunction = () => [{ title: "Awesome Radio" }];
|
export const meta: V2_MetaFunction = () => [{ title: "Awesome Radio" }];
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const user = useOptionalUser();
|
|
||||||
return (
|
return (
|
||||||
<div className="hero bg-base-200">
|
<div className="hero bg-base-200">
|
||||||
<div className="hero-content text-center">
|
<div className="hero-content text-center">
|
||||||
|
@ -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<typeof loader>();
|
|
||||||
return (
|
|
||||||
<StationPlayer station={station} />
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
import ListenChanelStation from "~/routes/listen.channel.$channel.$station";
|
|
||||||
|
|
||||||
export { loader } from "~/routes/listen.channel.$channel.$station";
|
|
||||||
export default ListenChanelStation;
|
|
@ -1,7 +0,0 @@
|
|||||||
import ListenChanelStation from "~/routes/listen.channel.$channel.$station";
|
|
||||||
|
|
||||||
export { loader } from "~/routes/listen.channel.$channel.$station";
|
|
||||||
export default ListenChanelStation;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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() {
|
export default function ListenLayout() {
|
||||||
|
const { tags, user, station } = useLoaderData<typeof loader>();
|
||||||
return (
|
return (
|
||||||
<Outlet />
|
<PageLayout tags={tags} user={user} station={station}>
|
||||||
|
<Outlet />
|
||||||
|
<StationPlayer station={station} />
|
||||||
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,168 +8,168 @@ import { createUserSession, getUserId } from "~/session.server";
|
|||||||
import { safeRedirect, validateEmail } from "~/utils";
|
import { safeRedirect, validateEmail } from "~/utils";
|
||||||
|
|
||||||
export const loader = async ({ request }: LoaderArgs) => {
|
export const loader = async ({ request }: LoaderArgs) => {
|
||||||
const userId = await getUserId(request);
|
const userId = await getUserId(request);
|
||||||
if (userId) return redirect("/");
|
if (userId) return redirect("/");
|
||||||
return json({});
|
return json({});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const action = async ({ request }: ActionArgs) => {
|
export const action = async ({ request }: ActionArgs) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const email = formData.get("email");
|
const email = formData.get("email");
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
const redirectTo = safeRedirect(formData.get("redirectTo"), "/");
|
const redirectTo = safeRedirect(formData.get("redirectTo"), "/");
|
||||||
const remember = formData.get("remember");
|
const remember = formData.get("remember");
|
||||||
|
|
||||||
if (!validateEmail(email)) {
|
if (!validateEmail(email)) {
|
||||||
return json(
|
return json(
|
||||||
{ errors: { email: "Email is invalid", password: null } },
|
{ errors: { email: "Email is invalid", password: null } },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof password !== "string" || password.length === 0) {
|
if (typeof password !== "string" || password.length === 0) {
|
||||||
return json(
|
return json(
|
||||||
{ errors: { email: null, password: "Password is required" } },
|
{ errors: { email: null, password: "Password is required" } },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
return json(
|
return json(
|
||||||
{ errors: { email: null, password: "Password is too short" } },
|
{ errors: { email: null, password: "Password is too short" } },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await verifyLogin(email, password);
|
const user = await verifyLogin(email, password);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return json(
|
return json(
|
||||||
{ errors: { email: "Invalid email or password", password: null } },
|
{ errors: { email: "Invalid email or password", password: null } },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return createUserSession({
|
return createUserSession({
|
||||||
redirectTo,
|
redirectTo,
|
||||||
remember: remember === "on" ? true : false,
|
remember: remember === "on",
|
||||||
request,
|
request,
|
||||||
userId: user.id,
|
userId: user.id
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const meta: V2_MetaFunction = () => [{ title: "Login" }];
|
export const meta: V2_MetaFunction = () => [{ title: "Login" }];
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const redirectTo = searchParams.get("redirectTo") || "/";
|
const redirectTo = searchParams.get("redirectTo") || "/listen";
|
||||||
const actionData = useActionData<typeof action>();
|
const actionData = useActionData<typeof action>();
|
||||||
const emailRef = useRef<HTMLInputElement>(null);
|
const emailRef = useRef<HTMLInputElement>(null);
|
||||||
const passwordRef = useRef<HTMLInputElement>(null);
|
const passwordRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (actionData?.errors?.email) {
|
if (actionData?.errors?.email) {
|
||||||
emailRef.current?.focus();
|
emailRef.current?.focus();
|
||||||
} else if (actionData?.errors?.password) {
|
} else if (actionData?.errors?.password) {
|
||||||
passwordRef.current?.focus();
|
passwordRef.current?.focus();
|
||||||
}
|
}
|
||||||
}, [actionData]);
|
}, [actionData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-full flex-col justify-center">
|
<div className="flex min-h-full flex-col justify-center">
|
||||||
<div className="mx-auto w-full max-w-md px-8">
|
<div className="mx-auto w-full max-w-md px-8">
|
||||||
<Form method="post" className="space-y-6">
|
<Form method="post" className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="block text-sm font-medium text-gray-700"
|
className="block text-sm font-medium text-gray-700"
|
||||||
>
|
>
|
||||||
Email address
|
Email address
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
ref={emailRef}
|
ref={emailRef}
|
||||||
id="email"
|
id="email"
|
||||||
required
|
required
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
aria-invalid={actionData?.errors?.email ? true : undefined}
|
aria-invalid={actionData?.errors?.email ? true : undefined}
|
||||||
aria-describedby="email-error"
|
aria-describedby="email-error"
|
||||||
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
|
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
|
||||||
/>
|
/>
|
||||||
{actionData?.errors?.email ? (
|
{actionData?.errors?.email ? (
|
||||||
<div className="pt-1 text-red-700" id="email-error">
|
<div className="pt-1 text-red-700" id="email-error">
|
||||||
{actionData.errors.email}
|
{actionData.errors.email}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
className="block text-sm font-medium text-gray-700"
|
className="block text-sm font-medium text-gray-700"
|
||||||
>
|
>
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
ref={passwordRef}
|
ref={passwordRef}
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
aria-invalid={actionData?.errors?.password ? true : undefined}
|
aria-invalid={actionData?.errors?.password ? true : undefined}
|
||||||
aria-describedby="password-error"
|
aria-describedby="password-error"
|
||||||
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
|
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
|
||||||
/>
|
/>
|
||||||
{actionData?.errors?.password ? (
|
{actionData?.errors?.password ? (
|
||||||
<div className="pt-1 text-red-700" id="password-error">
|
<div className="pt-1 text-red-700" id="password-error">
|
||||||
{actionData.errors.password}
|
{actionData.errors.password}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="redirectTo" value={redirectTo} />
|
<input type="hidden" name="redirectTo" value={redirectTo} />
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
|
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
|
||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
id="remember"
|
id="remember"
|
||||||
name="remember"
|
name="remember"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="remember"
|
htmlFor="remember"
|
||||||
className="ml-2 block text-sm text-gray-900"
|
className="ml-2 block text-sm text-gray-900"
|
||||||
>
|
>
|
||||||
Remember me
|
Remember me
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-sm text-gray-500">
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<Link
|
||||||
|
className="text-blue-500 underline"
|
||||||
|
to={{
|
||||||
|
pathname: "/join",
|
||||||
|
search: searchParams.toString()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-sm text-gray-500">
|
</div>
|
||||||
Don't have an account?{" "}
|
);
|
||||||
<Link
|
|
||||||
className="text-blue-500 underline"
|
|
||||||
to={{
|
|
||||||
pathname: "/join",
|
|
||||||
search: searchParams.toString(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Sign up
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import type { ActionArgs } from "@remix-run/node";
|
import type { LoaderArgs } from "@remix-run/node";
|
||||||
import { redirect } from "@remix-run/node";
|
|
||||||
|
|
||||||
import { logout } from "~/session.server";
|
import { logout } from "~/session.server";
|
||||||
|
|
||||||
export const action = async ({ request }: ActionArgs) => logout(request);
|
export async function loader({ request }: LoaderArgs) {
|
||||||
|
return logout(request);
|
||||||
export const loader = async () => redirect("/");
|
};
|
||||||
|
@ -90,7 +90,7 @@ export async function createUserSession({
|
|||||||
|
|
||||||
export async function logout(request: Request) {
|
export async function logout(request: Request) {
|
||||||
const session = await getSession(request);
|
const session = await getSession(request);
|
||||||
return redirect("/", {
|
return redirect("/listen", {
|
||||||
headers: {
|
headers: {
|
||||||
"Set-Cookie": await sessionStorage.destroySession(session)
|
"Set-Cookie": await sessionStorage.destroySession(session)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user