mirror of
https://github.com/ershisan99/podcaster.git
synced 2025-12-18 05:09:30 +00:00
chore: refactor, move things around for better maintainability
fix: add semantic tags to account for screen readers
This commit is contained in:
@@ -1,77 +0,0 @@
|
|||||||
import { Link, useParams } from "react-router-dom";
|
|
||||||
import { usePodcastQuery } from "../services/podcasts/podcast.hooks";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeadCell,
|
|
||||||
TableRow,
|
|
||||||
} from "./ui/table/table";
|
|
||||||
import { useTitle } from "../hooks/use-title";
|
|
||||||
|
|
||||||
export function PodcastEpisodesList() {
|
|
||||||
const { podcastId } = useParams<{ podcastId: string }>();
|
|
||||||
const { data: podcast } = usePodcastQuery(podcastId);
|
|
||||||
useTitle(podcast?.title ?? "Podcast");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={"p-3 text-xl font-bold shadow-md"}>
|
|
||||||
Episodes: {podcast?.trackCount}
|
|
||||||
</div>
|
|
||||||
<div className={"mt-6 p-3 shadow-md"}>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow className={"!bg-slate-50"}>
|
|
||||||
<TableHeadCell className={"text-start"}>Title</TableHeadCell>
|
|
||||||
<TableHeadCell className={"text-start"}>Date</TableHeadCell>
|
|
||||||
<TableHeadCell className={"text-end"}>Duration</TableHeadCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{podcast?.episodes?.map((episode) => {
|
|
||||||
const formattedDate = formatDate(episode.releaseDate);
|
|
||||||
const formattedDuration = formatDuration(episode.durationSeconds);
|
|
||||||
const url = `/podcast/${podcastId}/episode/${episode.id}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow key={episode.id}>
|
|
||||||
<TableCell>
|
|
||||||
<Link
|
|
||||||
to={url}
|
|
||||||
className={"text-indigo-500 hover:underline"}
|
|
||||||
>
|
|
||||||
{episode.title}
|
|
||||||
</Link>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{formattedDate}</TableCell>
|
|
||||||
<TableCell className={"text-end"}>
|
|
||||||
{formattedDuration}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(duration?: number) {
|
|
||||||
if (!duration) {
|
|
||||||
return "N/A";
|
|
||||||
}
|
|
||||||
const minutes = Math.floor(duration / 60);
|
|
||||||
const seconds = Math.floor(duration % 60);
|
|
||||||
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(date: string) {
|
|
||||||
return new Date(date).toLocaleDateString("es-ES", {
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
22
src/components/podcast/podcast-episodes-list.tsx
Normal file
22
src/components/podcast/podcast-episodes-list.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { usePodcastQuery } from "../../services/podcasts/podcast.hooks";
|
||||||
|
import { useTitle } from "../../hooks/use-title";
|
||||||
|
import { PodcastEpisodesTable } from "./podcast-episodes-table";
|
||||||
|
|
||||||
|
export function PodcastEpisodesList() {
|
||||||
|
const { podcastId } = useParams<{ podcastId: string }>();
|
||||||
|
const { data: podcast } = usePodcastQuery(podcastId);
|
||||||
|
|
||||||
|
useTitle(podcast?.title ?? "Podcast");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<header className={"p-3 text-xl font-bold shadow-md"}>
|
||||||
|
Episodes: {podcast?.trackCount}
|
||||||
|
</header>
|
||||||
|
<article className={"mt-6 p-3 shadow-md"}>
|
||||||
|
<PodcastEpisodesTable episodes={podcast?.episodes} />
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/components/podcast/podcast-episodes-table.tsx
Normal file
52
src/components/podcast/podcast-episodes-table.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { memo } from "react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeadCell,
|
||||||
|
TableRow,
|
||||||
|
} from "../ui/table/table";
|
||||||
|
import { formatDate, formatDuration } from "../../utils";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Episode } from "../../services/rss-parser";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
episodes: Episode[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PodcastEpisodesTable = memo(({ episodes }: Props) => {
|
||||||
|
return (
|
||||||
|
<Table className={"w-full"}>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow className={"!bg-slate-50"}>
|
||||||
|
<TableHeadCell>Title</TableHeadCell>
|
||||||
|
<TableHeadCell>Date</TableHeadCell>
|
||||||
|
<TableHeadCell className={"text-end"}>Duration</TableHeadCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{episodes?.map((episode) => {
|
||||||
|
const formattedDate = formatDate(episode.releaseDate);
|
||||||
|
const dateTime = new Date(episode.releaseDate).toISOString();
|
||||||
|
const formattedDuration = formatDuration(episode.durationSeconds);
|
||||||
|
const url = `episode/${episode.id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={episode.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Link to={url} className={"text-indigo-500 hover:underline"}>
|
||||||
|
{episode.title}
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<time dateTime={dateTime}>{formattedDate}</time>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={"text-end"}>{formattedDuration}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { Wrap } from "./wrap";
|
import { Wrap } from "../utils/wrap";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -39,10 +39,10 @@ export function PodcastInfoCard({
|
|||||||
<em className={"text-sm"}>by {author}</em>
|
<em className={"text-sm"}>by {author}</em>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className={"px-2 py-2"}>
|
||||||
<strong className={"text-sm"}>Description:</strong>
|
<strong className={"text-sm"}>Description:</strong>
|
||||||
<div
|
<div
|
||||||
className={"break-words text-sm italic"}
|
className={"mt-1 break-words text-sm italic"}
|
||||||
dangerouslySetInnerHTML={{ __html: description }}
|
dangerouslySetInnerHTML={{ __html: description }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
16
src/components/ui/input/input.tsx
Normal file
16
src/components/ui/input/input.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react";
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
|
||||||
|
type InputProps = ComponentPropsWithoutRef<"input">;
|
||||||
|
type InputRef = ElementRef<"input">;
|
||||||
|
|
||||||
|
export const Input = forwardRef<InputRef, InputProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
const classes = clsx(
|
||||||
|
"w-1/3 rounded-md border border-gray-300 p-2",
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
|
return <input ref={ref} className={classes} {...props} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link, Outlet } from "react-router-dom";
|
import { Link, Outlet } from "react-router-dom";
|
||||||
import { Spinner } from "./spinner";
|
import { Spinner } from "../spinner/spinner";
|
||||||
import { useIsFetching } from "@tanstack/react-query";
|
import { useIsFetching } from "@tanstack/react-query";
|
||||||
|
|
||||||
export function Layout() {
|
export function Layout() {
|
||||||
@@ -35,7 +35,7 @@ export const TableHeadCell = forwardRef<
|
|||||||
ElementRef<"th">,
|
ElementRef<"th">,
|
||||||
ComponentPropsWithoutRef<"th">
|
ComponentPropsWithoutRef<"th">
|
||||||
>(({ children, className, ...rest }, ref) => {
|
>(({ children, className, ...rest }, ref) => {
|
||||||
const classes = clsx(className, "py-3 px-4");
|
const classes = clsx(className, "py-3 px-4 text-start");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th className={classes} {...rest} ref={ref}>
|
<th className={classes} {...rest} ref={ref}>
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ export function Episode() {
|
|||||||
useTitle(episode?.title ?? "Episode");
|
useTitle(episode?.title ?? "Episode");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"h-fit w-full p-4 pb-6 shadow-md"}>
|
<section className={"h-fit w-full p-4 pb-6 shadow-md"}>
|
||||||
<h2 className={"text-2xl font-bold tracking-tight"}>{episode?.title}</h2>
|
<header className={"text-2xl font-bold tracking-tight"}>
|
||||||
<div
|
{episode?.title}
|
||||||
|
</header>
|
||||||
|
<article
|
||||||
className={"prose mt-2 max-w-full border-b pb-4 leading-snug"}
|
className={"prose mt-2 max-w-full border-b pb-4 leading-snug"}
|
||||||
dangerouslySetInnerHTML={{ __html: episode?.description ?? "" }}
|
dangerouslySetInnerHTML={{ __html: episode?.description ?? "" }}
|
||||||
/>
|
/>
|
||||||
<div>
|
<audio controls src={episode?.audioUrl} className={"mt-4 w-full"}>
|
||||||
<audio controls src={episode?.audioUrl} className={"mt-4 w-full"}>
|
Audio is not supported by your browser
|
||||||
Audio is not supported by your browser
|
</audio>
|
||||||
</audio>
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { PodcastPreviewCard } from "../components/podcast-preview-card";
|
import { PodcastPreviewCard } from "../components/podcast/podcast-preview-card";
|
||||||
import { useTopPodcastsQuery } from "../services/podcasts/podcast.hooks";
|
import { useTopPodcastsQuery } from "../services/podcasts/podcast.hooks";
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Outlet, useParams } from "react-router-dom";
|
import { Outlet, useParams } from "react-router-dom";
|
||||||
import { usePodcastQuery } from "../services/podcasts/podcast.hooks";
|
import { usePodcastQuery } from "../services/podcasts/podcast.hooks";
|
||||||
import { PodcastInfoCard } from "../components/podcast-info-card";
|
import { PodcastInfoCard } from "../components/podcast/podcast-info-card";
|
||||||
|
|
||||||
export function Podcast() {
|
export function Podcast() {
|
||||||
const { podcastId } = useParams<{ podcastId: string }>();
|
const { podcastId } = useParams<{ podcastId: string }>();
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
|||||||
import { Home } from "./pages/home";
|
import { Home } from "./pages/home";
|
||||||
import { Podcast } from "./pages/podcast";
|
import { Podcast } from "./pages/podcast";
|
||||||
import { Episode } from "./pages/episode";
|
import { Episode } from "./pages/episode";
|
||||||
import { PodcastEpisodesList } from "./components/podcast-episodes-list";
|
import { PodcastEpisodesList } from "./components/podcast/podcast-episodes-list";
|
||||||
import { Layout } from "./components/layout";
|
import { Layout } from "./components/ui/layout/layout";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
|||||||
16
src/utils/datetime.ts
Normal file
16
src/utils/datetime.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export function formatDate(date: string) {
|
||||||
|
return new Date(date).toLocaleDateString("es-ES", {
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDuration(duration?: number) {
|
||||||
|
if (!duration) {
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
|
const minutes = Math.floor(duration / 60);
|
||||||
|
const seconds = Math.floor(duration % 60);
|
||||||
|
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||||
|
}
|
||||||
1
src/utils/index.ts
Normal file
1
src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./datetime";
|
||||||
Reference in New Issue
Block a user