diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6e8698b..0703da0 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -14,5 +14,6 @@ module.exports = { "warn", { allowConstantExport: true }, ], + "no-duplicate-imports": ["error", { "includeExports": true }] }, }; diff --git a/package.json b/package.json index 8bd4b61..9c2ce8a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "idb-keyval": "^6.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.22.3" + "react-router-dom": "^6.22.3", + "tailwind-merge": "^2.3.0" }, "devDependencies": { "@tailwindcss/typography": "^0.5.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 591af15..3e5f0d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: react-router-dom: specifier: ^6.22.3 version: 6.22.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + tailwind-merge: + specifier: ^2.3.0 + version: 2.3.0 devDependencies: '@tailwindcss/typography': specifier: ^0.5.12 @@ -89,6 +92,10 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@babel/runtime@7.24.4': + resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==} + engines: {node: '>=6.9.0'} + '@esbuild/aix-ppc64@0.20.2': resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} engines: {node: '>=12'} @@ -1235,6 +1242,9 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1320,6 +1330,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwind-merge@2.3.0: + resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} + tailwindcss@3.4.3: resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==} engines: {node: '>=14.0.0'} @@ -1435,6 +1448,10 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@babel/runtime@7.24.4': + dependencies: + regenerator-runtime: 0.14.1 + '@esbuild/aix-ppc64@0.20.2': optional: true @@ -2431,6 +2448,8 @@ snapshots: dependencies: picomatch: 2.3.1 + regenerator-runtime@0.14.1: {} + resolve-from@4.0.0: {} resolve@1.22.8: @@ -2529,6 +2548,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tailwind-merge@2.3.0: + dependencies: + '@babel/runtime': 7.24.4 + tailwindcss@3.4.3: dependencies: '@alloc/quick-lru': 5.2.0 diff --git a/src/app.tsx b/src/app.tsx index 58e632c..6d86b26 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,7 +1,7 @@ -import { Router } from "./router"; import { QueryClient } from "@tanstack/react-query"; -import { createIDBPersister } from "./services/infrastructure/persisters/create-idb-persister"; import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; +import { Router } from "./router"; +import { createIDBPersister } from "./services/infrastructure"; const queryClient = new QueryClient({ defaultOptions: { diff --git a/src/components/episode/episode.tsx b/src/components/episode/episode.tsx index 742f0bd..cf88be6 100644 --- a/src/components/episode/episode.tsx +++ b/src/components/episode/episode.tsx @@ -1,6 +1,6 @@ import { useParams } from "react-router-dom"; -import { usePodcastQuery } from "../../services/podcasts/podcast.hooks"; -import { useTitle } from "../../hooks/use-title"; +import { usePodcastQuery } from "../../services"; +import { useTitle } from "../../hooks"; export function Episode() { const { podcastId, episodeId } = useParams<{ diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..c6f8e18 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,3 @@ +export * from "./podcast"; +export * from "./ui"; +export * from "./episode"; diff --git a/src/components/podcast/index.ts b/src/components/podcast/index.ts new file mode 100644 index 0000000..9985a73 --- /dev/null +++ b/src/components/podcast/index.ts @@ -0,0 +1,4 @@ +export * from "./podcast-episodes-list"; +export * from "./podcast-episodes-table"; +export * from "./podcast-info-card"; +export * from "./podcast-preview-card"; diff --git a/src/components/podcast/podcast-episodes-list.tsx b/src/components/podcast/podcast-episodes-list.tsx index 5d9fc65..1fee020 100644 --- a/src/components/podcast/podcast-episodes-list.tsx +++ b/src/components/podcast/podcast-episodes-list.tsx @@ -1,6 +1,6 @@ import { useParams } from "react-router-dom"; -import { usePodcastQuery } from "../../services/podcasts/podcast.hooks"; -import { useTitle } from "../../hooks/use-title"; +import { usePodcastQuery } from "../../services"; +import { useTitle } from "../../hooks"; import { PodcastEpisodesTable } from "./podcast-episodes-table"; export function PodcastEpisodesList() { diff --git a/src/components/podcast/podcast-episodes-table.tsx b/src/components/podcast/podcast-episodes-table.tsx index cb4ae4a..5528d53 100644 --- a/src/components/podcast/podcast-episodes-table.tsx +++ b/src/components/podcast/podcast-episodes-table.tsx @@ -6,13 +6,13 @@ import { TableHead, TableHeadCell, TableRow, -} from "../ui/table/table"; +} from "../ui"; import { formatDate, formatDuration } from "../../utils"; import { Link } from "react-router-dom"; -import { Episode } from "../../services/rss-parser"; +import { RssEpisode } from "../../services/rss-parser"; type Props = { - episodes: Episode[] | undefined; + episodes: RssEpisode[] | undefined; }; export const PodcastEpisodesTable = memo(({ episodes }: Props) => { diff --git a/src/components/podcast/podcast-info-card.tsx b/src/components/podcast/podcast-info-card.tsx index 5958dc5..f2f1fc3 100644 --- a/src/components/podcast/podcast-info-card.tsx +++ b/src/components/podcast/podcast-info-card.tsx @@ -1,5 +1,5 @@ import { Link, useParams } from "react-router-dom"; -import { Wrap } from "../utils/wrap"; +import { Wrap } from "../utils"; type Props = { title: string; diff --git a/src/components/podcast/podcast-preview-card.tsx b/src/components/podcast/podcast-preview-card.tsx index 60ec7b8..4535779 100644 --- a/src/components/podcast/podcast-preview-card.tsx +++ b/src/components/podcast/podcast-preview-card.tsx @@ -14,7 +14,7 @@ export function PodcastPreviewCard({ detailUrl, }: Props) { return ( - +
; +type InputProps = ComponentPropsWithoutRef<"input"> & { + onValueChange?: (value: string) => void; +}; type InputRef = ElementRef<"input">; export const Input = forwardRef( - ({ className, ...props }, ref) => { - const classes = clsx( - "w-1/3 rounded-md border border-gray-300 p-2", + ({ className, onChange, onValueChange, ...props }, ref) => { + const classes = cn( + "w-full rounded-md border border-gray-300 p-2", className, ); - return ; + function handleChange(e: ChangeEvent) { + onChange?.(e); + onValueChange?.(e.target.value); + } + + return ( + + ); }, ); diff --git a/src/components/ui/layout/index.ts b/src/components/ui/layout/index.ts new file mode 100644 index 0000000..eec9d1e --- /dev/null +++ b/src/components/ui/layout/index.ts @@ -0,0 +1 @@ +export * from "./layout"; diff --git a/src/components/ui/layout/layout.tsx b/src/components/ui/layout/layout.tsx index 8f629e1..942c68f 100644 --- a/src/components/ui/layout/layout.tsx +++ b/src/components/ui/layout/layout.tsx @@ -1,5 +1,5 @@ import { Link, Outlet } from "react-router-dom"; -import { Spinner } from "../spinner/spinner"; +import { Spinner } from "../spinner"; import { useIsFetching } from "@tanstack/react-query"; export function Layout() { diff --git a/src/components/ui/spinner/index.ts b/src/components/ui/spinner/index.ts new file mode 100644 index 0000000..eff2970 --- /dev/null +++ b/src/components/ui/spinner/index.ts @@ -0,0 +1 @@ +export * from "./spinner"; diff --git a/src/components/ui/table/index.ts b/src/components/ui/table/index.ts new file mode 100644 index 0000000..0e948df --- /dev/null +++ b/src/components/ui/table/index.ts @@ -0,0 +1 @@ +export * from "./table"; diff --git a/src/components/ui/table/table.tsx b/src/components/ui/table/table.tsx index f7af7b2..f931c7a 100644 --- a/src/components/ui/table/table.tsx +++ b/src/components/ui/table/table.tsx @@ -1,11 +1,11 @@ import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react"; -import { clsx } from "clsx"; +import { cn } from "../../../utils"; export const Table = forwardRef< HTMLTableElement, ComponentPropsWithoutRef<"table"> >(({ className, ...rest }, ref) => { - const classes = clsx(className, "w-full border-collapse"); + const classes = cn(className, "w-full border-collapse"); return ; }); @@ -27,7 +27,7 @@ export const TableRow = forwardRef< ElementRef<"tr">, ComponentPropsWithoutRef<"tr"> >(({ className, ...rest }, ref) => { - const classes = clsx(className, "odd:bg-gray-100"); + const classes = cn(className, "odd:bg-gray-100"); return ; }); @@ -35,7 +35,7 @@ export const TableHeadCell = forwardRef< ElementRef<"th">, ComponentPropsWithoutRef<"th"> >(({ children, className, ...rest }, ref) => { - const classes = clsx(className, "py-3 px-4 text-start"); + const classes = cn(className, "py-3 px-4 text-start"); return (
@@ -48,7 +48,7 @@ export const TableCell = forwardRef< ElementRef<"td">, ComponentPropsWithoutRef<"td"> >(({ className, ...rest }, ref) => { - const classes = clsx(className, "py-3 px-4 border-t border-slate-200"); + const classes = cn(className, "py-3 px-4 border-t border-slate-200"); return ; }); diff --git a/src/components/utils/index.ts b/src/components/utils/index.ts new file mode 100644 index 0000000..5bb85ff --- /dev/null +++ b/src/components/utils/index.ts @@ -0,0 +1 @@ +export * from "./wrap"; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..5dfbee9 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-title"; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 7da88a7..9f0a9f8 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import { PodcastPreviewCard } from "../components/podcast/podcast-preview-card"; -import { useTopPodcastsQuery } from "../services/podcasts/podcast.hooks"; +import { PodcastPreviewCard, Input } from "../components"; +import { useTopPodcastsQuery } from "../services"; export function Home() { const { data, isLoading } = useTopPodcastsQuery(); @@ -28,15 +28,14 @@ export function Home() { > {filteredData?.length} - setSearch(e.target.value)} + onValueChange={setSearch} placeholder="Filter podcasts..." - className={"w-1/3 rounded-md border border-gray-300 p-2"} /> -
+
{filteredData?.map((podcast) => ( ))} -
+ ); } diff --git a/src/pages/podcast.tsx b/src/pages/podcast.tsx index 5c33843..335aa6f 100644 --- a/src/pages/podcast.tsx +++ b/src/pages/podcast.tsx @@ -1,6 +1,6 @@ import { Outlet, useParams } from "react-router-dom"; -import { usePodcastQuery } from "../services/podcasts/podcast.hooks"; -import { PodcastInfoCard } from "../components/podcast/podcast-info-card"; +import { usePodcastQuery } from "../services"; +import { PodcastInfoCard } from "../components"; export function Podcast() { const { podcastId } = useParams<{ podcastId: string }>(); diff --git a/src/router.tsx b/src/router.tsx index 232c510..2159904 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,9 +1,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { Home } from "./pages/home"; import { Podcast } from "./pages/podcast"; -import { Episode } from "./components/episode"; -import { PodcastEpisodesList } from "./components/podcast/podcast-episodes-list"; -import { Layout } from "./components/ui/layout/layout"; +import { PodcastEpisodesList, Episode, Layout } from "./components"; const router = createBrowserRouter([ { diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..6a72c79 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1 @@ +export * from "./podcasts"; diff --git a/src/services/infrastructure/index.ts b/src/services/infrastructure/index.ts new file mode 100644 index 0000000..861b282 --- /dev/null +++ b/src/services/infrastructure/index.ts @@ -0,0 +1 @@ +export * from "./persisters"; diff --git a/src/services/infrastructure/persisters/index.ts b/src/services/infrastructure/persisters/index.ts new file mode 100644 index 0000000..1eef85a --- /dev/null +++ b/src/services/infrastructure/persisters/index.ts @@ -0,0 +1 @@ +export * from "./create-idb-persister"; diff --git a/src/services/podcasts/dto/index.ts b/src/services/podcasts/dto/index.ts new file mode 100644 index 0000000..0d0d12b --- /dev/null +++ b/src/services/podcasts/dto/index.ts @@ -0,0 +1,3 @@ +export * from "./episode.dto"; +export * from "./podcast.dto"; +export * from "./podcast-extra.dto"; diff --git a/src/services/podcasts/index.ts b/src/services/podcasts/index.ts new file mode 100644 index 0000000..7c74522 --- /dev/null +++ b/src/services/podcasts/index.ts @@ -0,0 +1,3 @@ +export * from "./podcast.hooks"; +export * from "./dto"; +export * from "./podcasts.types"; diff --git a/src/services/podcasts/podcasts.service.ts b/src/services/podcasts/podcasts.service.ts index 00ef918..7c3c1e6 100644 --- a/src/services/podcasts/podcasts.service.ts +++ b/src/services/podcasts/podcasts.service.ts @@ -1,7 +1,6 @@ import { GetEpisodesResponse, GetTopPodcastsResponse } from "./podcasts.types"; -import { PodcastDTO } from "./dto/podcast.dto"; +import { PodcastDTO, PodcastExtraDTO } from "./dto"; import { RssParser } from "../rss-parser"; -import { PodcastExtraDTO } from "./dto/podcast-extra.dto"; import { bypassCorsService } from "../bypass-cors"; class PodcastsService { diff --git a/src/services/rss-parser/index.ts b/src/services/rss-parser/index.ts index f98061c..a2c5a10 100644 --- a/src/services/rss-parser/index.ts +++ b/src/services/rss-parser/index.ts @@ -1 +1,2 @@ export * from "./rss-parser"; +export * from "./rss-parser.types"; diff --git a/src/services/rss-parser/rss-parser.ts b/src/services/rss-parser/rss-parser.ts index b132689..7acb01f 100644 --- a/src/services/rss-parser/rss-parser.ts +++ b/src/services/rss-parser/rss-parser.ts @@ -1,26 +1,14 @@ -export interface Episode { - id: string; - releaseDate: string; - audioUrl: string; - title: string; - durationSeconds: number; - description: string; -} - -export interface Podcast { - description: string; - episodes: Episode[]; -} +import { RssEpisode, RssPodcast } from "./rss-parser.types"; export class RssParser { - public static parse(rss: string): Podcast { + public static parse(rss: string): RssPodcast { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(rss, "text/xml"); const channel = xmlDoc.querySelector("channel"); const description = this.getElementInnerHtml(channel, "description"); - const episodes: Episode[] = []; + const episodes: RssEpisode[] = []; const items = Array.from(xmlDoc.querySelectorAll("item")); @@ -55,6 +43,7 @@ export class RssParser { episodes, }; } + public static parseDuration(durationStr: string): number { if (durationStr.includes(":")) { const parts = durationStr.split(":").map((part) => parseInt(part, 10)); @@ -71,6 +60,7 @@ export class RssParser { return parseInt(durationStr, 10); } } + public static getElementByTagName(element: Element | null, tagName: string) { return element?.getElementsByTagName(tagName)[0]; } diff --git a/src/services/rss-parser/rss-parser.types.ts b/src/services/rss-parser/rss-parser.types.ts new file mode 100644 index 0000000..bdc45a7 --- /dev/null +++ b/src/services/rss-parser/rss-parser.types.ts @@ -0,0 +1,13 @@ +export interface RssEpisode { + id: string; + releaseDate: string; + audioUrl: string; + title: string; + durationSeconds: number; + description: string; +} + +export interface RssPodcast { + description: string; + episodes: RssEpisode[]; +} diff --git a/src/utils/classnames.ts b/src/utils/classnames.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/src/utils/classnames.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 34541dd..7cc4747 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1,2 @@ export * from "./datetime"; +export * from "./classnames";