aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-04-02 22:28:14 -0700
committerFuwn <[email protected]>2024-04-02 22:28:14 -0700
commit9ab94f20c200f7e79297b9cbb7d654b8fba67115 (patch)
tree5d0b649fc89c24154acd44cde5944acb812b371b /src
parentfeat(layout): announcement feature (diff)
downloaddue.moe-9ab94f20c200f7e79297b9cbb7d654b8fba67115.tar.xz
due.moe-9ab94f20c200f7e79297b9cbb7d654b8fba67115.zip
feat(badges): custom css
Diffstat (limited to 'src')
-rw-r--r--src/lib/Database/userPreferences.ts17
-rw-r--r--src/routes/api/preferences/+server.ts31
-rw-r--r--src/routes/user/[user]/+page.svelte17
-rw-r--r--src/routes/user/[user]/badges/+page.svelte495
-rw-r--r--src/styles/input.css6
5 files changed, 319 insertions, 247 deletions
diff --git a/src/lib/Database/userPreferences.ts b/src/lib/Database/userPreferences.ts
index aed74e16..b2cc82f2 100644
--- a/src/lib/Database/userPreferences.ts
+++ b/src/lib/Database/userPreferences.ts
@@ -7,12 +7,14 @@ export interface UserPreferences {
pinned_hololive_streams: string[];
hide_missing_badges: boolean;
biography: string | null;
+ badge_wall_css: string;
}
interface NewUserPreferences {
updated_at?: string;
pinned_hololive_streams?: string[];
hide_missing_badges?: boolean;
+ badge_wall_css?: string;
}
export const getUserPreferences = async (userId: number) => {
@@ -35,7 +37,9 @@ export const setUserPreferences = async (userId: number, preferences: NewUserPre
preferences.pinned_hololive_streams ||
(userPreferences ? userPreferences.pinned_hololive_streams : []),
hide_missing_badges: preferences.hide_missing_badges || false,
- biography: userPreferences ? userPreferences.biography : null
+ biography: userPreferences ? userPreferences.biography : null,
+ badge_wall_css:
+ preferences.badge_wall_css || (userPreferences ? userPreferences.badge_wall_css : '')
},
{ onConflict: 'user_id' }
)
@@ -73,3 +77,14 @@ export const toggleHideMissingBadges = async (userId: number) => {
hide_missing_badges: userPreferences ? !userPreferences.hide_missing_badges : false
});
};
+
+export const setCSS = async (userId: number, css: string) => {
+ const userPreferences = await getUserPreferences(userId);
+
+ return await setUserPreferences(userId, {
+ updated_at: new Date().toISOString(),
+ pinned_hololive_streams: userPreferences ? userPreferences.pinned_hololive_streams : [],
+ hide_missing_badges: userPreferences ? userPreferences.hide_missing_badges : false,
+ badge_wall_css: css
+ });
+};
diff --git a/src/routes/api/preferences/+server.ts b/src/routes/api/preferences/+server.ts
index c69b4096..2d51c87a 100644
--- a/src/routes/api/preferences/+server.ts
+++ b/src/routes/api/preferences/+server.ts
@@ -1,4 +1,7 @@
-import { getUserPreferences, toggleHideMissingBadges } from '$lib/Database/userPreferences';
+import { userIdentity } from '$lib/Data/AniList/identity';
+import { getUserPreferences, toggleHideMissingBadges, setCSS } from '$lib/Database/userPreferences';
+
+const unauthorised = new Response('Unauthorised', { status: 401 });
export const GET = async ({ url }) => {
const preferences = await getUserPreferences(Number(url.searchParams.get('id') || 0));
@@ -10,9 +13,31 @@ export const GET = async ({ url }) => {
});
};
-export const PUT = async ({ url }) => {
+export const PUT = async ({ url, cookies, request }) => {
+ const userCookie = cookies.get('user');
+
+ if (!userCookie) return unauthorised;
+
+ const user = JSON.parse(userCookie);
+ const userId = (
+ await userIdentity({
+ tokenType: user['token_type'],
+ expiresIn: user['expires_in'],
+ accessToken: user['access_token'],
+ refreshToken: user['refresh_token']
+ })
+ ).id;
+
if (url.searchParams.get('toggleHideMissingBadges') !== null) {
- return Response.json(await toggleHideMissingBadges(Number(url.searchParams.get('id') || 0)), {
+ return Response.json(await toggleHideMissingBadges(userId), {
+ headers: {
+ 'Access-Control-Allow-Origin': 'https://due.moe'
+ }
+ });
+ }
+
+ if (url.searchParams.get('badgeWallCSS') !== null) {
+ return Response.json(await setCSS(userId, await request.text()), {
headers: {
'Access-Control-Allow-Origin': 'https://due.moe'
}
diff --git a/src/routes/user/[user]/+page.svelte b/src/routes/user/[user]/+page.svelte
index d725ad78..28e5fa7d 100644
--- a/src/routes/user/[user]/+page.svelte
+++ b/src/routes/user/[user]/+page.svelte
@@ -58,6 +58,9 @@
.then((JSONpreferences) => (preferences = JSONpreferences));
}
+ const getBadgeWallCSS = () =>
+ (document.getElementById('badgeWallCSS') as HTMLTextAreaElement).value;
+
// 8.5827814569536423841e0
</script>
@@ -198,6 +201,20 @@
/>
{$locale().user.preferences.hideMissingBadges.title}
<SettingHint lineBreak>{$locale().user.preferences.hideMissingBadges.hint}</SettingHint>
+
+ <p />
+
+ Badge Wall Custom CSS
+ <button
+ on:click={() => {
+ if (userData)
+ fetch(root(`/api/preferences?id=${userData.id}&badgeWallCSS`), {
+ method: 'PUT',
+ body: getBadgeWallCSS()
+ });
+ }}>Save</button
+ >
+ <textarea value={preferences.badge_wall_css} rows="10" cols="100" id="badgeWallCSS" />
</details>
{/if}
{/if}
diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte
index 8c9e5ebd..78b4cf3e 100644
--- a/src/routes/user/[user]/badges/+page.svelte
+++ b/src/routes/user/[user]/badges/+page.svelte
@@ -3,7 +3,7 @@
import { user, type User } from '$lib/Data/AniList/user';
import type { Badge } from '$lib/Database/userBadges';
// import { domToBlob } from 'modern-screenshot';
- import { onMount } from 'svelte';
+ import { onDestroy, onMount } from 'svelte';
import HeadTitle from '$lib/Home/HeadTitle.svelte';
import { databaseTimeToDate, dateToInputTime, inputTimeToDatabaseTime } from '$lib/Utility/time';
import root from '$lib/Utility/root.js';
@@ -21,6 +21,8 @@
import FallbackImage from '$lib/FallbackImage.svelte';
import FallbackBadge from '$lib/FallbackBadge.svelte';
import { page } from '$app/stores';
+ import type { UserPreferences } from '$lib/Database/userPreferences.js';
+ import { browser } from '$app/environment';
// import { io } from 'socket.io-client';
export let data;
@@ -51,6 +53,7 @@
let importReplies = false;
let badger: Partial<User>;
let migrateMode = false;
+ let preferences: UserPreferences;
$: categoryFilter = new URLSearchParams($page.url.searchParams).get('category');
@@ -87,6 +90,27 @@
badgesPromise = fetch(root(`/api/badges?id=${badger.id}`));
awcPromise = fetch(proxy(`https://awc.moe/challenger/${badger.name}`));
+ preferences = await (await fetch(root(`/api/preferences?id=${badger.id}`))).json();
+
+ if (preferences.badge_wall_css) {
+ const sanitise = (css: string) =>
+ css
+ .replace(/\/\*[\s\S]*?\*\//g, '')
+ .replace(/<\/?[^>]+(>|$)/g, '')
+ .replace(
+ /(expression|javascript|vbscript|onerror|onload|onclick|onmouseover|onmouseout|onmouseup|onmousedown|onkeydown|onkeyup|onkeypress|onblur|onfocus|onsubmit|onreset|onselect|onchange|ondblclick):/gi,
+ ''
+ )
+ .replace(/(behaviour|behavior|moz-binding|content):/gi, '')
+ .replace(/\s+/g, ' ')
+ .trim();
+ const style = document.createElement('style');
+
+ style.dataset.badgeWall = 'true';
+ style.innerHTML = sanitise(preferences.badge_wall_css);
+
+ document.head.appendChild(style);
+ }
if (data.user && !isId) {
currentUserIdentity = userIdentity(data.user);
@@ -103,6 +127,13 @@
}
});
+ onDestroy(() => {
+ if (browser)
+ Array.from(document.head.querySelectorAll('style')).forEach((style) => {
+ if (style.dataset.badgeWall) style.remove();
+ });
+ });
+
// const fallback = (event: Event, image: string | undefined) =>
// setTimeout(() => ((event.target as HTMLImageElement).src = image || ''), 1000);
@@ -417,270 +448,252 @@
<Skeleton grid={true} count={100} width="150px" height="170px" />
{:then badgesResponse}
{#if badgesResponse}
- {#await badgesResponse.json()}
+ {#await badgesResponse.clone().json()}
<Message message="Parsing badges ..." />
<Skeleton grid={true} count={100} width="150px" height="170px" />
{:then ungroupedBadges}
- {#await fetch(root(`/api/preferences?id=${badger.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={categoryFilter ? false : true}>
- <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 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">
- <button
- on:click={() => {
- if (editMode) selectedBadge = undefined;
+ <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={categoryFilter ? false : true}>
+ <summary>
+ Anime Watching Club <span class="opaque">|</span>
+ {group.group}
+ </summary>
- 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>
- <span style="margin: 0 0.625rem;">•</span>
- <button
- on:click={() => {
- if (migrateMode) selectedBadge = undefined;
-
- migrateMode = !migrateMode;
- }}
- >
- Migrate Category
- </button>
+ <p />
- {#if editMode && isOwner}
- {@const groups = groupedBadges
- .map((group) => group[0])
- .filter((group) => group !== 'Uncategorised')}
+ <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 />
+ <p />
+ {/each}
+ {/if}
+ {/await}
+ {/await}
- {#if error}
- <p style="color: red;">{error}</p>
- {/if}
+ {#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">
+ <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>
+ <span style="margin: 0 0.625rem;">•</span>
+ <button
+ on:click={() => {
+ if (migrateMode) selectedBadge = undefined;
+
+ migrateMode = !migrateMode;
+ }}
+ >
+ Migrate Category
+ </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}
+ <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">
<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"
+ placeholder={$locale().user.badges.editMode.category}
+ name="category"
minlength="1"
maxlength="1000"
size="15"
value={selectedBadge
- ? selectedBadge.post === '#'
+ ? selectedBadge.category === 'Uncategorised'
? ''
- : selectedBadge.post
+ : selectedBadge.category
: ''}
+ list="categories"
/>
- <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">
- <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
- >
- {#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
+ </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
>
- </div>
+ {/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={categoryFilter ? categoryFilter === category : true}>
+ <summary>{category}</summary>
- {#each groupedBadges as [category, badges]}
- <details open={categoryFilter ? categoryFilter === category : true}>
- <summary>{category}</summary>
-
- <p />
+ <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>
+ <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 groupedBadges[groupedBadges.length - 1][0] !== category}
+ <p />
{/if}
- </div>
- {:catch}
- <Popup fullscreen locked>Could not parse badges</Popup>
- {/await}
- {:catch}
- <Popup fullscreen locked>Could not fetch badges</Popup>
- {/await}
+ {/each}
+ {/if}
+ </div>
{:catch}
<Popup fullscreen locked>Could not parse badges</Popup>
{/await}
diff --git a/src/styles/input.css b/src/styles/input.css
index a18cce32..df6603c2 100644
--- a/src/styles/input.css
+++ b/src/styles/input.css
@@ -1,5 +1,6 @@
input,
-select {
+select,
+textarea {
cursor: pointer;
position: relative;
background-color: var(--base01);
@@ -18,7 +19,8 @@ select {
@media (prefers-color-scheme: dark) {
input,
- select {
+ select,
+ textarea {
background-color: var(--base07);
/* color: var(--base05); */
}