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";