diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index c451d4e..5e07f7e 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -1,56 +1,61 @@
-// eslint-disable-next-line @typescript-eslint/no-var-requires
-const path = require('path');
-
/** @type {import("eslint").Linter.Config} */
const config = {
- overrides: [
- {
- extends: ['plugin:@typescript-eslint/recommended-requiring-type-checking'],
- files: ['*.ts', '*.tsx'],
- parserOptions: {
- project: path.join(__dirname, 'tsconfig.json'),
- },
- rules: {
- '@typescript-eslint/no-floating-promises': 'off',
- '@typescript-eslint/no-misused-promises': 'off',
- },
- },
- ],
- parser: '@typescript-eslint/parser',
+ parser: "@typescript-eslint/parser",
parserOptions: {
- project: path.join(__dirname, 'tsconfig.json'),
+ project: true,
},
- plugins: ['@typescript-eslint', 'simple-import-sort'],
- extends: ['next/core-web-vitals', 'plugin:@typescript-eslint/recommended', 'prettier'],
+ plugins: ["@typescript-eslint", "simple-import-sort", "prettier"],
+ extends: [
+ "next/core-web-vitals",
+ "plugin:@typescript-eslint/recommended-type-checked",
+ "plugin:@typescript-eslint/stylistic-type-checked",
+ "plugin:prettier/recommended",
+ ],
rules: {
- '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
- 'simple-import-sort/imports': [
- 'warn',
+ "@typescript-eslint/array-type": "off",
+ "@typescript-eslint/consistent-type-definitions": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ argsIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ },
+ ],
+ "@typescript-eslint/require-await": "off",
+ "@typescript-eslint/no-misused-promises": [
+ "error",
+ {
+ checksVoidReturn: {
+ attributes: false,
+ },
+ },
+ ],
+ "simple-import-sort/imports": [
+ "warn",
{
groups: [
[
- '^react',
- '^\\u0000',
- '^node:',
- '^@?\\w',
- '^',
- '^\\.',
- '^node:.*\\u0000$',
- '^@?\\w.*\\u0000$',
- '^[^.].*\\u0000$',
- '^\\..*\\u0000$',
+ "^react",
+ "^\\u0000",
+ "^node:",
+ "^@?\\w",
+ "^",
+ "^\\.",
+ "^node:.*\\u0000$",
+ "^@?\\w.*\\u0000$",
+ "^[^.].*\\u0000$",
+ "^\\..*\\u0000$",
],
],
},
],
- 'simple-import-sort/exports': 'warn',
- 'import/first': 'warn',
- 'import/no-duplicates': 'warn',
- '@typescript-eslint/consistent-type-imports': 'warn',
- 'import/consistent-type-specifier-style': ['warn', 'prefer-top-level'],
- 'react-hooks/exhaustive-deps': 'off',
+ "simple-import-sort/exports": "warn",
+ "import/first": "warn",
+ "import/no-duplicates": "warn",
+ "@typescript-eslint/consistent-type-imports": "warn",
+ "@next/next/no-img-element": "off",
+ "react-hooks/exhaustive-deps": "off",
},
- reportUnusedDisableDirectives: true,
};
module.exports = config;
diff --git a/.github/workflows/docker-main.yml b/.github/workflows/docker-main.yml
index 289ccac..f27ec8b 100644
--- a/.github/workflows/docker-main.yml
+++ b/.github/workflows/docker-main.yml
@@ -4,11 +4,11 @@ on:
workflow_dispatch:
push:
branches: [main]
- tags: 'v*.*.*'
+ tags: "v*.*.*"
jobs:
build:
- name: 'Build and Push Docker Image'
+ name: "Build and Push Docker Image"
runs-on: ubuntu-latest
steps:
- name: Checkout
diff --git a/Dockerfile b/Dockerfile
index d9a2354..d92b686 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,14 +1,13 @@
-FROM node:18-alpine
-
-RUN apk add --no-cache yarn
+FROM node:lts
WORKDIR /app
COPY ./package.json ./package.json
-COPY ./yarn.lock ./yarn.lock
-RUN yarn
+RUN npm install
+
+ENV SKIP_ENV_VALIDATION=true
COPY . .
-RUN export SKIP_ENV_VALIDATION && yarn build
+RUN npm run build
-CMD ["yarn", "start"]
+CMD ["npm", "run", "start"]
diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000..f1280f5
Binary files /dev/null and b/bun.lockb differ
diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..9bfe4a0
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,10 @@
+/**
+ * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
+ * for Docker builds.
+ */
+await import("./src/env.js");
+
+/** @type {import("next").NextConfig} */
+const config = {};
+
+export default config;
diff --git a/next.config.mjs b/next.config.mjs
deleted file mode 100644
index 96a4c2b..0000000
--- a/next.config.mjs
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
- * for Docker builds.
- */
-await import('./src/env.mjs');
-
-/** @type {import("next").NextConfig} */
-const config = {
- reactStrictMode: true,
- async rewrites() {
- return {
- beforeFiles: [
- {
- source: '/:path/feed',
- destination: '/api/:path',
- },
- ],
- afterFiles: [],
- fallback: [],
- };
- },
-
- /**
- * If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config
- * out.
- *
- * @see https://github.com/vercel/next.js/issues/41980
- */
- // i18n: {
- // locales: ['en'],
- // defaultLocale: 'en',
- // },
-};
-
-export default config;
diff --git a/package-old.json b/package-old.json
new file mode 100644
index 0000000..2853b29
--- /dev/null
+++ b/package-old.json
@@ -0,0 +1,55 @@
+{
+ "name": "podmod",
+ "version": "1.0.0",
+ "license": "MIT",
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "tsc --noEmit && prettier --check . && next lint",
+ "prettier": "prettier --write ."
+ },
+ "dependencies": {
+ "@heroicons/react": "2.0.18",
+ "@hookform/error-message": "2.0.1",
+ "@hookform/resolvers": "3.1.0",
+ "@t3-oss/env-nextjs": "0.3.1",
+ "@tailwindcss/forms": "0.5.3",
+ "@tanstack/react-query": "4.29.7",
+ "@trpc/client": "10.26.0",
+ "@trpc/next": "10.26.0",
+ "@trpc/react-query": "10.26.0",
+ "@trpc/server": "10.26.0",
+ "brotli-wasm": "1.3.1",
+ "clsx": "1.2.1",
+ "fast-xml-parser": "4.0.12",
+ "next": "13.4.2",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-hook-form": "7.44.2",
+ "superjson": "1.12.2",
+ "zod": "3.21.4"
+ },
+ "devDependencies": {
+ "@types/eslint": "8.37.0",
+ "@types/node": "18.16.0",
+ "@types/prettier": "2.7.2",
+ "@types/react": "18.2.14",
+ "@types/react-dom": "18.2.4",
+ "@typescript-eslint/eslint-plugin": "5.59.6",
+ "@typescript-eslint/parser": "5.59.6",
+ "autoprefixer": "10.4.14",
+ "eslint": "8.40.0",
+ "eslint-config-next": "13.4.2",
+ "eslint-config-prettier": "8.8.0",
+ "eslint-plugin-simple-import-sort": "10.0.0",
+ "postcss": "8.4.21",
+ "prettier": "2.8.8",
+ "prettier-plugin-tailwindcss": "0.2.8",
+ "tailwindcss": "3.3.0",
+ "typescript": "5.0.4"
+ },
+ "ct3aMetadata": {
+ "initVersion": "7.15.0"
+ }
+}
diff --git a/package.json b/package.json
index 2853b29..c93b5b2 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,8 @@
{
- "name": "podmod",
+ "name": "podmod.app",
"version": "1.0.0",
"license": "MIT",
+ "type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
@@ -10,46 +11,40 @@
"prettier": "prettier --write ."
},
"dependencies": {
- "@heroicons/react": "2.0.18",
- "@hookform/error-message": "2.0.1",
- "@hookform/resolvers": "3.1.0",
- "@t3-oss/env-nextjs": "0.3.1",
- "@tailwindcss/forms": "0.5.3",
- "@tanstack/react-query": "4.29.7",
- "@trpc/client": "10.26.0",
- "@trpc/next": "10.26.0",
- "@trpc/react-query": "10.26.0",
- "@trpc/server": "10.26.0",
- "brotli-wasm": "1.3.1",
- "clsx": "1.2.1",
- "fast-xml-parser": "4.0.12",
- "next": "13.4.2",
+ "@heroicons/react": "^2.1.3",
+ "@hookform/error-message": "^2.0.1",
+ "@hookform/resolvers": "^3.3.4",
+ "@t3-oss/env-nextjs": "^0.9.2",
+ "@tailwindcss/forms": "^0.5.7",
+ "brotli-compress": "^1.3.3",
+ "clsx": "^2.1.0",
+ "fast-xml-parser": "^4.3.6",
+ "next": "^14.1.3",
"react": "18.2.0",
"react-dom": "18.2.0",
- "react-hook-form": "7.44.2",
- "superjson": "1.12.2",
- "zod": "3.21.4"
+ "react-hook-form": "^7.51.3",
+ "superjson": "^2.2.1",
+ "zod": "^3.22.4"
},
"devDependencies": {
- "@types/eslint": "8.37.0",
- "@types/node": "18.16.0",
- "@types/prettier": "2.7.2",
- "@types/react": "18.2.14",
- "@types/react-dom": "18.2.4",
- "@typescript-eslint/eslint-plugin": "5.59.6",
- "@typescript-eslint/parser": "5.59.6",
- "autoprefixer": "10.4.14",
- "eslint": "8.40.0",
- "eslint-config-next": "13.4.2",
- "eslint-config-prettier": "8.8.0",
- "eslint-plugin-simple-import-sort": "10.0.0",
- "postcss": "8.4.21",
- "prettier": "2.8.8",
- "prettier-plugin-tailwindcss": "0.2.8",
- "tailwindcss": "3.3.0",
- "typescript": "5.0.4"
+ "@types/eslint": "^8.56.2",
+ "@types/node": "^20.11.20",
+ "@types/react": "^18.2.57",
+ "@types/react-dom": "^18.2.19",
+ "@typescript-eslint/eslint-plugin": "^7.1.1",
+ "@typescript-eslint/parser": "^7.1.1",
+ "eslint": "^8.57.0",
+ "eslint-config-next": "^14.1.3",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-simple-import-sort": "^12.1.0",
+ "postcss": "^8.4.34",
+ "prettier": "^3.2.5",
+ "prettier-plugin-tailwindcss": "^0.5.11",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5.4.2"
},
"ct3aMetadata": {
- "initVersion": "7.15.0"
+ "initVersion": "7.30.1"
}
}
diff --git a/postcss.config.cjs b/postcss.config.cjs
index e305dd9..4cdb2f4 100644
--- a/postcss.config.cjs
+++ b/postcss.config.cjs
@@ -1,7 +1,6 @@
const config = {
plugins: {
tailwindcss: {},
- autoprefixer: {},
},
};
diff --git a/prettier.config.cjs b/prettier.config.cjs
deleted file mode 100644
index bd6035b..0000000
--- a/prettier.config.cjs
+++ /dev/null
@@ -1,9 +0,0 @@
-/** @type {import("prettier").Config} */
-const config = {
- plugins: [require.resolve('prettier-plugin-tailwindcss')],
- tabWidth: 2,
- singleQuote: true,
- printWidth: 100,
-};
-
-module.exports = config;
diff --git a/prettier.config.js b/prettier.config.js
new file mode 100644
index 0000000..8db6ee9
--- /dev/null
+++ b/prettier.config.js
@@ -0,0 +1,7 @@
+/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
+const config = {
+ plugins: ["prettier-plugin-tailwindcss"],
+ printWidth: 100,
+};
+
+export default config;
diff --git a/src/app/[feedId]/feed/route.ts b/src/app/[feedId]/feed/route.ts
new file mode 100644
index 0000000..f9571ce
--- /dev/null
+++ b/src/app/[feedId]/feed/route.ts
@@ -0,0 +1,30 @@
+import { NextResponse } from "next/server";
+import { decompressModConfig } from "~/services/compressionService";
+import { buildFeed, fetchFeedData } from "~/services/feedService";
+import { applyMods } from "~/services/modService";
+
+const GET = async (request: Request, { params }: { params: { feedId: string } }) => {
+ try {
+ const { feedId } = params;
+
+ const host = request.headers.get("host") ?? "";
+ const searchParams =
+ host && request.url ? new URL(`http://${host}${request.url}`).searchParams : undefined;
+
+ const modConfig = await decompressModConfig(feedId);
+ const feedData = await fetchFeedData(modConfig.sources, searchParams);
+ if (!feedData) throw "Could not find feed data for sources";
+
+ const moddedFeedData = applyMods(feedData, modConfig);
+ const feed = buildFeed(moddedFeedData, feedId, host);
+
+ return new NextResponse(feed, { headers: { "Cache-Control": "s-maxage=600" } });
+ } catch (errorMessage) {
+ console.error(errorMessage);
+ return new NextResponse((errorMessage as string | undefined) ?? "Unexpected Error", {
+ status: 500,
+ });
+ }
+};
+
+export { GET };
diff --git a/src/app/[feedId]/page.tsx b/src/app/[feedId]/page.tsx
new file mode 100644
index 0000000..51cb931
--- /dev/null
+++ b/src/app/[feedId]/page.tsx
@@ -0,0 +1,15 @@
+import MainPage from "~/components/MainPage";
+import { decompressModConfig } from "~/services/compressionService";
+
+type PageProps = {
+ params: {
+ feedId: string;
+ };
+};
+
+const Page = async ({ params }: PageProps) => {
+ const modConfig = await decompressModConfig(params.feedId);
+ return ;
+};
+
+export default Page;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..628f9e3
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,34 @@
+import "~/styles/globals.css";
+import type { Metadata, Viewport } from "next";
+import type { ReactNode } from "react";
+
+export const metadata: Metadata = {
+ title: "podmod.app",
+ description: "Modify any podcast feed with custom filters, artwork, titles, and more!",
+ icons: {
+ icon: { rel: "icon", url: "/favicon.ico" },
+ apple: { rel: "icon", url: "/apple-touch-icon.png" },
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: "#00A6FB",
+};
+
+type LayoutProps = {
+ children: ReactNode;
+};
+
+const Layout = ({ children }: LayoutProps) => {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export default Layout;
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..a23a0a1
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,5 @@
+import MainPage from "~/components/MainPage";
+
+const Page = () => ;
+
+export default Page;
diff --git a/src/components/AddButton.tsx b/src/components/AddButton.tsx
index 27c5086..180ded7 100644
--- a/src/components/AddButton.tsx
+++ b/src/components/AddButton.tsx
@@ -1,5 +1,7 @@
-import { PlusIcon } from '@heroicons/react/20/solid';
-import type { MouseEventHandler } from 'react';
+"use client";
+
+import { PlusIcon } from "@heroicons/react/20/solid";
+import type { MouseEventHandler } from "react";
type ButtonProps = {
onClick?: MouseEventHandler | undefined;
diff --git a/src/components/CopyFeedButton.tsx b/src/components/CopyFeedButton.tsx
index 8890ede..7471a19 100644
--- a/src/components/CopyFeedButton.tsx
+++ b/src/components/CopyFeedButton.tsx
@@ -1,20 +1,27 @@
-import { useState } from 'react';
-import { LinkIcon } from '@heroicons/react/20/solid';
+"use client";
+
+import { useState } from "react";
+import { LinkIcon } from "@heroicons/react/20/solid";
+import { compressModConfig } from "~/services/compressionService";
+import type { ModConfig } from "~/types/ModConfig";
+
+const defaultButtonText = "Copy Feed URL";
type ButtonProps = {
- textToCopy: string;
+ modConfig: ModConfig;
};
-const CopyFeedButton = ({ textToCopy }: ButtonProps) => {
- const defaultText = 'Copy Feed URL';
-
- const [buttonText, setButtonText] = useState(defaultText);
+const CopyFeedButton = ({ modConfig }: ButtonProps) => {
+ const [buttonText, setButtonText] = useState(defaultButtonText);
const onClick = () =>
- navigator.clipboard.writeText(textToCopy).then(() => {
- setButtonText('Copied!');
- setTimeout(() => setButtonText(defaultText), 2000);
- });
+ compressModConfig(modConfig)
+ .then((feedId) => `${window.location.origin}/${feedId}/feed`)
+ .then((textToCopy) => navigator.clipboard.writeText(textToCopy))
+ .then(() => {
+ setButtonText("Copied!");
+ setTimeout(() => setButtonText(defaultButtonText), 2000);
+ });
return (