- UI
- Moved source routes under `/listen` and required user to access - Fixed player color - Fixed page title - Updated favicon
@ -1,7 +1,6 @@
|
||||
import { RadioIcon } from "@heroicons/react/24/solid";
|
||||
import { RadioIcon, UserCircleIcon } from "@heroicons/react/24/solid";
|
||||
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 { RemixLinkProps, RemixNavLinkProps } from "@remix-run/react/dist/components";
|
||||
import type { ReactNode } from "react";
|
||||
import * as React from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
@ -39,6 +38,26 @@ export function ListenNavLink(props: RemixNavLinkProps & React.RefAttributes<HTM
|
||||
return <NavLink {...props} to={url}>{props.children}</NavLink>;
|
||||
}
|
||||
|
||||
export type ManageContentNavProps = {
|
||||
user?: UserWithFavoriteStationsClientSide;
|
||||
};
|
||||
|
||||
export function ManageContentNav({ user }: ManageContentNavProps) {
|
||||
if (!user) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<li className="menu-title">
|
||||
<span>Manage Content</span>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/listen/sources">Sources</NavLink>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageLayout({ children, tags, user, station }: PageLayoutProps) {
|
||||
|
||||
return (
|
||||
@ -64,12 +83,10 @@ export function PageLayout({ children, tags, user, station }: PageLayoutProps) {
|
||||
{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>
|
||||
<UserCircleIcon className="w-8 h-8" />
|
||||
</label>
|
||||
<ul tabIndex={0}
|
||||
className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
|
||||
className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-200 rounded-box w-30">
|
||||
<li>
|
||||
<Link to="/logout">Logout</Link>
|
||||
</li>
|
||||
@ -112,13 +129,7 @@ export function PageLayout({ children, tags, user, station }: PageLayoutProps) {
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
<li className="menu-title">
|
||||
<span>Manage Content</span>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/sources">Sources</NavLink>
|
||||
</li>
|
||||
|
||||
<ManageContentNav user={user} />
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
@ -10,8 +10,7 @@ export function StationPlayer({ station }: StationPlayerProps) {
|
||||
}
|
||||
return (
|
||||
<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"
|
||||
data-theme="aqua">
|
||||
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 bg-accent text-accent-content">
|
||||
<h3 className="text-xl">Now Playing: <strong>{station.name}</strong></h3>
|
||||
|
||||
<audio controls autoPlay src={station.streamUrl}>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { cssBundleHref } from "@remix-run/css-bundle";
|
||||
import type { LinksFunction } from "@remix-run/node";
|
||||
import { V2_MetaFunction } from "@remix-run/node";
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
@ -18,6 +19,8 @@ export const links: LinksFunction = () => [
|
||||
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : [])
|
||||
];
|
||||
|
||||
export const meta: V2_MetaFunction = () => [{ title: "Awesome Radio" }];
|
||||
|
||||
export type DocumentProps = {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
|
@ -1,7 +1,3 @@
|
||||
import type { V2_MetaFunction } from "@remix-run/node";
|
||||
|
||||
export const meta: V2_MetaFunction = () => [{ title: "Awesome Radio" }];
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<div className="hero bg-base-200">
|
||||
|
@ -3,22 +3,27 @@ import type { ActionArgs, LoaderArgs } from "@remix-run/node";
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { Link, useLoaderData } from "@remix-run/react";
|
||||
import { Breadcrumbs } from "~/components/breadcrumbs";
|
||||
import { importSource } from "~/lib/importer.server";
|
||||
import { getSource, saveSource } from "~/models/source.server";
|
||||
import { requireUser } from "~/session.server";
|
||||
import { notFound } from "~/utils";
|
||||
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
export async function action({ request }: ActionArgs) {
|
||||
await requireUser(request);
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const name = formData.get("name") as string;
|
||||
const type = formData.get("type") as string;
|
||||
const description = formData.get("description") as string;
|
||||
const connectionUrl = formData.get("connectionUrl") as string;
|
||||
await saveSource({ id, name, description, type, connectionUrl });
|
||||
return redirect("/sources");
|
||||
const source = await saveSource({ id, name, description, type, connectionUrl });
|
||||
await importSource(source);
|
||||
return redirect("/listen/sources");
|
||||
}
|
||||
|
||||
export async function loader({ params }: LoaderArgs) {
|
||||
export async function loader({ params, request }: LoaderArgs) {
|
||||
await requireUser(request);
|
||||
const { id } = params;
|
||||
if (!id) {
|
||||
throw notFound();
|
||||
@ -39,8 +44,8 @@ export default function SourcePage() {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs>
|
||||
<Link to="/sources">Sources</Link>
|
||||
<Link to={`/sources/${source.id}`}>{source.name ?? "New Source"}</Link>
|
||||
<Link to="/listen/sources">Sources</Link>
|
||||
<Link to={`/listen/sources/${source.id}`}>{source.name ?? "New Source"}</Link>
|
||||
</Breadcrumbs>
|
||||
<form method="post">
|
||||
<input type="hidden" name="id" value={source.id} />
|
||||
@ -78,7 +83,7 @@ export default function SourcePage() {
|
||||
defaultValue={source.type} />
|
||||
</div>
|
||||
<div className="form-control w-full max-w-lg mt-3">
|
||||
<button type="submit" className="btn btn-primary">Save</button>
|
||||
<button type="submit" className="btn btn-primary">Import Stations</button>
|
||||
</div>
|
||||
|
||||
</form>
|
54
app/routes/listen.sources._index.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { PlusSmallIcon } from "@heroicons/react/24/solid";
|
||||
import type { LoaderArgs } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Link, useLoaderData } from "@remix-run/react";
|
||||
import { Breadcrumbs } from "~/components/breadcrumbs";
|
||||
import { getSources } from "~/models/source.server";
|
||||
import { requireUser } from "~/session.server";
|
||||
|
||||
export async function loader({ request }: LoaderArgs) {
|
||||
await requireUser(request);
|
||||
const sources = await getSources();
|
||||
|
||||
return json({ sources });
|
||||
}
|
||||
|
||||
export default function SourceIndex() {
|
||||
const { sources } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs>
|
||||
<Link to="/listen/sources">Sources</Link>
|
||||
<Link to="/listen/sources/new">
|
||||
<PlusSmallIcon className="w-4 h-4 mr-2 stroke-current" />
|
||||
Add Source
|
||||
</Link>
|
||||
</Breadcrumbs>
|
||||
<table className="table table-compact w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Type</th>
|
||||
<th>Connection URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sources.map(source => {
|
||||
return (
|
||||
<tr key={source.id} className="hover">
|
||||
<td><Link className="underline" to={"/listen/sources/" + source.id}>{source.name}</Link>
|
||||
</td>
|
||||
<td>{source.description}</td>
|
||||
<td>{source.type}</td>
|
||||
<td>{source.connectionUrl}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
14
app/routes/listen.sources.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import type { LoaderArgs } from "@remix-run/node";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { requireUser } from "~/session.server";
|
||||
|
||||
export async function loader({ request }: LoaderArgs) {
|
||||
await requireUser(request);
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function SourceLayout() {
|
||||
return (
|
||||
<Outlet />
|
||||
);
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import { PlusSmallIcon } from "@heroicons/react/24/solid";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Link, useLoaderData } from "@remix-run/react";
|
||||
import { Breadcrumbs } from "~/components/breadcrumbs";
|
||||
import { getSources } from "~/models/source.server";
|
||||
|
||||
export async function loader() {
|
||||
const sources = await getSources();
|
||||
|
||||
return json({ sources });
|
||||
}
|
||||
|
||||
export default function SourceIndex() {
|
||||
const { sources } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs>
|
||||
<Link to="/sources">Sources</Link>
|
||||
<Link to="/sources/new">
|
||||
<PlusSmallIcon className="w-4 h-4 mr-2 stroke-current" />
|
||||
Add Source
|
||||
</Link>
|
||||
</Breadcrumbs>
|
||||
<ul className="menu bg-base-100 w-56">
|
||||
{sources.map(source => {
|
||||
return (
|
||||
<li key={source.id}><Link to={"/sources/" + source.id}>{source.name}</Link></li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
|
||||
export default function SourceLayout() {
|
||||
return (
|
||||
<Outlet />
|
||||
);
|
||||
}
|
@ -57,11 +57,11 @@ export async function requireUserId(
|
||||
|
||||
export async function requireUser(request: Request) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const user = await getUserById(userId);
|
||||
if (user) return user;
|
||||
|
||||
if (!user) {
|
||||
throw await logout(request);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function createUserSession({
|
||||
|
BIN
public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 511 B |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |