-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(page): add landing page and proxies * feat(page): improve search
- Loading branch information
1 parent
21c54e4
commit 6c8ae28
Showing
15 changed files
with
468 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
<template> | ||
<div class="max-w-[800px] flex items-center flex-col"> | ||
<Search :query="query" @search="search" :loading="loading" class="mb-10" /> | ||
<Transition name="slide-fade"> | ||
<ul v-if="results.length > 0"> | ||
<Item | ||
v-for="podcast in results" | ||
:title="podcast.title" | ||
:description="podcast.description" | ||
:feed="podcast.feed" | ||
:image="podcast.image" | ||
/> | ||
</ul> | ||
</Transition> | ||
<Transition name="slide-fade"> | ||
<div | ||
v-if=" | ||
results.length === 0 && | ||
query.length > 0 && | ||
loading === false && | ||
feedError === false && | ||
searchError === false | ||
" | ||
class="border p-4 rounded border-[rgb(228,70,59)]" | ||
> | ||
Podcast not found? Maybe it's registered at | ||
<a class="text-[rgb(56,126,25)]" href="https://fyyd.de/add-feed">fyyd</a> yet? 😊 | ||
</div> | ||
</Transition> | ||
<Transition name="slide-fade"> | ||
<div | ||
v-if="(searchError || feedError) && !loading" | ||
class="border p-4 rounded border-[rgb(228,70,59)]" | ||
> | ||
<span v-if="feedError">Invalid feed, check the url or search for a Podcast. 😓</span> | ||
<span v-if="searchError" | ||
>Search failed, maybe something is stuck at | ||
<a class="text-[rgb(56,126,25)]" href="https://fyyd.de">fyyd</a> 🤔</span | ||
> | ||
</div> | ||
</Transition> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { ref, watch } from 'vue'; | ||
import { get } from 'lodash-es'; | ||
import Search from './components/Search.vue'; | ||
import Item from './components/Item.vue'; | ||
import { debounceAsync } from '../../lib/debounce-async'; | ||
const query = ref(''); | ||
const loading = ref(false); | ||
const searchError = ref(false); | ||
const feedError = ref(false); | ||
const search = (search: string) => { | ||
query.value = search; | ||
}; | ||
interface ListItem { | ||
title: string; | ||
feed: string; | ||
image: string | null; | ||
description: string | null; | ||
author: string | null; | ||
} | ||
const results = ref<ListItem[]>([]); | ||
const isValidUrl = (url: string) => { | ||
try { | ||
new URL(url); | ||
return true; | ||
} catch (_err) { | ||
return false; | ||
} | ||
}; | ||
const queryFeed = async (url: string, signal: AbortSignal): Promise<ListItem[]> => { | ||
try { | ||
const feed = await fetch(`/api/feed?url=${url}`, { signal }).then((res) => res.json()); | ||
return [ | ||
{ | ||
title: get(feed, ['show', 'title']), | ||
feed: url, | ||
description: get(feed, ['show', 'summary'], null), | ||
image: get(feed, ['show', 'poster'], null), | ||
author: get(feed, ['author', 'owner'], null) | ||
} | ||
]; | ||
} catch (err: any) { | ||
if (err.name !== 'AbortError') { | ||
return []; | ||
} | ||
throw err; | ||
} | ||
}; | ||
const queryFyyd = async (query: string, signal: AbortSignal): Promise<ListItem[]> => { | ||
try { | ||
const data = await fetch(`https://api.fyyd.de/0.2/search/podcast?title=${query}`, { | ||
signal | ||
}).then((res) => res.json()); | ||
return get(data, ['data'], []) | ||
.map((item: any) => ({ | ||
title: get(item, 'title', null) as string | null, | ||
image: get(item, 'imgURL', null) as string | null, | ||
feed: get(item, 'xmlURL', null) as string | null, | ||
description: get(item, 'description', null) as string | null, | ||
author: get(item, 'author', null) as string | null | ||
})) | ||
.filter(({ title, feed }: { title: string; feed: string }) => title && feed); | ||
} catch (err: any) { | ||
if (err.name !== 'AbortError') { | ||
return []; | ||
} | ||
throw err; | ||
} | ||
}; | ||
const handleInput = debounceAsync(async (query: string, signal: AbortSignal) => { | ||
searchError.value = false; | ||
feedError.value = false; | ||
if (query.length === 0) { | ||
return; | ||
} | ||
if (isValidUrl(query)) { | ||
[results.value, feedError.value] = await queryFeed(query, signal) | ||
.then((results): [ListItem[], boolean] => [results, false]) | ||
.catch((err: any) => [[], err.name !== 'AbortError']); | ||
} else { | ||
[results.value, searchError.value] = await queryFyyd(query, signal) | ||
.then((results): [ListItem[], boolean] => [results, false]) | ||
.catch((err: any) => [[], err.name !== 'AbortError']); | ||
} | ||
}, 300); | ||
const controller: AbortController | null = new AbortController(); | ||
let queryRunning = false; | ||
watch(query, async (value: string) => { | ||
if (queryRunning) { | ||
// controller.abort(); | ||
} | ||
loading.value = true; | ||
queryRunning = true; | ||
await handleInput(value, controller.signal) | ||
queryRunning = false; | ||
loading.value = false; | ||
}); | ||
</script> | ||
|
||
<style> | ||
.slide-fade-enter-active { | ||
transition: all 0.3s ease-out; | ||
} | ||
.slide-fade-leave-active { | ||
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1); | ||
} | ||
.slide-fade-enter-from, | ||
.slide-fade-leave-to { | ||
transform: translateY(20px); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<template> | ||
<svg viewBox="0 0 2515 1098"> | ||
<path | ||
d="m11 547c0-301 245-546 546-546s546 245 546 546c0 301-245 546-546 546s-546-245-546-546zm446 355c18 18 46 18 64 0l323-323c18-18 18-46 0-64l-323-323c-18-18-46-18-64 0l-73 73c-18 18-18 46 0 64l218 218-218 218c-18 18-18 46 0 64l73 73z" | ||
fill="#008000" | ||
></path> | ||
<path | ||
d="m1339 260c-13 0-23 3-29 9-7 6-10 17-10 34v26h75v90h-75v344h-119v-467c0-78 37-117 110-117h84v82h-36z" | ||
></path> | ||
<path | ||
d="m1629 328h119v467c0 78-37 117-110 117h-89c-74 0-110-39-110-117v-14h116v29c0 5 2 9 5 12 3 4 7 5 12 5h41c5 0 9-2 12-5 3-4 5-8 5-12v-88c-18 14-42 21-73 21h-19c-74 0-110-39-110-117v-297h119v311c0 5 2 9 5 12 3 4 7 5 12 5h49c5 0 9-2 12-5 3-4 5-8 5-12v-311z" | ||
></path> | ||
<path | ||
d="m2018 328h119v467c0 78-37 117-110 117h-89c-74 0-110-39-110-117v-14h116v29c0 5 2 9 5 12 3 4 7 5 12 5h41c5 0 9-2 12-5 3-4 5-8 5-12v-88c-18 14-42 21-73 21h-19c-74 0-110-39-110-117v-297h119v311c0 5 2 9 5 12 3 4 7 5 12 5h49c5 0 9-2 12-5 3-4 5-8 5-12v-311z" | ||
></path> | ||
<path | ||
d="m2311 319c30 0 55 7 73 21v-154h119v575h-101v-32c-18 27-49 41-92 41h-19c-74 0-110-39-110-117v-217c0-78 37-117 110-117h19zm73 349v-245c0-5-2-9-5-12-3-4-7-5-12-5h-49c-5 0-9 2-12 5-3 4-5 8-5 12v245c0 5 2 9 5 12 3 4 7 5 12 5h49c5 0 9-2 12-5 3-4 5-8 5-12z" | ||
></path> | ||
</svg> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<template> | ||
<li class="relative flex justify-between gap-x-6 py-2 podcast-list-item"> | ||
<a class="flex min-w-0 gap-x-4 w-full hover:bg-primary-200 p-2 rounded" :href="`/feed/${feed}`"> | ||
<podcast-cover class="size-12 flex-none rounded-lg bg-gray-50 mt-1 overflow-hidden shadow-md" :url="image || ''" /> | ||
<div class="min-w-0 flex-auto w-full"> | ||
<p class="text-sm/6 font-semibold text-gray-900"> | ||
<span class="absolute inset-x-0 -top-px bottom-0" /> | ||
{{ title }} | ||
</p> | ||
<p class="mt-1 flex text-xs/5 text-gray-500"> | ||
{{ description }} | ||
</p> | ||
</div> | ||
<div class="flex shrink-0 items-center gap-x-4"> | ||
<ChevronRightIcon class="size-5 flex-none text-gray-400" aria-hidden="true" /> | ||
</div> | ||
</a> | ||
</li> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { ChevronRightIcon } from '@heroicons/vue/20/solid'; | ||
import { Image as PodcastCover } from "@podlove/components" | ||
defineProps<{ title: string; image: string | null; feed: string; description: string | null }>(); | ||
</script> | ||
|
||
<style> | ||
.podcast-list-item { | ||
--podlove-component--image--background: rgba(29, 86, 240, 0.5); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<template> | ||
<div> | ||
<div class="mt-2 grid grid-cols-1 podcast-search"> | ||
<div | ||
class="flex items-center rounded-md bg-white px-3 outline outline-1 -outline-offset-1 outline-gray-300 focus-within:outline focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-complementary-600 w-128" | ||
> | ||
<div class="shrink-0 select-none text-base text-gray-500 flex items-center w-6"> | ||
<LoadingIcon | ||
v-if="loading" | ||
class="pointer-events-none col-start-1 row-start-1 size-5" | ||
aria-hidden="true" | ||
/> | ||
<MagnifyingGlassIcon | ||
v-else | ||
class="pointer-events-none col-start-1 row-start-1 size-5 text-gray-400" | ||
aria-hidden="true" | ||
/> | ||
</div> | ||
<input | ||
type="text" | ||
class="block min-w-0 w-full grow pt-1 pb-1.5 pl-1.5 pr-1.5 text-gray-900 placeholder:text-gray-400 focus:outline focus:outline-0 text-lg font-extralight" | ||
placeholder="Search for a Podcast or enter a Feed" | ||
:value="query" | ||
@input="input" | ||
@focusin="showPoweredBy = true" | ||
/> | ||
</div> | ||
</div> | ||
<div | ||
class="w-full flex justify-end p-2 transition-opacity opacity-0" | ||
:class="{ 'opacity-100': showPoweredBy }" | ||
> | ||
<span class="text-xs text-gray-400 mr-1">Search powered by</span | ||
><a href="https://fyyd.de/"><Fyyd class="w-11" /></a> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { MagnifyingGlassIcon } from '@heroicons/vue/16/solid'; | ||
import { get } from 'lodash-es'; | ||
import { LoadingIcon } from '@podlove/components'; | ||
import { ref } from 'vue'; | ||
import Fyyd from './Fyyd.vue'; | ||
defineProps<{ query: string | null; loading: boolean; }>(); | ||
const showPoweredBy = ref(false); | ||
const emits = defineEmits(['search']); | ||
const input = (event: Event) => { | ||
const value = get(event, ['target', 'value'], ''); | ||
emits('search', value); | ||
}; | ||
</script> | ||
|
||
<style> | ||
.podcast-search { | ||
--podlove-component--icon--color: rgba(var(--gray-color-400), 1); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
|
||
export function debounceAsync<T, Callback extends (...args: any[]) => Promise<T>>( | ||
callback: Callback, | ||
wait: number, | ||
): (...args: Parameters<Callback>) => Promise<T> { | ||
let timeoutId: number | null = null; | ||
|
||
return (...args: any[]) => { | ||
if (timeoutId) { | ||
clearTimeout(timeoutId); | ||
} | ||
|
||
return new Promise<T>((resolve) => { | ||
const timeoutPromise = new Promise<void>((resolve) => { | ||
timeoutId = setTimeout(resolve, wait) as unknown as number; | ||
}); | ||
timeoutPromise.then(async () => { | ||
resolve(await callback(...args)); | ||
}); | ||
}); | ||
}; | ||
} |
Oops, something went wrong.