diff options
| author | Fuwn <[email protected]> | 2024-02-17 21:50:17 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-02-17 21:50:17 -0800 |
| commit | 4f07a27df74775e35775d34e800abfd683c5fa86 (patch) | |
| tree | 2a7fb42fd15f1193c3c14a6535691730fe428565 /src | |
| parent | fix(preferences): return preferences (diff) | |
| download | due.moe-4f07a27df74775e35775d34e800abfd683c5fa86.tar.xz due.moe-4f07a27df74775e35775d34e800abfd683c5fa86.zip | |
feat(badges): optionally hide missing badges
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/Database/userPreferences.ts | 12 | ||||
| -rw-r--r-- | src/lib/FallbackBadge.svelte | 69 | ||||
| -rw-r--r-- | src/lib/FallbackImage.svelte | 3 | ||||
| -rw-r--r-- | src/routes/api/preferences/+server.ts | 18 | ||||
| -rw-r--r-- | src/routes/api/preferences/badges/+server.ts | 31 | ||||
| -rw-r--r-- | src/routes/user/[user]/+page.svelte | 24 | ||||
| -rw-r--r-- | src/routes/user/[user]/badges/+page.svelte | 454 |
7 files changed, 387 insertions, 224 deletions
diff --git a/src/lib/Database/userPreferences.ts b/src/lib/Database/userPreferences.ts index ca872ea5..988ca0c0 100644 --- a/src/lib/Database/userPreferences.ts +++ b/src/lib/Database/userPreferences.ts @@ -61,3 +61,15 @@ export const toggleHololiveStreamPinning = async (userId: number, streamId: stri hide_missing_badges: userPreferences.hide_missing_badges }); }; + +export const toggleHideMissingBadges = async (userId: number) => { + const userPreferences = await getUserPreferences(userId); + + if (!userPreferences) return null; + + return await setUserPreferences(userId, { + updated_at: new Date().toISOString(), + pinned_hololive_streams: userPreferences.pinned_hololive_streams, + hide_missing_badges: !userPreferences.hide_missing_badges + }); +}; diff --git a/src/lib/FallbackBadge.svelte b/src/lib/FallbackBadge.svelte new file mode 100644 index 00000000..faa021d4 --- /dev/null +++ b/src/lib/FallbackBadge.svelte @@ -0,0 +1,69 @@ +<script lang="ts"> + import locale from '$stores/locale'; + import type { Badge } from './Database/userBadges'; + import tooltip from './Tooltip/tooltip'; + import { databaseTimeToDate } from './Utility/time'; + + export let source: string | undefined; + export let alternative: string | undefined; + export let fallback: string | undefined; + export let maxReplaceCount = 1; + export let replaceDelay = 1000; + export let error = 'https://i2.kym-cdn.com/photos/images/newsfeed/000/290/992/0aa.jpg'; + export let hideOnError = false; + export let badge: Badge; + + let replaceCount = 0; + + const delayedReplace = (event: Event, image: string | undefined) => { + if (replaceCount >= maxReplaceCount) return; + + setTimeout(() => { + (event.target as HTMLImageElement).src = image || ''; + + replaceCount += 1; + }, replaceDelay); + }; +</script> + +{#if replaceCount < maxReplaceCount} + <a + href={badge.post} + target="_blank" + id={`badge-${badge.id}`} + class="badge" + title={`${badge.time ? $locale().dateFormatter(databaseTimeToDate(badge.time)) : ''}${ + badge.description ? `\n${badge.description}` : '' + }`} + use:tooltip + > + <img + src={source} + alt={alternative} + loading="lazy" + class="badge" + on:error={(e) => delayedReplace(e, fallback)} + /> + </a> +{:else if !hideOnError} + <img src={error} alt="Not found" loading="lazy" class="badge" /> +{/if} + +<style> + .badge { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + transition: transform 0.3s ease; + box-sizing: border-box; + border-radius: 8px; + } + + .badge:hover { + transform: scale(1.05); + position: relative; + z-index: 2; + transition: transform 0.3s ease; + } +</style> diff --git a/src/lib/FallbackImage.svelte b/src/lib/FallbackImage.svelte index 4a53698d..0ea49c6c 100644 --- a/src/lib/FallbackImage.svelte +++ b/src/lib/FallbackImage.svelte @@ -5,6 +5,7 @@ export let maxReplaceCount = 1; export let replaceDelay = 1000; export let error = 'https://i2.kym-cdn.com/photos/images/newsfeed/000/290/992/0aa.jpg'; + export let hideOnError = false; let replaceCount = 0; @@ -27,7 +28,7 @@ class="badge" on:error={(e) => delayedReplace(e, fallback)} /> -{:else} +{:else if !hideOnError} <img src={error} alt="Not found" loading="lazy" class="badge" /> {/if} diff --git a/src/routes/api/preferences/+server.ts b/src/routes/api/preferences/+server.ts index a36bd236..0aaaa52e 100644 --- a/src/routes/api/preferences/+server.ts +++ b/src/routes/api/preferences/+server.ts @@ -1,4 +1,4 @@ -import { getUserPreferences } from '$lib/Database/userPreferences'; +import { getUserPreferences, toggleHideMissingBadges } from '$lib/Database/userPreferences'; export const GET = async ({ url }) => Response.json(await getUserPreferences(Number(url.searchParams.get('id') || 0)), { @@ -6,3 +6,19 @@ export const GET = async ({ url }) => 'Access-Control-Allow-Origin': 'https://due.moe' } }); + +export const PUT = async ({ url }) => { + if (url.searchParams.get('toggleHideMissingBadges') !== null) { + return Response.json(await toggleHideMissingBadges(Number(url.searchParams.get('id') || 0)), { + headers: { + 'Access-Control-Allow-Origin': 'https://due.moe' + } + }); + } + + return Response.json(await getUserPreferences(Number(url.searchParams.get('id') || 0)), { + headers: { + 'Access-Control-Allow-Origin': 'https://due.moe' + } + }); +}; diff --git a/src/routes/api/preferences/badges/+server.ts b/src/routes/api/preferences/badges/+server.ts new file mode 100644 index 00000000..24d4924b --- /dev/null +++ b/src/routes/api/preferences/badges/+server.ts @@ -0,0 +1,31 @@ +import { userIdentity } from '$lib/Data/AniList/identity'; +import { toggleHideMissingBadges } from '$lib/Database/userPreferences'; + +const unauthorised = new Response('Unauthorised', { status: 401 }); + +export const PUT = async ({ cookies }) => { + const userCookie = cookies.get('user'); + + if (!userCookie) return unauthorised; + + const user = JSON.parse(userCookie); + + return Response.json( + await toggleHideMissingBadges( + ( + await userIdentity({ + tokenType: user['token_type'], + expiresIn: user['expires_in'], + accessToken: user['access_token'], + refreshToken: user['refresh_token'] + }) + ).id + ), + { + headers: { + method: 'PUT', + 'Access-Control-Allow-Origin': 'https://due.moe' + } + } + ); +}; diff --git a/src/routes/user/[user]/+page.svelte b/src/routes/user/[user]/+page.svelte index 4103b7de..c93647f4 100644 --- a/src/routes/user/[user]/+page.svelte +++ b/src/routes/user/[user]/+page.svelte @@ -10,6 +10,7 @@ import authorisedUsers from '$lib/Data/Static/authorised.json'; import tooltip from '$lib/Tooltip/tooltip.js'; import AnimeRateLimited from '$lib/Error/AnimeRateLimited.svelte'; + import identity from '$stores/identity'; export let data; @@ -120,6 +121,29 @@ </div> {/if} </div> + + {#if userData && userData.id === $identity.id} + {#await fetch(root(`/api/preferences?id=${userData.id}`)) then rawPreferences} + {#await rawPreferences.json() then preferences} + <p /> + + <details open> + <summary>User Preferences</summary> + + <input + type="checkbox" + on:change={() => { + if (userData) + fetch(root(`/api/preferences?id=${userData.id}&toggleHideMissingBadges`), { + method: 'PUT' + }); + }} + checked={preferences.hide_missing_badges} + /> Hide missing badges from Badge Wall + </details> + {/await} + {/await} + {/if} {/if} <style> diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte index bf575161..9b74abd8 100644 --- a/src/routes/user/[user]/badges/+page.svelte +++ b/src/routes/user/[user]/badges/+page.svelte @@ -19,6 +19,7 @@ import SettingHint from '$lib/Settings/SettingHint.svelte'; import Popup from '$lib/Popup.svelte'; import FallbackImage from '$lib/FallbackImage.svelte'; + import FallbackBadge from '$lib/FallbackBadge.svelte'; // import { io } from 'socket.io-client'; export let data; @@ -401,250 +402,259 @@ <Skeleton grid={true} count={100} width="150px" height="170px" /> {:then ungroupedBadges} - <div id="badges"> - {#await awcPromise then badges} - {#await badges.text() then text} - {@const parsedBadges = awcBadgesGrouped(text)} + {#await fetch(root(`/api/preferences?id=${identity.id}`))} + <Message message="Loading user preferences ..." /> + + <Skeleton grid={true} count={100} width="150px" height="170px" /> + {:then rawPreferences} + {#await rawPreferences.json()} + <Message message="Parsing user preferences ..." /> + + <Skeleton grid={true} count={100} width="150px" height="170px" /> + {:then preferences} + <div id="badges"> + {#await awcPromise then badges} + {#await badges.text() then text} + {@const parsedBadges = awcBadgesGrouped(text)} + + {#if parsedBadges.length > 0} + {#each parsedBadges as group} + <details open> + <summary> + Anime Watching Club <span class="opaque">|</span> + {group.group} + </summary> + + <p /> + + <div class="badges"> + {#each group.badges as badge} + <a + href={badge.link} + target="_blank" + class="badge" + id={`badge-${badge.link}`} + title={badge.description} + use:tooltip + > + <FallbackImage + source={cdn(thumbnail(badge.image))} + alternative={badge.description} + fallback={thumbnail(badge.image)} + /> + </a> + {/each} + </div> + </details> + + <p /> + {/each} + {/if} + {/await} + {/await} - {#if parsedBadges.length > 0} - {#each parsedBadges as group} - <details open> - <summary> - Anime Watching Club <span class="opaque">|</span> - {group.group} - </summary> + {#if ungroupedBadges === null} + <Message message="Loading badges ..." /> - <p /> + <Skeleton grid={true} count={10} width="150px" height="170px" /> + {:else} + {@const groupedBadges = Object.entries(groupBadges(ungroupedBadges))} - <div class="badges"> - {#each group.badges as badge} - <a - href={badge.link} - target="_blank" - class="badge" - id={`badge-${badge.link}`} - title={badge.description} - use:tooltip - > - <FallbackImage - source={cdn(thumbnail(badge.image))} - alternative={badge.description} - fallback={thumbnail(badge.image)} - /> - </a> - {/each} - </div> - </details> + {#if isOwner} + <div class="card"> + <a href={root(`/user/${data.username}`)} + >{$locale().user.badges.backToProfile}</a + > + <span style="margin: 0 0.625rem;">•</span> + <button + on:click={() => { + if (editMode) selectedBadge = undefined; - <p /> - {/each} - {/if} - {/await} - {/await} + editMode = !editMode; + }} + > + {editMode + ? $locale().user.badges.editMode.disable + : $locale().user.badges.editMode.enable} + </button> + <span style="margin: 0 0.625rem;">•</span> + <button + on:click={() => { + if (importMode) selectedBadge = undefined; - {#if ungroupedBadges === null} - <Message message="Loading badges ..." /> - - <Skeleton grid={true} count={10} width="150px" height="170px" /> - {:else} - {@const groupedBadges = Object.entries(groupBadges(ungroupedBadges))} - - {#if isOwner} - <div class="card"> - <a href={root(`/user/${data.username}`)}>{$locale().user.badges.backToProfile}</a> - <span style="margin: 0 0.625rem;">•</span> - <button - on:click={() => { - if (editMode) selectedBadge = undefined; - - editMode = !editMode; - }} - > - {editMode - ? $locale().user.badges.editMode.disable - : $locale().user.badges.editMode.enable} - </button> - <span style="margin: 0 0.625rem;">•</span> - <button - on:click={() => { - if (importMode) selectedBadge = undefined; - - importMode = !importMode; - }} - > - {importMode - ? $locale().user.badges.importMode.disable - : $locale().user.badges.importMode.enable} - </button> - - {#if editMode && isOwner} - {@const groups = groupedBadges - .map((group) => group[0]) - .filter((group) => group !== 'Uncategorised')} - - <p /> - - {#if error} - <p style="color: red;">{error}</p> - {/if} + importMode = !importMode; + }} + > + {importMode + ? $locale().user.badges.importMode.disable + : $locale().user.badges.importMode.enable} + </button> - <input - type="text" - placeholder={$locale().user.badges.editMode.imageURL} - name="image_url" - minlength="1" - maxlength="1000" - size="15" - value={selectedBadge ? selectedBadge.image : ''} - /> - <input - type="text" - placeholder={$locale().user.badges.editMode.activityURL} - name="activity_url" - minlength="1" - maxlength="1000" - size="15" - value={selectedBadge - ? selectedBadge.post === '#' - ? '' - : selectedBadge.post - : ''} - /> - <input - type="text" - placeholder={$locale().user.badges.editMode.description} - name="description" - minlength="1" - maxlength="1000" - size="15" - value={selectedBadge ? selectedBadge.description : ''} - /> - <Dropdown - items={groups.map((group) => ({ - name: group, - url: '#', - onClick: () => { - const category = document.querySelector('input[name="category"]'); - - if (category instanceof HTMLInputElement) category.value = group; - } - }))} - header={false} - center={false} - > - <span slot="title"> + {#if editMode && isOwner} + {@const groups = groupedBadges + .map((group) => group[0]) + .filter((group) => group !== 'Uncategorised')} + + <p /> + + {#if error} + <p style="color: red;">{error}</p> + {/if} + + <input + type="text" + placeholder={$locale().user.badges.editMode.imageURL} + name="image_url" + minlength="1" + maxlength="1000" + size="15" + value={selectedBadge ? selectedBadge.image : ''} + /> <input type="text" - placeholder={$locale().user.badges.editMode.category} - name="category" + placeholder={$locale().user.badges.editMode.activityURL} + name="activity_url" minlength="1" maxlength="1000" size="15" value={selectedBadge - ? selectedBadge.category === 'Uncategorised' + ? selectedBadge.post === '#' ? '' - : selectedBadge.category + : selectedBadge.post : ''} - list="categories" /> - </span> - </Dropdown> - <button class="button-lined" on:click={submitBadge} - >{selectedBadge - ? $locale().user.badges.editMode.update - : $locale().user.badges.editMode.add}</button - > - {#if selectedBadge} - {$locale().user.badges.editMode.or} - <button - class="button-lined" - on:click={() => { - if (selectedBadge) removeBadge(selectedBadge); - }}>{$locale().user.badges.editMode.delete}</button - > - {/if} - <span style="float: right;"> - <input - type="datetime-local" - value={selectedBadge && selectedBadge.time - ? dateToInputTime(databaseTimeToDate(selectedBadge.time)) - : ''} - /> - <small>Must be full date and time, defaults to now if any fields empty</small> - </span> - {/if} - </div> - {/if} - - <p /> - - {#if ungroupedBadges.length === 0} - <div class="card"> - No due.moe registered badges found for this user. <a - href={'#'} - on:click={(e) => e.preventDefault()} - title="This alert does not include AWC badges." - use:tooltip>?</a - > - </div> - {/if} - - {#each groupedBadges as [category, badges]} - <details open> - <summary>{category}</summary> - - <p /> - - <div class="badges"> - {#each badges as badge} - {#if editMode} - <a - href={`#`} - on:click={() => (selectedBadge = badge)} - id={`badge-${badge.id}`} - title={`${ - badge.time - ? $locale().dateFormatter(databaseTimeToDate(badge.time)) - : '' - }${badge.description ? `\n${badge.description}` : ''}`} - use:tooltip + <input + type="text" + placeholder={$locale().user.badges.editMode.description} + name="description" + minlength="1" + maxlength="1000" + size="15" + value={selectedBadge ? selectedBadge.description : ''} + /> + <Dropdown + items={groups.map((group) => ({ + name: group, + url: '#', + onClick: () => { + const category = document.querySelector('input[name="category"]'); + + if (category instanceof HTMLInputElement) category.value = group; + } + }))} + header={false} + center={false} > - <FallbackImage - source={cdn(thumbnail(badge.image))} - alternative={badge.description} - fallback={thumbnail(badge.image)} - /> - </a> - {:else} - <a - href={badge.post} - target="_blank" - id={`badge-${badge.id}`} - class="badge" - title={`${ - badge.time - ? $locale().dateFormatter(databaseTimeToDate(badge.time)) - : '' - }${badge.description ? `\n${badge.description}` : ''}`} - use:tooltip + <span slot="title"> + <input + type="text" + placeholder={$locale().user.badges.editMode.category} + name="category" + minlength="1" + maxlength="1000" + size="15" + value={selectedBadge + ? selectedBadge.category === 'Uncategorised' + ? '' + : selectedBadge.category + : ''} + list="categories" + /> + </span> + </Dropdown> + <button class="button-lined" on:click={submitBadge} + >{selectedBadge + ? $locale().user.badges.editMode.update + : $locale().user.badges.editMode.add}</button > - <FallbackImage - source={cdn(thumbnail(badge.image))} - alternative={badge.description} - fallback={thumbnail(badge.image)} + {#if selectedBadge} + {$locale().user.badges.editMode.or} + <button + class="button-lined" + on:click={() => { + if (selectedBadge) removeBadge(selectedBadge); + }}>{$locale().user.badges.editMode.delete}</button + > + {/if} + <span style="float: right;"> + <input + type="datetime-local" + value={selectedBadge && selectedBadge.time + ? dateToInputTime(databaseTimeToDate(selectedBadge.time)) + : ''} /> - </a> + <small + >Must be full date and time, defaults to now if any fields empty</small + > + </span> {/if} - {/each} - </div> - </details> + </div> + {/if} - {#if groupedBadges[groupedBadges.length - 1][0] !== category} <p /> + + {#if ungroupedBadges.length === 0} + <div class="card"> + No due.moe registered badges found for this user. <a + href={'#'} + on:click={(e) => e.preventDefault()} + title="This alert does not include AWC badges." + use:tooltip>?</a + > + </div> + {/if} + + {#each groupedBadges as [category, badges]} + <details open> + <summary>{category}</summary> + + <p /> + + <div class="badges"> + {#each badges as badge} + {#if editMode} + <a + href={`#`} + on:click={() => (selectedBadge = badge)} + id={`badge-${badge.id}`} + title={`${ + badge.time + ? $locale().dateFormatter(databaseTimeToDate(badge.time)) + : '' + }${badge.description ? `\n${badge.description}` : ''}`} + use:tooltip + > + <FallbackImage + source={cdn(thumbnail(badge.image))} + alternative={badge.description} + fallback={thumbnail(badge.image)} + /> + </a> + {:else} + <FallbackBadge + {badge} + source={cdn(thumbnail(badge.image))} + alternative={badge.description} + fallback={thumbnail(badge.image)} + hideOnError={preferences.hide_missing_badges} + /> + {/if} + {/each} + </div> + </details> + + {#if groupedBadges[groupedBadges.length - 1][0] !== category} + <p /> + {/if} + {/each} {/if} - {/each} - {/if} - </div> + </div> + {:catch} + <Popup fullscreen locked>Could not parse badges</Popup> + {/await} + {:catch} + <Popup fullscreen locked>Could not fetch badges</Popup> + {/await} {:catch} <Popup fullscreen locked>Could not parse badges</Popup> {/await} |