├── .nvmrc ├── ui ├── src │ ├── pages │ │ ├── Podcast │ │ │ ├── styles.scss │ │ │ ├── PodcastInfo │ │ │ │ └── styles.scss │ │ │ ├── index.tsx │ │ │ └── Value4Value │ │ │ │ └── styles.scss │ │ ├── Stats │ │ │ ├── styles.scss │ │ │ ├── StatsCard │ │ │ │ ├── styles.scss │ │ │ │ └── index.tsx │ │ │ ├── NewFeedStatsCard │ │ │ │ ├── styles.scss │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Developers │ │ │ ├── styles.scss │ │ │ ├── Access │ │ │ │ ├── styles.scss │ │ │ │ └── index.tsx │ │ │ ├── Home │ │ │ │ ├── styles.scss │ │ │ │ └── index.tsx │ │ │ ├── Search │ │ │ │ ├── styles.scss │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Apps │ │ │ ├── styles.scss │ │ │ ├── AppsWebPart │ │ │ │ ├── SingleApp │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── styles.scss │ │ │ │ ├── FilterTags │ │ │ │ │ └── styles.scss │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Search │ │ │ └── styles.scss │ │ ├── Donations │ │ │ └── index.tsx │ │ ├── styles.scss │ │ ├── mobile.scss │ │ └── AddFeed │ │ │ └── styles.scss │ ├── components │ │ ├── ResultsFeeds │ │ │ ├── styles.scss │ │ │ └── index.tsx │ │ ├── ResultsEpisodes │ │ │ ├── styles.scss │ │ │ └── index.tsx │ │ ├── EpisodesPlayer │ │ │ └── styles.scss │ │ ├── SphinxChat │ │ │ ├── styles.scss │ │ │ └── index.tsx │ │ ├── ThemeButton │ │ │ ├── styles.scss │ │ │ └── index.tsx │ │ ├── ScrollToTop │ │ │ └── index.tsx │ │ ├── Card │ │ │ ├── styles.scss │ │ │ └── index.tsx │ │ ├── InfiniteList │ │ │ └── styles.scss │ │ ├── Value │ │ │ ├── styles.scss │ │ │ └── index.tsx │ │ ├── KPI │ │ │ ├── index.tsx │ │ │ └── styles.scss │ │ ├── Button │ │ │ ├── styles.scss │ │ │ └── index.tsx │ │ ├── CommentMenu │ │ │ └── styles.scss │ │ ├── Boostagram │ │ │ └── styles.scss │ │ ├── Player │ │ │ └── styles.scss │ │ ├── ResultItem │ │ │ ├── index.tsx │ │ │ └── styles.scss │ │ ├── SearchBar │ │ │ ├── index.tsx │ │ │ └── styles.scss │ │ ├── EpisodeItem │ │ │ └── styles.scss │ │ ├── TopBar │ │ │ └── styles.scss │ │ ├── Comments │ │ │ └── styles.scss │ │ └── PodcastHeader │ │ │ └── styles.scss │ ├── state │ │ ├── modules │ │ │ ├── index.ts │ │ │ ├── users │ │ │ │ ├── types.ts │ │ │ │ ├── actions.ts │ │ │ │ ├── api.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── reducers.ts │ │ │ └── _abstract │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── actions.ts │ │ │ │ ├── types.ts │ │ │ │ ├── reducer.ts │ │ │ │ └── operations.ts │ │ └── store.ts │ ├── custom.d.ts │ ├── routes.tsx │ ├── index.tsx │ ├── styles.scss │ └── utils.ts ├── fonts │ ├── D-DIN.otf │ ├── D-DINExp.otf │ ├── D-DIN-Bold.otf │ ├── D-DIN-Italic.otf │ ├── D-DINCondensed.otf │ ├── D-DINExp-Bold.otf │ ├── D-DINExp-Italic.otf │ ├── D-DINCondensed-Bold.otf │ └── DIN-Alternate-Bold.ttf ├── images │ ├── app-store.png │ ├── pci_avatar.jpg │ ├── pci_banner.jpg │ ├── no-cover-art.png │ ├── lightning.svg │ ├── dark_mode.svg │ ├── play-circle.svg │ ├── pause-circle.svg │ ├── chevron-back-outline.svg │ ├── chevron-forward-outline.svg │ ├── feed.svg │ ├── download-outline.svg │ ├── earth.svg │ ├── landing-bg.svg │ ├── link.svg │ ├── light_mode.svg │ ├── search.svg │ ├── menu.svg │ ├── brand-icon.svg │ ├── transcript.svg │ ├── podlink.svg │ └── donation-page.svg └── public │ └── pci_avatar.svg ├── .environments └── .gitignore ├── .yarnrc.yml ├── server ├── www │ └── robots.txt ├── assets │ ├── alby.png │ ├── fyyd.png │ ├── msp.png │ ├── n2n2.png │ ├── onda.png │ ├── sf.png │ ├── unde.png │ ├── UpNext.png │ ├── alitu.png │ ├── breez.png │ ├── bubbl.png │ ├── castos.png │ ├── castro.jpg │ ├── fathom.png │ ├── github.jpg │ ├── ionofm.png │ ├── ivyfm.jpg │ ├── kasts.png │ ├── libsyn.png │ ├── plink.png │ ├── podlp.png │ ├── podtoo.png │ ├── rsscom.png │ ├── skimr.png │ ├── vodio.jpg │ ├── AIPodNav.png │ ├── audiowave.jpg │ ├── aushalogo.png │ ├── blubrry.png │ ├── boostcli.png │ ├── caproni.png │ ├── captivate.png │ ├── castopod.png │ ├── cooler.webp │ ├── digilore.png │ ├── disctopia.png │ ├── escapepod.png │ ├── fireside.jpg │ ├── fountain.png │ ├── gpodder.png │ ├── headliner.png │ ├── hubhopper.png │ ├── icatcher.webp │ ├── jellypod.png │ ├── jumplink.png │ ├── justcast.png │ ├── lnbeats.png │ ├── metacast.png │ ├── mpmoney.jpg │ ├── myweblog.png │ ├── oncetold.png │ ├── peertube.png │ ├── pinepods.png │ ├── playapod.png │ ├── podCloud.png │ ├── podbean.webp │ ├── podcastai.png │ ├── podextra.png │ ├── podferry.png │ ├── podfriend.jpg │ ├── podhome.png │ ├── podnews.jpg │ ├── podpage.png │ ├── podrank.png │ ├── podrocket.png │ ├── pods.bg.png │ ├── podserve.png │ ├── podverse.jpg │ ├── podvine.png │ ├── radioport.png │ ├── redcircle.png │ ├── rssblue.png │ ├── sounder.png │ ├── split-kit.png │ ├── spreaker.png │ ├── stenofm.png │ ├── taddydev.png │ ├── truefans.jpg │ ├── tsacdop.png │ ├── usocial.png │ ├── v4vmusic.png │ ├── wherever.png │ ├── 3speaklogo.png │ ├── antennapod.png │ ├── anytimelogo.png │ ├── audicylogo.png │ ├── audiomeans.png │ ├── buzzsprout.png │ ├── castamatic.png │ ├── castgarden.png │ ├── chaptersapp.jpg │ ├── chaptertool.png │ ├── curiocaster.png │ ├── mediavault.png │ ├── overhaulfm.png │ ├── playdioCast.png │ ├── pocketCasts.png │ ├── podcastguru.jpg │ ├── podcastpage.png │ ├── podcatcher.png │ ├── pods-blitz.png │ ├── podstation.jpg │ ├── powerpress.png │ ├── simplecast.png │ ├── sphinxchat.png │ ├── transistor.jpg │ ├── turtlecast.png │ ├── FocusPodcast.png │ ├── applepodcasts.jpg │ ├── castcoverage.png │ ├── doerfelverse.png │ ├── hypercatcher.jpg │ ├── ipfspodcasting.png │ ├── moonbeam_logo.png │ ├── podcastaddict.png │ ├── podcastdetails.png │ ├── podcastindex.jpg │ ├── podcastmirror.png │ ├── proxyfeed-io.png │ ├── podcastrepublic.jpg │ ├── podtrics_p_logo.png │ ├── podcastindextwitter.png │ ├── podlove-publisher.png │ ├── satoshisstream-bot.png │ ├── spotifyforcreators.png │ ├── seriouslysimplepodcasting.png │ ├── podigee.svg │ ├── musixmatch.svg │ ├── podcastaddict.svg │ └── podlink.svg └── data │ ├── stats.json │ └── newfeedstats.json ├── .prettierrc.yaml ├── _old ├── dev │ ├── favicon.ico │ ├── images │ │ ├── spinner.gif │ │ └── overcast-button.png │ └── style │ │ ├── default.css │ │ └── style.css ├── www │ ├── favicon.ico │ └── images │ │ ├── pci_avatar.jpg │ │ └── pci_banner.jpg └── README.md ├── .env-example ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── .github └── workflows │ └── validate-apps.yml ├── LICENSE ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.20.2 2 | -------------------------------------------------------------------------------- /ui/src/pages/Podcast/styles.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/pages/Stats/styles.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.environments/.gitignore: -------------------------------------------------------------------------------- 1 | .env.* 2 | -------------------------------------------------------------------------------- /ui/src/pages/Developers/styles.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /ui/src/pages/Developers/Access/styles.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/pages/Developers/Home/styles.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/pages/Developers/Search/styles.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/www/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /podcast/ 3 | -------------------------------------------------------------------------------- /ui/src/components/ResultsFeeds/styles.scss: -------------------------------------------------------------------------------- 1 | .results-list { 2 | } 3 | -------------------------------------------------------------------------------- /ui/src/state/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { usersReducer } from "./users"; 2 | -------------------------------------------------------------------------------- /ui/src/components/ResultsEpisodes/styles.scss: -------------------------------------------------------------------------------- 1 | .results-episodes { 2 | } 3 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: 'es5' 2 | tabWidth: 2 3 | semi: false 4 | singleQuote: true 5 | -------------------------------------------------------------------------------- /ui/fonts/D-DIN.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/fonts/D-DIN.otf -------------------------------------------------------------------------------- /_old/dev/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/_old/dev/favicon.ico -------------------------------------------------------------------------------- /_old/www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/_old/www/favicon.ico -------------------------------------------------------------------------------- /server/assets/alby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/alby.png -------------------------------------------------------------------------------- /server/assets/fyyd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/fyyd.png -------------------------------------------------------------------------------- /server/assets/msp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/msp.png -------------------------------------------------------------------------------- /server/assets/n2n2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/n2n2.png -------------------------------------------------------------------------------- /server/assets/onda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/onda.png -------------------------------------------------------------------------------- /server/assets/sf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/sf.png -------------------------------------------------------------------------------- /server/assets/unde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/unde.png -------------------------------------------------------------------------------- /ui/fonts/D-DINExp.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/fonts/D-DINExp.otf -------------------------------------------------------------------------------- /server/assets/UpNext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/UpNext.png -------------------------------------------------------------------------------- /server/assets/alitu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/alitu.png -------------------------------------------------------------------------------- /server/assets/breez.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/breez.png -------------------------------------------------------------------------------- /server/assets/bubbl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/bubbl.png -------------------------------------------------------------------------------- /server/assets/castos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/castos.png -------------------------------------------------------------------------------- /server/assets/castro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/castro.jpg -------------------------------------------------------------------------------- /server/assets/fathom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/fathom.png -------------------------------------------------------------------------------- /server/assets/github.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/github.jpg -------------------------------------------------------------------------------- /server/assets/ionofm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/ionofm.png -------------------------------------------------------------------------------- /server/assets/ivyfm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/ivyfm.jpg -------------------------------------------------------------------------------- /server/assets/kasts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/kasts.png -------------------------------------------------------------------------------- /server/assets/libsyn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/libsyn.png -------------------------------------------------------------------------------- /server/assets/plink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/plink.png -------------------------------------------------------------------------------- /server/assets/podlp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podlp.png -------------------------------------------------------------------------------- /server/assets/podtoo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podtoo.png -------------------------------------------------------------------------------- /server/assets/rsscom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/rsscom.png -------------------------------------------------------------------------------- /server/assets/skimr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/skimr.png -------------------------------------------------------------------------------- /server/assets/vodio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/vodio.jpg -------------------------------------------------------------------------------- /ui/fonts/D-DIN-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/fonts/D-DIN-Bold.otf -------------------------------------------------------------------------------- /ui/images/app-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/images/app-store.png -------------------------------------------------------------------------------- /ui/images/pci_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/images/pci_avatar.jpg -------------------------------------------------------------------------------- /ui/images/pci_banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/images/pci_banner.jpg -------------------------------------------------------------------------------- /ui/src/components/EpisodesPlayer/styles.scss: -------------------------------------------------------------------------------- 1 | .podcast-header-player { 2 | margin-top: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /_old/dev/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/_old/dev/images/spinner.gif -------------------------------------------------------------------------------- /server/assets/AIPodNav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/AIPodNav.png -------------------------------------------------------------------------------- /server/assets/audiowave.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/audiowave.jpg -------------------------------------------------------------------------------- /server/assets/aushalogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/aushalogo.png -------------------------------------------------------------------------------- /server/assets/blubrry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/blubrry.png -------------------------------------------------------------------------------- /server/assets/boostcli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/boostcli.png -------------------------------------------------------------------------------- /server/assets/caproni.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/caproni.png -------------------------------------------------------------------------------- /server/assets/captivate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/captivate.png -------------------------------------------------------------------------------- /server/assets/castopod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/castopod.png -------------------------------------------------------------------------------- /server/assets/cooler.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/cooler.webp -------------------------------------------------------------------------------- /server/assets/digilore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/digilore.png -------------------------------------------------------------------------------- /server/assets/disctopia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/disctopia.png -------------------------------------------------------------------------------- /server/assets/escapepod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/escapepod.png -------------------------------------------------------------------------------- /server/assets/fireside.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/fireside.jpg -------------------------------------------------------------------------------- /server/assets/fountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/fountain.png -------------------------------------------------------------------------------- /server/assets/gpodder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/gpodder.png -------------------------------------------------------------------------------- /server/assets/headliner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/headliner.png -------------------------------------------------------------------------------- /server/assets/hubhopper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/hubhopper.png -------------------------------------------------------------------------------- /server/assets/icatcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/icatcher.webp -------------------------------------------------------------------------------- /server/assets/jellypod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/jellypod.png -------------------------------------------------------------------------------- /server/assets/jumplink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/jumplink.png -------------------------------------------------------------------------------- /server/assets/justcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/justcast.png -------------------------------------------------------------------------------- /server/assets/lnbeats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/lnbeats.png -------------------------------------------------------------------------------- /server/assets/metacast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/metacast.png -------------------------------------------------------------------------------- /server/assets/mpmoney.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/mpmoney.jpg -------------------------------------------------------------------------------- /server/assets/myweblog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/myweblog.png -------------------------------------------------------------------------------- /server/assets/oncetold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/oncetold.png -------------------------------------------------------------------------------- /server/assets/peertube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/peertube.png -------------------------------------------------------------------------------- /server/assets/pinepods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/pinepods.png -------------------------------------------------------------------------------- /server/assets/playapod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/playapod.png -------------------------------------------------------------------------------- /server/assets/podCloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podCloud.png -------------------------------------------------------------------------------- /server/assets/podbean.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podbean.webp -------------------------------------------------------------------------------- /server/assets/podcastai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcastai.png -------------------------------------------------------------------------------- /server/assets/podextra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podextra.png -------------------------------------------------------------------------------- /server/assets/podferry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podferry.png -------------------------------------------------------------------------------- /server/assets/podfriend.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podfriend.jpg -------------------------------------------------------------------------------- /server/assets/podhome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podhome.png -------------------------------------------------------------------------------- /server/assets/podnews.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podnews.jpg -------------------------------------------------------------------------------- /server/assets/podpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podpage.png -------------------------------------------------------------------------------- /server/assets/podrank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podrank.png -------------------------------------------------------------------------------- /server/assets/podrocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podrocket.png -------------------------------------------------------------------------------- /server/assets/pods.bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/pods.bg.png -------------------------------------------------------------------------------- /server/assets/podserve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podserve.png -------------------------------------------------------------------------------- /server/assets/podverse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podverse.jpg -------------------------------------------------------------------------------- /server/assets/podvine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podvine.png -------------------------------------------------------------------------------- /server/assets/radioport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/radioport.png -------------------------------------------------------------------------------- /server/assets/redcircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/redcircle.png -------------------------------------------------------------------------------- /server/assets/rssblue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/rssblue.png -------------------------------------------------------------------------------- /server/assets/sounder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/sounder.png -------------------------------------------------------------------------------- /server/assets/split-kit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/split-kit.png -------------------------------------------------------------------------------- /server/assets/spreaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/spreaker.png -------------------------------------------------------------------------------- /server/assets/stenofm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/stenofm.png -------------------------------------------------------------------------------- /server/assets/taddydev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/taddydev.png -------------------------------------------------------------------------------- /server/assets/truefans.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/truefans.jpg -------------------------------------------------------------------------------- /server/assets/tsacdop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/tsacdop.png -------------------------------------------------------------------------------- /server/assets/usocial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/usocial.png -------------------------------------------------------------------------------- /server/assets/v4vmusic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/v4vmusic.png -------------------------------------------------------------------------------- /server/assets/wherever.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/wherever.png -------------------------------------------------------------------------------- /ui/fonts/D-DIN-Italic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/fonts/D-DIN-Italic.otf -------------------------------------------------------------------------------- /ui/fonts/D-DINCondensed.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/fonts/D-DINCondensed.otf -------------------------------------------------------------------------------- /ui/fonts/D-DINExp-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/fonts/D-DINExp-Bold.otf -------------------------------------------------------------------------------- /ui/images/no-cover-art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/images/no-cover-art.png -------------------------------------------------------------------------------- /server/assets/3speaklogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/3speaklogo.png -------------------------------------------------------------------------------- /server/assets/antennapod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/antennapod.png -------------------------------------------------------------------------------- /server/assets/anytimelogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/anytimelogo.png -------------------------------------------------------------------------------- /server/assets/audicylogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/audicylogo.png -------------------------------------------------------------------------------- /server/assets/audiomeans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/audiomeans.png -------------------------------------------------------------------------------- /server/assets/buzzsprout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/buzzsprout.png -------------------------------------------------------------------------------- /server/assets/castamatic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/castamatic.png -------------------------------------------------------------------------------- /server/assets/castgarden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/castgarden.png -------------------------------------------------------------------------------- /server/assets/chaptersapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/chaptersapp.jpg -------------------------------------------------------------------------------- /server/assets/chaptertool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/chaptertool.png -------------------------------------------------------------------------------- /server/assets/curiocaster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/curiocaster.png -------------------------------------------------------------------------------- /server/assets/mediavault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/mediavault.png -------------------------------------------------------------------------------- /server/assets/overhaulfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/overhaulfm.png -------------------------------------------------------------------------------- /server/assets/playdioCast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/playdioCast.png -------------------------------------------------------------------------------- /server/assets/pocketCasts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/pocketCasts.png -------------------------------------------------------------------------------- /server/assets/podcastguru.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcastguru.jpg -------------------------------------------------------------------------------- /server/assets/podcastpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcastpage.png -------------------------------------------------------------------------------- /server/assets/podcatcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcatcher.png -------------------------------------------------------------------------------- /server/assets/pods-blitz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/pods-blitz.png -------------------------------------------------------------------------------- /server/assets/podstation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podstation.jpg -------------------------------------------------------------------------------- /server/assets/powerpress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/powerpress.png -------------------------------------------------------------------------------- /server/assets/simplecast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/simplecast.png -------------------------------------------------------------------------------- /server/assets/sphinxchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/sphinxchat.png -------------------------------------------------------------------------------- /server/assets/transistor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/transistor.jpg -------------------------------------------------------------------------------- /server/assets/turtlecast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/turtlecast.png -------------------------------------------------------------------------------- /ui/fonts/D-DINExp-Italic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/fonts/D-DINExp-Italic.otf -------------------------------------------------------------------------------- /_old/www/images/pci_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/_old/www/images/pci_avatar.jpg -------------------------------------------------------------------------------- /_old/www/images/pci_banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/_old/www/images/pci_banner.jpg -------------------------------------------------------------------------------- /server/assets/FocusPodcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/FocusPodcast.png -------------------------------------------------------------------------------- /server/assets/applepodcasts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/applepodcasts.jpg -------------------------------------------------------------------------------- /server/assets/castcoverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/castcoverage.png -------------------------------------------------------------------------------- /server/assets/doerfelverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/doerfelverse.png -------------------------------------------------------------------------------- /server/assets/hypercatcher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/hypercatcher.jpg -------------------------------------------------------------------------------- /server/assets/ipfspodcasting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/ipfspodcasting.png -------------------------------------------------------------------------------- /server/assets/moonbeam_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/moonbeam_logo.png -------------------------------------------------------------------------------- /server/assets/podcastaddict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcastaddict.png -------------------------------------------------------------------------------- /server/assets/podcastdetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcastdetails.png -------------------------------------------------------------------------------- /server/assets/podcastindex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcastindex.jpg -------------------------------------------------------------------------------- /server/assets/podcastmirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcastmirror.png -------------------------------------------------------------------------------- /server/assets/proxyfeed-io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/proxyfeed-io.png -------------------------------------------------------------------------------- /ui/fonts/D-DINCondensed-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/fonts/D-DINCondensed-Bold.otf -------------------------------------------------------------------------------- /ui/fonts/DIN-Alternate-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/ui/fonts/DIN-Alternate-Bold.ttf -------------------------------------------------------------------------------- /server/assets/podcastrepublic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcastrepublic.jpg -------------------------------------------------------------------------------- /server/assets/podtrics_p_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podtrics_p_logo.png -------------------------------------------------------------------------------- /_old/dev/images/overcast-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/_old/dev/images/overcast-button.png -------------------------------------------------------------------------------- /server/assets/podcastindextwitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podcastindextwitter.png -------------------------------------------------------------------------------- /server/assets/podlove-publisher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/podlove-publisher.png -------------------------------------------------------------------------------- /server/assets/satoshisstream-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/satoshisstream-bot.png -------------------------------------------------------------------------------- /server/assets/spotifyforcreators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/spotifyforcreators.png -------------------------------------------------------------------------------- /ui/src/state/modules/users/types.ts: -------------------------------------------------------------------------------- 1 | export const SEND_MESSAGE = 'SEND_MESSAGE'; 2 | export const DELETE_MESSAGE = 'DELETE_MESSAGE'; -------------------------------------------------------------------------------- /ui/src/pages/Apps/styles.scss: -------------------------------------------------------------------------------- 1 | .csb { 2 | background-color: darkolivegreen; 3 | color: yellow; 4 | font-size: 12px; 5 | } 6 | -------------------------------------------------------------------------------- /server/assets/seriouslysimplepodcasting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Podcastindex-org/web-ui/HEAD/server/assets/seriouslysimplepodcasting.png -------------------------------------------------------------------------------- /ui/src/state/modules/users/actions.ts: -------------------------------------------------------------------------------- 1 | import { createBaseActions } from '../_abstract'; 2 | 3 | export const { Creators, Types } = createBaseActions('users'); 4 | -------------------------------------------------------------------------------- /ui/src/state/modules/_abstract/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { operations } from './operations'; 3 | export * from './reducer'; 4 | export { BaseState } from './schema'; 5 | export * from './actions'; -------------------------------------------------------------------------------- /ui/src/state/modules/_abstract/schema.ts: -------------------------------------------------------------------------------- 1 | export interface BaseState { 2 | readonly loading: boolean 3 | readonly hydrated: boolean, 4 | readonly data: any[] 5 | readonly errors?: object 6 | } -------------------------------------------------------------------------------- /ui/src/components/SphinxChat/styles.scss: -------------------------------------------------------------------------------- 1 | // Mobile resize 2 | @media only screen and (max-width: 767px) { 3 | sphinx-widget { 4 | > div { 5 | width: 300px !important; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/pages/Podcast/PodcastInfo/styles.scss: -------------------------------------------------------------------------------- 1 | .episode-header { 2 | font-family: var(--font-family-exp); 3 | font-weight: bold; 4 | width: 100%; 5 | font-size: 30px; 6 | color: var(--fg-main); 7 | text-align: left; 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/state/modules/users/api.ts: -------------------------------------------------------------------------------- 1 | import { ApiBase } from '../_abstract/api'; 2 | 3 | export class UsersApi extends ApiBase { 4 | constructor() { 5 | super('users'); 6 | } 7 | } 8 | 9 | export default new UsersApi(); 10 | -------------------------------------------------------------------------------- /server/data/stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "feedCountTotal": "4,073,767", 3 | "feedCount3days": "110,972", 4 | "feedCount10days": "249,286", 5 | "feedCount30days": "384,458", 6 | "feedCount60days": "522,450", 7 | "feedCount90days": "618,924" 8 | } 9 | -------------------------------------------------------------------------------- /_old/README.md: -------------------------------------------------------------------------------- 1 | # Website UI 2 | 3 | This repository contains the websites for the home page (in the www folder) and for the developer portal (in the dev folder). These are PHP backed sites, 4 | but that has been removed to just leave the HTML output to make design easier. 5 | 6 | Feel free to make this beautiful. -------------------------------------------------------------------------------- /ui/src/state/modules/users/index.ts: -------------------------------------------------------------------------------- 1 | import { operations } from '../_abstract'; 2 | import UsersApi from './api'; 3 | import { Creators } from './actions'; 4 | // --- 5 | export { default as usersReducer } from "./reducers"; 6 | export { UserState, User } from './schema'; 7 | 8 | export default operations(Creators, UsersApi); -------------------------------------------------------------------------------- /ui/images/lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /ui/images/dark_mode.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/src/pages/Search/styles.scss: -------------------------------------------------------------------------------- 1 | .results-page { 2 | margin: 0 auto; 3 | position: relative; 4 | z-index: 1; 5 | width: 1024px; 6 | } 7 | .loader-wrapper { 8 | margin: 0 auto; 9 | position: relative; 10 | height: 100%; 11 | z-index: 1; 12 | display: flex; 13 | flex-direction: row; 14 | justify-content: center; 15 | align-items: center; 16 | } 17 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | # Copy this file and rename .env, then place your api credentials in here 2 | API_KEY=ABC 3 | API_SECRET=ABC 4 | # separate key for add action 5 | API_ADD_KEY=ABC 6 | API_ADD_SECRET=ABC 7 | # API User Agent 8 | API_USER_AGENT=PodcastIndexBot/@podcast@noagendasocial.com 9 | # This is the port that the UI server starts on 10 | PORT=5001 11 | # this can be 'development' or 'production' 12 | NODE_ENV=development 13 | -------------------------------------------------------------------------------- /ui/src/components/ThemeButton/styles.scss: -------------------------------------------------------------------------------- 1 | .theme-button { 2 | width: 20px; 3 | height: 20px; 4 | 5 | position: fixed; 6 | right: 15px; 7 | bottom: 15px; 8 | 9 | text-align: center; 10 | z-index: 99999; 11 | 12 | &:hover { 13 | cursor: grab; 14 | } 15 | 16 | img { 17 | width: 100%; 18 | height: 100%; 19 | background: inherit; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | tab_width = 2 10 | 11 | [{*.ats,*.cts,*.mts,*.ts,*.tsx}] 12 | ij_typescript_spaces_within_imports = true 13 | 14 | [{*.cjs,*.js}] 15 | ij_javascript_spaces_within_imports = true 16 | 17 | [{*.yaml,*.yml}] 18 | indent_size = 2 19 | 20 | [*.json] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "target": "es6", 6 | "jsx": "react", 7 | "module": "es6", 8 | "moduleResolution": "node", 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "allowSyntheticDefaultImports": true, 12 | "resolveJsonModule": true 13 | }, 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /ui/images/play-circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Environments 4 | .env 5 | .venv 6 | env/ 7 | venv/ 8 | ENV/ 9 | env.bak/ 10 | venv.bak/ 11 | 12 | .DS_Store 13 | .vscode 14 | .idea 15 | 16 | *.log 17 | www 18 | 19 | package-lock.json 20 | 21 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 22 | # We are not using Zero-installs 23 | .pnp.* 24 | .yarn/* 25 | !.yarn/patches 26 | !.yarn/plugins 27 | !.yarn/releases 28 | !.yarn/sdks 29 | !.yarn/versions -------------------------------------------------------------------------------- /ui/images/pause-circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ui/src/pages/Developers/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import './styles.scss' 4 | 5 | interface IProps {} 6 | 7 | export default class Card extends React.Component { 8 | static defaultProps = {} 9 | 10 | constructor(props: IProps) { 11 | super(props) 12 | } 13 | 14 | render() { 15 | const {} = this.props 16 | return ( 17 |
18 |
19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/pages/Developers/Access/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import './styles.scss' 4 | 5 | interface IProps {} 6 | 7 | export default class Card extends React.Component { 8 | static defaultProps = {} 9 | 10 | constructor(props: IProps) { 11 | super(props) 12 | } 13 | 14 | render() { 15 | const {} = this.props 16 | return ( 17 |
18 |
19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/pages/Developers/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import './styles.scss' 4 | 5 | interface IProps {} 6 | 7 | export default class Card extends React.Component { 8 | static defaultProps = {} 9 | 10 | constructor(props: IProps) { 11 | super(props) 12 | } 13 | 14 | render() { 15 | const {} = this.props 16 | return ( 17 |
18 |
19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/pages/Developers/Search/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import './styles.scss' 4 | 5 | interface IProps {} 6 | 7 | export default class Card extends React.Component { 8 | static defaultProps = {} 9 | 10 | constructor(props: IProps) { 11 | super(props) 12 | } 13 | 14 | render() { 15 | const {} = this.props 16 | return ( 17 |
18 |
19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/data/newfeedstats.json: -------------------------------------------------------------------------------- 1 | { 2 | "top1name": "Buzzsprout", 3 | "top1count": 1870, 4 | "top2name": "RSS.com", 5 | "top2count": 967, 6 | "top3name": "Captivate", 7 | "top3count": 381, 8 | "top4name": "Transistor", 9 | "top4count": 376, 10 | "top5name": "Blubrry", 11 | "top5count": 96, 12 | "top6name": "Podserve", 13 | "top6count": 43, 14 | "top7name": "Fireside", 15 | "top7count": 38, 16 | "top8name": "Podiant", 17 | "top8count": 2, 18 | "totalCount": 3773 19 | } 20 | -------------------------------------------------------------------------------- /ui/images/chevron-back-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/images/chevron-forward-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/state/modules/users/schema.ts: -------------------------------------------------------------------------------- 1 | import { BaseState } from '../_abstract/schema'; 2 | 3 | export interface User { 4 | id?: number, 5 | birthMonth?: number, 6 | birthDay?: number, 7 | birthYear?: number, 8 | firstName?: string, 9 | lastName?: string, 10 | email: string, 11 | hasPreexistingResults?: boolean, 12 | token?: string, 13 | facebookId?: string, 14 | avatar?: string, 15 | slug?: string, 16 | referralUri?: string, 17 | activeProfile?: boolean, 18 | } 19 | 20 | export interface UserState extends BaseState { 21 | data: User[] 22 | } -------------------------------------------------------------------------------- /ui/src/components/ScrollToTop/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | // from https://stackoverflow.com/questions/36904185/react-router-scroll-to-top-on-every-transition+&cd=1&hl=en&ct=clnk&gl=us 5 | 6 | function ScrollToTop({history}) { 7 | useEffect(() => { 8 | const unlisten = history.listen(() => { 9 | window.scrollTo(0, 0); 10 | }); 11 | return () => { 12 | unlisten(); 13 | } 14 | }, []); 15 | 16 | return (null); 17 | } 18 | 19 | export default withRouter(ScrollToTop); 20 | -------------------------------------------------------------------------------- /ui/src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | const content: any 3 | export default content 4 | } 5 | 6 | declare module '*.svg' { 7 | const content: any 8 | export default content 9 | } 10 | 11 | declare module '*.png' { 12 | const content: any 13 | export default content 14 | } 15 | 16 | declare module '*.jpg' { 17 | const content: any 18 | export default content 19 | } 20 | 21 | declare module '*.otf' { 22 | const content: any 23 | export default content 24 | } 25 | 26 | declare module '*.ttf' { 27 | const content: any 28 | export default content 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/pages/Donations/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default class DonationThankYou extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 |

Thank you!

8 |

9 | It's people like you that help keeps podcasting as a 10 | platform for free speech possible. 11 |

12 |

13 | Back to the home page 14 |

15 |
16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/images/feed.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /ui/images/download-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/components/Card/styles.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | display: flex; 3 | flex-direction: column; 4 | height: inherit; 5 | padding: 20px; 6 | background: #ffffff; 7 | box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.15); 8 | border-radius: 3px; 9 | } 10 | 11 | .card-header { 12 | display: inline-flex; 13 | flex-direction: row; 14 | } 15 | 16 | .card-title { 17 | // font-weight: bold; 18 | font-size: 20px; 19 | text-transform: uppercase; 20 | color: #708693; 21 | text-align: left; 22 | } 23 | 24 | .card-body { 25 | margin-top: 10px; 26 | height: calc(100% - 10px); 27 | // height: calc(); 28 | } 29 | -------------------------------------------------------------------------------- /ui/images/earth.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/validate-apps.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | workflow_dispatch: 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Validate JSON 16 | uses: nhalstead/validate-json-action@0.1.3 17 | with: 18 | # Relative file path under the repository of a JSON schema file to validate the other JSON files with. 19 | schema: server/data/apps-schema.json 20 | # One or more relative file paths under the repository (separated by comma) of the JSON files to validate with the schema provided. 21 | jsons: server/data/apps.json 22 | -------------------------------------------------------------------------------- /ui/images/landing-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/components/InfiniteList/styles.scss: -------------------------------------------------------------------------------- 1 | .infinite-list{ 2 | 3 | .infinite-list-header { 4 | 5 | .infinite-list-count-info { 6 | margin-top: 10px; 7 | display: flex; 8 | flex-direction: row; 9 | align-items: center; 10 | 11 | font-size: 12px; 12 | 13 | .button { 14 | margin: 0; 15 | } 16 | 17 | .count { 18 | margin: 0 0 0 auto; 19 | justify-self: flex-end; 20 | } 21 | } 22 | 23 | .infinite-list-item-unknown { 24 | text-align: center; 25 | } 26 | } 27 | 28 | .loader { 29 | text-align: center; 30 | margin: 0 auto; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/images/link.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/src/state/modules/users/reducers.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'reduxsauce'; 2 | import { createBaseHandlers, BaseState } from '../_abstract'; 3 | import { Creators, Types } from './actions'; 4 | 5 | const INITIAL_STATE: BaseState = { 6 | data: [], 7 | errors: undefined, 8 | loading: false, 9 | hydrated: false 10 | } 11 | 12 | // ---------------- CREATE REDUCERS ---------------- 13 | 14 | const crudReducers = createBaseHandlers('users', Creators, Types); 15 | 16 | // const customHandler = (state, { tab }) => crudReducers[Types.INDEX_SUCCESS](state, { data: null }); 17 | 18 | const additionalReducers = { 19 | // [Types.CUSTOM_TYPE_REQUEST]: crudReducers[Types.INDEX_REQUEST] 20 | // [Types.CUSTOM_TYPE_SUCCESS]: customHandler, 21 | // [Types.CUSTOM_TYPE_ERROR]: crudReducers[Types.INDEX_ERROR] 22 | }; 23 | 24 | export default createReducer(INITIAL_STATE, { ...crudReducers, ...additionalReducers }); -------------------------------------------------------------------------------- /ui/src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import './styles.scss' 4 | 5 | interface IProps { 6 | title?: string 7 | children?: any 8 | } 9 | 10 | export default class Card extends React.Component { 11 | static defaultProps = {} 12 | state = { 13 | open: false, 14 | } 15 | 16 | constructor(props: IProps) { 17 | super(props) 18 | } 19 | 20 | render() { 21 | const { title, children } = this.props 22 | // const { open } = this.state 23 | return ( 24 |
25 | {title && ( 26 |
27 |
{title}
28 |
29 |
30 | )} 31 |
{children}
32 |
33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui/images/light_mode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui/src/components/Value/styles.scss: -------------------------------------------------------------------------------- 1 | .podcast-value { 2 | font-size: 18px; 3 | 4 | h4 { 5 | font-size: 18px; 6 | margin: 0 0 .5rem; 7 | } 8 | 9 | ul { 10 | margin: 0; 11 | padding-left: 0; 12 | 13 | li { 14 | display: flex; 15 | align-items: center; 16 | margin: .25rem 0; 17 | } 18 | } 19 | 20 | progress[value] { 21 | appearance: none; 22 | border: 0; 23 | border-radius: .25rem; 24 | width: 100px; 25 | height: 1rem; 26 | margin-right: .5rem; 27 | } 28 | 29 | progress[value]::-webkit-progress-bar { 30 | background-color: #eee; 31 | border-radius: .25rem; 32 | } 33 | 34 | // though both use the same styles, it does not work as a combined selector! 35 | progress[value]::-webkit-progress-value { 36 | background-color: var(--color-primary); 37 | border-radius: .25rem; 38 | } 39 | progress[value]::-moz-progress-bar { 40 | background-color: var(--color-primary); 41 | border-radius: .25rem; 42 | } 43 | } 44 | 45 | @media only screen and (max-width: 767px) { 46 | .podcast-value { 47 | font-size: 14px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Podcastindex.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ui/src/pages/Apps/AppsWebPart/SingleApp/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --dimmed-color: #333; 3 | 4 | &[data-theme="dark"] { 5 | --dimmed-color: #808080; 6 | } 7 | } 8 | 9 | .podcastIndexAppsSingleApp { 10 | display: grid; 11 | grid-template-columns: 1fr 1fr 192px; 12 | grid-template-rows: 1fr; 13 | gap: 0px 32px; 14 | grid-template-rows: auto; 15 | margin-bottom: 16px; 16 | } 17 | 18 | .podcastIndexAppIdentifier { 19 | display: flex; 20 | } 21 | 22 | .podcastIndexAppIcon { 23 | align-self: center; 24 | 25 | img { 26 | width: 60px; 27 | height: 60px; 28 | border-radius: 5px; 29 | } 30 | } 31 | 32 | .podcastIndexAppTitleAndType { 33 | align-self: center; 34 | margin-left: 32px; 35 | 36 | .podcastIndexAppTitle { 37 | font-size: 24px; 38 | } 39 | .podcastIndexAppType { 40 | color: var(--dimmed-color); 41 | margin: 0; 42 | } 43 | } 44 | 45 | .podcastIndexAppSupportedElements { 46 | align-self: center; 47 | line-height: 1.5; 48 | } 49 | 50 | .podcastIndexAppPlatforms { 51 | align-self: center; 52 | 53 | color: var(--dimmed-color); 54 | line-height: 1.5; 55 | } 56 | -------------------------------------------------------------------------------- /ui/src/state/modules/_abstract/actions.ts: -------------------------------------------------------------------------------- 1 | import { createActions } from 'reduxsauce'; 2 | import pluralize from 'pluralize'; 3 | 4 | export const createBaseActions = (name, additionalActions = {}) => { 5 | const moduleName = pluralize(name); 6 | return createActions( 7 | { 8 | indexRequest: [`${name}Ids`], 9 | indexSuccess: [moduleName], 10 | indexFailure: ['errors'], 11 | createRequest: [moduleName, 'tempId'], 12 | createSuccess: [moduleName, 'tempId'], 13 | createFailure: ['errors', 'tempId'], 14 | destroyRequest: ['id'], 15 | destroySuccess: ['id', 'response'], 16 | destroyFailure: ['id', 'errors'], 17 | showRequest: ['id'], 18 | showSuccess: ['id', moduleName], 19 | showFailure: ['id', 'errors'], 20 | updateRequest: ['id', moduleName], 21 | updateSuccess: ['id', moduleName], 22 | updateFailure: ['id', 'errors'], 23 | reset: [], 24 | resetErrors: [], 25 | ...additionalActions, 26 | }, 27 | { prefix: `${pluralize(name).toUpperCase()}_` }, 28 | ) 29 | } 30 | 31 | export default { createBaseActions }; 32 | -------------------------------------------------------------------------------- /ui/src/components/KPI/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import './styles.scss' 4 | 5 | // Separate state props + dispatch props to their own interfaces. 6 | interface PropsFromState { 7 | big?: boolean 8 | title?: string 9 | value: string 10 | unit?: string 11 | trending?: { 12 | direction: 'up' | 'down' 13 | amount?: number 14 | } 15 | } 16 | 17 | // Combine both state + dispatch props - as well as any props we want to pass - in a union type. 18 | type AllProps = PropsFromState 19 | 20 | export default class KPI extends React.PureComponent { 21 | static defaultProps = {} 22 | 23 | render() { 24 | const { big, title, value, unit, trending } = this.props 25 | const bigClass = big ? 'big' : '' 26 | return ( 27 |
28 | {title &&
{title}
} 29 |
30 |
{value}
31 |
32 |
33 |
{unit}
34 |
35 |
36 |
37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/assets/podigee.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Fragment } from "react"; 3 | import { Route, Switch } from 'react-router-dom' 4 | import ScrollToTop from "./components/ScrollToTop"; 5 | import AddFeed from "./pages/AddFeed"; 6 | import Apps from './pages/Apps' 7 | import DonationThankYou from './pages/Donations' 8 | 9 | import Landing from './pages/landing' 10 | import Podcast from './pages/Podcast' 11 | import Search from './pages/Search' 12 | import Stats from './pages/Stats' 13 | import { history } from './state/store' 14 | 15 | const Routes: React.FunctionComponent = () => ( 16 | 17 | 18 | 19 | }/> 20 | } 23 | /> 24 | 25 | }/> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Not Found
}/> 34 |
35 |
36 | ) 37 | 38 | export default Routes 39 | -------------------------------------------------------------------------------- /ui/images/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/state/modules/_abstract/types.ts: -------------------------------------------------------------------------------- 1 | // Response object for GET /heroes 2 | // https://docs.opendota.com/#tag/heroes%2Fpaths%2F~1heroes%2Fget 3 | export interface Hero { 4 | id: number 5 | name: string 6 | localized_name: string 7 | primary_attr: string 8 | attack_type: string 9 | roles: string[] 10 | legs: number 11 | } 12 | 13 | // This type is basically shorthand for `{ [key: string]: any }`. Feel free to replace `any` with 14 | // the expected return type of your API response. 15 | export type ApiResponse = Record 16 | 17 | // Use `enum`s for better autocompletion of action type names. These will 18 | // be compiled away leaving only the final value in your compiled code. 19 | // 20 | // Define however naming conventions you'd like for your action types, but 21 | // personally, I use the `@@context/ACTION_TYPE` convention, to follow the convention 22 | // of Redux's `@@INIT` action. 23 | export enum HeroesActionTypes { 24 | FETCH_REQUEST = 'FETCH_REQUEST', 25 | FETCH_SUCCESS = 'FETCH_SUCCESS', 26 | FETCH_ERROR = 'FETCH_ERROR', 27 | SELECTED = 'SELECTED' 28 | } 29 | 30 | // Declare state types with `readonly` modifier to get compile time immutability. 31 | // https://github.com/piotrwitek/react-redux-typescript-guide#state-with-type-level-immutability 32 | export interface HeroesState { 33 | readonly loading: boolean 34 | readonly data: Hero[] 35 | readonly errors?: string 36 | } -------------------------------------------------------------------------------- /ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import ThemeButton from "./components/ThemeButton"; 4 | import store, { history } from './state/store' 5 | import { Provider } from 'react-redux' 6 | import { ConnectedRouter } from 'connected-react-router' 7 | import { Store } from 'redux' 8 | import { History } from 'history' 9 | 10 | import Topbar from './components/TopBar' 11 | import Routes from './routes' 12 | 13 | import LandingBG from '../images/landing-bg.svg' 14 | import './styles.scss' 15 | 16 | import { ApplicationState } from './state/store' 17 | 18 | interface MainProps { 19 | store: Store 20 | history: History 21 | } 22 | 23 | const Index: React.FC = ({ store, history }) => { 24 | 25 | return ( 26 | 27 | 28 | 29 | Sidebar logo 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | ReactDOM.render( 45 | , 46 | document.getElementById('root') 47 | ) 48 | -------------------------------------------------------------------------------- /ui/src/pages/Podcast/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Route, Switch} from 'react-router-dom' 3 | import {updateTitle} from '../../utils' 4 | import PodcastInfo from './PodcastInfo' 5 | import Value4Value from "./Value4Value"; 6 | 7 | import './styles.scss' 8 | 9 | interface IProps { 10 | match: any 11 | } 12 | 13 | export default class Podcast extends React.PureComponent { 14 | render() { 15 | updateTitle('Podcast') 16 | return ( 17 |
18 | 19 | ( 23 |
24 |

Podcasts

25 |

Please search for a podcast above

26 |
27 | )} 28 | /> 29 | } 33 | /> 34 | } 37 | /> 38 |
39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/components/SphinxChat/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Avatar from '../../../images/pci_avatar.jpg' 3 | 4 | import './styles.scss' 5 | 6 | interface SphinxWidgetProps 7 | extends React.DetailedHTMLProps< 8 | React.HTMLAttributes, 9 | HTMLElement 10 | > { 11 | pubkey: string 12 | title: string 13 | subtitle: string 14 | name: string 15 | amount: string 16 | imgurl: string 17 | } 18 | 19 | declare global { 20 | namespace JSX { 21 | interface IntrinsicElements { 22 | 'sphinx-widget': SphinxWidgetProps 23 | } 24 | } 25 | } 26 | 27 | interface IProps {} 28 | 29 | export default class SphinxChat extends React.PureComponent { 30 | constructor(props: IProps) { 31 | super(props) 32 | } 33 | 34 | componentDidMount() { 35 | // load the external script 36 | const script = document.createElement('script') 37 | script.src = 'https://sphinx.chat/donation/widget.js' 38 | script.async = true 39 | document.body.appendChild(script) 40 | } 41 | 42 | render() { 43 | return ( 44 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ui/src/pages/Apps/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import AppsWebPart from './AppsWebPart' 4 | 5 | import './styles.scss' 6 | 7 | interface IProps {} 8 | 9 | export default class Apps extends React.Component { 10 | static defaultProps = {} 11 | 12 | constructor(props: IProps) { 13 | super(props) 14 | } 15 | 16 | render() { 17 | const {} = this.props 18 | return ( 19 |
20 | 21 |
22 |

Your application or website missing?

23 |

24 | {'Create a '} 25 | Pull Request 26 | {' to update the '} 27 | data file. 28 |

29 |

30 | {'Also support namespace features? Add your app or website to the '} 31 | application list 32 | {' by creating a '} 33 | Pull Request. 34 |

35 |
36 |
37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/components/ResultsEpisodes/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import EpisodesPlayer from "../EpisodesPlayer"; 3 | 4 | import './styles.scss' 5 | 6 | interface IProps { 7 | episodes: Array<{}> 8 | initialDisplay?: number 9 | } 10 | 11 | export default class ResultsEpisodes extends React.PureComponent { 12 | state = { 13 | selectedEpisode: Object(), 14 | } 15 | _isMounted = false 16 | 17 | constructor(props) { 18 | super(props) 19 | } 20 | 21 | async componentDidMount(): Promise { 22 | this._isMounted = true 23 | } 24 | 25 | componentWillUnmount() { 26 | this._isMounted = false 27 | } 28 | 29 | render() { 30 | const {episodes, initialDisplay} = this.props 31 | const {selectedEpisode} = this.state 32 | 33 | const podcast = { 34 | id: selectedEpisode.feedId, 35 | url: selectedEpisode.feedUrl, 36 | author: selectedEpisode.feedAuthor, 37 | title: selectedEpisode.feedTitle, 38 | language: selectedEpisode.feedLanguage, 39 | medium: "podcast", // for search results, don't want sort 40 | } 41 | 42 | return ( 43 |
44 | 50 |
51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ui/src/pages/Apps/AppsWebPart/styles.scss: -------------------------------------------------------------------------------- 1 | .podcastIndexAppsWebPart > h4 { 2 | font-size: 20px; 3 | margin-top: 3px; 4 | margin-bottom: 3px; 5 | padding-top: 20px; 6 | padding-bottom: 10px; 7 | } 8 | 9 | .podcastIndexAppsHeader { 10 | display: grid; 11 | grid-template-columns: 1fr 1fr 192px; 12 | grid-template-rows: 1fr; 13 | gap: 0px 32px; 14 | grid-template-rows: auto; 15 | margin-top: 36px; 16 | } 17 | .podcastIndexAppsHeader > div > h4, 18 | .podcastIndexAppsHeader > h4 { 19 | font-size: 16px; 20 | font-weight: 700; 21 | margin: 0; 22 | } 23 | 24 | .podcastIndexAppsHeader > div > h4 { 25 | margin-left: 92px; 26 | } 27 | 28 | .podcastIndexAppList { 29 | padding: 0; 30 | } 31 | 32 | $podcastIndexAppsMobileLeftMargin: 84px; 33 | $podcastIndexAppsMobileTopPadding: 30px; 34 | 35 | @media only screen and (max-width: 767px) { 36 | .podcastIndexAppsHeader { 37 | display: none; 38 | } 39 | .podcastIndexAppsSingleApp { 40 | display: grid; 41 | grid-template-columns: 1fr; 42 | grid-template-rows: 1fr 1fr; 43 | gap: 0px 0px; 44 | grid-template-rows: auto; 45 | margin-top: 36px; 46 | } 47 | 48 | .podcastIndexAppPlatforms { 49 | display: none; 50 | } 51 | 52 | .podcastIndexAppTitleAndType { 53 | align-self: flex-start; 54 | margin-left: 24px; 55 | } 56 | 57 | .podcastIndexAppSupportedElements { 58 | margin-left: 84px; 59 | position: relative; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ui/src/pages/Apps/AppsWebPart/FilterTags/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --inactive-button-color: #c7c7c7; 3 | --inactive-button-hover-color: #b3b2b2; 4 | 5 | &[data-theme="dark"] { 6 | --inactive-button-color: #808080; 7 | --inactive-button-hover-color: #505050; 8 | } 9 | } 10 | 11 | .podcastIndexAppsFilterTags { 12 | font-weight: 700; 13 | margin: 0 0 12px 0; 14 | } 15 | 16 | .podcastIndexAppsFilterTags > button { 17 | cursor: pointer; 18 | margin: 8px 4px; 19 | padding: 4px 8px; 20 | border-radius: 20px; 21 | border: none; 22 | color: white; 23 | background-color: var(--color-primary); 24 | &:hover { 25 | background-color: darken($color: #e90000, $amount: 5%); 26 | } 27 | &.inactive { 28 | color: var(--fg-main); 29 | background: initial; 30 | 31 | background: var(--inactive-button-color); 32 | &:hover { 33 | background: var(--inactive-button-hover-color); 34 | } 35 | } 36 | &.clear-button { 37 | width: 47.88px; 38 | background-color: #aa0000; 39 | color: white; 40 | } 41 | &.clear-button:hover { 42 | background-color: darken($color: #aa0000, $amount: 5%); 43 | } 44 | } 45 | 46 | .podcastIndexAppsFilterTagContainer > h4 { 47 | cursor: pointer; 48 | user-select: none; 49 | margin: 0 0 8px 0; 50 | & > .podcastIndexAppsFilterArrow { 51 | font-size: 0.75em; 52 | } 53 | } 54 | 55 | .podcastIndexAppsFilterCategories { 56 | height: 0; 57 | overflow: hidden; 58 | transition: height 0.2s linear; 59 | } 60 | -------------------------------------------------------------------------------- /ui/src/state/store.ts: -------------------------------------------------------------------------------- 1 | import { Store, combineReducers, applyMiddleware, createStore } from 'redux' 2 | import { 3 | connectRouter, 4 | RouterState, 5 | routerMiddleware, 6 | } from 'connected-react-router' 7 | import thunkMiddleware from 'redux-thunk' 8 | import { composeWithDevTools } from 'redux-devtools-extension' 9 | import { createBrowserHistory, History } from 'history' 10 | import { UserState, usersReducer } from './modules/users' 11 | 12 | // The top-level state object. 13 | // 14 | // `connected-react-router` already injects the router state typings for us, 15 | // so we can ignore them here. 16 | export interface ApplicationState { 17 | users: UserState 18 | router: RouterState 19 | } 20 | export const history = createBrowserHistory() 21 | // @ts-ignore: window error 22 | const initialState = window.INITIAL_REDUX_STATE 23 | 24 | // Whenever an action is dispatched, Redux will update each top-level application state property 25 | // using the reducer with the matching name. It's important that the names match exactly, and that 26 | // the reducer acts on the corresponding ApplicationState property type. 27 | export const createRootReducer = (history: History) => 28 | combineReducers({ 29 | users: usersReducer, 30 | router: connectRouter(history), 31 | }) 32 | 33 | // create the composing function for our middlewares 34 | const composeEnhancers = composeWithDevTools({}) 35 | const middlewares = [thunkMiddleware, routerMiddleware(history)] 36 | // We'll create our store with the combined reducers/sagas, and the initial Redux state that 37 | // we'll be passing from our entry point. 38 | export default createStore( 39 | createRootReducer(history), 40 | initialState, 41 | composeEnhancers(applyMiddleware(...middlewares)) 42 | ) 43 | -------------------------------------------------------------------------------- /ui/src/components/Button/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --button-background: #f7f7f7; 3 | --button-hover-color: #ededed; 4 | 5 | &[data-theme="dark"] { 6 | --button-background: #1a1a1a; 7 | --button-hover-color: #302d2d; 8 | } 9 | } 10 | 11 | .button { 12 | border-radius: 17px; 13 | transition: var(--transition-secs); 14 | font-family: var(--font-family-exp); 15 | font-size: 16px; 16 | color: var(--fg-main); 17 | background-color: var(--button-background); 18 | padding: 8px 20px; 19 | text-align: center; 20 | white-space: nowrap; 21 | display: block; 22 | &:hover { 23 | transition: var(--transition-secs); 24 | background: var(--button-hover-color); 25 | color: var(--fg-main); 26 | cursor: pointer; 27 | } 28 | &.primary { 29 | color: white; 30 | background-color: var(--color-primary); 31 | &:hover { 32 | background-color: darken($color: #e90000, $amount: 5%); 33 | } 34 | } 35 | &.big { 36 | font-size: 26px; 37 | border-radius: 22px; 38 | } 39 | &.small { 40 | font-size: 12px; 41 | border-radius: 13px; 42 | padding: 6px 13px; 43 | } 44 | &.slim { 45 | display: inline-block; 46 | } 47 | &.disabled { 48 | color: #7e7e7e !important; 49 | border-spacing: 0; 50 | border-width: 0; 51 | cursor: default; 52 | background: transparent; 53 | &:hover { 54 | background: transparent; 55 | cursor: default; 56 | } 57 | } 58 | } 59 | 60 | a, 61 | .button { 62 | text-decoration: none !important; 63 | } 64 | 65 | input, 66 | .button { 67 | outline: 0; 68 | border-spacing: 0; 69 | border-width: 0; 70 | text-decoration: none !important; 71 | } 72 | -------------------------------------------------------------------------------- /ui/src/components/ResultsFeeds/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import InfiniteList from "../../components/InfiniteList"; 3 | import ResultItem from '../../components/ResultItem' 4 | 5 | import { getImage } from '../../utils' 6 | 7 | import './styles.scss' 8 | 9 | interface IProps { 10 | results?: Array<{}> 11 | initialDisplay?: number 12 | } 13 | 14 | export default class ResultsFeeds extends React.PureComponent { 15 | state = { 16 | loading: true, 17 | } 18 | _isMounted = false 19 | 20 | async componentDidMount(): Promise { 21 | this._isMounted = true 22 | } 23 | 24 | componentWillUnmount() { 25 | this._isMounted = false 26 | } 27 | 28 | async componentDidUpdate(prevProps) { 29 | } 30 | 31 | renderItem(item, index: number, selected: boolean) { 32 | let {title, author, description, categories, id} = item 33 | const image = getImage(item) 34 | 35 | return ( 36 |
37 | 46 |
47 | ) 48 | } 49 | 50 | render() { 51 | const {results, initialDisplay} = this.props 52 | const {loading} = this.state 53 | 54 | return ( 55 |
56 | 61 |
62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ui/src/components/KPI/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --kpi-title-color: #494949; 3 | 4 | &[data-theme="dark"] { 5 | --kpi-title-color: #cdcdcd; 6 | } 7 | } 8 | 9 | .kpi { 10 | display: flex; 11 | flex-direction: column; 12 | 13 | &.big { 14 | .kpi-trending { 15 | display: flex; 16 | } 17 | .kpi-title { 18 | flex: 1; 19 | opacity: 0.64; 20 | font-family: var(--font-family-exp); 21 | font-size: 20px; 22 | color: var(--kpi-title-color); 23 | text-align: left; 24 | margin-right: 50px; 25 | } 26 | 27 | .kpi-value { 28 | // font-weight: bold; 29 | font-family: var(--font-family-exp); 30 | line-height: 72px; 31 | font-size: 72px; 32 | color: #ff1c41; 33 | text-align: left; 34 | } 35 | .kpi-unit { 36 | margin-left: 10px; 37 | font-size: 45px; 38 | color: #bfccd6; 39 | text-align: left; 40 | } 41 | .kpi-metadata { 42 | justify-content: space-between; 43 | } 44 | } 45 | } 46 | 47 | .kpi-data { 48 | display: inline-flex; 49 | flex-direction: row; 50 | } 51 | .kpi-title { 52 | opacity: 0.63; 53 | text-transform: uppercase; 54 | font-size: 18px; 55 | color: var(--kpi-title-color); 56 | text-align: left; 57 | margin-bottom: 10px; 58 | } 59 | .kpi-value { 60 | font-size: 50px; 61 | font-family: var(--font-family-exp); 62 | line-height: 50px; 63 | color: #ff1c41; 64 | text-align: left; 65 | } 66 | 67 | .kpi-metadata { 68 | display: inline-flex; 69 | flex-direction: column; 70 | justify-content: flex-end; 71 | } 72 | 73 | .kpi-trending { 74 | display: none; 75 | } 76 | 77 | .kpi-unit { 78 | margin-left: 5px; 79 | font-size: 12px; 80 | color: #bfccd6; 81 | text-align: left; 82 | } 83 | -------------------------------------------------------------------------------- /ui/src/components/CommentMenu/styles.scss: -------------------------------------------------------------------------------- 1 | .context-menu { 2 | all: unset; 3 | position: relative; 4 | } 5 | 6 | .context-menu svg { 7 | display: block; 8 | } 9 | 10 | .context-menu menu { 11 | position: absolute; 12 | top: 100%; 13 | right: 0; 14 | display: flex; 15 | flex-direction: column; 16 | background: var(--button-background); 17 | margin: 0; 18 | padding: .75rem; 19 | z-index: 1; 20 | border-radius: .25rem; 21 | box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25); 22 | } 23 | 24 | .context-menu menu a { 25 | white-space: nowrap; 26 | text-decoration: none; 27 | line-height: 2; 28 | } 29 | 30 | .dialog-homeinstance { 31 | border: 1px solid rgba(0, 0, 0, 0.25); 32 | border-radius: 0.5rem; 33 | box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25); 34 | width: calc((100% - 6px) - 2em); 35 | max-width: 28rem; 36 | } 37 | 38 | .dialog-homeinstance form { 39 | display: flex; 40 | flex-direction: column; 41 | gap: 0.5rem; 42 | margin: 0; 43 | } 44 | 45 | .dialog-homeinstance input { 46 | padding: 0.5rem; 47 | } 48 | 49 | .dialog-homeinstance menu { 50 | display: flex; 51 | flex-wrap: wrap; 52 | flex-direction: row; 53 | justify-content: flex-end; 54 | gap: .5rem; 55 | margin: 1.5rem 0 0 0; 56 | } 57 | 58 | .dialog-homeinstance button { 59 | border: none; 60 | padding: .5rem; 61 | border-radius: 4px; 62 | min-width: 4.5rem; 63 | background-color: #2962ff; 64 | font-size: 0.875rem; 65 | font-weight: 500; 66 | letter-spacing: .25px; 67 | line-height: 1rem; 68 | outline: none; 69 | color: #FFF; 70 | } 71 | 72 | .dialog-homeinstance button:hover { 73 | background-color: #2F7DE2; 74 | } 75 | 76 | .dialog-homeinstance button[type="reset"] { 77 | background-color: transparent; 78 | box-shadow: inset 0 0 0 1px #DADCE0; 79 | color: #2962ff; 80 | } 81 | 82 | .dialog-homeinstance button[type="reset"]:hover { 83 | background-color: #EAF2FD; 84 | } -------------------------------------------------------------------------------- /server/assets/musixmatch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ui/src/components/Boostagram/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | .boostagram-corner{ 3 | display: flex; 4 | margin: 0 0 8px 0; 5 | align-items: center; 6 | height: 90px 7 | } 8 | .boostagram-corner > div{ 9 | width: 260px 10 | } 11 | 12 | .boostagram-corner textarea{ 13 | flex-grow:1; 14 | margin: 0 16px; 15 | font-size: 1.2em; 16 | border-radius: 5px; 17 | height: 88px; 18 | resize: none; 19 | padding: 8px 20 | } 21 | 22 | .boostagram-corner label{ 23 | font-size: 1.1em; 24 | font-weight: 550; 25 | } 26 | 27 | .boostagram-corner input{ 28 | border:1px solid gray; 29 | padding: 8px; 30 | font-size: 1.1em; 31 | font-weight: 550; 32 | text-align: right ; 33 | width: 100px; 34 | margin-right: 4px; 35 | border-radius: 5px 36 | } 37 | 38 | .boostagram-corner .boostagram-sender-name{ 39 | width: 100%; 40 | text-align: left; 41 | margin: 0 0 8px 0; 42 | height: 41px; 43 | display:flex; 44 | align-items: center; 45 | justify-content: center; 46 | } 47 | 48 | .boostagram-corner .boostagram-sender-name input{ 49 | width: 100%; 50 | text-align: left; 51 | margin: 0 52 | } 53 | 54 | .boostagram-corner .boostagram-sat-input{ 55 | width: 100%; 56 | text-align: left; 57 | height: 41px; 58 | display:flex; 59 | align-items: center; 60 | justify-content: center; 61 | } 62 | 63 | 64 | .boostagram-corner input:focus{ 65 | border:2px solid black; 66 | } 67 | 68 | .boostagram-corner button{ 69 | margin: 0 0 0 16px; 70 | padding: 8px 12px; 71 | width: 100px; 72 | border: none; 73 | background-color: #E90000; 74 | border-radius: 5px; 75 | color: #fff; 76 | cursor: pointer; 77 | font-size: 1.1em; 78 | font-weight: 600; 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /ui/src/pages/Stats/StatsCard/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: #ffffff; 3 | 4 | &[data-theme="dark"] { 5 | --bg-color: #180e0e; 6 | } 7 | } 8 | 9 | .kpi-card { 10 | display: inline-flex; 11 | flex-direction: row; 12 | justify-content: center; 13 | align-items: center; 14 | width: inherit; 15 | .card { 16 | display: inline-flex; 17 | flex-direction: column; 18 | width: inherit; 19 | padding: 30px; 20 | // box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.15); 21 | background: var(--bg-color); 22 | border-radius: 12px; 23 | } 24 | .kpi-massive-title { 25 | font-family: var(--font-family-exp); 26 | font-size: 32px; 27 | color: var(--fg-main); 28 | text-align: left; 29 | } 30 | .kpi-massive-value { 31 | font-family: var(--font-family-exp); 32 | font-weight: bold; 33 | font-size: 186px; 34 | color: #ff1c41; 35 | text-align: left; 36 | } 37 | .kpi-title-2 { 38 | margin-top: 40px; 39 | } 40 | } 41 | 42 | .kpi-row { 43 | width: 100%; 44 | display: inline-flex; 45 | flex-direction: row; 46 | justify-content: flex-start; 47 | flex-wrap: wrap; 48 | margin: -7px -20px; 49 | margin-top: 30px; 50 | 51 | .kpi { 52 | margin: 7px 20px; 53 | } 54 | } 55 | 56 | @media only screen and (max-width: 1167px) { 57 | .kpi-massive-value { 58 | font-size: 100px !important; 59 | } 60 | } 61 | 62 | @media only screen and (max-width: 767px) { 63 | .kpi-massive-title { 64 | font-size: 26px !important; 65 | } 66 | .kpi-massive-value { 67 | font-size: 56px !important; 68 | } 69 | .kpi-card { 70 | margin: 0 10px; 71 | width: calc(100% - 20px); 72 | .card { 73 | max-width: 500px; 74 | } 75 | } 76 | /* phones */ 77 | .kpi-row { 78 | flex-direction: column !important; 79 | } 80 | .kpi { 81 | margin-top: 15px; 82 | margin-bottom: 15px; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ui/src/pages/Stats/NewFeedStatsCard/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --kpi-bg-color: #f3f3f3; 3 | 4 | &[data-theme="dark"] { 5 | --kpi-bg-color: #302d2d; 6 | } 7 | } 8 | 9 | .kpi-card { 10 | display: inline-flex; 11 | flex-direction: row; 12 | justify-content: center; 13 | align-items: center; 14 | width: inherit; 15 | .card { 16 | display: inline-flex; 17 | flex-direction: column; 18 | width: inherit; 19 | padding: 30px; 20 | // box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.15); 21 | background: var(--kpi-bg-color); 22 | border-radius: 12px; 23 | } 24 | .kpi-massive-title { 25 | font-family: var(--font-family-exp); 26 | font-size: 32px; 27 | color: var(--fg-main); 28 | text-align: left; 29 | } 30 | .kpi-massive-value { 31 | font-family: var(--font-family-exp); 32 | font-weight: bold; 33 | font-size: 186px; 34 | color: #ff1c41; 35 | text-align: left; 36 | } 37 | .kpi-title-2 { 38 | margin-top: 40px; 39 | } 40 | } 41 | 42 | .kpi-row { 43 | width: 100%; 44 | display: inline-flex; 45 | flex-direction: row; 46 | justify-content: flex-start; 47 | flex-wrap: wrap; 48 | margin: -7px -20px; 49 | margin-top: 30px; 50 | 51 | .kpi { 52 | margin: 7px 20px; 53 | } 54 | } 55 | 56 | @media only screen and (max-width: 1167px) { 57 | .kpi-massive-value { 58 | font-size: 100px !important; 59 | } 60 | } 61 | 62 | @media only screen and (max-width: 767px) { 63 | .kpi-massive-title { 64 | font-size: 26px !important; 65 | } 66 | .kpi-massive-value { 67 | font-size: 56px !important; 68 | } 69 | .kpi-card { 70 | margin: 0 10px; 71 | width: calc(100% - 20px); 72 | .card { 73 | max-width: 500px; 74 | } 75 | } 76 | /* phones */ 77 | .kpi-row { 78 | flex-direction: column !important; 79 | } 80 | .kpi { 81 | margin-top: 15px; 82 | margin-bottom: 15px; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ui/src/state/modules/_abstract/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, AnyAction } from 'redux' 2 | import { BaseState } from './schema'; 3 | import pluralize from 'pluralize'; 4 | 5 | // Type-safe initialState! 6 | export const INITIAL_STATE: BaseState = { 7 | data: [], 8 | errors: undefined, 9 | loading: false, 10 | hydrated: false 11 | } 12 | 13 | 14 | export const createBaseHandlers = (name: string, actions: object, Types: any) => { 15 | const requestReducer: Reducer = (state: BaseState, action: AnyAction) => { 16 | return { ...state, loading: true }; 17 | } 18 | const successReducer: Reducer = (state: BaseState, action: AnyAction) => { 19 | console.log('reducer hit', action) 20 | return { ...state, loading: false, hydrated: true, data: action[pluralize(name)] } 21 | } 22 | const errorReducer: Reducer = (state: BaseState, action: AnyAction) => { 23 | console.log('reducer hit', action) 24 | return { ...state, loading: false, hydrated: true, errors: action.payload } 25 | } 26 | const reset = () => INITIAL_STATE; 27 | const resetErrors = (state: BaseState = INITIAL_STATE) => ({ ...state, errors: {} }); 28 | return { 29 | [Types.CREATE_REQUEST]: requestReducer, 30 | [Types.CREATE_FAILURE]: errorReducer, 31 | [Types.CREATE_SUCCESS]: successReducer, 32 | [Types.DESTROY_REQUEST]: requestReducer, 33 | [Types.DESTROY_FAILURE]: errorReducer, 34 | [Types.DESTROY_SUCCESS]: successReducer, 35 | [Types.SHOW_REQUEST]: requestReducer, 36 | [Types.SHOW_FAILURE]: errorReducer, 37 | [Types.SHOW_SUCCESS]: successReducer, 38 | [Types.UPDATE_REQUEST]: requestReducer, 39 | [Types.UPDATE_FAILURE]: errorReducer, 40 | [Types.UPDATE_SUCCESS]: successReducer, 41 | [Types.INDEX_REQUEST]: requestReducer, 42 | [Types.INDEX_FAILURE]: errorReducer, 43 | [Types.INDEX_SUCCESS]: successReducer, 44 | [Types.RESET]: reset, 45 | [Types.RESET_ERRORS]: resetErrors, 46 | }; 47 | }; 48 | 49 | export default { createBaseHandlers }; 50 | -------------------------------------------------------------------------------- /ui/src/pages/Stats/StatsCard/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import KPI from '../../../components/KPI' 4 | import Card from '../../../components/Card' 5 | 6 | import './styles.scss' 7 | 8 | interface IProps { 9 | total: string 10 | threedays: string 11 | tendays: string 12 | lastMonth: string 13 | last60: string 14 | last90: string 15 | } 16 | 17 | export default class StatsCard extends React.Component { 18 | static defaultProps = { 19 | total: '', 20 | threedays: '', 21 | tendays: '', 22 | lastWeek: '', 23 | lastMonth: '', 24 | last60: '', 25 | last90: '', 26 | } 27 | 28 | constructor(props: IProps) { 29 | super(props) 30 | } 31 | 32 | numberWithCommas(number: number) { 33 | return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') 34 | } 35 | 36 | render() { 37 | const { total, threedays, tendays, lastMonth, last60, last90 } = this.props 38 | return ( 39 |
40 | 41 |
42 | Total podcasts in the index ... 43 |
44 |
{total}
45 |
46 | Shows published in the last ... 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | {/* */} 58 |
59 |
60 |
61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ui/src/pages/Podcast/Value4Value/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --selected-page-color: #f2f2f2; 3 | 4 | &[data-theme="dark"] { 5 | --selected-page-color: #505050; 6 | } 7 | } 8 | 9 | .v4v { 10 | margin: 0 auto; 11 | padding: 0 10px; 12 | position: relative; 13 | z-index: 1; 14 | width: 1024px; 15 | 16 | h2 { 17 | font-size: 24px; 18 | } 19 | 20 | .pages { 21 | .page-buttons { 22 | display: flex; 23 | flex-direction: row; 24 | flex-wrap: wrap; 25 | row-gap: 15px; 26 | column-gap: 5px; 27 | justify-content: center; 28 | 29 | .page { 30 | min-width: 20px; 31 | 32 | border-radius: 9px; 33 | padding: 6px 9px; 34 | color: var(--fg-main); 35 | 36 | &.selected { 37 | background: var(--selected-page-color); 38 | } 39 | } 40 | 41 | &.hidden { 42 | display: None; 43 | } 44 | } 45 | 46 | .page-menu-row { 47 | text-align: center; 48 | display: none; 49 | 50 | .pages-menu { 51 | width: 100px; 52 | 53 | border-radius: 4px; 54 | transition: var(--transition-secs); 55 | font-family: var(--font-family-exp); 56 | font-size: 16px; 57 | padding: 2px 5px; 58 | text-align: center; 59 | white-space: nowrap; 60 | } 61 | } 62 | } 63 | } 64 | 65 | @media only screen and (max-width: 720px) { 66 | /* phones */ 67 | .v4v { 68 | .pages { 69 | .page-buttons { 70 | row-gap: 20px; 71 | .page { 72 | font-size: 18px; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | @media only screen and (max-width: 620px) { 80 | /* phones */ 81 | .v4v { 82 | .pages { 83 | .page-buttons { 84 | display: none; 85 | } 86 | 87 | .page-menu-row { 88 | display: block; 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /server/assets/podcastaddict.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | -------------------------------------------------------------------------------- /ui/src/components/Value/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { titleizeString } from '../../utils' 4 | import './styles.scss' 5 | 6 | interface IProps { 7 | model: { 8 | type: string 9 | method: string 10 | suggested: string 11 | } 12 | destinations?: Array<{ 13 | name: string 14 | type: string 15 | address: string 16 | split: string 17 | }> 18 | } 19 | 20 | type PodState = { copyMessage: string }; 21 | 22 | export default class Value extends React.PureComponent { 23 | 24 | constructor(props) { 25 | super(props); 26 | } 27 | 28 | getLink(dest: any): string { 29 | if (dest.type === "lnaddress") { 30 | const match = dest.address.toLowerCase().match(/^([a-z0-9._-]+)@([a-z0-9.-]+)$/); 31 | 32 | if (match) { 33 | const [ _, username, domain ] = match; 34 | return "https://" + domain + "/.well-known/lnurlp/" + username; 35 | } 36 | } 37 | 38 | return "https://amboss.space/node/" + dest.address; 39 | } 40 | 41 | render() { 42 | const { destinations, model } = this.props 43 | const splitTotal = destinations ? destinations.reduce((total, d) => total + parseInt(d.split, 10), 0) : null 44 | 45 | if (destinations && destinations.length > 1 && destinations[(destinations.length - 1)].name.toLowerCase() === "podcastindex.org") { 46 | destinations.pop(); 47 | } 48 | 49 | // list of old node addresses for alby and fountain 50 | const knownDeadNodes = { 51 | "030a58b8653d32b99200a2334cfe913e51dc7d155aa0116c176657a4f1722677a3": "Alby", 52 | "0332d57355d673e217238ce3e4be8491aa6b2a13f95494133ee243e57df1653ace": "Fountain", 53 | "03bc290b26637eb8f25de69fca83c85c014796aa03d90bf0d4c03c18947e12127d": "Fountain" 54 | } 55 | 56 | return ( 57 |
58 |

Value for Value via {titleizeString(model.type)}

59 |
    60 | {destinations.map(dest => ( 61 |
  • 62 | 63 | {knownDeadNodes[dest.address] && ⚠️} 64 | {dest.name} 65 |
  • 66 | ))} 67 |
68 |
69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ui/src/pages/styles.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .landing-content, 6 | .page-content { 7 | position: relative; 8 | z-index: 1; 9 | padding: 0 10px 20px 10px; 10 | margin: 0 auto; 11 | max-width: 1200px; 12 | } 13 | 14 | .hero-pitch { 15 | display: flex; 16 | font-family: var(--font-family-exp); 17 | width: inherit; 18 | margin: auto; 19 | margin-top: 30px; 20 | margin-bottom: 80px; 21 | } 22 | 23 | .hero-pitch-left { 24 | display: inline-flex; 25 | flex: 1; 26 | flex-direction: column; 27 | justify-content: center; 28 | audio { 29 | outline: none; 30 | } 31 | h5 { 32 | max-width: 400px; 33 | } 34 | .listen-row { 35 | display: flex; 36 | flex-direction: row; 37 | align-items: center; 38 | } 39 | .subscribe-badge { 40 | margin-left: 10px; 41 | 42 | img { 43 | width: 50px; 44 | height: 50px; 45 | } 46 | } 47 | } 48 | 49 | .hero-pitch-text { 50 | // font-family: var(--font-family-exp); 51 | font-size: 55px; 52 | color: var(--fg-main); 53 | } 54 | 55 | .hero-pitch-subtitle { 56 | font-size: 22px; 57 | max-width: 425px; 58 | color: var(--text-color); 59 | margin-bottom: 10px; 60 | } 61 | 62 | .hero-pitch-right { 63 | display: inline-flex; 64 | flex: 1; 65 | flex-direction: column; 66 | justify-content: center; 67 | align-items: center; 68 | width: inherit; 69 | // height: 640px; 70 | } 71 | 72 | .info-section { 73 | margin-top: 50px; 74 | h3 { 75 | margin: 22px 0; 76 | } 77 | p { 78 | font-family: var(--font-family-exp); 79 | font-size: 18px; 80 | line-height: 20px; 81 | color: var(--text-color); 82 | } 83 | .donation-providers { 84 | width: 100%; 85 | display: flex; 86 | flex-direction: row; 87 | 88 | .paypal, 89 | .sphinx-chat { 90 | flex: 1; 91 | text-align: center; 92 | } 93 | 94 | .paypal { 95 | form { 96 | .button { 97 | margin: 0 auto; 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | #donate { 105 | margin-bottom: 20px; 106 | } 107 | 108 | .footer { 109 | margin-top: 40px; 110 | padding-top: 20px; 111 | border-top: 1px solid #dbdbdb; 112 | } 113 | 114 | @import './mobile.scss'; 115 | -------------------------------------------------------------------------------- /ui/src/pages/Stats/NewFeedStatsCard/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import KPI from '../../../components/KPI' 4 | import Card from '../../../components/Card' 5 | 6 | import './styles.scss' 7 | 8 | interface IProps { 9 | total: number 10 | top1name: string 11 | top1count: number 12 | top2name: string 13 | top2count: number 14 | top3name: string 15 | top3count: number 16 | top4name: string 17 | top4count: number 18 | top5name: string 19 | top5count: number 20 | } 21 | 22 | export default class NewFeedStatsCard extends React.Component { 23 | static defaultProps = { 24 | total: 0, 25 | top1name: '', 26 | top1count: 0, 27 | top2name: '', 28 | top2count: 0, 29 | top3name: '', 30 | top3count: 0, 31 | top4name: '', 32 | top4count: 0, 33 | top5name: '', 34 | top5count: 0 35 | } 36 | 37 | constructor(props: IProps) { 38 | super(props) 39 | } 40 | 41 | numberWithCommas(number: number) { 42 | return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') 43 | } 44 | 45 | render() { 46 | const { 47 | total, 48 | top1name, 49 | top1count, 50 | top2name, 51 | top2count, 52 | top3name, 53 | top3count, 54 | top4name, 55 | top4count, 56 | top5name, 57 | top5count 58 | } = this.props 59 | 60 | return ( 61 |
62 | 63 |
64 | Host submitted feeds (30 days)... 65 |
66 |
{total.toLocaleString()}
67 |
68 | 69 | 70 | 71 | 72 | 73 | {/* */} 77 |
78 |
79 |
80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ui/src/styles.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: D-DIN; 3 | src: url(../fonts/D-DIN.otf) format('otf'); 4 | font-weight: normal; 5 | } 6 | 7 | @font-face { 8 | font-family: D-DIN-Bold; 9 | src: url(../fonts/D-DIN-Bold.otf) format('otf'); 10 | font-weight: bold; 11 | } 12 | 13 | @font-face { 14 | font-family: D-DINExp; 15 | src: url(../fonts/D-DINExp.otf) format('otf'); 16 | font-weight: normal; 17 | } 18 | @font-face { 19 | font-family: D-DINExp-Bold; 20 | src: url(../fonts/D-DINExp-Bold.otf) format('otf'); 21 | font-weight: bold; 22 | } 23 | 24 | @font-face { 25 | font-family: DINAlternate-Bold; 26 | src: url(../fonts/DIN-Alternate-Bold.ttf) format('ttf'); 27 | font-weight: bold; 28 | } 29 | 30 | :root { 31 | --transition-secs: 0.2s; 32 | --bg-main: #ffffff; 33 | --fg-main: #2d2d2d; 34 | --text-color: #868686; 35 | --link-color: #002752; 36 | --link-hover-color: #cc2799; 37 | --color-primary: #e90000; 38 | --font-family: D-DIN, sans-serif; 39 | --font-family-bold: D-DIN-Bold, sans-serif; 40 | --font-family-exp: D-DINExp, sans-serif; 41 | --font-family-exp-bold: D-DINExp-Bold, sans-serif; 42 | --font-alt: DINAlternate-Bold, sans-serif; 43 | 44 | &[data-theme="dark"] { 45 | --bg-main: #080808; 46 | --fg-main: #f1f1f1; 47 | --text-color: #d9d9d9; 48 | --link-color: #fff; 49 | } 50 | } 51 | 52 | body { 53 | min-height: 100vh; 54 | // max-width: 100vw; 55 | font-family: var(--font-family); 56 | margin: 0; 57 | color: var(--fg-main); 58 | background-color: var(--bg-main); 59 | overflow-y: scroll; // Disable scrolling on body 60 | } 61 | 62 | h1 { 63 | font-family: var(--font-family-exp); 64 | font-weight: bold; 65 | font-size: 55px; 66 | } 67 | 68 | h2 { 69 | font-family: var(--font-family-exp); 70 | font-weight: bold; 71 | font-size: 44px; 72 | } 73 | 74 | h3 { 75 | font-family: var(--font-family-exp); 76 | font-weight: bold; 77 | font-size: 33px; 78 | } 79 | 80 | h4 { 81 | font-family: var(--font-family-exp); 82 | font-weight: bold; 83 | font-size: 27px; 84 | } 85 | 86 | a:link, 87 | a:visited, 88 | a:active { 89 | color: var(--link-color); 90 | } 91 | 92 | a:hover { 93 | color: var(--link-hover-color); 94 | } 95 | 96 | .landing-graphic { 97 | opacity: 0.75; 98 | top: 120px; 99 | left: -300px; 100 | position: absolute; 101 | z-index: -1000; 102 | } 103 | 104 | @media only screen and (max-width: 767px) { 105 | /* phones */ 106 | .landing-graphic { 107 | display: none; 108 | } 109 | .hide-mobile { 110 | display: none; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ui/src/components/Player/styles.scss: -------------------------------------------------------------------------------- 1 | .player-info { 2 | display: inline-flex; 3 | flex-direction: column; 4 | align-items: center; 5 | width: 100%; 6 | } 7 | 8 | .player-bottom { 9 | display: inline-flex; 10 | flex-direction: column; 11 | align-items: center; 12 | width: inherit; 13 | margin-top: 5px; 14 | padding: 0 10px; 15 | // width: calc100%; 16 | } 17 | 18 | .player-media-controls { 19 | width: calc(100% - 20px); 20 | .player-show-title { 21 | a { 22 | color: var(--fg-main); 23 | } 24 | 25 | p { 26 | font-size: 20px; 27 | font-family: var(--font-family); 28 | font-weight: bold; 29 | text-align: center; 30 | width: 450px; 31 | white-space: nowrap; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | margin-bottom: 5px; 35 | color: var(--fg-main); 36 | } 37 | } 38 | .player-podcast-name { 39 | text-align: center; 40 | width: 450px; 41 | white-space: nowrap; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | margin-bottom: 5px; 45 | 46 | a { 47 | font-size: 16px; 48 | font-family: var(--font-family); 49 | color: var(--fg-main); 50 | } 51 | } 52 | .player-feed-button { 53 | cursor: pointer; 54 | } 55 | p { 56 | margin: 5px 0 0; 57 | font-family: var(--font-family-exp); 58 | font-size: 14px; 59 | //line-height: 16px; 60 | color: var(--text-color); 61 | } 62 | .rhap_container { 63 | outline: none; 64 | box-shadow: none; 65 | button { 66 | outline: none; 67 | } 68 | .rhap_volume-bar-area { 69 | outline: none; 70 | } 71 | .rhap_progress-container { 72 | outline: none; 73 | } 74 | .rhap_progress-indicator { 75 | width: 15px; 76 | height: 15px; 77 | top: -5px; 78 | background: var(--color-primary); 79 | } 80 | .rhap_play-pause-button { 81 | svg path { 82 | fill: var(--color-primary); 83 | } 84 | } 85 | } 86 | } 87 | :root[data-theme="dark"] { 88 | .rhap_container { 89 | background-color: var(--bg-main); 90 | } 91 | .rhap_time { 92 | color: var(--fg-main); 93 | } 94 | } 95 | 96 | @media only screen and (max-width: 520px) { 97 | .player-show-title p { 98 | max-width: 350px; 99 | } 100 | .player-podcast-name { 101 | max-width: 350px; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ui/images/brand-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/pages/Stats/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Stats } from 'webpack' 3 | 4 | import NewFeedStatsCard from './NewFeedStatsCard' 5 | import StatsCard from "./StatsCard"; 6 | 7 | import './styles.scss' 8 | 9 | 10 | interface IProps {} 11 | 12 | 13 | 14 | export default class Card extends React.Component { 15 | static defaultProps = {} 16 | state = { 17 | loading: true, 18 | overallStats: { 19 | feedCountTotal: '4,013,180', 20 | feedCount3days: '100,201', 21 | feedCount10days: '208,264', 22 | feedCount30days: '303,007', 23 | feedCount60days: '416,576', 24 | feedCount90days: '616,576', 25 | }, 26 | stats: { 27 | totalCount: 3773, 28 | top1name: 'Buzzsprout', 29 | top1count: 1870, 30 | top2name: 'RSS.com', 31 | top2count: 967, 32 | top3name: 'Captivate', 33 | top3count: 381, 34 | top4name: 'Transistor', 35 | top4count: 376, 36 | top5name: 'Blubrry', 37 | top5count: 96, 38 | } 39 | } 40 | _isMounted = false 41 | 42 | constructor(props: IProps) { 43 | super(props) 44 | } 45 | 46 | async componentDidMount(): Promise { 47 | this._isMounted = true 48 | //const stats = await this.getNewFeedStats() 49 | const overallStats = await this.getStats() 50 | 51 | if (this._isMounted) { 52 | this.setState({ 53 | loading: false, 54 | overallStats, 55 | // /stats 56 | }) 57 | } 58 | } 59 | 60 | // async getNewFeedStats() { 61 | // let response = await fetch('/api/newfeedstats', { 62 | // credentials: 'same-origin', 63 | // method: 'GET', 64 | // }) 65 | // return await response.json() 66 | // } 67 | 68 | async getStats() { 69 | let response = await fetch('/api/stats', { 70 | credentials: 'same-origin', 71 | method: 'GET', 72 | }) 73 | return await response.json() 74 | } 75 | 76 | render() { 77 | const {} = this.props 78 | const { loading, overallStats, stats } = this.state 79 | return ( 80 |
81 | 89 |
90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ui/src/components/ResultItem/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import {Link} from "react-router-dom"; 4 | import {truncateString} from '../../utils' 5 | import NoImage from '../../../images/no-cover-art.png' 6 | 7 | import './styles.scss' 8 | 9 | interface IProps { 10 | title?: string 11 | description?: string 12 | author?: string 13 | categories?: any 14 | image?: any 15 | id?: string 16 | className?: string 17 | } 18 | 19 | export default class ResultItem extends React.PureComponent { 20 | static defaultProps = {} 21 | 22 | constructor(props: IProps) { 23 | super(props) 24 | } 25 | 26 | renderCategories(categories) { 27 | let categoryArray = [] 28 | // categories = { 29 | // '': '', 30 | // '9': 'Business', 31 | // } 32 | for (let prop in categories) { 33 | let category = categories[prop] 34 | if (category !== '') { 35 | categoryArray.push(category) 36 | } 37 | } 38 | if (categoryArray.length === 0) { 39 | return ( 40 |
No Categories
41 | ) 42 | } 43 | // Only render a max of 5 categories 44 | return categoryArray.slice(0, 5).map((cat, i) => ( 45 |
46 | {cat} 47 |
48 | )) 49 | } 50 | 51 | render() { 52 | const {title, description, author, categories, image, id, className} = this.props 53 | // const { open } = this.state 54 | return ( 55 |
56 |
57 |
58 | 59 | { 63 | ev.target.src = NoImage 64 | }} 65 | loading="lazy" 66 | /> 67 | 68 |
69 |
70 |
{title}
71 |

by {author}

72 |
73 | {this.renderCategories(categories)} 74 |
75 |
76 |
77 |

78 | {truncateString(description)} 79 |

80 |
81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ui/src/pages/mobile.scss: -------------------------------------------------------------------------------- 1 | @media only screen and (min-width: 1168px) { 2 | /* tablets and desktop */ 3 | .landing-content, 4 | .page-content { 5 | width: 100%; 6 | } 7 | .hero-pitch { 8 | flex-direction: row; 9 | } 10 | } 11 | 12 | /* TABLETS PORTRAIT */ 13 | @media only screen and (max-width: 1167px) { 14 | /* tablets and desktop */ 15 | .landing-content, 16 | .page-content { 17 | min-width: 768px; 18 | } 19 | // .hero-pitch { 20 | // align-items: center; 21 | // flex-direction: column; 22 | // flex-flow: column; 23 | // text-align: center; 24 | // } 25 | .hero-pitch-text { 26 | font-size: 42px; 27 | } 28 | .hero-pitch-subtitle { 29 | margin-top: 0 !important; 30 | max-width: 100% !important; 31 | font-size: 20px; 32 | } 33 | .info-section { 34 | padding: 0 20px; 35 | } 36 | .hero-pitch-right { 37 | margin-top: 40px; 38 | // padding: 0 10px; 39 | } 40 | } 41 | 42 | @media only screen and (max-width: 1010px) { 43 | .results-page, .results-list, .v4v { 44 | width: 100% !important; 45 | } 46 | } 47 | 48 | @media only screen and (max-width: 980px) { 49 | .hero-pitch { 50 | align-items: center; 51 | flex-direction: column; 52 | flex-flow: column; 53 | text-align: center; 54 | max-width: 600px; 55 | } 56 | .hero-pitch-text { 57 | padding: 0 30px; 58 | } 59 | .hero-pitch-subtitle { 60 | padding: 0 30px; 61 | } 62 | .results-page, .results-list, .v4v { 63 | width: 100% !important; 64 | } 65 | .hero-pitch-left { 66 | padding: 0 20px; 67 | align-items: center; 68 | h5 { 69 | text-align: center; 70 | padding: 0 10px; 71 | } 72 | .listen-row { 73 | justify-content: center; 74 | padding: 0 10px; 75 | } 76 | } 77 | .info-section { 78 | .donation-providers { 79 | width: 100%; 80 | flex-direction: column; 81 | } 82 | } 83 | } 84 | 85 | @media only screen and (max-width: 767px) { 86 | .hero-pitch { 87 | margin-top: 0px; 88 | margin-bottom: 40px; 89 | } 90 | .hero-pitch-left { 91 | align-items: center; 92 | padding: 0 0; 93 | h5 { 94 | padding: 0 0; 95 | width: calc(100% - 20px); 96 | margin: inherit auto; 97 | } 98 | .listen-row { 99 | padding: 0 0; 100 | width: calc(100% - 20px); 101 | margin: 0 auto; 102 | } 103 | } 104 | /* phones */ 105 | .landing-content, 106 | .page-content { 107 | min-width: 350px; 108 | max-width: 600px; 109 | width: 100%; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /ui/src/components/SearchBar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import SearchIcon from '../../../images/search.svg' 3 | import './styles.scss' 4 | 5 | interface IProps { 6 | search?: string 7 | searchType?: string 8 | onSearchChange: (evt: React.ChangeEvent) => void 9 | onSearchTypeChange: (evt: React.ChangeEvent) => void 10 | onSearchSubmit?: (evt: React.ChangeEvent) => void 11 | filterFunction?: any 12 | } 13 | 14 | export default class Searchbar extends React.Component { 15 | static defaultProps = {} 16 | searchInputRef = React.createRef() 17 | 18 | constructor(props: IProps) { 19 | super(props) 20 | 21 | this.onSubmit = this.onSubmit.bind(this); 22 | } 23 | 24 | onSubmit(evt: React.ChangeEvent) { 25 | const {onSearchSubmit} = this.props 26 | if (onSearchSubmit !== null) { 27 | onSearchSubmit(evt) 28 | } 29 | // this will remove focus of the input form and close the keyboard on mobile 30 | this.searchInputRef.current.blur() 31 | evt.preventDefault() 32 | } 33 | 34 | render() { 35 | const {search, searchType, onSearchChange, onSearchTypeChange} = this.props 36 | let cleanSearchType = searchType || "" 37 | if (cleanSearchType === "") { 38 | cleanSearchType = "all" 39 | } 40 | cleanSearchType = cleanSearchType.toLowerCase() 41 | 42 | // noinspection SpellCheckingInspection 43 | const inputProps = { 44 | enterkeyhint: "done" // not recognized by typescript so set here and append to input, lowercase to avoid react error in console 45 | } 46 | return ( 47 |
48 | 49 | 59 |
60 | 70 |
71 |
72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ui/src/state/modules/_abstract/operations.ts: -------------------------------------------------------------------------------- 1 | import shortid from 'shortid'; 2 | import { Dispatch, AnyAction } from 'redux' 3 | import { ThunkDispatch } from 'redux-thunk' 4 | import { ApiBase } from './api'; 5 | 6 | export const operations = (creators, api: ApiBase, additionalOperations: object = {}) => { 7 | const index = (ids?: string) => { 8 | return (dispatch) => { 9 | dispatch(creators.indexRequest(ids)); 10 | return api 11 | .index(ids) 12 | .then( 13 | (models) => dispatch(creators.indexSuccess(models)), 14 | (errors) => dispatch(creators.indexFailure(errors)), 15 | ); 16 | }; 17 | }; 18 | 19 | const create = (form) => { 20 | const tempId = shortid.generate(); // need this to keep track of the created object 21 | return (dispatch: Dispatch) => { 22 | dispatch(creators.createRequest(form, tempId)); 23 | return api 24 | .create(form) 25 | .then( 26 | (model) => dispatch(creators.createSuccess(model, tempId)), 27 | (errors) => dispatch(creators.createFailure(errors, tempId)), 28 | ); 29 | }; 30 | }; 31 | 32 | const destroy = (id) => { 33 | return (dispatch: Dispatch) => { 34 | dispatch(creators.destroyRequest(id)); 35 | return api 36 | .destroy(id) 37 | .then( 38 | (response) => dispatch(creators.destroySuccess(id, response)), 39 | (errors) => dispatch(creators.destroyFailure(id, errors)), 40 | ); 41 | }; 42 | }; 43 | 44 | const show = (id) => { 45 | return (dispatch: Dispatch) => { 46 | dispatch(creators.showRequest(id)); 47 | return api 48 | .show(id) 49 | .then( 50 | (model) => dispatch(creators.showSuccess(id, model)), 51 | (errors) => dispatch(creators.showFailure(id, errors)), 52 | ); 53 | }; 54 | }; 55 | 56 | const update = (id, form) => { 57 | return (dispatch: Dispatch) => { 58 | dispatch(creators.updateRequest(id, form)); 59 | return api 60 | .update(id, form) 61 | .then( 62 | (model) => dispatch(creators.updateSuccess(id, model)), 63 | (errors) => dispatch(creators.updateFailure(id, errors)), 64 | ); 65 | }; 66 | }; 67 | 68 | const reset = () => (dispatch: Dispatch) => dispatch(creators.reset()); 69 | const resetErrors = () => (dispatch: Dispatch) => dispatch(creators.resetErrors()); 70 | 71 | return { 72 | index, 73 | create, 74 | destroy, 75 | show, 76 | update, 77 | reset, 78 | resetErrors, 79 | ...additionalOperations, 80 | } 81 | }; 82 | 83 | export default operations; 84 | 85 | export * from './reducer'; 86 | export * from './schema'; 87 | -------------------------------------------------------------------------------- /ui/src/pages/Apps/AppsWebPart/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import SingleApp from './SingleApp' 3 | import FilterTags from './FilterTags' 4 | 5 | import './styles.scss' 6 | 7 | async function getApps(setApps, setFilterTypes) { 8 | if (setApps) { 9 | let response = await fetch(`/api/apps`, { 10 | credentials: 'same-origin', 11 | method: 'GET', 12 | }) 13 | const apps = await response.json() 14 | let filterSets = { 15 | appType: new Set(), 16 | supportedElements: new Set(), 17 | platforms: new Set(), 18 | } 19 | 20 | apps.forEach((app, index) => { 21 | app.key = index 22 | app.appType.forEach((type) => { 23 | filterSets.appType.add(type) 24 | }) 25 | 26 | app.supportedElements.forEach((elem) => 27 | filterSets.supportedElements.add(elem.elementName) 28 | ) 29 | app.platforms.forEach((elem) => filterSets.platforms.add(elem)) 30 | }) 31 | 32 | //converts sets to arrays 33 | // let filters = { appType: [], supportedElements: [], platforms: [] } 34 | // filters.appType = [...filterSets.appType] 35 | // filters.supportedElements = [...filterSets.supportedElements] 36 | // filters.platforms = [...filterSets.platforms] 37 | // console.log(apps) 38 | // console.log(filters) 39 | setApps(apps) 40 | setFilterTypes(filterSets) 41 | } 42 | } 43 | 44 | function AppsWebPart() { 45 | const [apps, setApps] = useState([]) 46 | const [filteredApps, setFilteredApps] = useState([]) 47 | const [filterTypes, setFilterTypes] = useState([]) 48 | 49 | useEffect(() => { 50 | getApps(setApps, setFilterTypes) 51 | }, []) 52 | 53 | return ( 54 |
55 | 60 |

Supporting Apps, Directories and Hosting Companies

61 |
62 |
63 |
64 |

App

65 |
66 |

Supported Elements

67 |

Platforms

68 |
69 | 70 |
    71 | {filteredApps 72 | .sort(function (a, b) { 73 | if (a.supportedElements > b.supportedElements) { 74 | return -1 75 | } 76 | if (a.supportedElements < b.supportedElements) { 77 | return 1 78 | } 79 | return 0 80 | }) 81 | .map((item) => SingleApp(item))} 82 |
83 |
84 | ) 85 | } 86 | 87 | export default AppsWebPart 88 | -------------------------------------------------------------------------------- /_old/dev/style/default.css: -------------------------------------------------------------------------------- 1 | /* ----- Developer Search Page ----- */ 2 | @keyframes spin { 3 | 0% { transform: rotate(0deg); } 4 | 100% { transform: rotate(359deg); } 5 | } 6 | div.pageContentWrapper.DeveloperSearch div#divSearch svg.loading { 7 | animation: spin 2s linear infinite; 8 | text-align: center; 9 | display: block; 10 | margin-left:auto; 11 | margin-right:auto; 12 | } 13 | div.pageContentWrapper.DeveloperSearch div#divSearch ul li.podcast { 14 | padding:10px; 15 | 16 | } 17 | div.pageContentWrapper.DeveloperSearch div#divSearch ul li.podcast + li.podcast { 18 | border-top: solid #ccc 1px; 19 | } 20 | div.pageContentWrapper.DeveloperSearch div#divSearch ul li.podcast img.albumart { 21 | margin-right:20px; 22 | width:128px; 23 | } 24 | div.pageContentWrapper.DeveloperSearch div#divSearch ul li.podcast div.infocard { 25 | max-width:900px; 26 | } 27 | div.pageContentWrapper.DeveloperSearch div#divSearch ul li.podcast a.subscribeBadge { 28 | margin-bottom:8px; 29 | display: inline-block; 30 | } 31 | div.pageContentWrapper.DeveloperSearch div#divSearch ul li.podcast div.infocard p.description { 32 | overflow-wrap: break-word; 33 | word-break: break-all; 34 | } 35 | div.pageContentWrapper.DeveloperSearch div#divSearch ul li.podcast audio { 36 | outline:none; 37 | } 38 | div.pageContentWrapper.DeveloperSearch div#divSearch ul li.podcast video { 39 | outline:none; 40 | max-width:100%; 41 | } 42 | div.pageContentWrapper.DeveloperSearch div#divSearch ul li.podcast div.enclosurePlayer { 43 | 44 | margin-left:auto; 45 | margin-right:auto; 46 | } 47 | div.pageContentWrapper.DeveloperSearch div#divSearch input.feedSearch { 48 | width:80%; 49 | margin-auto:left; 50 | margin-auto:right; 51 | } 52 | div.pageContentWrapper.DeveloperSearch div#divSearch div.searchForm { 53 | text-align: center; 54 | } 55 | div.pageContentWrapper.DeveloperSearch div#divMain span.feedDisplayTitle { 56 | font-size:medium; 57 | color:blueviolet; 58 | } 59 | 60 | 61 | /* ----- Developer Documentation Page ----- */ 62 | div.pageContentWrapper.DeveloperDocs div.row.developerDocs { 63 | margin-top:60px; 64 | } 65 | 66 | div.pageContentWrapper.DeveloperDocs div.row.developerDocs p { 67 | padding-left:10px; 68 | } 69 | 70 | div.pageContentWrapper.DeveloperDocs div.row.developerDocs div.codeExampleBlock { 71 | background-color:#e9e9e9; 72 | border-radius:12px; 73 | padding:20px; 74 | } 75 | 76 | div.pageContentWrapper.DeveloperDocs div.row.developerDocs div.codeExampleBlock div.code { 77 | margin-left:10px; 78 | } 79 | 80 | div.pageContentWrapper.DeveloperDocs div.row.developerDocs h4 { 81 | margin-bottom:20px; 82 | } 83 | 84 | div.pageContentWrapper.DeveloperDocs div.row.developerDocs div.codeExampleBlock { 85 | margin-bottom:30px; 86 | } 87 | div.pageContentWrapper.DeveloperDocs div.row.developerDocs li.api-endpoint { 88 | border-left: solid #ddd 4px; 89 | margin-top:10px; 90 | border-bottom: solid white 0; 91 | } 92 | 93 | div.pageContentWrapper.DeveloperDocs div.row.developerDocs li.api-endpoint-write { 94 | border-left: solid orange 4px; 95 | } -------------------------------------------------------------------------------- /ui/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | import './styles.scss' 5 | 6 | interface IProps { 7 | className?: string 8 | big?: boolean 9 | small?: boolean 10 | slim?: boolean 11 | primary?: boolean 12 | link?: boolean 13 | children?: any 14 | style?: string 15 | href?: string 16 | type?: string 17 | alt?: string 18 | onClick?: (e?: any) => void 19 | disabled?: boolean 20 | dataValue?: string 21 | selected?: boolean 22 | } 23 | 24 | export default class Button extends React.PureComponent { 25 | static defaultProps = {} 26 | 27 | render() { 28 | const { 29 | className, 30 | children, 31 | href, 32 | onClick, 33 | link, 34 | primary, 35 | big, 36 | small, 37 | slim, 38 | type, 39 | alt, 40 | disabled, 41 | dataValue, 42 | selected, 43 | } = this.props 44 | let buttonEl = null 45 | let buttonClasses = [ 46 | "button", 47 | className, 48 | disabled ? "disabled" : "", 49 | primary ? 'primary' : '', 50 | big ? 'big' : '', 51 | small ? 'small' : '', 52 | slim ? 'slim' : '', 53 | selected ? 'selected' : '', 54 | ] 55 | let buttonClass = buttonClasses.join(" ") 56 | if (type === 'submit') { 57 | buttonEl = ( 58 | 67 | ) 68 | } else if (href && !link) { 69 | buttonEl = ( 70 | 76 | {children} 77 | 78 | ) 79 | } else if (href && link) { 80 | buttonEl = ( 81 | 87 | {children} 88 | 89 | ) 90 | } else { 91 | buttonEl = ( 92 | 100 | ) 101 | } 102 | return
{buttonEl}
103 | } 104 | } 105 | -------------------------------------------------------------------------------- /ui/src/utils.ts: -------------------------------------------------------------------------------- 1 | import queryStringHelper from 'query-string' 2 | import NoImage from '../images/no-cover-art.png' 3 | 4 | export const updateTitle = (tile?: string) => { 5 | let newTitle = 'Podcastindex.org' 6 | if (tile !== undefined) 7 | newTitle = tile + ' | ' + newTitle 8 | document.title = newTitle 9 | } 10 | 11 | export const encodeSearch = (searchString: string): string => { 12 | return encodeURIComponent(searchString) 13 | } 14 | 15 | export const cleanSearchQuery = (queryString: string, field: string = 'q'): string => { 16 | let params = queryStringHelper.parse(queryString) 17 | let queryAr = params[field] as string 18 | if (!queryAr) { 19 | return '' 20 | } 21 | return queryAr 22 | } 23 | 24 | export const truncateString = (input: string) => { 25 | if (input.length > 200) 26 | return `${input.substring(0, 300)}...` 27 | else 28 | return input 29 | } 30 | 31 | export const titleizeString = (input: string) => { 32 | return input 33 | .split(/\W+/gi) 34 | .map(w => w.charAt(0).toUpperCase() + w.slice(1)) 35 | .join(' ') 36 | } 37 | 38 | export const getPrettyDate = (time: number, includeTime: boolean = true) => { 39 | // Javascript epoch is in milliseconds; datePublished is in seconds 40 | const dateObj = new Date(time * 1000) 41 | if (includeTime) 42 | return dateObj.toLocaleString() 43 | return dateObj.toLocaleDateString() 44 | } 45 | 46 | export const getISODate = (time: number): string => { 47 | // Javascript epoch is in milliseconds; datePublished is in seconds 48 | const dateObj = new Date(time * 1000) 49 | return dateObj.toISOString() 50 | } 51 | 52 | 53 | export const fixURL = (url: string) => { 54 | if (url === undefined || url === null) { 55 | return url 56 | } else { 57 | url = url.trim() 58 | if (url !== '') { 59 | let prefix = url.substring(0, 4).toLowerCase() 60 | if (prefix !== 'http') 61 | url = `http://${url}` 62 | } 63 | } 64 | return url 65 | } 66 | 67 | export const isValidURL = (urlString: string) => { 68 | try { 69 | let url = new URL(urlString) 70 | return url.protocol === 'http:' || url.protocol === 'https:' 71 | } catch (_) { 72 | return false 73 | } 74 | } 75 | 76 | /** 77 | * Return the best image available 78 | * 79 | * If item doesn't contain an image, returns a placeholder image 80 | * 81 | * @param item the episode, item, or feed to get image for 82 | */ 83 | export const getImage = (item) => { 84 | return item.artwork || item.image || item.feedImage || NoImage 85 | } 86 | 87 | /** 88 | * Encodes text to URL safe base64 89 | * 90 | * Based on example from https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem 91 | * 92 | * @param text the text to encode 93 | * @return the base 64 encoded text 94 | */ 95 | export const encodeURLSafeBase64 = (text: string): string => { 96 | const binString = Array.from(new TextEncoder().encode(text), (byte) => 97 | String.fromCodePoint(byte), 98 | ).join('') 99 | // since Podlink doesn't need trailing =, remove them 100 | return btoa(binString).replace(/=+$/, ''); 101 | } 102 | -------------------------------------------------------------------------------- /ui/images/transcript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | image/svg+xml 12 | 13 | Paper Symbolic Icon Theme 14 | 15 | 16 | 17 | Paper Symbolic Icon Theme 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ui/src/pages/Apps/AppsWebPart/SingleApp/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import './styles.scss' 4 | 5 | function SingleApp(app) { 6 | return ( 7 |
  • 8 |
    9 |
    10 | 13 |
    14 |
    15 | 16 | {app.appName} 17 | 18 |

    19 | {app.appType.map((type, j) => ( 20 | 21 | {j > 0 && ', '} 22 | {type} 23 | 24 | ))} 25 |

    26 |
    27 |
    28 | 29 |
    30 | {app.supportedElements 31 | .sort((a, b) => (a.elementName > b.elementName ? 1 : -1)) 32 | .map((suppElement, j) => ( 33 | 34 | {j > 0 && ', '} 35 | 36 | {suppElement.elementName} 37 | 38 | 39 | ))} 40 |
    41 | 42 |
    43 | {app.platforms 44 | .sort((a, b) => (a > b ? 1 : -1)) 45 | .map((platform, j) => { 46 | const hideNAAppPlatformOnMobile = platform === 'N/A' 47 | const platformLink = app.platformLinks?.[platform] 48 | 49 | return ( 50 | 58 | {j > 0 && ', '} 59 | {platformLink ? ( 60 | 65 | {platform} 66 | 67 | ) : ( 68 | platform 69 | )} 70 | 71 | ) 72 | })} 73 |
    74 |
  • 75 | ) 76 | } 77 | 78 | export default SingleApp 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PodcastIndex Web UI 2 | 3 | Podcast Web UI is a project that houses the code for the React app and express server for [podcastindex.org](https://podcastindex.org/). 4 | 5 | - Landing page for PodcastIndex. 6 | - Search for podcasts in index. 7 | - Simple podcast player for listening 8 | - List of apps using the PodcastIndex 9 | - Documentation and developer login for credential management 10 | 11 | ## Project Structure 12 | 13 | The project is split into two folders, `ui`, and `server`. 14 | 15 | . 16 | ├── ... 17 | ├── ui 18 | │ ├── public # Static files for index.html and favicon 19 | │ ├── fonts # Fonts used in the UI 20 | │ ├── images # Images and icons that are part of the UI 21 | │ └── src # All React and client code 22 | ├── server 23 | │ ├── assets # static files that are dynamically updated 24 | │ ├── data # static data files (ie. json) that are dynamically updated 25 | │ └── index.js # express app that serves the UI and is a reverse proxy (replaces need for NGINX) 26 | └── ... 27 | 28 | The folder `ui` houses all the React and client based code and assets. 29 | 30 | The folder `server` houses all of the API, static server data, and the reverse proxy to the PodcastIndex API using [`comster/podcast-index-api`](https://github.com/comster/podcast-index-api). 31 | 32 | ### Server `data` and `assets` 33 | 34 | The reason to build a custom express server for serving React and other data is due to the need for script updated `.json` files and dynamically adding apps to the `/apps` page. This data should not be bundled with the client compiled code. 35 | 36 | ### CORS 37 | 38 | The custom express server also is used to [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy#:~:text=In%20computer%20networks%2C%20a%20reverse,the%20reverse%20proxy%20server%20itself.) requests through the same domain to prevent [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) issues. Using the same domain to server up the UI content and to send api requests prevents CORS issues in modern browsers. 39 | 40 | ## Getting Started 41 | 42 | ### Set .env 43 | 44 | You should see a `.env-example` file. Copy this and remove the `-example`. The file `.env` is ignored by GIT and is needed to set the `API_KEY`, `API_SECRET`, `API_ADD_KEY` and `API_ADD_SECRET` variables 45 | 46 | ### Starting the dev server 47 | 48 | In order to have the UI hot reload for development, we utilized `webpack-dev-server` this allows for easier debugging, etc. In order for the dev-server to connect to the API, you must first have set the `.env` file variables and have started the server with `yarn start` 49 | 50 | ```zsh 51 | # Install dependencies 52 | yarn install 53 | 54 | # Start dev server 55 | yarn dev 56 | 57 | # Start the node server in another terminal window. 58 | yarn start 59 | ``` 60 | 61 | ## Running production 62 | 63 | To start the server, simply run after setting the `.env` file 64 | 65 | **Note**: Make sure to set `NODE_ENV=production` in the `.env` file 66 | 67 | The below script will compile the code and then start the node server. 68 | 69 | ```zsh 70 | npm run build 71 | npm start 72 | ``` 73 | 74 | ## Tech List 75 | 76 | - [Express](https://expressjs.com/) 77 | - [React](https://reactjs.org/) 78 | - [Webpack](https://webpack.js.org/) 79 | 80 | ## TODO 81 | 82 | - Font should load through webpack properly 83 | - Better image loading handling in the search results page. 84 | - Developers page and login 85 | -------------------------------------------------------------------------------- /ui/src/components/EpisodeItem/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: #ffffff; 3 | --border-color: #d6d6d6; 4 | --cover-art-border-color: lighten(#d6d6d6, 0.2); 5 | 6 | &[data-theme="dark"] { 7 | --bg-color: #180e0e; 8 | --border-color: #505050; 9 | --cover-art-border-color: var(--border-color); 10 | } 11 | } 12 | 13 | .episode { 14 | background: var(--bg-color); 15 | border: 1px solid var(--border-color); 16 | border-radius: 6px; 17 | margin: 10px 0px; 18 | height: auto; 19 | padding: 5px; 20 | box-sizing: border-box; 21 | 22 | &.selected-item { 23 | box-shadow: inset 0 0 10px 0; 24 | } 25 | } 26 | 27 | .episode-row { 28 | display: flex; 29 | flex-direction: row; 30 | align-items: center; 31 | width: 100%; 32 | } 33 | 34 | .episode-cover-art { 35 | display: inline-flex; 36 | flex-direction: column; 37 | align-items: center; 38 | justify-content: center; 39 | padding: 15px 15px 15px 15px; 40 | img { 41 | max-height: 140px; 42 | width: 140px; 43 | //object-fit: contain; 44 | border-radius: 10px; 45 | border: 1px solid var(--cover-art-border-color); 46 | } 47 | } 48 | 49 | .episode-info { 50 | display: inline-flex; 51 | flex-direction: column; 52 | margin-left: 10px; 53 | overflow: hidden; 54 | p { 55 | margin: 10px 0; 56 | } 57 | } 58 | 59 | .episode-title { 60 | font-family: var(--font-family-exp); 61 | font-weight: bold; 62 | width: 100%; 63 | font-size: 26px; 64 | color: var(--fg-main); 65 | text-align: left; 66 | overflow: hidden; 67 | text-overflow: ellipsis; 68 | } 69 | 70 | .episode-date { 71 | color: var(--text-color); 72 | } 73 | 74 | .episode-links { 75 | display: inline-flex; 76 | flex-direction: row; 77 | gap: 5px; 78 | } 79 | 80 | .episode-link img, .episode-play-pause-mobile { 81 | width: 30px; 82 | height: 30px; 83 | } 84 | .episode-play-pause-mobile { 85 | display: none; 86 | margin-left: 20px; 87 | } 88 | 89 | .episode-play-pause{ 90 | margin-left: auto; 91 | margin-right: 10px; 92 | min-width: 50px; 93 | min-height: 50px; 94 | cursor: pointer; 95 | } 96 | 97 | .episode-description { 98 | font-family: var(--font-family); 99 | overflow: hidden; 100 | text-overflow: ellipsis; 101 | font-size: 18px; 102 | margin: 0 !important; 103 | padding: 10px; 104 | width: 100%; 105 | } 106 | 107 | .episode .podcast-value { 108 | padding: 10px; 109 | } 110 | 111 | @media only screen and (max-width: 767px) { 112 | /* phones */ 113 | .episode { 114 | height: fit-content; 115 | display: flex; 116 | flex-direction: column; 117 | align-items: flex-start; 118 | padding: 10px 10px; 119 | } 120 | .episode-cover-art { 121 | img { 122 | height: 90px; 123 | width: 90px; 124 | } 125 | } 126 | 127 | .episode-title { 128 | width: 100%; 129 | font-size: 22px; 130 | } 131 | .episode-play-pause-mobile { 132 | display: inherit; 133 | } 134 | .episode-play-pause{ 135 | display: none; 136 | } 137 | .episode-description { 138 | font-size: 15px; 139 | } 140 | .episode-info { 141 | p { 142 | font-size: 15px; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /ui/src/pages/AddFeed/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --input-bg-color: #f7f7f7; 3 | --input-border-color: #d6d6d6; 4 | --input-placeholder-color: #b6b6b6; 5 | 6 | &[data-theme="dark"] { 7 | --input-bg-color: #000000; 8 | --input-border-color: #303030; 9 | --input-placeholder-color: #969696; 10 | } 11 | } 12 | 13 | .add-feed { 14 | margin: 0 auto; 15 | padding: 0 10px; 16 | position: relative; 17 | z-index: 1; 18 | max-width: 1024px; 19 | 20 | h2 { 21 | font-size: 24px; 22 | } 23 | 24 | h3 { 25 | font-size: 20px; 26 | } 27 | 28 | .add-result a { 29 | text-decoration: underline !important; 30 | color: red; 31 | } 32 | 33 | .add-feed-form { 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | 38 | .feed { 39 | width: 100%; 40 | flex: 1; 41 | display: inline-flex; 42 | flex-direction: row; 43 | align-items: center; 44 | background: var(--input-bg-color); 45 | border: 1px solid var(--input-border-color); 46 | border-radius: 17px; 47 | height: 34px; 48 | margin: 10px 15px; 49 | padding: 0 15px; 50 | 51 | input { 52 | width: 100%; 53 | //height: 32px; 54 | font-family: var(--font-family-expily-exp); 55 | font-size: 16px; 56 | line-height: 16px; 57 | border-top-style: hidden !important; 58 | border-right-style: hidden !important; 59 | border-left-style: hidden !important; 60 | border-bottom-style: hidden !important; 61 | background-color: transparent !important; 62 | color: var(--text-color); 63 | } 64 | 65 | ::-webkit-input-placeholder { 66 | /* Chrome/Opera/Safari */ 67 | font-family: var(--font-family-expily-exp); 68 | font-size: 14px; 69 | color: var(--input-placeholder-color); 70 | } 71 | 72 | ::-moz-placeholder { 73 | /* Firefox 19+ */ 74 | font-family: var(--font-family-expily-exp); 75 | font-size: 14px; 76 | color: var(--input-placeholder-color); 77 | } 78 | 79 | :-ms-input-placeholder { 80 | /* IE 10+ */ 81 | font-family: var(--font-family-expily-exp); 82 | font-size: 14px; 83 | color: var(--input-placeholder-color); 84 | } 85 | 86 | :-moz-placeholder { 87 | /* Firefox 18- */ 88 | font-family: var(--font-family-expily-exp); 89 | font-size: 14px; 90 | color: var(--input-placeholder-color); 91 | } 92 | 93 | input:focus { 94 | outline: none !important; 95 | } 96 | } 97 | 98 | .add-button { 99 | margin-top: 5px; 100 | } 101 | } 102 | 103 | } 104 | 105 | .loader-wrapper { 106 | //margin: 0 auto; 107 | //position: relative; 108 | //height: 100%; 109 | //z-index: 1; 110 | //display: flex; 111 | //flex-direction: row; 112 | //justify-content: center; 113 | //align-items: center; 114 | } 115 | -------------------------------------------------------------------------------- /ui/src/components/TopBar/styles.scss: -------------------------------------------------------------------------------- 1 | .topbar { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | position: sticky; 6 | padding: 23px 0; 7 | z-index: 3; 8 | max-width: 1300px; 9 | margin: 10px auto; 10 | } 11 | 12 | .topbar-brand { 13 | width: 290px; 14 | display: inline-flex; 15 | flex-direction: row; 16 | align-items: center; 17 | cursor: pointer; 18 | } 19 | 20 | a, 21 | .topbar-brand { 22 | text-decoration: none !important; 23 | } 24 | 25 | .topbar-title { 26 | display: inline-flex; 27 | flex-direction: row; 28 | align-items: center; 29 | margin-left: 15px; 30 | color: var(--color-primary); 31 | } 32 | 33 | .topbar-span { 34 | margin-top: 5px; 35 | flex: 3; 36 | display: inline-flex; 37 | flex-direction: row; 38 | align-items: center; 39 | } 40 | 41 | .topbar-links { 42 | margin-top: 5px; 43 | display: inline-flex; 44 | flex-direction: row; 45 | align-items: center; 46 | justify-content: flex-end; 47 | #topbar-nav-links { 48 | display: inline-flex; 49 | flex-direction: row; 50 | } 51 | 52 | a:link, 53 | a:visited, 54 | a:active{ 55 | color: var(--fg-main); 56 | } 57 | } 58 | 59 | .topbar-mobile-dropdown { 60 | display: none; 61 | width: 35px; 62 | flex-direction: row; 63 | align-items: center; 64 | justify-content: center; 65 | border-radius: 4px; 66 | &.open { 67 | background: lighten(#d6d6d6, 0.2); 68 | } 69 | } 70 | 71 | @media only screen and (max-width: 1200px) { 72 | .topbar { 73 | width: 100%; 74 | } 75 | #topbar-nav-links { 76 | .button { 77 | padding: 8px 13px; 78 | } 79 | } 80 | } 81 | 82 | @media only screen and (max-width: 980px) { 83 | .topbar { 84 | //padding: 23px 15px; 85 | } 86 | .topbar-brand { 87 | width: 40px; 88 | } 89 | .topbar-title { 90 | display: none; 91 | } 92 | } 93 | 94 | @media only screen and (max-width: 860px) { 95 | .topbar-links { 96 | flex: 0; 97 | } 98 | #topbar-nav-links { 99 | .button-wrapper { 100 | display: none; 101 | } 102 | } 103 | #topbar-nav-links { 104 | &.topbar-dropdown-open { 105 | z-index: 4; 106 | position: absolute; 107 | right: 0px; 108 | top: 60px; 109 | width: calc(100% - 20px); 110 | border: 1px solid lighten(#d6d6d6, 0.2); 111 | margin: 10px 10px; 112 | display: flex; 113 | background: var(--bg-color); 114 | flex-direction: column; 115 | border-radius: 4px; 116 | box-shadow: 0 0 9px 0 rgba(192, 192, 192, 0.5); 117 | .button-wrapper { 118 | width: 100%; 119 | .button { 120 | width: inherit; 121 | border-radius: 0 !important; 122 | } 123 | display: flex !important; 124 | } 125 | } 126 | } 127 | /* phones */ 128 | .topbar-brand { 129 | width: 40px; 130 | } 131 | // .topbar-title { 132 | // display: none; 133 | // } 134 | .landing-content { 135 | min-width: 600px; 136 | //width: 600px; 137 | } 138 | .topbar-mobile-dropdown { 139 | display: flex; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /ui/public/pci_avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/components/SearchBar/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --input-bg-color: #f7f7f7; 3 | --input-border-color: #d6d6d6; 4 | --input-placeholder-color: #b6b6b6; 5 | 6 | &[data-theme="dark"] { 7 | --input-bg-color: #000000; 8 | --input-border-color: #303030; 9 | --input-placeholder-color: #969696; 10 | } 11 | } 12 | 13 | .topbar-search { 14 | flex: 1; 15 | display: inline-flex; 16 | flex-direction: row; 17 | align-items: center; 18 | background: var(--input-bg-color); 19 | border: 1px solid var(--input-border-color); 20 | border-radius: 17px; 21 | height: 34px; 22 | margin: 0 15px; 23 | padding: 0 0 0 15px; 24 | input { 25 | flex: 1; 26 | margin-left: 10px; 27 | height: 32px; 28 | font-family: var(--font-family-expily-exp); 29 | font-size: 16px; 30 | line-height: 16px; 31 | border-top-style: hidden !important; 32 | border-right-style: hidden !important; 33 | border-left-style: hidden !important; 34 | border-bottom-style: hidden !important; 35 | background-color: transparent !important; 36 | color: var(--text-color); 37 | width: 80%; 38 | text-overflow: ellipsis; 39 | } 40 | ::-webkit-input-placeholder { 41 | /* Chrome/Opera/Safari */ 42 | font-family: var(--font-family-expily-exp); 43 | font-size: 14px; 44 | color: var(--input-placeholder-color); 45 | } 46 | ::-moz-placeholder { 47 | /* Firefox 19+ */ 48 | font-family: var(--font-family-expily-exp); 49 | font-size: 14px; 50 | color: var(--input-placeholder-color); 51 | } 52 | :-ms-input-placeholder { 53 | /* IE 10+ */ 54 | font-family: var(--font-family-expily-exp); 55 | font-size: 14px; 56 | color: var(--input-placeholder-color); 57 | } 58 | :-moz-placeholder { 59 | /* Firefox 18- */ 60 | font-family: var(--font-family-expily-exp); 61 | font-size: 14px; 62 | color: var(--input-placeholder-color); 63 | } 64 | 65 | input:focus { 66 | outline: none !important; 67 | } 68 | 69 | .search-type-wrapper { 70 | display: grid; 71 | grid-template-areas: "select"; 72 | align-items: center; 73 | 74 | &::after, 75 | select { 76 | grid-area: select; 77 | } 78 | 79 | &::after { 80 | justify-self: end; 81 | content: ""; 82 | width: 0.8em; 83 | height: 0.5em; 84 | background-color: var(--fg-main); 85 | clip-path: polygon(100% 0%, 0 0%, 50% 100%); 86 | position: relative; 87 | left: -15px; 88 | } 89 | 90 | select, 91 | select::before, 92 | select::after { 93 | // A reset of styles, including removing the default dropdown arrow 94 | appearance: none; 95 | // Additional resets for further consistency 96 | background-color: var(--button-background); 97 | color: var(--fg-main); 98 | border: none; 99 | padding: 5px 15px; 100 | margin: 0 5px 0 0; 101 | width: 90px; 102 | font-family: inherit; 103 | font-size: 14px; 104 | cursor: inherit; 105 | line-height: inherit; 106 | //outline: none; 107 | //text-align: center; 108 | border-radius: 17px; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "podcastindex", 3 | "version": "0.2.1", 4 | "user-agent-org": "Podcastindex.org/", 5 | "user-agent-app": "www", 6 | "license": "MIT", 7 | "private": true, 8 | "packageManager": "yarn@3.4.1", 9 | "dependencies": { 10 | "@hcaptcha/react-hcaptcha": "^0.3.9", 11 | "@types/dompurify": "^2.4.0", 12 | "@types/history": "^4.7.5", 13 | "@types/react": "^16.9.20", 14 | "@types/react-dom": "^16.9.5", 15 | "@types/react-redux": "^7.1.7", 16 | "@types/react-router-dom": "^5.1.3", 17 | "@types/redux": "^3.6.0", 18 | "@types/redux-actions": "^2.6.1", 19 | "@types/redux-thunk": "^2.1.0", 20 | "apexcharts": "^3.20.0", 21 | "canvas-confetti": "^1.5.1", 22 | "connected-react-router": "^6.7.0", 23 | "crypto-js": "^4.0.0", 24 | "dompurify": "^2.4.3", 25 | "dotenv": "^8.2.0", 26 | "dotenv-webpack": "^8.0.1", 27 | "ejs": "^3.1.9", 28 | "express": "^4.17.1", 29 | "history": "^4.10.1", 30 | "moment": "^2.29.0", 31 | "node-fetch": "^2.6.8", 32 | "pluralize": "^8.0.0", 33 | "podcast-index-api": "^1.1.9", 34 | "query-string": "^6.13.5", 35 | "react": "^16.12.0", 36 | "react-dom": "^16.12.0", 37 | "react-h5-audio-player": "^3.8.2", 38 | "react-infinite-scroll-component": "^6.1.0", 39 | "react-loading": "^2.0.3", 40 | "react-redux": "^7.2.0", 41 | "react-router-dom": "^5.1.2", 42 | "redux": "^4.0.5", 43 | "redux-actions": "^2.6.5", 44 | "redux-devtools-extension": "^2.13.8", 45 | "redux-thunk": "^2.3.0", 46 | "reduxsauce": "^1.1.2", 47 | "shortid": "^2.2.15", 48 | "threadcap": "^0.1.9", 49 | "typesafe-actions": "^5.1.0", 50 | "uuid": "^8.3.2", 51 | "webln": "^0.3.0" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.11.6", 55 | "@storybook/addon-actions": "^6.0.21", 56 | "@storybook/addon-essentials": "^6.0.21", 57 | "@storybook/addon-links": "^6.0.21", 58 | "@storybook/node-logger": "^6.0.21", 59 | "@storybook/preset-create-react-app": "^3.1.4", 60 | "@storybook/react": "^6.0.21", 61 | "babel-loader": "^8.1.0", 62 | "copy-webpack-plugin": "^6.3.2", 63 | "css-loader": "^3.4.2", 64 | "favicons-webpack-plugin": "^4.2.0", 65 | "file-loader": "^6.1.0", 66 | "html-loader": "^0.5.5", 67 | "html-webpack-plugin": "^4.5.0", 68 | "prettier": "^2.1.1", 69 | "react-is": "^16.13.1", 70 | "react-scripts": "3.2.0", 71 | "sass": "^1.26.10", 72 | "sass-loader": "^9.0.3", 73 | "style-loader": "^1.1.3", 74 | "ts-loader": "^6.2.1", 75 | "typescript": "^3.7.5", 76 | "url-loader": "^4.1.0", 77 | "webpack": "^4.41.6", 78 | "webpack-cli": "^3.3.11", 79 | "webpack-dev-server": "^3.10.3" 80 | }, 81 | "scripts": { 82 | "dev": "webpack-dev-server --env.file=development --mode development --devtool inline-source-map", 83 | "build": "webpack", 84 | "production": "NODE_ENV='production' && webpack --env.file=production --mode production && node server", 85 | "start": "node server", 86 | "test": "react-scripts test --env=jsdom", 87 | "eject": "react-scripts eject", 88 | "flow": "flow", 89 | "format": "prettier --write 'src/**/*.js'", 90 | "storybook": "start-storybook -p 6006 -s public", 91 | "build-storybook": "build-storybook -s public" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ui/src/components/Comments/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --thread: clamp(1rem, 8vw, 2rem); 3 | } 4 | 5 | details { 6 | margin: .5rem 0; 7 | position: relative; 8 | } 9 | 10 | details details { 11 | margin-left: calc(var(--thread) + .5rem); 12 | } 13 | 14 | details.content-warning { 15 | margin-left: 0; 16 | } 17 | 18 | summary { 19 | list-style: none; 20 | display: flex; 21 | gap: .25rem; 22 | place-items: center; 23 | font-size: .75rem; 24 | line-height: 1rem; 25 | width: 100%; 26 | } 27 | 28 | details > summary::-webkit-details-marker { 29 | display: none; 30 | } 31 | 32 | details:not([open]) > summary::before { 33 | content: ""; 34 | background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='2' stroke='currentColor' %3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M8.25 4.5l7.5 7.5-7.5 7.5' /%3E%3C/svg%3E") center no-repeat; 35 | width: 1rem; 36 | height: 1rem; 37 | opacity: .4; 38 | } 39 | 40 | details[open]:not(.content-warning) > summary::before { 41 | content: ""; 42 | position: absolute; 43 | top: calc(var(--thread) + .5rem); 44 | right: 0; 45 | bottom: 0; 46 | left: 0; 47 | width: var(--thread); 48 | display: block; 49 | color: rgba(0,0,0,.4); 50 | background: linear-gradient( 51 | to right, 52 | transparent calc(var(--thread) / 2 - 1px), 53 | currentColor calc(var(--thread) / 2 - 1px), 54 | currentColor calc(var(--thread) / 2 + 1px), 55 | transparent calc(var(--thread) / 2 + 1px) 56 | ); 57 | } 58 | 59 | .profile { 60 | display: flex; 61 | gap: .5rem; 62 | place-items: center; 63 | text-decoration: none; 64 | color: #000; 65 | } 66 | 67 | .profile-img { 68 | width: var(--thread); 69 | height: var(--thread); 70 | border-radius: var(--thread); 71 | object-fit: cover; 72 | } 73 | 74 | .user { 75 | display: flex; 76 | flex-direction: column; 77 | } 78 | 79 | .handle { 80 | display: none; 81 | } 82 | 83 | .permalink { 84 | text-align: right; 85 | text-decoration: none; 86 | color: rgba(0,0,0,.6); 87 | } 88 | 89 | .contents { 90 | padding-left: calc(var(--thread) + .5rem); 91 | font-size: 0.875rem; 92 | } 93 | 94 | .ellipsis::after { 95 | content: "…"; 96 | } 97 | 98 | .invisible { 99 | position: absolute; 100 | font-size: 0; 101 | line-height: 0; 102 | display: inline-block; 103 | width: 0; 104 | height: 0; 105 | } 106 | 107 | details.content-warning > summary { 108 | font-size: 0.875rem; 109 | } 110 | 111 | details.content-warning > summary::before { 112 | display: none; 113 | } 114 | 115 | details.content-warning > summary::after { 116 | content: "Show More"; 117 | display: inline-block; 118 | background: rgba(0,0,0,.1); 119 | border-radius: .75rem; 120 | padding: .25rem .5rem; 121 | font-size: 0.75rem; 122 | cursor: pointer; 123 | white-space: nowrap; 124 | } 125 | 126 | details.content-warning[open] > summary::after { 127 | content: "Show Less"; 128 | } 129 | 130 | @media screen and (min-width: 25rem) { 131 | .handle { 132 | display: block; 133 | } 134 | 135 | .profile { 136 | flex: 1; 137 | } 138 | 139 | .profile + span { 140 | display: none; 141 | } 142 | } 143 | 144 | * { 145 | box-sizing: border-box; 146 | } 147 | 148 | .comments-container { 149 | width: 100%; 150 | } -------------------------------------------------------------------------------- /server/assets/podlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/components/PodcastHeader/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --category-bg-color: #f3f3f3; 3 | --category-border-color: #cecece; 4 | --cover-art-border-color: lighten(#d6d6d6, 0.2); 5 | 6 | &[data-theme="dark"] { 7 | --category-bg-color: #302d2d; 8 | --category-border-color: #505050; 9 | --cover-art-border-color: var(--border-color); 10 | } 11 | } 12 | 13 | .podcast-header-row { 14 | display: flex; 15 | flex-direction: column; 16 | align-items: flex-start; 17 | flex-direction: row; 18 | } 19 | 20 | .podcast-header-cover-art { 21 | display: inline-flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | img { 25 | height: 300px; 26 | width: 300px; 27 | object-fit: contain; 28 | border-radius: 10px; 29 | border: 1px solid var(--cover-art-border-color); 30 | } 31 | } 32 | 33 | .podcast-header-info { 34 | margin-left: 20px; 35 | p { 36 | margin: 10px 0; 37 | } 38 | margin-right: 15px; 39 | address { 40 | font-size: 20px; 41 | } 42 | width: 100%; 43 | } 44 | 45 | .podcast-header-title { 46 | font-family: var(--font-family-exp); 47 | font-weight: bold; 48 | width: 100%; 49 | font-size: 45px; 50 | color: var(--fg-main); 51 | text-align: left; 52 | margin-top: 0; 53 | } 54 | 55 | .podcast-header-categories { 56 | margin-top: 10px; 57 | margin-bottom: 10px; 58 | display: inline-flex; 59 | flex-direction: row; 60 | flex-wrap: wrap; 61 | justify-content: flex-start; 62 | align-items: flex-start; 63 | &::-webkit-scrollbar { 64 | visibility: hidden; 65 | } 66 | } 67 | .podcast-header-category { 68 | background: var(--category-bg-color); 69 | border: 1px solid var(--category-border-color); 70 | border-radius: 5px; 71 | margin: 3px; 72 | 73 | padding: 4px 12px; 74 | } 75 | .no-category { 76 | opacity: 0.6; 77 | } 78 | 79 | .podcast-header-external-links { 80 | margin-top: 10px; 81 | margin-bottom: 10px; 82 | display: inline-flex; 83 | flex-direction: row; 84 | flex-wrap: wrap; 85 | justify-content: flex-start; 86 | align-items: center; 87 | &::-webkit-scrollbar { 88 | visibility: hidden; 89 | } 90 | 91 | img { 92 | margin-right: 5px; 93 | width: 35px; 94 | height: 35px; 95 | } 96 | } 97 | 98 | .podcast-header-external-links a#aLightningClaim img { 99 | padding-top:2px; 100 | width: 30px; 101 | height: 30px; 102 | } 103 | 104 | .podcast-header-description { 105 | font-family: var(--font-family); 106 | overflow: hidden; 107 | text-overflow: ellipsis; 108 | font-size: 18px; 109 | margin: 0 !important; 110 | //padding: 10px; 111 | } 112 | 113 | .podcast-header .podcast-value { 114 | padding: 10px 0; 115 | } 116 | 117 | @media only screen and (max-width: 767px) { 118 | /* phones */ 119 | .podcast-header { 120 | height: fit-content; 121 | display: flex; 122 | flex-direction: column; 123 | align-items: flex-start; 124 | padding: 10px 10px; 125 | } 126 | .podcast-header-cover-art { 127 | img { 128 | height: 150px; 129 | width: 150px; 130 | } 131 | } 132 | 133 | .podcast-header-title { 134 | font-size: 35px; 135 | } 136 | .podcast-header-description { 137 | font-size: 15px; 138 | } 139 | .podcast-header-info { 140 | p { 141 | font-size: 15px; 142 | } 143 | } 144 | .podcast-header-category { 145 | font-size: 14px; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /ui/src/components/ResultItem/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: #ffffff; 3 | --border-color: #d6d6d6; 4 | --category-bg-color: #f3f3f3; 5 | --category-border-color: #cecece; 6 | --cover-art-border-color: lighten(#d6d6d6, 0.2); 7 | 8 | &[data-theme="dark"] { 9 | --bg-color: #180e0e; 10 | --border-color: #505050; 11 | --category-bg-color: #302d2d; 12 | --category-border-color: #505050; 13 | --cover-art-border-color: var(--border-color); 14 | } 15 | } 16 | 17 | .result { 18 | background: var(--bg-color); 19 | border: 1px solid var(--border-color); 20 | border-radius: 6px; 21 | margin: 10px 20px; 22 | height: auto; 23 | padding: 5px; 24 | } 25 | 26 | .result-row { 27 | display: flex; 28 | flex-direction: column; 29 | align-items: flex-start; 30 | flex-direction: row; 31 | // height: 170px; 32 | align-items: center; 33 | } 34 | 35 | .result-cover-art { 36 | display: inline-flex; 37 | flex-direction: column; 38 | align-items: center; 39 | justify-content: center; 40 | padding: 15px 15px 15px 15px; 41 | img { 42 | height: 140px; 43 | width: 140px; 44 | object-fit: contain; 45 | border-radius: 10px; 46 | border: 1px solid var(--cover-art-border-color); 47 | } 48 | } 49 | 50 | .result-info { 51 | margin-left: 10px; 52 | p { 53 | margin: 10px 0; 54 | } 55 | margin-right: 15px; 56 | } 57 | 58 | .result-title, .result-title a { 59 | font-family: var(--font-family-exp); 60 | font-weight: bold; 61 | width: 100%; 62 | font-size: 26px; 63 | color: var(--fg-main); 64 | text-align: left; 65 | } 66 | 67 | .result-categories { 68 | display: inline-flex; 69 | flex-direction: row; 70 | margin: -3px -3px; /* grid trick, has to match box margin */ 71 | flex-wrap: wrap; 72 | justify-content: flex-start; 73 | align-items: flex-start; 74 | &::-webkit-scrollbar { 75 | visibility: hidden; 76 | } 77 | } 78 | .result-category { 79 | background: var(--category-bg-color); 80 | border: 1px solid var(--category-border-color); 81 | border-radius: 5px; 82 | margin: 3px; 83 | 84 | padding: 4px 12px; 85 | } 86 | .no-category { 87 | opacity: 0.6; 88 | } 89 | .result-description { 90 | font-family: var(--font-family); 91 | overflow: hidden; 92 | text-overflow: ellipsis; 93 | font-size: 18px; 94 | margin: 0 !important; 95 | padding: 10px; 96 | } 97 | 98 | @media only screen and (max-width: 767px) { 99 | /* phones */ 100 | .result-row { 101 | // height: 170px; 102 | } 103 | .result { 104 | height: fit-content; 105 | display: flex; 106 | flex-direction: column; 107 | align-items: flex-start; 108 | padding: 10px 10px; 109 | } 110 | .result-cover-art { 111 | img { 112 | height: 90px; 113 | width: 90px; 114 | } 115 | } 116 | 117 | .result-title { 118 | width: 100%; 119 | font-size: 22px; 120 | } 121 | .result-description { 122 | font-size: 15px; 123 | } 124 | .result-info { 125 | p { 126 | font-size: 15px; 127 | } 128 | } 129 | .result-categories { 130 | display: inline-flex; 131 | flex-direction: row; 132 | margin: -3px -3px; /* grid trick, has to match box margin */ 133 | flex-wrap: wrap; 134 | justify-content: flex-start; 135 | align-items: flex-start; 136 | &::-webkit-scrollbar { 137 | visibility: hidden; 138 | } 139 | } 140 | .result-category { 141 | font-size: 14px; 142 | border-radius: 5px; 143 | margin: 3px; 144 | padding: 4px 12px; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /ui/images/podlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/images/donation-page.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ui/src/components/ThemeButton/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Display button for toggling stream sync on/off 3 | */ 4 | import React, {ReactNode} from "react" 5 | import DarkImage from "../../../images/dark_mode.svg" 6 | import LightImage from "../../../images/light_mode.svg" 7 | 8 | import "./styles.scss" 9 | 10 | /** 11 | * Theme options 12 | */ 13 | export enum Theme { 14 | /** 15 | * Use light theme 16 | */ 17 | light = "light", 18 | /** 19 | * Use dark theme 20 | */ 21 | dark = "dark", 22 | /** 23 | * Automatically detect theme 24 | */ 25 | auto = "auto", 26 | } 27 | 28 | /** 29 | * Arguments/properties of ThemeButton 30 | */ 31 | interface ThemeButtonProps { 32 | } 33 | 34 | /** 35 | * States for ThemeButton 36 | */ 37 | interface ThemeButtonState { 38 | /** 39 | * Current theme 40 | */ 41 | theme: Theme, 42 | } 43 | 44 | export default class ThemeButton extends React.PureComponent { 45 | STORAGE_KEY = "theme" 46 | defaultState: ThemeButtonState = { 47 | theme: Theme.auto, 48 | } 49 | state: ThemeButtonState = { 50 | theme: Theme.auto, 51 | } 52 | 53 | constructor(props) { 54 | super(props) 55 | 56 | this.handleClick = this.handleClick.bind(this) 57 | } 58 | 59 | componentDidMount(): void { 60 | let {theme} = this.state 61 | const storageTheme = localStorage.getItem(this.STORAGE_KEY) 62 | let auto = true 63 | 64 | if (storageTheme) { 65 | theme = storageTheme as Theme 66 | } 67 | 68 | // check for auto theme 69 | if (theme === Theme.auto) { 70 | // detect 71 | if (window.matchMedia) { 72 | theme = window.matchMedia('(prefers-color-scheme: light)').matches ? Theme.light : Theme.dark 73 | } else { 74 | // can't detect 75 | theme = Theme.dark 76 | auto = false 77 | } 78 | } else { 79 | auto = false 80 | } 81 | 82 | this.setState( 83 | { 84 | theme: theme, 85 | }, 86 | () => { 87 | document.documentElement.setAttribute("data-theme", theme) 88 | localStorage.setItem(this.STORAGE_KEY, auto ? Theme.auto : theme) 89 | } 90 | ) 91 | } 92 | 93 | /** 94 | * Handle sync button onClick event 95 | */ 96 | private readonly handleClick = (): void => { 97 | const {theme} = this.state 98 | let newTheme = this.oppositeTheme(theme) 99 | this.setState( 100 | { 101 | theme: newTheme, 102 | }, 103 | () => { 104 | document.documentElement.setAttribute("data-theme", newTheme) 105 | localStorage.setItem(this.STORAGE_KEY, newTheme) 106 | } 107 | ) 108 | } 109 | 110 | /** 111 | * Get the opposite theme 112 | * 113 | * @param theme current theme 114 | * @return Opposite theme 115 | **/ 116 | private oppositeTheme = (theme: Theme): Theme => { 117 | if (theme == Theme.auto) 118 | return theme 119 | return theme === Theme.dark ? Theme.light : Theme.dark 120 | } 121 | 122 | render = (): ReactNode => { 123 | const {theme} = this.state 124 | 125 | const lightMode = theme === Theme.light 126 | const image = lightMode ? LightImage : DarkImage 127 | const altText = lightMode ? "Switch to Dark Mode" : "Switch to Light Mode" 128 | return ( 129 |
    130 | {altText} 137 |
    138 | ) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /_old/dev/style/style.css: -------------------------------------------------------------------------------- 1 | #divHome a { text-decoration: none; } 2 | .aPreauthLogout { float:right; } 3 | #divNavSearchResults { display:none; } 4 | img { outline:none; } 5 | .bodyspacer { 6 | padding-top: 30px; 7 | } 8 | .smallspacer { 9 | padding-top: 20px; 10 | } 11 | #divPageTitle h1 { 12 | font-size: 250%; 13 | margin-left: auto; 14 | margin-right: auto; 15 | } 16 | #imgSpinner,.imgSpinner { 17 | display:none; 18 | } 19 | #navSpinner { 20 | display:none; 21 | } 22 | .imgSpinner,.spinner { 23 | display:none; 24 | } 25 | #spnTweetWarning { 26 | display:none; 27 | font-weight:bold !important; 28 | color: #f30 !important; 29 | text-shadow:0 0 9px #777 !important; 30 | } 31 | .urlinput { 32 | width: 400px; 33 | } 34 | .hourinput { 35 | width:30px; 36 | } 37 | .dropTarget { 38 | border:dashed green 2px; 39 | } 40 | #divSubscribeUpload { 41 | display:none; 42 | } 43 | .elHidden { 44 | display:none; 45 | } 46 | #divMessageBox { 47 | position:fixed; 48 | top:0; 49 | left:0; 50 | z-index:99999; 51 | text-align:center; 52 | } 53 | #messagebox { 54 | position:relative; 55 | display:block; 56 | width:500px; 57 | padding:50px 20px; 58 | margin-left:auto; 59 | margin-right:auto; 60 | font-size:14px; 61 | text-align:center; 62 | border-radius:2px; 63 | } 64 | #messagebox #btnMessageBoxClose { 65 | position:absolute; 66 | bottom:0; 67 | right:0; 68 | } 69 | .msggood { 70 | background:#9acd32; 71 | border:1px solid #006400; 72 | font-weight:bold !important; 73 | color: #006400 !important; 74 | } 75 | .msgwarn { 76 | background:#ffd700; 77 | border:1px solid #b8860b; 78 | font-weight:bold !important; 79 | color: #f30 !important; 80 | } 81 | .msgbad { 82 | background:#cd5c5c; 83 | border:1px solid #700; 84 | font-weight:bold !important; 85 | color: #700 !important; 86 | } 87 | .table tbody tr:hover td, 88 | .table tbody tr:hover th { 89 | background-color: inherit; 90 | } 91 | 92 | /**** Transitions ****/ 93 | 94 | .transitions-enabled.masonry, 95 | .transitions-enabled.masonry .masonry-brick { 96 | -webkit-transition-duration: 0.7s; 97 | -moz-transition-duration: 0.7s; 98 | -ms-transition-duration: 0.7s; 99 | -o-transition-duration: 0.7s; 100 | transition-duration: 0.7s; 101 | } 102 | 103 | .transitions-enabled.masonry { 104 | -webkit-transition-property: height, width; 105 | -moz-transition-property: height, width; 106 | -ms-transition-property: height, width; 107 | -o-transition-property: height, width; 108 | transition-property: height, width; 109 | } 110 | 111 | .transitions-enabled.masonry .masonry-brick { 112 | -webkit-transition-property: left, right, top; 113 | -moz-transition-property: left, right, top; 114 | -ms-transition-property: left, right, top; 115 | -o-transition-property: left, right, top; 116 | transition-property: left, right, top; 117 | } 118 | 119 | 120 | /* disable transitions on container */ 121 | .transitions-enabled.infinite-scroll.masonry { 122 | -webkit-transition-property: none; 123 | -moz-transition-property: none; 124 | -ms-transition-property: none; 125 | -o-transition-property: none; 126 | transition-property: none; 127 | } 128 | 129 | .black_overlay{ 130 | position: absolute; 131 | top: 0%; 132 | left: 0%; 133 | width: 100%; 134 | height: 100%; 135 | background-color: #222; 136 | z-index:1001; 137 | -moz-opacity: 0.8; 138 | opacity:.80; 139 | filter: alpha(opacity=80); 140 | } 141 | 142 | .white_content { 143 | display: none; 144 | position: absolute; 145 | top: 25%; 146 | left: 25%; 147 | width: 50%; 148 | height: 50%; 149 | padding: 16px; 150 | border: 16px solid orange; 151 | background-color: white; 152 | z-index:1002; 153 | overflow: auto; 154 | } 155 | 156 | .center { 157 | float: none; 158 | margin-left: auto; 159 | margin-right: auto; 160 | } 161 | 162 | header { 163 | margin-bottom:20px; 164 | } --------------------------------------------------------------------------------