mirror of
https://github.com/ershisan99/podcaster.git
synced 2025-12-16 20:59:26 +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 { Wrap } from "./wrap";
|
||||
import { Wrap } from "../utils/wrap";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
@@ -39,10 +39,10 @@ export function PodcastInfoCard({
|
||||
<em className={"text-sm"}>by {author}</em>
|
||||
</Wrap>
|
||||
</div>
|
||||
<div>
|
||||
<div className={"px-2 py-2"}>
|
||||
<strong className={"text-sm"}>Description:</strong>
|
||||
<div
|
||||
className={"break-words text-sm italic"}
|
||||
className={"mt-1 break-words text-sm italic"}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
></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 { Spinner } from "./spinner";
|
||||
import { Spinner } from "../spinner/spinner";
|
||||
import { useIsFetching } from "@tanstack/react-query";
|
||||
|
||||
export function Layout() {
|
||||
@@ -35,7 +35,7 @@ export const TableHeadCell = forwardRef<
|
||||
ElementRef<"th">,
|
||||
ComponentPropsWithoutRef<"th">
|
||||
>(({ children, className, ...rest }, ref) => {
|
||||
const classes = clsx(className, "py-3 px-4");
|
||||
const classes = clsx(className, "py-3 px-4 text-start");
|
||||
|
||||
return (
|
||||
<th className={classes} {...rest} ref={ref}>
|
||||
|
||||
@@ -17,17 +17,17 @@ export function Episode() {
|
||||
useTitle(episode?.title ?? "Episode");
|
||||
|
||||
return (
|
||||
<div className={"h-fit w-full p-4 pb-6 shadow-md"}>
|
||||
<h2 className={"text-2xl font-bold tracking-tight"}>{episode?.title}</h2>
|
||||
<div
|
||||
<section className={"h-fit w-full p-4 pb-6 shadow-md"}>
|
||||
<header className={"text-2xl font-bold tracking-tight"}>
|
||||
{episode?.title}
|
||||
</header>
|
||||
<article
|
||||
className={"prose mt-2 max-w-full border-b pb-4 leading-snug"}
|
||||
dangerouslySetInnerHTML={{ __html: episode?.description ?? "" }}
|
||||
/>
|
||||
<div>
|
||||
<audio controls src={episode?.audioUrl} className={"mt-4 w-full"}>
|
||||
Audio is not supported by your browser
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
<audio controls src={episode?.audioUrl} className={"mt-4 w-full"}>
|
||||
Audio is not supported by your browser
|
||||
</audio>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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";
|
||||
|
||||
export function Home() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Outlet, useParams } from "react-router-dom";
|
||||
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() {
|
||||
const { podcastId } = useParams<{ podcastId: string }>();
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import { Home } from "./pages/home";
|
||||
import { Podcast } from "./pages/podcast";
|
||||
import { Episode } from "./pages/episode";
|
||||
import { PodcastEpisodesList } from "./components/podcast-episodes-list";
|
||||
import { Layout } from "./components/layout";
|
||||
import { PodcastEpisodesList } from "./components/podcast/podcast-episodes-list";
|
||||
import { Layout } from "./components/ui/layout/layout";
|
||||
|
||||
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