├── public ├── echo.mp3 ├── nova.mp3 ├── onyx.mp3 ├── alloy.mp3 ├── fable.mp3 ├── shimmer.mp3 ├── images │ ├── bg-img.png │ └── player1.png └── icons │ ├── profile.svg │ ├── search.svg │ ├── home.svg │ ├── hamburger.svg │ ├── forward.svg │ ├── reverse.svg │ ├── watch.svg │ ├── randomPlay.svg │ ├── discover.svg │ ├── play-gray.svg │ ├── upload-image.svg │ ├── Pause.svg │ ├── Play.svg │ ├── headphone.svg │ ├── right-arrow.svg │ ├── microphone.svg │ ├── mute.svg │ ├── verified.svg │ ├── three-dots.svg │ ├── unmute.svg │ ├── edit.svg │ ├── delete.svg │ ├── emptyState.svg │ ├── avatar.svg │ ├── logo.svg │ └── auth-logo.svg ├── postcss.config.mjs ├── lib ├── utils.ts ├── formatTime.ts └── useDebounce.ts ├── convex ├── auth.config.ts ├── files.ts ├── _generated │ ├── api.js │ ├── api.d.ts │ ├── dataModel.d.ts │ ├── server.js │ └── server.d.ts ├── tsconfig.json ├── schema.ts ├── openai.ts ├── http.ts ├── README.md ├── users.ts └── podcasts.ts ├── app ├── (auth) │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ ├── sign-up │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ └── layout.tsx ├── layout.tsx ├── (root) │ ├── page.tsx │ ├── layout.tsx │ ├── discover │ │ └── page.tsx │ ├── profile │ │ └── [profileId] │ │ │ └── page.tsx │ ├── podcast │ │ └── [podcastId] │ │ │ └── page.tsx │ └── create-podcast │ │ └── page.tsx └── globals.css ├── components ├── Loader.tsx ├── Header.tsx ├── ui │ ├── label.tsx │ ├── progress.tsx │ ├── toaster.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── button.tsx │ ├── use-toast.ts │ ├── sheet.tsx │ ├── form.tsx │ ├── toast.tsx │ └── select.tsx ├── Searchbar.tsx ├── PodcastCard.tsx ├── EmptyState.tsx ├── DotButton.tsx ├── MobileNav.tsx ├── Carousel.tsx ├── RightSidebar.tsx ├── LeftSidebar.tsx ├── ProfileCard.tsx ├── GeneratePodcast.tsx ├── PodcastDetailPlayer.tsx ├── PodcastPlayer.tsx └── GenerateThumbnail.tsx ├── components.json ├── middleware.ts ├── next.config.mjs ├── .gitignore ├── .eslintrc.json ├── tsconfig.json ├── .vscode └── settings.json ├── providers ├── AudioProvider.tsx └── ConvexClerkProvider.tsx ├── README.md ├── package.json ├── tailwind.config.ts ├── constants └── index.ts └── types └── index.ts /public/echo.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavaScript-Mastery-Pro/podcastr/HEAD/public/echo.mp3 -------------------------------------------------------------------------------- /public/nova.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavaScript-Mastery-Pro/podcastr/HEAD/public/nova.mp3 -------------------------------------------------------------------------------- /public/onyx.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavaScript-Mastery-Pro/podcastr/HEAD/public/onyx.mp3 -------------------------------------------------------------------------------- /public/alloy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavaScript-Mastery-Pro/podcastr/HEAD/public/alloy.mp3 -------------------------------------------------------------------------------- /public/fable.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavaScript-Mastery-Pro/podcastr/HEAD/public/fable.mp3 -------------------------------------------------------------------------------- /public/shimmer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavaScript-Mastery-Pro/podcastr/HEAD/public/shimmer.mp3 -------------------------------------------------------------------------------- /public/images/bg-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavaScript-Mastery-Pro/podcastr/HEAD/public/images/bg-img.png -------------------------------------------------------------------------------- /public/images/player1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavaScript-Mastery-Pro/podcastr/HEAD/public/images/player1.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /convex/auth.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-anonymous-default-export */ 2 | export default { 3 | providers: [ 4 | { 5 | domain: "https://quality-hound-80.clerk.accounts.dev", 6 | applicationID: "convex", 7 | }, 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /lib/formatTime.ts: -------------------------------------------------------------------------------- 1 | export const formatTime = (seconds: number) => { 2 | const minutes = Math.floor(seconds / 60); 3 | const remainingSeconds = Math.floor(seconds % 60); 4 | return `${minutes}:${remainingSeconds < 10 ? "0" : ""}${remainingSeconds}`; 5 | }; 6 | -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { Loader } from "lucide-react"; 2 | 3 | const LoaderSpinner = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default LoaderSpinner; 12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /convex/files.ts: -------------------------------------------------------------------------------- 1 | // mutation to generate a signed url for uploading a file to the storage service 2 | // this mutation should should be called inside useUploadFiles hook from uploadStuff in frontend 3 | import { mutation } from "./_generated/server"; 4 | 5 | export const generateUploadUrl = mutation({ 6 | args: {}, 7 | handler: async (ctx, args) => { 8 | return await ctx.storage.generateUploadUrl(); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /lib/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useDebounce = (value: T, delay = 500) => { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const timeout = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(timeout); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | }; 18 | -------------------------------------------------------------------------------- /public/icons/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; 2 | 3 | const isProtectedRoute = createRouteMatcher([ 4 | "/", 5 | "/discover", 6 | "/podcast(.*)", 7 | "/sign-in(.*)", 8 | "/sign-up(.*)", 9 | ]); 10 | 11 | export default clerkMiddleware((auth, req) => { 12 | if (!isProtectedRoute(req)) auth().protect(); 13 | }); 14 | 15 | export const config = { 16 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], 17 | }; 18 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "img.clerk.com", 8 | }, 9 | { 10 | protocol: "https", 11 | hostname: "oaidalleapiprodscus.blob.core.windows.net", 12 | }, 13 | { 14 | protocol: "https", 15 | hostname: "lovely-flamingo-139.convex.cloud", 16 | }, 17 | ], 18 | }, 19 | }; 20 | 21 | export default nextConfig; 22 | -------------------------------------------------------------------------------- /public/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { ReactNode } from "react"; 3 | 4 | const AuthLayout = ({ children }: { children: ReactNode }) => { 5 | return ( 6 |
7 |
8 | auth bg 14 |
15 | {children} 16 |
17 | ); 18 | }; 19 | 20 | export default AuthLayout; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.11.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /public/icons/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/icons/forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/reverse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/watch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/randomPlay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["import"], 3 | "extends": [ 4 | "next/core-web-vitals", 5 | "standard", 6 | "plugin:tailwindcss/recommended", 7 | "prettier" 8 | ], 9 | "rules": { 10 | "no-undef": "off", 11 | "import/order": [ 12 | "error", 13 | { 14 | "groups": [ 15 | "builtin", 16 | "external", 17 | "internal", 18 | "parent", 19 | "sibling", 20 | "index" 21 | ], 22 | "newlines-between": "always", 23 | "alphabetize": { 24 | "order": "asc", 25 | "caseInsensitive": true 26 | } 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/icons/discover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /public/icons/play-gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/upload-image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit", 6 | "source.addMissingImports": "explicit" 7 | }, 8 | "prettier.tabWidth": 2, 9 | "prettier.useTabs": false, 10 | "prettier.semi": true, 11 | "prettier.singleQuote": false, 12 | "prettier.jsxSingleQuote": false, 13 | "prettier.trailingComma": "es5", 14 | "prettier.arrowParens": "always", 15 | "[typescriptreact]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "files.associations": { 19 | "*.css": "tailwindcss" 20 | }, 21 | "editor.quickSuggestions": { 22 | "strings": "on" 23 | } 24 | } -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Header = ({ 6 | headerTitle, 7 | titleClassName, 8 | }: { 9 | headerTitle?: string; 10 | titleClassName?: string; 11 | }) => { 12 | return ( 13 |
14 | {headerTitle ? ( 15 |

16 | {headerTitle} 17 |

18 | ) : ( 19 |
20 | )} 21 | 25 | See all 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default Header; 32 | -------------------------------------------------------------------------------- /public/icons/Pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "skipLibCheck": true, 20 | "noEmit": true 21 | }, 22 | "include": ["./**/*"], 23 | "exclude": ["./_generated"] 24 | } 25 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /public/icons/Play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Manrope } from "next/font/google"; 3 | 4 | import "./globals.css"; 5 | import AudioProvider from "@/providers/AudioProvider"; 6 | import ConvexClerkProvider from "@/providers/ConvexClerkProvider"; 7 | 8 | const manrope = Manrope({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Podcastr", 12 | description: "Generate your podcast using AI", 13 | icons: { 14 | icon: "/icons/logo.svg", 15 | }, 16 | }; 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: Readonly<{ 21 | children: React.ReactNode; 22 | }>) { 23 | return ( 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /public/icons/headphone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Progress = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, value, ...props }, ref) => ( 12 | 20 | 24 | 25 | )); 26 | Progress.displayName = ProgressPrimitive.Root.displayName; 27 | 28 | export { Progress }; 29 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |