Minor refactoring to breakup large components into smaller ones.
This commit is contained in:
parent
e8aaa4bd05
commit
c4b1c4842a
10
README.md
10
README.md
@ -64,13 +64,21 @@ Awesome Radio is a personal internet radio station aggregator. See the [demo](ht
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Migrate & Seed the SQLite DB
|
2. Install dependencies
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Migrate & Seed the SQLite DB
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npx prisma migrate deploy
|
npx prisma migrate deploy
|
||||||
npx prisma db seed
|
npx prisma db seed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: The db schema, migrations, and seed steps are located in the [prisma](prisma) folder.
|
||||||
|
|
||||||
### Running
|
### Running
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -1,13 +1,46 @@
|
|||||||
import { RadioIcon, UserCircleIcon } from "@heroicons/react/24/solid";
|
|
||||||
import { Link, NavLink } from "@remix-run/react";
|
import { Link, NavLink } from "@remix-run/react";
|
||||||
import type { RemixLinkProps, RemixNavLinkProps } from "@remix-run/react/dist/components";
|
import type { RemixLinkProps, RemixNavLinkProps } from "@remix-run/react/dist/components";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode, RefAttributes } from "react";
|
||||||
import * as React from "react";
|
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
import { SideNav } from "~/components/side-nav";
|
||||||
|
import { TopNavbar } from "~/components/top-navbar";
|
||||||
import type { StationWithTagsClientSide } from "~/models/station.server";
|
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 StationContextType = {
|
||||||
|
station: StationWithTagsClientSide | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StationContext = createContext<StationContextType>({ station: null });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that provides the active radio station. Station is null if one is not selected.
|
||||||
|
*/
|
||||||
|
export function useStationContext() {
|
||||||
|
return useContext(StationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper component that wraps <Link> and appends `?station={station.id}` to the link `to` prop if
|
||||||
|
* there's an active station playing
|
||||||
|
*/
|
||||||
|
export function ListenLink(props: RemixLinkProps & RefAttributes<HTMLAnchorElement>) {
|
||||||
|
const { station } = useStationContext();
|
||||||
|
const url = props.to + (station ? `?station=${station.id}` : "");
|
||||||
|
return <Link {...props} to={url}>{props.children}</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper component that wraps <NavLink> and appends `?station={station.id}` to the link `to` prop if
|
||||||
|
* there's an active station playing
|
||||||
|
*/
|
||||||
|
export function ListenNavLink(props: RemixNavLinkProps & RefAttributes<HTMLAnchorElement>) {
|
||||||
|
const { station } = useStationContext();
|
||||||
|
const url = props.to + (station ? `?station=${station.id}` : "");
|
||||||
|
return <NavLink {...props} to={url}>{props.children}</NavLink>;
|
||||||
|
}
|
||||||
|
|
||||||
export type PageLayoutProps = {
|
export type PageLayoutProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
tags: TagWithStationsClientSide[];
|
tags: TagWithStationsClientSide[];
|
||||||
@ -15,123 +48,20 @@ export type PageLayoutProps = {
|
|||||||
station: StationWithTagsClientSide | null;
|
station: StationWithTagsClientSide | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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) {
|
export function PageLayout({ children, tags, user, station }: PageLayoutProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StationContext.Provider value={{ station }}>
|
<StationContext.Provider value={{ station }}>
|
||||||
<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" />
|
||||||
<div className="drawer-content flex flex-col">
|
<div className="drawer-content flex flex-col">
|
||||||
<div className="w-full navbar bg-base-300 lg:justify-end">
|
<TopNavbar user={user} />
|
||||||
<div className="flex-none lg:hidden">
|
|
||||||
<label htmlFor="primary-drawer" className="btn btn-square btn-ghost">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
|
||||||
className="inline-block w-6 h-6 stroke-current">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
|
||||||
d="M4 6h16M4 12h16M4 18h16"></path>
|
|
||||||
</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">
|
|
||||||
<UserCircleIcon className="w-8 h-8" />
|
|
||||||
</label>
|
|
||||||
<ul tabIndex={0}
|
|
||||||
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>
|
|
||||||
</ul>
|
|
||||||
</div> :
|
|
||||||
<Link to="/join" className="btn">Join</Link>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="py-2 px-6">
|
<div className="py-2 px-6">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="drawer-side">
|
<div className="drawer-side">
|
||||||
<label htmlFor="primary-drawer" className="drawer-overlay"></label>
|
<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">
|
<SideNav tags={tags} user={user} />
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<ManageContentNav user={user} />
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</StationContext.Provider>
|
</StationContext.Provider>
|
||||||
|
63
app/components/side-nav.tsx
Normal file
63
app/components/side-nav.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { RadioIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { NavLink } from "@remix-run/react";
|
||||||
|
import { ListenNavLink } from "~/components/page-layout";
|
||||||
|
import type { TagWithStationsClientSide } from "~/models/tag.server";
|
||||||
|
import type { UserWithFavoriteStationsClientSide } from "~/models/user.server";
|
||||||
|
|
||||||
|
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 type SideNavProps = {
|
||||||
|
tags: TagWithStationsClientSide[];
|
||||||
|
user?: UserWithFavoriteStationsClientSide;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SideNav({ tags, user }: SideNavProps) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ManageContentNav user={user} />
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
@ -13,7 +13,7 @@ export function StationPlayer({ station }: StationPlayerProps) {
|
|||||||
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">
|
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>
|
<h3 className="text-xl">Now Playing: <strong>{station.name}</strong></h3>
|
||||||
|
|
||||||
<audio controls autoPlay src={station.streamUrl}>
|
<audio controls autoPlay src={station.streamUrl} title={station.name}>
|
||||||
Your browser does not support the audio element.
|
Your browser does not support the audio element.
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
|
63
app/components/top-navbar.tsx
Normal file
63
app/components/top-navbar.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { RadioIcon, UserCircleIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { Link } from "@remix-run/react";
|
||||||
|
import type { UserWithFavoriteStationsClientSide } from "~/models/user.server";
|
||||||
|
|
||||||
|
export function GithubIconLink() {
|
||||||
|
return (
|
||||||
|
<a aria-label="Github | Awesome Radio" target="_blank" href="https://github.com/wmluke/awesome-radio"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-ghost drawer-button btn-square normal-case">
|
||||||
|
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
|
||||||
|
className="inline-block h-8 w-8 fill-current">
|
||||||
|
<path
|
||||||
|
d="M256,32C132.3,32,32,134.9,32,261.7c0,101.5,64.2,187.5,153.2,217.9a17.56,17.56,0,0,0,3.8.4c8.3,0,11.5-6.1,11.5-11.4,0-5.5-.2-19.9-.3-39.1a102.4,102.4,0,0,1-22.6,2.7c-43.1,0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1,1.4-14.1h.1c22.5,2,34.3,23.8,34.3,23.8,11.2,19.6,26.2,25.1,39.6,25.1a63,63,0,0,0,25.6-6c2-14.8,7.8-24.9,14.2-30.7-49.7-5.8-102-25.5-102-113.5,0-25.1,8.7-45.6,23-61.6-2.3-5.8-10-29.2,2.2-60.8a18.64,18.64,0,0,1,5-.5c8.1,0,26.4,3.1,56.6,24.1a208.21,208.21,0,0,1,112.2,0c30.2-21,48.5-24.1,56.6-24.1a18.64,18.64,0,0,1,5,.5c12.2,31.6,4.5,55,2.2,60.8,14.3,16.1,23,36.6,23,61.6,0,88.2-52.4,107.6-102.3,113.3,8,7.1,15.2,21.1,15.2,42.5,0,30.7-.3,55.5-.3,63,0,5.4,3.1,11.5,11.4,11.5a19.35,19.35,0,0,0,4-.4C415.9,449.2,480,363.1,480,261.7,480,134.9,379.7,32,256,32Z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserDropdown() {
|
||||||
|
return (
|
||||||
|
<div className="dropdown dropdown-end">
|
||||||
|
<label tabIndex={0} className="btn btn-ghost btn-circle avatar placeholder">
|
||||||
|
<UserCircleIcon className="w-8 h-8" />
|
||||||
|
</label>
|
||||||
|
<ul tabIndex={0}
|
||||||
|
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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TopNavbarProps = { user?: UserWithFavoriteStationsClientSide; };
|
||||||
|
|
||||||
|
export function TopNavbar({ user }: TopNavbarProps) {
|
||||||
|
return (
|
||||||
|
<div className="w-full navbar bg-base-300 lg:justify-end">
|
||||||
|
<div className="flex-none lg:hidden">
|
||||||
|
<label htmlFor="primary-drawer" className="btn btn-square btn-ghost">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
|
className="inline-block w-6 h-6 stroke-current">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
|
</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 gap-4">
|
||||||
|
<GithubIconLink />
|
||||||
|
{user ?
|
||||||
|
<UserDropdown /> :
|
||||||
|
<Link to="/join" className="btn btn-accent btn-sm">Join</Link>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,59 +0,0 @@
|
|||||||
import type { LoaderArgs } from "@remix-run/node";
|
|
||||||
import { json } from "@remix-run/node";
|
|
||||||
import { Link, Outlet, useLoaderData } from "@remix-run/react";
|
|
||||||
import { Breadcrumbs } from "~/components/breadcrumbs";
|
|
||||||
import { StationsGallery } from "~/components/stations-gallery";
|
|
||||||
import type { StationWithTags } from "~/models/station.server";
|
|
||||||
import { findStationsByTags } from "~/models/station.server";
|
|
||||||
import { notFound } from "~/utils";
|
|
||||||
|
|
||||||
|
|
||||||
export type Channel = {
|
|
||||||
tags: string[];
|
|
||||||
slug: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const channels: { [channel: string]: Channel } = {
|
|
||||||
"music": {
|
|
||||||
tags: ["music"],
|
|
||||||
slug: "music",
|
|
||||||
name: "Music"
|
|
||||||
},
|
|
||||||
"news": {
|
|
||||||
tags: ["news"],
|
|
||||||
slug: "news",
|
|
||||||
name: "News"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export async function loader({ params }: LoaderArgs) {
|
|
||||||
if (!params.channel) {
|
|
||||||
throw notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const channel = channels[params.channel];
|
|
||||||
if (!channel) {
|
|
||||||
throw notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const stations: StationWithTags[] = await findStationsByTags(channel.tags);
|
|
||||||
return json({ channel, stations });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ListenChannel() {
|
|
||||||
const { channel, stations } = useLoaderData<typeof loader>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Breadcrumbs>
|
|
||||||
<Link to="/listen">Home</Link>
|
|
||||||
<Link to={`/listen/${channel.slug}`}>{channel.name}</Link>
|
|
||||||
</Breadcrumbs>
|
|
||||||
<StationsGallery stations={stations} channel={channel} />
|
|
||||||
<Outlet />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user