""" PodcastItLater Episode Detail Components. Components for displaying individual episode pages with media player, share functionality, and signup prompts for non-authenticated users. """ # : out podcastitlater-episode # : dep ludic import Biz.PodcastItLater.UI as UI import ludic.html as html import typing from ludic.attrs import Attrs from ludic.components import Component from ludic.types import AnyChildren from typing import override class EpisodePlayerAttrs(Attrs): """Attributes for EpisodePlayer component.""" audio_url: str title: str class EpisodePlayer(Component[AnyChildren, EpisodePlayerAttrs]): """HTML5 audio player for episode playback.""" @override def render(self) -> html.div: audio_url = self.attrs["audio_url"] return html.div( html.div( html.div( html.h5( html.i(classes=["bi", "bi-play-circle", "me-2"]), "Listen", classes=["card-title", "mb-3"], ), html.audio( html.source(src=audio_url, type="audio/mpeg"), "Your browser does not support the audio element.", controls=True, preload="metadata", classes=["w-100"], style={"max-width": "100%"}, ), classes=["card-body"], ), classes=["card", "mb-4"], ), ) class ShareButtonAttrs(Attrs): """Attributes for ShareButton component.""" share_url: str class ShareButton(Component[AnyChildren, ShareButtonAttrs]): """Button to copy episode URL to clipboard.""" @override def render(self) -> html.div: share_url = self.attrs["share_url"] return html.div( html.div( html.div( html.h5( html.i(classes=["bi", "bi-share", "me-2"]), "Share Episode", classes=["card-title", "mb-3"], ), html.div( html.div( html.button( html.i(classes=["bi", "bi-copy", "me-1"]), "Copy", type="button", id="share-button", on_click=f"navigator.clipboard.writeText('{share_url}'); " # noqa: E501 "const btn = document.getElementById('share-button'); " # noqa: E501 "const originalHTML = btn.innerHTML; " "btn.innerHTML = 'Copied!'; " # noqa: E501 "btn.classList.remove('btn-outline-secondary'); " # noqa: E501 "btn.classList.add('btn-success'); " "setTimeout(() => {{ " "btn.innerHTML = originalHTML; " "btn.classList.remove('btn-success'); " "btn.classList.add('btn-outline-secondary'); " "}}, 2000);", classes=["btn", "btn-outline-secondary"], ), html.input( type="text", value=share_url, readonly=True, on_focus="this.select()", classes=["form-control"], ), classes=["input-group"], ), ), classes=["card-body"], ), classes=["card"], ), classes=["mb-4"], ) class SignupBannerAttrs(Attrs): """Attributes for SignupBanner component.""" creator_email: str base_url: str class SignupBanner(Component[AnyChildren, SignupBannerAttrs]): """Banner prompting non-authenticated users to sign up.""" @override def render(self) -> html.div: creator_email = self.attrs["creator_email"] return html.div( html.div( html.div( html.div( html.i( classes=[ "bi", "bi-info-circle-fill", "me-2", ], ), html.strong("This episode was created by "), html.code( creator_email, classes=["text-dark"], ), html.strong(" using PodcastItLater."), classes=["mb-3"], ), html.div( html.p( "Want to convert your own articles " "to podcast episodes?", classes=["mb-2"], ), html.form( html.div( html.input( type="email", name="email", placeholder="Enter your email to start", required=True, classes=["form-control"], ), html.button( html.i( classes=[ "bi", "bi-arrow-right-circle", "me-2", ], ), "Sign Up", type="submit", classes=["btn", "btn-primary"], ), classes=["input-group"], ), hx_post="/login", hx_target="#signup-result", hx_swap="innerHTML", ), html.div(id="signup-result", classes=["mt-2"]), ), classes=["card-body"], ), classes=["card", "border-primary"], ), classes=["mb-4"], ) class EpisodeDetailPageAttrs(Attrs): """Attributes for EpisodeDetailPage component.""" episode: dict[str, typing.Any] creator_email: str | None user: dict[str, typing.Any] | None base_url: str class EpisodeDetailPage(Component[AnyChildren, EpisodeDetailPageAttrs]): """Full page view for a single episode.""" @override def render(self) -> UI.PageLayout: episode = self.attrs["episode"] creator_email = self.attrs.get("creator_email") user = self.attrs.get("user") base_url = self.attrs["base_url"] share_url = f"{base_url}/episode/{episode['id']}" duration_str = UI.format_duration(episode.get("duration")) # Build page title page_title = f"{episode['title']} - PodcastItLater" # Build meta tags for Open Graph meta_tags = [ html.meta(property="og:title", content=episode["title"]), html.meta(property="og:type", content="website"), html.meta(property="og:url", content=share_url), html.meta( property="og:description", content=f"Listen to this article read aloud. " f"Duration: {duration_str}" + (f" by {episode['author']}" if episode.get("author") else ""), ), html.meta( property="og:site_name", content="PodcastItLater", ), html.meta(property="og:audio", content=episode["audio_url"]), html.meta(property="og:audio:type", content="audio/mpeg"), ] # Add Twitter Card tags meta_tags.extend([ html.meta(name="twitter:card", content="summary"), html.meta(name="twitter:title", content=episode["title"]), html.meta( name="twitter:description", content=f"Listen to this article. Duration: {duration_str}", ), ]) # Add author if available if episode.get("author"): meta_tags.append( html.meta(property="article:author", content=episode["author"]), ) return UI.PageLayout( # Show signup banner if user is not logged in SignupBanner( creator_email=creator_email or "a user", base_url=base_url, ) if not user and creator_email else html.div(), # Episode title and metadata html.div( html.h2( episode["title"], classes=["display-6", "mb-3"], ), html.div( html.span( html.i(classes=["bi", "bi-person", "me-1"]), f"by {episode['author']}", classes=["text-muted", "me-3"], ) if episode.get("author") else html.span(), html.span( html.i(classes=["bi", "bi-clock", "me-1"]), f"Duration: {duration_str}", classes=["text-muted", "me-3"], ), html.span( html.i(classes=["bi", "bi-calendar", "me-1"]), f"Created: {episode['created_at']}", classes=["text-muted"], ), classes=["mb-3"], ), html.div( html.a( html.i(classes=["bi", "bi-link-45deg", "me-1"]), "View original article", href=episode["original_url"], target="_blank", rel="noopener", classes=["btn", "btn-sm", "btn-outline-secondary"], ), ) if episode.get("original_url") else html.div(), classes=["mb-4"], ), # Audio player EpisodePlayer( audio_url=episode["audio_url"], title=episode["title"], ), # Share button ShareButton(share_url=share_url), # Back to home link html.div( html.a( html.i(classes=["bi", "bi-arrow-left", "me-1"]), "Back to Home", href="/", classes=["btn", "btn-link"], ), classes=["mt-4"], ), user=user, current_page="", error=None, page_title=page_title, meta_tags=meta_tags, )