diff options
| author | Fuwn <[email protected]> | 2024-10-06 01:41:44 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-10-06 01:41:44 -0700 |
| commit | a84d9c9f47c7cd1b345d0283bef9f211a9727893 (patch) | |
| tree | 554827840b67e8a0068e707fad973a8780f47957 | |
| parent | feat(graphql): add subtitles (diff) | |
| download | due.moe-a84d9c9f47c7cd1b345d0283bef9f211a9727893.tar.xz due.moe-a84d9c9f47c7cd1b345d0283bef9f211a9727893.zip | |
feat(badges): move badge operations to graphql
| -rw-r--r-- | src/graphql/user/resolvers.ts | 125 | ||||
| -rw-r--r-- | src/graphql/user/schema.graphql | 19 | ||||
| -rw-r--r-- | src/lib/Database/SB/User/badges.ts | 148 | ||||
| -rw-r--r-- | src/routes/api/badges/+server.ts | 257 | ||||
| -rw-r--r-- | src/routes/user/[user]/+page.svelte | 2 | ||||
| -rw-r--r-- | src/routes/user/[user]/badges/+page.gql | 17 | ||||
| -rw-r--r-- | src/routes/user/[user]/badges/+page.server.ts | 5 | ||||
| -rw-r--r-- | src/routes/user/[user]/badges/+page.svelte | 853 | ||||
| -rw-r--r-- | src/routes/user/[user]/badges/+page.ts | 20 |
9 files changed, 831 insertions, 615 deletions
diff --git a/src/graphql/user/resolvers.ts b/src/graphql/user/resolvers.ts index af71d384..1e5b9ec0 100644 --- a/src/graphql/user/resolvers.ts +++ b/src/graphql/user/resolvers.ts @@ -1,14 +1,121 @@ -import { getUserBadges } from '$lib/Database/SB/User/badges'; +import { userIdentity, type UserIdentity } from '$lib/Data/AniList/identity'; +import { + addUserBadge, + getUserBadges, + removeAllUserBadges, + removeUserBadge, + setShadowHidden, + setShadowHiddenBadge, + updateUserBadge +} from '$lib/Database/SB/User/badges'; import type { WithIndex } from '../$types'; import type { Resolvers, Badge } from './$types'; +import authorisedJson from '$lib/Data/Static/authorised.json'; +import type { RequestEvent } from '@sveltejs/kit'; + +type Context = RequestEvent<Partial<Record<string, string>>, string | null>; + +const auth = async (context: Context) => { + const userCookie = context.cookies.get('user'); + + if (!userCookie) return Error('Unauthorised'); + + const user = JSON.parse(userCookie); + + return await userIdentity({ + tokenType: user['token_type'], + expiresIn: user['expires_in'], + accessToken: user['access_token'], + refreshToken: user['refresh_token'] + }); +}; + +const authenticatedOperation = async ( + context: Context, + operation: (identity: UserIdentity, authorised: boolean) => void +) => { + const identity = await auth(context); + + if (identity instanceof Error) return []; + + const authorised = authorisedJson.includes(identity.id); + + operation(identity, authorised); + + return await getUserBadges(identity.id); +}; export const resolvers: WithIndex<Resolvers> = { - Query: { - User: async (_, args /* , _context */) => { - return { - id: args.id, - badges: (await getUserBadges(args.id)) as Badge[] - }; - } - } + Query: { + User: async (_, args) => { + return { + id: args.id, + badges: (await getUserBadges(args.id)) as Badge[] + }; + } + }, + Mutation: { + shadowHideBadges: async (_, args, context) => + await authenticatedOperation( + context, + async (_, authorised) => await setShadowHidden(args.userId, authorised) + ), + shadowHideBadge: async (_, args, context) => + await authenticatedOperation( + context, + async () => + await setShadowHiddenBadge(args.id, args.state == null ? true : args.state) + ), + hideBadge: async (_, args, context) => + await authenticatedOperation(context, async (identity) => { + const allBadges = await getUserBadges(identity.id); + const category = args.category || ''; + + await Promise.all( + allBadges + .filter((badge) => badge.category === category) + .map(async (badge) => { + await updateUserBadge(identity.id, badge.id as number, { + ...badge, + hidden: + allBadges + .filter((badge) => badge.category === category) + .filter((badge) => badge.hidden).length > + allBadges.filter((badge) => badge.category === category).length / 2 + ? false + : true + }); + }) + ); + }), + updateBadge: async (_, args, context) => + await authenticatedOperation(context, async (identity) => { + const badge = { + post: args.post || undefined, + image: args.image || undefined, + description: args.description || null, + time: args.time || undefined, + category: args.category || null, + hidden: args.hidden || false, + source: args.source || null, + designer: args.designer || null + }; + + if ((await getUserBadges(identity.id)).find((badge) => badge.id === args.id)) { + await updateUserBadge(identity.id, args.id as number, badge); + } else { + await addUserBadge(identity.id, badge); + } + }), + deleteBadge: async (_, args, context) => + await authenticatedOperation( + context, + async (identity) => await removeUserBadge(identity.id, args.id) + ), + pruneUserBadges: async (_, context) => + await authenticatedOperation( + context as Context, + async (identity) => await removeAllUserBadges(identity.id) + ) + } }; diff --git a/src/graphql/user/schema.graphql b/src/graphql/user/schema.graphql index 942ee72d..1e54f866 100644 --- a/src/graphql/user/schema.graphql +++ b/src/graphql/user/schema.graphql @@ -2,6 +2,25 @@ type Query { User(id: Int!): User! } +type Mutation { + shadowHideBadges(userId: Int!): [Badge]! + shadowHideBadge(id: Int!, state: Boolean): [Badge]! + hideBadge(category: String): [Badge]! + updateBadge( + id: Int + post: String + image: String + description: String + time: String + category: String + hidden: Boolean + source: String + designer: String + ): [Badge]! + deleteBadge(id: Int!): [Badge]! + pruneUserBadges: [Badge]! +} + type User { id: Int! badges: [Badge]! diff --git a/src/lib/Database/SB/User/badges.ts b/src/lib/Database/SB/User/badges.ts index 14f848c5..bea8f3e3 100644 --- a/src/lib/Database/SB/User/badges.ts +++ b/src/lib/Database/SB/User/badges.ts @@ -2,105 +2,101 @@ import { databaseTimeToDate } from '$lib/Utility/time'; import sb from '../../sb'; export interface Badge { - post?: string; - image?: string; - description?: string | null; - id?: number; - time?: string; - category?: string | null; - hidden?: boolean; - source?: string | null; - designer?: string | null; - shadow_hidden?: boolean; - click_count?: number; + post?: string; + image?: string; + description?: string | null; + id?: number; + time?: string; + category?: string | null; + hidden?: boolean; + source?: string | null; + designer?: string | null; + shadow_hidden?: boolean; + click_count?: number; } export const getUserBadges = async (userId: number): Promise<Badge[]> => { - const { data, error } = await sb.from('user_badges').select('*').eq('user_id', userId); + const { data, error } = await sb.from('user_badges').select('*').eq('user_id', userId); - if (error) return []; + if (error) return []; - return data.sort((a, b) => - databaseTimeToDate((a as Badge).time ?? '').getTime() > - databaseTimeToDate((b as Badge).time ?? '').getTime() - ? -1 - : 1 - ) as Badge[]; + return data.sort((a, b) => + databaseTimeToDate((a as Badge).time ?? '').getTime() > + databaseTimeToDate((b as Badge).time ?? '').getTime() + ? -1 + : 1 + ) as Badge[]; }; export const addUserBadge = async (userId: number, badge: Badge) => { - const { post, image, description, time, category, hidden, source, designer } = badge; - - if (post === undefined || image === undefined) return; - - if (time) { - await sb.from('user_badges').insert({ - user_id: userId, - post, - image, - description, - time, - category, - hidden, - source, - designer - }); - } else { - await sb - .from('user_badges') - .insert({ user_id: userId, post, image, description, category, hidden, source, designer }); - } + const { post, image, description, time, category, hidden, source, designer } = badge; + + if (post === undefined || image === undefined) return; + + if (time) { + await sb.from('user_badges').insert({ + user_id: userId, + post, + image, + description, + time, + category, + hidden, + source, + designer + }); + } else { + await sb + .from('user_badges') + .insert({ user_id: userId, post, image, description, category, hidden, source, designer }); + } }; export const removeUserBadge = async (userId: number, id: number) => { - if (!isNaN(id)) await sb.from('user_badges').delete().eq('id', id).eq('user_id', userId); + if (!isNaN(id)) await sb.from('user_badges').delete().eq('id', id).eq('user_id', userId); }; export const updateUserBadge = async (userId: number, id: number, badge: Badge) => { - if (badge.post === undefined || badge.image === undefined) return; - - await sb - .from('user_badges') - .update({ - post: badge.post, - image: badge.image, - description: badge.description, - category: badge.category, - time: badge.time, - hidden: badge.hidden, - source: badge.source, - designer: badge.designer - }) - .eq('id', id) - .eq('user_id', userId); + if (badge.post === undefined || badge.image === undefined) return; + + await sb + .from('user_badges') + .update({ + post: badge.post, + image: badge.image, + description: badge.description, + category: badge.category, + time: badge.time, + hidden: badge.hidden, + source: badge.source, + designer: badge.designer + }) + .eq('id', id) + .eq('user_id', userId); }; export const renameCategory = async (userId: number, oldName: string, newName: string) => - await sb - .from('user_badges') - .update({ category: newName }) - .eq('category', oldName) - .eq('user_id', userId); + await sb + .from('user_badges') + .update({ category: newName }) + .eq('category', oldName) + .eq('user_id', userId); export const removeAllUserBadges = async (userId: number) => - await sb.from('user_badges').delete().eq('user_id', userId); + await sb.from('user_badges').delete().eq('user_id', userId); export const migrateCategory = async (userId: number, oldName: string, newName: string) => - await sb - .from('user_badges') - .update({ category: newName }) - .eq('category', oldName) - .eq('user_id', userId); + await sb + .from('user_badges') + .update({ category: newName }) + .eq('category', oldName) + .eq('user_id', userId); export const setShadowHidden = async (userId: number, shadowHide: boolean) => - await sb.from('user_badges').update({ shadow_hidden: shadowHide }).eq('user_id', userId); + await sb.from('user_badges').update({ shadow_hidden: shadowHide }).eq('user_id', userId); -export const setShadowHiddenBadge = async (userId: number, id: number, shadowHide: boolean) => - await sb - .from('user_badges') - .update({ shadow_hidden: shadowHide }) - .eq('id', id) - .eq('user_id', userId); +export const setShadowHiddenBadge = async (id: number, shadowHide: boolean) => + await sb.from('user_badges').update({ shadow_hidden: shadowHide }).eq('id', id); export const incrementClickCount = async (id: number) => - await sb.rpc('user_badges_increment_click_count', { user_badge_id: id }); + await sb.rpc('user_badges_increment_click_count', { user_badge_id: id }); diff --git a/src/routes/api/badges/+server.ts b/src/routes/api/badges/+server.ts index dfe25e7e..e7a26320 100644 --- a/src/routes/api/badges/+server.ts +++ b/src/routes/api/badges/+server.ts @@ -1,150 +1,149 @@ import { userIdentity } from '$lib/Data/AniList/identity'; import { - removeAllUserBadges, - removeUserBadge, - updateUserBadge, - getUserBadges, - addUserBadge, - type Badge, - migrateCategory, - setShadowHidden, - setShadowHiddenBadge, - incrementClickCount + removeAllUserBadges, + removeUserBadge, + updateUserBadge, + getUserBadges, + addUserBadge, + type Badge, + migrateCategory, + setShadowHidden, + setShadowHiddenBadge, + incrementClickCount } from '$lib/Database/SB/User/badges'; import authorisedJson from '$lib/Data/Static/authorised.json'; const unauthorised = new Response('Unauthorised', { status: 401 }); const badges = async (id: number) => - Response.json(await getUserBadges(id), { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); + Response.json(await getUserBadges(id), { + headers: { + 'Access-Control-Allow-Origin': 'https://due.moe' + } + }); export const GET = async ({ url }) => { - return await badges(Number(url.searchParams.get('id') || 0)); + return await badges(Number(url.searchParams.get('id') || 0)); }; export const DELETE = async ({ url, cookies }) => { - const userCookie = cookies.get('user'); + const userCookie = cookies.get('user'); - if (!userCookie) return unauthorised; + if (!userCookie) return unauthorised; - const user = JSON.parse(userCookie); - const identity = await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }); + const user = JSON.parse(userCookie); + const identity = await userIdentity({ + tokenType: user['token_type'], + expiresIn: user['expires_in'], + accessToken: user['access_token'], + refreshToken: user['refresh_token'] + }); - if ((url.searchParams.get('prune') || 0) === 'true') { - await removeAllUserBadges(identity.id); - } else { - await removeUserBadge(identity.id, Number(url.searchParams.get('id'))); - } + if ((url.searchParams.get('prune') || 0) === 'true') { + await removeAllUserBadges(identity.id); + } else { + await removeUserBadge(identity.id, Number(url.searchParams.get('id'))); + } - return await badges(identity.id); + return await badges(identity.id); }; export const PUT = async ({ cookies, url, request }) => { - if (url.searchParams.get('incrementClickCount') || undefined) { - await incrementClickCount(Number(url.searchParams.get('incrementClickCount'))); - - return new Response('Incremented', { status: 200 }); - } - - const userCookie = cookies.get('user'); - - if (!userCookie) return unauthorised; - - const user = JSON.parse(userCookie); - const identity = await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }); - const authorised = authorisedJson.includes(identity.id); - - if (url.searchParams.get('shadowHide')) - setShadowHidden(Number(url.searchParams.get('shadowHide')), authorised); - - if (url.searchParams.get('import') || undefined) { - await Promise.all( - (await request.json()).map(async (badge: Badge) => await addUserBadge(identity.id, badge)) - ); - - return await badges(identity.id); - } else if (url.searchParams.get('migrate') || undefined) { - await migrateCategory( - identity.id, - url.searchParams.get('original') || '', - url.searchParams.get('new') || '' - ); - - return await badges(identity.id); - } - - if (url.searchParams.get('hide') || undefined) { - const allBadges = await getUserBadges(identity.id); - - await Promise.all( - allBadges - .filter((badge) => badge.category === (url.searchParams.get('category') || '')) - .map(async (badge) => { - await updateUserBadge(identity.id, badge.id as number, { - ...badge, - hidden: - allBadges - .filter((badge) => badge.category === (url.searchParams.get('category') || '')) - .filter((badge) => badge.hidden).length > - allBadges.filter( - (badge) => badge.category === (url.searchParams.get('category') || '') - ).length / - 2 - ? false - : true - }); - }) - ); - - return await badges(identity.id); - } - - if (url.searchParams.get('shadowHideBadge') || undefined) { - if (!authorised) return unauthorised; - - await setShadowHiddenBadge( - Number(url.searchParams.get('id')), - Number(url.searchParams.get('shadowHideBadge')), - url.searchParams.get('status') == 'true' ? false : true - ); - - return await badges(Number(url.searchParams.get('id'))); - } - - const badge = { - post: url.searchParams.get('post') || undefined, - image: url.searchParams.get('image') || undefined, - description: url.searchParams.get('description') || null, - time: url.searchParams.get('time') || undefined, - category: url.searchParams.get('category') || null, - hidden: url.searchParams.get('hidden') || false, - source: url.searchParams.get('source') || null, - designer: url.searchParams.get('designer') || null - }; - - if ( - (await getUserBadges(identity.id)).find( - (badge) => Number(badge.id) === Number(url.searchParams.get('update')) - ) - ) { - await updateUserBadge(identity.id, Number(url.searchParams.get('update')), badge as Badge); - } else { - await addUserBadge(identity.id, badge as Badge); - } - - return await badges(identity.id); + if (url.searchParams.get('incrementClickCount') || undefined) { + await incrementClickCount(Number(url.searchParams.get('incrementClickCount'))); + + return new Response('Incremented', { status: 200 }); + } + + const userCookie = cookies.get('user'); + + if (!userCookie) return unauthorised; + + const user = JSON.parse(userCookie); + const identity = await userIdentity({ + tokenType: user['token_type'], + expiresIn: user['expires_in'], + accessToken: user['access_token'], + refreshToken: user['refresh_token'] + }); + const authorised = authorisedJson.includes(identity.id); + + if (url.searchParams.get('shadowHide')) + setShadowHidden(Number(url.searchParams.get('shadowHide')), authorised); + + if (url.searchParams.get('import') || undefined) { + await Promise.all( + (await request.json()).map(async (badge: Badge) => await addUserBadge(identity.id, badge)) + ); + + return await badges(identity.id); + } else if (url.searchParams.get('migrate') || undefined) { + await migrateCategory( + identity.id, + url.searchParams.get('original') || '', + url.searchParams.get('new') || '' + ); + + return await badges(identity.id); + } + + if (url.searchParams.get('hide') || undefined) { + const allBadges = await getUserBadges(identity.id); + + await Promise.all( + allBadges + .filter((badge) => badge.category === (url.searchParams.get('category') || '')) + .map(async (badge) => { + await updateUserBadge(identity.id, badge.id as number, { + ...badge, + hidden: + allBadges + .filter((badge) => badge.category === (url.searchParams.get('category') || '')) + .filter((badge) => badge.hidden).length > + allBadges.filter( + (badge) => badge.category === (url.searchParams.get('category') || '') + ).length / + 2 + ? false + : true + }); + }) + ); + + return await badges(identity.id); + } + + if (url.searchParams.get('shadowHideBadge') || undefined) { + if (!authorised) return unauthorised; + + await setShadowHiddenBadge( + Number(url.searchParams.get('shadowHideBadge')), + url.searchParams.get('status') == 'true' ? false : true + ); + + return await badges(Number(url.searchParams.get('id'))); + } + + const badge = { + post: url.searchParams.get('post') || undefined, + image: url.searchParams.get('image') || undefined, + description: url.searchParams.get('description') || null, + time: url.searchParams.get('time') || undefined, + category: url.searchParams.get('category') || null, + hidden: url.searchParams.get('hidden') || false, + source: url.searchParams.get('source') || null, + designer: url.searchParams.get('designer') || null + }; + + if ( + (await getUserBadges(identity.id)).find( + (badge) => Number(badge.id) === Number(url.searchParams.get('update')) + ) + ) { + await updateUserBadge(identity.id, Number(url.searchParams.get('update')), badge as Badge); + } else { + await addUserBadge(identity.id, badge as Badge); + } + + return await badges(identity.id); }; diff --git a/src/routes/user/[user]/+page.svelte b/src/routes/user/[user]/+page.svelte index 0369a920..14c304f3 100644 --- a/src/routes/user/[user]/+page.svelte +++ b/src/routes/user/[user]/+page.svelte @@ -2,7 +2,7 @@ import settings from '$stores/settings'; import ParallaxImage from '../../../lib/Image/ParallaxImage.svelte'; import { typeSchedule, type ParseResult } from '$lib/Hololive/hololive'; - import { type User } from '$lib/Data/AniList/user'; + import type { User } from '$lib/Data/AniList/user'; import HeadTitle from '$lib/Home/HeadTitle.svelte'; import Message from '$lib/Loading/Message.svelte'; import { estimatedDayReading } from '$lib/Media/Manga/time'; diff --git a/src/routes/user/[user]/badges/+page.gql b/src/routes/user/[user]/badges/+page.gql new file mode 100644 index 00000000..44c35bd8 --- /dev/null +++ b/src/routes/user/[user]/badges/+page.gql @@ -0,0 +1,17 @@ +query UserBadges($id: Int!) { + User(id: $id) { + badges { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } + } +} diff --git a/src/routes/user/[user]/badges/+page.server.ts b/src/routes/user/[user]/badges/+page.server.ts deleted file mode 100644 index f18892a7..00000000 --- a/src/routes/user/[user]/badges/+page.server.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const load = async ({ params }) => { - return { - username: params.user - }; -}; diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte index 499233ce..c4d48454 100644 --- a/src/routes/user/[user]/badges/+page.svelte +++ b/src/routes/user/[user]/badges/+page.svelte @@ -3,7 +3,6 @@ import { userIdentity } from '$lib/Data/AniList/identity'; import { user, type User } from '$lib/Data/AniList/user'; import type { Badge } from '$lib/Database/SB/User/badges'; - // import { domToBlob } from 'modern-screenshot'; import { onDestroy, onMount } from 'svelte'; import HeadTitle from '$lib/Home/HeadTitle.svelte'; import { databaseTimeToDate, dateToInputTime, inputTimeToDatabaseTime } from '$lib/Utility/time'; @@ -20,16 +19,118 @@ import { page } from '$app/stores'; import type { UserPreferences } from '$lib/Database/SB/User/preferences'; import { browser } from '$app/environment'; - // import { io } from 'socket.io-client'; import BadgePreview from '$lib/User/BadgeWall/BadgePreview.svelte'; import authorisedJson from '$lib/Data/Static/authorised.json'; import identity from '$stores/identity'; import '$lib/User/BadgeWall/badges.css'; import Badges from '$lib/User/BadgeWall/Badges.svelte'; import type { IndexedBadge } from '$lib/User/BadgeWall/badge'; + import { graphql } from '$houdini'; export let data; + $: ({ UserBadges } = data); + + const updateBadgeQuery = graphql(` + mutation UpdateBadge( + $id: Int + $post: String + $image: String + $description: String + $time: String + $category: String + $hidden: Boolean + $source: String + $designer: String + ) { + updateBadge( + id: $id + post: $post + image: $image + description: $description + time: $time + category: $category + hidden: $hidden + source: $source + designer: $designer + ) { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } + } + `); + + const pruneBadgesQuery = graphql(` + mutation PruneUserBadges { + pruneUserBadges { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } + } + `); + + const hideCategoryQuery = graphql(` + mutation HideCategory($category: String) { + hideBadge(category: $category) { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } + } + `); + + const deleteBadgeQuery = graphql(` + mutation DeleteBadge($id: Int!) { + deleteBadge(id: $id) { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } + } + `); + + const shadowHideBadgeQuery = graphql(` + mutation ShadowHideBadge($id: Int!, $state: Boolean) { + shadowHideBadge(id: $id, state: $state) { + id + } + } + `); + interface ImportImage { link?: string; image: string; @@ -40,7 +141,6 @@ let currentUserIdentity: ReturnType<typeof userIdentity>; let error: null | string; // const socket = io(); - let badgesPromise: Promise<Response>; let awcPromise: Promise<Response>; // let dark = true; // let transparent = false; @@ -67,16 +167,12 @@ type GroupedBadges = { [key: string]: IndexedBadge[] }; - const getBadges = () => (badgesPromise = fetch(root(`/api/badges?id=${badger.id}`))); - const setShadowHide = () => - fetch(`/api/badges?shadowHide=${badger.id}`, { - method: 'PUT' - }).then(getBadges); + shadowHideBadgeQuery.mutate({ + id: badger.id as number + }); onMount(async () => { - // socket.on('badges', (message) => (badges = message)); - if (browser && localStorage.getItem('badgeWallNoticeDismissed')) noticeDismissed = true; badger = isId @@ -92,7 +188,6 @@ return; } - 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(); @@ -118,8 +213,6 @@ if (data.user && !isId) { currentUserIdentity = userIdentity(data.user); - - // socket.emit('badges', data.user); } else { currentUserIdentity = new Promise((resolve) => resolve({ @@ -181,32 +274,27 @@ return; } - badgesPromise = fetch( - `/api/badges?image=${encodeURIComponent(imageURL.value)}&post=${encodeURIComponent( - activityURL.value || '#' - )}${ - description.value.length > 0 ? `&description=${encodeURIComponent(description.value)}` : '' - }${category.value.length > 0 ? `&category=${encodeURIComponent(category.value)}` : ''}${ - time.valueAsDate - ? `&time=${encodeURIComponent(inputTimeToDatabaseTime(time.valueAsDate))}` - : '' - }${ - selectedBadge && selectedBadge.id ? `&update=${encodeURIComponent(selectedBadge.id)}` : '' - }&hidden=${hidden.value === 'Hidden'}${ - source.value.length > 0 ? `&source=${encodeURIComponent(source.value)}` : '' - }${designer.value.length > 0 ? `&designer=${encodeURIComponent(designer.value)}` : ''}`, - { - method: 'PUT' - } - ); - - error = null; - imageURL.value = ''; - activityURL.value = ''; - description.value = ''; - category.value = ''; - hidden.value = 'Shown'; - selectedBadge = undefined; + updateBadgeQuery + .mutate({ + id: selectedBadge?.id, + image: imageURL.value, + post: activityURL.value || '#', + description: description.value, + category: category.value, + time: time.valueAsDate ? inputTimeToDatabaseTime(time.valueAsDate) : undefined, + hidden: hidden.value === 'Hidden', + source: source.value, + designer: designer.value + }) + .then(() => { + error = null; + imageURL.value = ''; + activityURL.value = ''; + description.value = ''; + category.value = ''; + hidden.value = 'Shown'; + selectedBadge = undefined; + }); }; const removeAllBadges = () => { @@ -223,9 +311,8 @@ } selectedBadge = undefined; - badgesPromise = fetch(root(`/api/badges?prune=true`), { - method: 'DELETE' - }); + + pruneBadgesQuery.mutate(null).then(); }; const removeBadge = (badge: Badge) => { @@ -245,9 +332,12 @@ (document.querySelector(`#badge-${badge.id}`) as HTMLAnchorElement).style.display = 'none'; selectedBadge = undefined; - badgesPromise = fetch(root(`/api/badges?id=${badge.id}`), { - method: 'DELETE' - }); + + deleteBadgeQuery + .mutate({ + id: badge.id + }) + .then(); }; const groupBadges = (badges: IndexedBadge[]) => { @@ -369,7 +459,7 @@ }); const migrateCategory = () => { - badgesPromise = fetch( + fetch( `/api/badges?migrate=true&original=${encodeURIComponent( (document.querySelector('#migrate_original') as HTMLInputElement).value )}&new=${encodeURIComponent( @@ -378,22 +468,15 @@ { method: 'PUT' } - ); - - migrateMode = false; + ).then(() => (migrateMode = false)); }; const hideCategory = () => { - badgesPromise = fetch( - `/api/badges?hide=true&category=${encodeURIComponent( - (document.querySelector('#category_hide') as HTMLInputElement).value - )}`, - { - method: 'PUT' - } - ); - - hideMode = false; + hideCategoryQuery + .mutate({ + category: (document.querySelector('#category_hide') as HTMLInputElement).value + }) + .then(() => (hideMode = false)); }; // const exportBadges = (groupedBadges: [string, Badge[]][]) => { @@ -455,12 +538,12 @@ const shadowHideBadge = () => { if (!selectedBadge && !authorised) return; - badgesPromise = fetch( - `/api/badges?shadowHideBadge=${selectedBadge?.id}&status=${selectedBadge?.shadow_hidden}&id=${badger.id}`, - { - method: 'PUT' - } - ); + shadowHideBadgeQuery + .mutate({ + id: badger.id as number, + state: selectedBadge?.shadow_hidden as boolean + }) + .then(); }; </script> @@ -478,376 +561,356 @@ {:then identity} {@const isOwner = identity && (isId ? identity.id : identity.name) === data.username} - {#await badgesPromise} + {#if $UserBadges.fetching || !$UserBadges.data} <Message message="Loading badges ..." /> <Skeleton grid={true} count={100} width="150px" height="170px" /> - {:then badgesResponse} - {#if badgesResponse} - {#await badgesResponse.clone().json()} - <Message message="Parsing badges ..." /> - - <Skeleton grid={true} count={100} width="150px" height="170px" /> - {:then ungroupedBadgesAny} - {@const ungroupedBadges = castBadgesToIndexedBadges(ungroupedBadgesAny)} - {@const isBadgeSelected = - selectedBadge && - selectedBadge !== undefined && - selectedBadge.image && - selectedBadge.image !== undefined && - !editMode} - - <div id="badges"> - {#if preferences && !preferences.hide_awc_badges} - <AWC {awcPromise} {categoryFilter} {isOwner} {preferences} /> + {:else} + {@const ungroupedBadges = castBadgesToIndexedBadges($UserBadges.data.User.badges)} + {@const isBadgeSelected = + selectedBadge && + selectedBadge !== undefined && + selectedBadge.image && + selectedBadge.image !== undefined && + !editMode} + + <div id="badges"> + {#if preferences && !preferences.hide_awc_badges} + <AWC {awcPromise} {categoryFilter} {isOwner} {preferences} /> + {/if} + + {#if ungroupedBadges === null} + <Message message="Loading badges ..." /> + + <Skeleton grid={true} count={10} width="150px" height="170px" /> + {:else} + {@const groupedBadges = Object.entries( + groupBadges(removeHiddenBadges(isOwner, ungroupedBadges)) + )} + + {#if isOwner || authorised} + {@const shadowHiddenCount = ungroupedBadges.filter( + (badge) => badge.shadow_hidden + ).length} + {@const shadowHidden = shadowHiddenCount > 0} + + {#if shadowHidden} + <div class="card"> + <b>Notice:</b> The Badge Wall overseer system has detected badges containing + AI-generated material on your wall. {shadowHiddenCount} of your badges have been shadow + hidden. + <p /> + You may use the "Un-shadow Hide Badges" button to unhide these badges, from where you + will be required to use the hide feature to hide these badges from the public, while + allowing them to stay visible to you as the account holder. + </div> + {:else if !noticeDismissed} + <div class="card"> + <b>Notice:</b> AniList has begun purging outbound links which contain AI-generated + material, this includes Badge Wall. If you have collected badges with AI-generated + elements, kindly use the hide feature to hide these badges from the public, while + allowing them to stay visible to you as the account holder. + <p /> + Failure to comply with this request at your earliest convenience will result in the hiding + of all badges from your Badge Wall. + <p /> + <button + on:click={() => { + noticeDismissed = true; + + localStorage.setItem('badgeWallNoticeDismissed', 'true'); + }} + > + Dismiss + </button> + </div> {/if} - {#if ungroupedBadges === null} - <Message message="Loading badges ..." /> + <p /> - <Skeleton grid={true} count={10} width="150px" height="170px" /> - {:else} - {@const groupedBadges = Object.entries( - groupBadges(removeHiddenBadges(isOwner, ungroupedBadges)) - )} + <div class="card"> + {#if authorised} + <button on:click={setShadowHide}>Shadow Hide Badges</button> + {/if} - {#if isOwner || authorised} - {@const shadowHiddenCount = ungroupedBadges.filter( - (badge) => badge.shadow_hidden - ).length} - {@const shadowHidden = shadowHiddenCount > 0} + {#if isOwner && authorised} + <span style="margin: 0 0.625rem;">•</span> + {/if} + + {#if isOwner} + <button + on:click={() => { + 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={() => { + 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={() => { + selectedBadge = undefined; + migrateMode = !migrateMode; + }} + > + Migrate Category + </button> + <span style="margin: 0 0.625rem;">•</span> + <button + on:click={() => { + selectedBadge = undefined; + hideMode = !hideMode; + }} + > + Hide Category + </button> + <!-- <!-- <span style="margin: 0 0.625rem;">•</span> --> + <!-- <button on:click={() => exportBadges(groupedBadges)}>Export Badges</button> --> {#if shadowHidden} - <div class="card"> - <b>Notice:</b> The Badge Wall overseer system has detected badges containing - AI-generated material on your wall. {shadowHiddenCount} of your badges have been - shadow hidden. - <p /> - You may use the "Un-shadow Hide Badges" button to unhide these badges, from where - you will be required to use the hide feature to hide these badges from the public, - while allowing them to stay visible to you as the account holder. - </div> - {:else if !noticeDismissed} - <div class="card"> - <b>Notice:</b> AniList has begun purging outbound links which contain - AI-generated material, this includes Badge Wall. If you have collected badges - with AI-generated elements, kindly use the hide feature to hide these badges - from the public, while allowing them to stay visible to you as the account - holder. - <p /> - Failure to comply with this request at your earliest convenience will result in the - hiding of all badges from your Badge Wall. - <p /> - <button - on:click={() => { - noticeDismissed = true; - - localStorage.setItem('badgeWallNoticeDismissed', 'true'); - }} - > - Dismiss - </button> - </div> + <span style="margin: 0 0.625rem;">•</span> + <button on:click={setShadowHide}>Un-shadow Hide Badges</button> {/if} - <p /> - - <div class="card"> - {#if authorised} - <button on:click={setShadowHide}>Shadow Hide Badges</button> - {/if} - - {#if isOwner && authorised} - <span style="margin: 0 0.625rem;">•</span> - {/if} - - {#if isOwner} - <button - on:click={() => { - 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={() => { - 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={() => { - selectedBadge = undefined; - migrateMode = !migrateMode; - }} - > - Migrate Category - </button> - <span style="margin: 0 0.625rem;">•</span> - <button - on:click={() => { - selectedBadge = undefined; - hideMode = !hideMode; - }} - > - Hide Category - </button> - <!-- <!-- <span style="margin: 0 0.625rem;">•</span> --> - <!-- <button on:click={() => exportBadges(groupedBadges)}>Export Badges</button> --> - - {#if shadowHidden} - <span style="margin: 0 0.625rem;">•</span> - <button on:click={setShadowHide}>Un-shadow Hide Badges</button> - {/if} - - {#if editMode && isOwner} - {@const groups = groupedBadges - .map((group) => group[0]) - .filter((group) => group !== 'Uncategorised')} - {@const designers = castAsStringArray([ - ...new Set( - ungroupedBadges - .map((badge) => badge.designer) - .filter((designer) => designer !== undefined && designer !== null) - .filter( - (designer, index, array) => - array.indexOf(designer) === index && !array.includes(`@${designer}`) - ) + {#if editMode && isOwner} + {@const groups = groupedBadges + .map((group) => group[0]) + .filter((group) => group !== 'Uncategorised')} + {@const designers = castAsStringArray([ + ...new Set( + ungroupedBadges + .map((badge) => badge.designer) + .filter((designer) => designer !== undefined && designer !== null) + .filter( + (designer, index, array) => + array.indexOf(designer) === index && !array.includes(`@${designer}`) ) - ])} + ) + ])} - <p /> + <p /> - {#if error} - <p style="color: red;">{error}</p> - {/if} + {#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> - <span style="float: right;"> + </span> + </Dropdown> + <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> + + <p /> + + <div class="edit-row-2"> + <input + type="text" + placeholder={$locale().user.badges.editMode.source} + name="source" + minlength="1" + maxlength="1000" + size="16" + value={selectedBadge ? selectedBadge.source : ''} + /> + <Dropdown + items={designers.map((designer) => ({ + name: designer, + url: '#', + onClick: () => { + const designerField = document.querySelector('input[name="designer"]'); + + if (designerField instanceof HTMLInputElement) + designerField.value = designer; + } + }))} + header={false} + center={false} + > + <span slot="title"> <input - type="datetime-local" - value={selectedBadge && selectedBadge.time - ? dateToInputTime(databaseTimeToDate(selectedBadge.time)) - : ''} + type="text" + placeholder={$locale().user.badges.editMode.designer} + name="designer" + minlength="1" + maxlength="1000" + size="17" + value={selectedBadge ? selectedBadge.designer : ''} /> - <small - >Must be full date and time, defaults to now if any fields empty</small - > </span> - - <p /> - - <div class="edit-row-2"> + </Dropdown> + <Dropdown + items={[false, true].map((hidden) => ({ + name: hidden ? 'Hidden' : 'Shown', + url: '#', + onClick: () => { + const hiddenInput = document.querySelector('input[name="hidden"]'); + + if (hiddenInput instanceof HTMLInputElement) + hiddenInput.value = hidden ? 'Hidden' : 'Shown'; + } + }))} + header={false} + center={false} + > + <span slot="title"> <input type="text" - placeholder={$locale().user.badges.editMode.source} - name="source" + placeholder="Shown" + name="hidden" minlength="1" maxlength="1000" - size="16" - value={selectedBadge ? selectedBadge.source : ''} + size="15" + value={selectedBadge + ? selectedBadge.hidden + ? 'Hidden' + : 'Shown' + : 'Shown'} /> - <Dropdown - items={designers.map((designer) => ({ - name: designer, - url: '#', - onClick: () => { - const designerField = - document.querySelector('input[name="designer"]'); - - if (designerField instanceof HTMLInputElement) - designerField.value = designer; - } - }))} - header={false} - center={false} - > - <span slot="title"> - <input - type="text" - placeholder={$locale().user.badges.editMode.designer} - name="designer" - minlength="1" - maxlength="1000" - size="17" - value={selectedBadge ? selectedBadge.designer : ''} - /> - </span> - </Dropdown> - <Dropdown - items={[false, true].map((hidden) => ({ - name: hidden ? 'Hidden' : 'Shown', - url: '#', - onClick: () => { - const hiddenInput = document.querySelector('input[name="hidden"]'); - - if (hiddenInput instanceof HTMLInputElement) - hiddenInput.value = hidden ? 'Hidden' : 'Shown'; - } - }))} - header={false} - center={false} - > - <span slot="title"> - <input - type="text" - placeholder="Shown" - name="hidden" - minlength="1" - maxlength="1000" - size="15" - value={selectedBadge - ? selectedBadge.hidden - ? 'Hidden' - : 'Shown' - : 'Shown'} - /> - </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} - </div> + </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} - {/if} - </div> + </div> + {/if} {/if} + </div> + {/if} - <p /> - - <Badges - {ungroupedBadges} - {groupedBadges} - {categoryFilter} - {editMode} - {preferences} - bind:selectedBadge - /> - {/if} - </div> - - {#if isBadgeSelected} - <!-- {@const anyAdjacentBadgeExists = + <p /> + + <Badges + {ungroupedBadges} + {groupedBadges} + {categoryFilter} + {editMode} + {preferences} + bind:selectedBadge + /> + {/if} + </div> + + {#if isBadgeSelected} + <!-- {@const anyAdjacentBadgeExists = adjacentBadgeExists(selectedBadge, ungroupedBadges, -1) || adjacentBadgeExists(selectedBadge, ungroupedBadges, 1)} --> - <Popup - fullscreen - show={isBadgeSelected} - onLeave={() => { - selectedBadge = undefined; - }} - > - <BadgePreview - bind:selectedBadge - onNext={() => setAdjacentCursor(ungroupedBadges, 1)} - onPrevious={() => setAdjacentCursor(ungroupedBadges, -1)} - hasNext={adjacentBadgeExists(selectedBadge, ungroupedBadges, 1) !== undefined} - hasPrevious={adjacentBadgeExists(selectedBadge, ungroupedBadges, -1) !== undefined} - /> - - {#if authorised} - <button on:click={shadowHideBadge}> - {#if selectedBadge && selectedBadge.shadow_hidden} - Un-shadow - {:else} - Shadow - {/if} Hide Badge ({selectedBadge ? selectedBadge.id : 0}) - </button> - {/if} - </Popup> + <Popup + fullscreen + show={isBadgeSelected} + onLeave={() => { + selectedBadge = undefined; + }} + > + <BadgePreview + bind:selectedBadge + onNext={() => setAdjacentCursor(ungroupedBadges, 1)} + onPrevious={() => setAdjacentCursor(ungroupedBadges, -1)} + hasNext={adjacentBadgeExists(selectedBadge, ungroupedBadges, 1) !== undefined} + hasPrevious={adjacentBadgeExists(selectedBadge, ungroupedBadges, -1) !== undefined} + /> + + {#if authorised} + <button on:click={shadowHideBadge}> + {#if selectedBadge && selectedBadge.shadow_hidden} + Un-shadow + {:else} + Shadow + {/if} Hide Badge ({selectedBadge ? selectedBadge.id : 0}) + </button> {/if} - {:catch} - <Popup fullscreen locked>Could not parse badges</Popup> - {/await} - {:else} - <Message message="Loading badges ..." /> - - <Skeleton grid={true} count={100} width="150px" height="170px" /> + </Popup> {/if} - {:catch} - <Popup fullscreen locked>Could not fetch badges</Popup> - {/await} + {/if} {:catch} <AnimeRateLimited>This user's badges could not be loaded.</AnimeRateLimited> {/await} diff --git a/src/routes/user/[user]/badges/+page.ts b/src/routes/user/[user]/badges/+page.ts new file mode 100644 index 00000000..8b7204ad --- /dev/null +++ b/src/routes/user/[user]/badges/+page.ts @@ -0,0 +1,20 @@ +import { load_UserBadges } from '$houdini'; +import { user } from '$lib/Data/AniList/user'; +import type { LoadEvent } from '@sveltejs/kit'; + +export const load = async (event: LoadEvent) => { + const username = event.params.user as string; + const userData = await user(username, /^\d+$/.test(username)); + + return { + ...(await load_UserBadges({ + event, + variables: { + id: userData.id + } + })), + username, + userData, + event + }; +}; |