diff options
| author | Fuwn <[email protected]> | 2024-04-02 22:28:14 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-04-02 22:28:14 -0700 |
| commit | 9ab94f20c200f7e79297b9cbb7d654b8fba67115 (patch) | |
| tree | 5d0b649fc89c24154acd44cde5944acb812b371b /src | |
| parent | feat(layout): announcement feature (diff) | |
| download | due.moe-9ab94f20c200f7e79297b9cbb7d654b8fba67115.tar.xz due.moe-9ab94f20c200f7e79297b9cbb7d654b8fba67115.zip | |
feat(badges): custom css
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/Database/userPreferences.ts | 17 | ||||
| -rw-r--r-- | src/routes/api/preferences/+server.ts | 31 | ||||
| -rw-r--r-- | src/routes/user/[user]/+page.svelte | 17 | ||||
| -rw-r--r-- | src/routes/user/[user]/badges/+page.svelte | 495 | ||||
| -rw-r--r-- | src/styles/input.css | 6 |
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); */ } |