aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-02-17 21:50:17 -0800
committerFuwn <[email protected]>2024-02-17 21:50:17 -0800
commit4f07a27df74775e35775d34e800abfd683c5fa86 (patch)
tree2a7fb42fd15f1193c3c14a6535691730fe428565 /src
parentfix(preferences): return preferences (diff)
downloaddue.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.ts12
-rw-r--r--src/lib/FallbackBadge.svelte69
-rw-r--r--src/lib/FallbackImage.svelte3
-rw-r--r--src/routes/api/preferences/+server.ts18
-rw-r--r--src/routes/api/preferences/badges/+server.ts31
-rw-r--r--src/routes/user/[user]/+page.svelte24
-rw-r--r--src/routes/user/[user]/badges/+page.svelte454
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}