diff --git a/Dockerfile b/Dockerfile index 915af286..fdb99554 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,14 @@ -FROM node:18-alpine AS builder +FROM node:20-alpine -# Set working directory inside the container WORKDIR /app COPY package*.json ./ -# Install dependencies with legacy peer deps fix RUN npm install --legacy-peer-deps COPY . . -RUN npm run build -# Production Image -FROM node:18-alpine -WORKDIR /app -COPY --from=builder /app /app +# No need to run 'npm run build' for development-mode Docker EXPOSE 3000 -CMD ["npm", "run","serve"] \ No newline at end of file +CMD [ "npm", "run", "dev" ] diff --git a/docker-compose.yml b/docker-compose.yml index 3b09ec5c..6a9e77b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ version: "3.9" - services: recodehive: build: @@ -8,8 +7,8 @@ services: - "3000:3000" volumes: - .:/app - - /app/node_modules + - /app/node_modules # Prevents node_modules being overwritten by the mount working_dir: /app - command: npm run start + command: npm run dev # THIS is the crucial change for hot-reload! environment: - NODE_ENV=development diff --git a/package.json b/package.json index b42bb8b0..d79e8d6f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "docusaurus": "docusaurus", + "dev": "docusaurus start", "start": "docusaurus start", "build": "docusaurus build", "swizzle": "docusaurus swizzle", @@ -15,14 +16,14 @@ "typecheck": "tsc" }, "dependencies": { - "@docusaurus/core": "^3.8.1", - "@docusaurus/plugin-content-docs": "3.8.1", - "@docusaurus/plugin-google-analytics": "^3.8.1", - "@docusaurus/plugin-ideal-image": "3.8.1", - "@docusaurus/preset-classic": "^3.8.1", - "@docusaurus/theme-classic": "^3.8.1", - "@docusaurus/theme-mermaid": "3.8.1", - "@docusaurus/theme-search-algolia": "3.8.1", + "@docusaurus/core": "^3.9.1", + "@docusaurus/plugin-content-docs": "3.9.1", + "@docusaurus/plugin-google-analytics": "^3.9.1", + "@docusaurus/plugin-ideal-image": "3.9.1", + "@docusaurus/preset-classic": "^3.9.1", + "@docusaurus/theme-classic": "^3.9.1", + "@docusaurus/theme-mermaid": "3.9.1", + "@docusaurus/theme-search-algolia": "3.9.1", "@floating-ui/react": "^0.27.8", "@giscus/react": "^3.1.0", "@mdx-js/react": "^3.0.0", diff --git a/src/pages/podcasts/details.tsx b/src/pages/podcasts/details.tsx index d699c5fb..9adbaf72 100644 --- a/src/pages/podcasts/details.tsx +++ b/src/pages/podcasts/details.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import Layout from '@theme/Layout'; import type { ReactElement } from 'react'; import { useLocation, useHistory } from '@docusaurus/router'; @@ -85,26 +85,7 @@ export default function PodcastDetails(): ReactElement { const history = useHistory(); const state = location.state as LocationState; const podcast = state?.podcast; - - const [favorites, setFavorites] = useState(() => { - if (typeof window !== "undefined") { - const saved = localStorage.getItem("podcast-favorites"); - return saved ? JSON.parse(saved) : []; - } - return []; - }); - const isFavorited = podcast ? favorites.includes(podcast.id) : false; - const toggleFavorite = () => { - if (!podcast) return; - setFavorites(prev => { - const updated = prev.includes(podcast.id) - ? prev.filter(id => id !== podcast.id) - : [...prev, podcast.id]; - localStorage.setItem("podcast-favorites", JSON.stringify(updated)); - return updated; - }); - }; - + // Enhanced descriptions with categories const descriptions = { episode: [ @@ -147,7 +128,7 @@ export default function PodcastDetails(): ReactElement { history.goBack(); }; - const handleShare = async (podcast?: PodcastData) => { + const handleShare = async () => { if (navigator.share) { try { await navigator.share({ @@ -193,25 +174,13 @@ export default function PodcastDetails(): ReactElement { Back to Podcasts
- - -
+ + + {/* Main Content */} @@ -278,7 +247,7 @@ export default function PodcastDetails(): ReactElement {
- diff --git a/src/pages/podcasts/index.css b/src/pages/podcasts/index.css index a7c677ad..7e4e21e5 100644 --- a/src/pages/podcasts/index.css +++ b/src/pages/podcasts/index.css @@ -359,7 +359,6 @@ html[data-theme='light'] { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; position: relative; - z-index: 1; overflow: hidden; animation: fadeInUp 0.6s ease-out both; } @@ -405,40 +404,12 @@ html[data-theme='light'] { gap: 8px; opacity: 0; transition: opacity 0.3s ease; - position: relative; - z-index: 10; - pointer-events: auto; } .enhanced-podcast-card:hover .card-actions { opacity: 1; } -.action-btn.share { - background-color: rgba(123, 124, 128, 0.15); /* same subtle bg as unfavorited */ - border: 1px solid rgba(123, 124, 128, 0.3); /* same border */ - transition: all 0.3s ease; - color: inherit; /* keep the existing icon color */ - border-radius: 8px; - width: 36px; - height: 36px; - cursor: pointer; - font-size: 14px; - position: relative; - z-index: 15; - user-select: none; -} - -.action-btn.share:hover { - background-color: rgba(123, 124, 128, 0.25); /* slightly darker on hover */ - border-color: rgba(123, 124, 128, 0.5); - transform: scale(1.1); -} - -.action-btn.share:active { - transform: scale(0.95); -} - .action-btn { width: 36px; height: 36px; @@ -448,35 +419,35 @@ html[data-theme='light'] { cursor: pointer; transition: all 0.3s ease; font-size: 14px; - position: relative; - z-index: 15; - pointer-events: auto !important; - user-select: none; } .action-btn:hover { background: var(--podcast-bg-card-hover); transform: scale(1.1); } -/* Enhanced favorite button styling */ + .action-btn.favorite { - transition: all 0.2s ease; - border: 1px solid transparent; + position: relative; + padding: 12px; /* Increase clickable padding */ + border-radius: 50%; + cursor: pointer; + transition: background-color 0.3s ease; } -.action-btn.favorite:active { - transform: scale(0.95); +.action-btn.favorite:hover, +.action-btn.favorite.favorited { + background-color: rgba(224, 36, 94, 0.1); } -.action-btn.favorite.unfavorited { - background-color: rgba(123, 124, 128, 0.15); /* light purple-blue tint */ - color: white; /* keep the heart white */ - border-color: rgba(123, 124, 128, 0.3); +.action-btn.favorite > svg, +.action-btn.favorite > img, +.action-btn.favorite > span { + pointer-events: none; + font-size: 1.5rem; + display: inline-block; + vertical-align: middle; } -.action-btn.favorite.unfavorited:hover { - background-color: rgba(123, 124, 128, 0.15); -} /* Podcast Title */ .podcast-title { @@ -774,4 +745,4 @@ html[data-theme='light'] { .search-input { font-size: 14px; } -} \ No newline at end of file +} diff --git a/src/pages/podcasts/index.tsx b/src/pages/podcasts/index.tsx index 8676c83b..d7042d39 100644 --- a/src/pages/podcasts/index.tsx +++ b/src/pages/podcasts/index.tsx @@ -10,19 +10,16 @@ interface PodcastData { type: 'episode' | 'show' | 'playlist'; } -// Function to extract Spotify ID from URL const getSpotifyEmbedId = (url: string): string => { const match = url.match(/(?:spotify\.com|open\.spotify\.com)\/(episode|show|playlist)\/([a-zA-Z0-9]+)/); return match ? match[2] : url; }; -// Function to determine content type from URL const getSpotifyContentType = (url: string): 'episode' | 'show' | 'playlist' => { const match = url.match(/(?:spotify\.com|open\.spotify\.com)\/(episode|show|playlist)/); return (match?.[1] as 'episode' | 'show' | 'playlist') || 'show'; }; -// Add your podcasts here const podcastUrls: string[] = [ "https://open.spotify.com/show/6oPJ7ZBlN7y34yiSMguIda?si=729edf3b64a246d7", "https://open.spotify.com/episode/1zbmUPmGRjC8o8YIMMx2Z6?si=Q4QAS3IJQVGaQYhYApckdA", @@ -42,13 +39,7 @@ const podcastData: PodcastData[] = podcastUrls.map((url, index) => ({ type: getSpotifyContentType(url) })); -interface SpotifyTitleProps { - spotifyUrl: string; - type: 'episode' | 'show' | 'playlist'; -} - -// Fetches the podcast/show/episode title from Spotify oEmbed API -const SpotifyTitle: React.FC = ({ spotifyUrl, type }) => { +const SpotifyTitle: React.FC<{spotifyUrl: string; type: 'episode' | 'show' | 'playlist'}> = ({ spotifyUrl, type }) => { const [title, setTitle] = React.useState(''); const [loading, setLoading] = React.useState(true); @@ -82,14 +73,10 @@ const SpotifyTitle: React.FC = ({ spotifyUrl, type }) => { ) : ( <>
- - {type === 'episode' ? '🎙️' : type === 'show' ? '📻' : '🎵'} - + {type === 'episode' ? '🎙️' : type === 'show' ? '📻' : '🎵'} {type.charAt(0).toUpperCase() + type.slice(1)}
-

- {title || `${type.charAt(0).toUpperCase() + type.slice(1)} #${Math.floor(Math.random() * 100) + 1}`} -

+

{title || `${type.charAt(0).toUpperCase() + type.slice(1)} #${Math.floor(Math.random() * 100) + 1}`}

)}
@@ -99,46 +86,41 @@ const SpotifyTitle: React.FC = ({ spotifyUrl, type }) => { export default function Podcasts(): ReactElement { const history = useHistory(); const [currentPage, setCurrentPage] = useState(1); - const [searchTerm, setSearchTerm] = useState(""); - const [selectedFilter, setSelectedFilter] = useState< - "all" | "episode" | "show" | "playlist" - >("all"); - const [favorites, setFavorites] = useState(() => { - // Load favorites from localStorage on component mount - if (typeof window !== "undefined") { - const saved = localStorage.getItem("podcast-favorites"); - return saved ? JSON.parse(saved) : []; - } - return []; - }); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedFilter, setSelectedFilter] = useState<'all' | 'episode' | 'show' | 'playlist'>('all'); const podcastsPerPage = 9; - // Filter podcasts based on search and filter - const filteredPodcasts = podcastData.filter(podcast => { - const matchesFilter = selectedFilter === 'all' || podcast.type === selectedFilter; - return matchesFilter; + const [favorites, setFavorites] = useState(() => { + const saved = localStorage.getItem('favoritePodcasts'); + return saved ? JSON.parse(saved) : []; }); - // Calculate podcasts for current page + const toggleFavorite = (podcastId: string) => { + setFavorites(prev => { + const updated = prev.includes(podcastId) + ? prev.filter(id => id !== podcastId) + : [...prev, podcastId]; + localStorage.setItem('favoritePodcasts', JSON.stringify(updated)); + return updated; + }); + }; + + const filteredPodcasts = podcastData.filter(podcast => selectedFilter === 'all' || podcast.type === selectedFilter); const indexOfLastPodcast = currentPage * podcastsPerPage; const indexOfFirstPodcast = indexOfLastPodcast - podcastsPerPage; const currentPodcasts = filteredPodcasts.slice(indexOfFirstPodcast, indexOfLastPodcast); const totalPages = Math.ceil(filteredPodcasts.length / podcastsPerPage); - const handlePageChange = (pageNumber: number) => { - setCurrentPage(pageNumber); - window.scrollTo({ top: 0, behavior: 'smooth' }); + const handlePageChange = (page: number) => { + setCurrentPage(page); + window.scrollTo({top: 0, behavior: 'smooth'}); }; const handleShare = async (podcast: PodcastData) => { if (navigator.share) { try { - await navigator.share({ - title: `Check out this ${podcast.type}`, - url: podcast.spotifyUrl, - }); - } catch (err) { - // Fallback to clipboard + await navigator.share({title: `Check out this ${podcast.type}`, url: podcast.spotifyUrl}); + } catch { navigator.clipboard.writeText(podcast.spotifyUrl); } } else { @@ -146,65 +128,29 @@ export default function Podcasts(): ReactElement { } }; - const handleFavorite = (podcast: PodcastData, event: React.MouseEvent) => { - // Prevent card click when clicking favorite button - event.stopPropagation(); - - setFavorites((prev) => { - const isFavorited = prev.includes(podcast.id); - const newFavorites = isFavorited - ? prev.filter((id) => id !== podcast.id) // Remove from favorites - : [...prev, podcast.id]; // Add to favorites - - // Save to localStorage for persistence - if (typeof window !== "undefined") { - localStorage.setItem("podcast-favorites", JSON.stringify(newFavorites)); - } - - return newFavorites; - }); - }; - - const handlePodcastClick = ( - podcast: PodcastData, - event: React.MouseEvent | React.KeyboardEvent - ) => { + const handlePodcastClick = (podcast: PodcastData, event: React.MouseEvent | React.KeyboardEvent) => { const target = event.target as HTMLElement; - - // Prevent navigation if clicking on buttons or action area - if ( - target.tagName === "IFRAME" || - target.closest(".podcast-embed") || - target.closest(".action-btn") || // Don't navigate if clicking buttons - target.closest(".card-actions") || // Don't navigate if clicking action area - target.classList.contains("action-btn") || - target.classList.contains("favorite") || - target.classList.contains("share") - ) { + if (target.tagName === 'IFRAME' || target.closest('.podcast-embed') || target.closest('.action-btn')) { return; } - - history.push("/podcasts/details", { podcast }); + history.push('/podcasts/details', {podcast}); }; + React.useEffect(() => setCurrentPage(1), [searchTerm, selectedFilter]); + return (
- {/* Hero Section */}
🎙️ Premium Audio Content
-

- Discover Top Podcasts -

+

Discover Top Podcasts

Stream the best podcasts from your favorite stations. Dive into episodes that inspire, educate, and entertain from leading voices in tech, business, and beyond.

- - {/* Stats */}
{podcastData.length}+
@@ -222,112 +168,70 @@ export default function Podcasts(): ReactElement {
- {/* Filter Section */}
- setSearchTerm(e.target.value)} - className="search-input" - /> + setSearchTerm(e.target.value)} className="search-input" />
- - - -
- {/* Content Grid */}
{currentPodcasts.length > 0 ? ( <>
- {currentPodcasts.map((podcast, index) => ( + {currentPodcasts.map((podcast, idx) => (
handlePodcastClick(podcast, e)} + onClick={e => handlePodcastClick(podcast, e)} role="button" tabIndex={0} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - handlePodcastClick(podcast, e); - } + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') handlePodcastClick(podcast, e); }} - style={{ animationDelay: `${index * 0.1}s` }} + style={{ animationDelay: `${idx * 0.1}s` }} >
- -
{ - e.stopPropagation(); - e.preventDefault(); - }} - onMouseDown={(e) => { - e.stopPropagation(); - }} - > + +
-
-
e.stopPropagation()}> +
e.stopPropagation()}>