aboutsummaryrefslogtreecommitdiff
path: root/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes')
-rw-r--r--src/routes/+error.svelte28
-rw-r--r--src/routes/+layout.server.ts22
-rw-r--r--src/routes/+layout.svelte354
-rw-r--r--src/routes/+page.svelte381
-rw-r--r--src/routes/anilist-badges-easter-event-2025/+page.svelte64
-rw-r--r--src/routes/api/animeschedule/oauth/callback/+server.ts30
-rw-r--r--src/routes/api/authentication/log-out/+server.ts22
-rw-r--r--src/routes/api/badges/+server.ts296
-rw-r--r--src/routes/api/birthdays/primary/+server.ts74
-rw-r--r--src/routes/api/birthdays/secondary/+server.ts51
-rw-r--r--src/routes/api/configuration/+server.ts109
-rw-r--r--src/routes/api/events/+server.ts12
-rw-r--r--src/routes/api/events/group/+server.ts4
-rw-r--r--src/routes/api/events/groups/+server.ts2
-rw-r--r--src/routes/api/health/+server.ts2
-rw-r--r--src/routes/api/myanimelist/oauth/callback/+server.ts32
-rw-r--r--src/routes/api/notifications/subscribe/+server.ts42
-rw-r--r--src/routes/api/notifications/unsubscribe/+server.ts33
-rw-r--r--src/routes/api/oauth/callback/+server.ts28
-rw-r--r--src/routes/api/oauth/refresh/+server.ts46
-rw-r--r--src/routes/api/preferences/+server.ts171
-rw-r--r--src/routes/api/preferences/pin/+server.ts56
-rw-r--r--src/routes/api/subsplease/+server.ts34
-rw-r--r--src/routes/api/updates/all-novels/+server.ts38
-rw-r--r--src/routes/api/updates/manga/+server.ts27
-rw-r--r--src/routes/api/updates/novels/+server.ts27
-rw-r--r--src/routes/completed/+page.svelte117
-rw-r--r--src/routes/events/+page.svelte8
-rw-r--r--src/routes/events/group/[group]/+page.server.ts6
-rw-r--r--src/routes/events/group/[group]/+page.svelte31
-rw-r--r--src/routes/events/groups/+page.svelte22
-rw-r--r--src/routes/feeds/activity-notifications/+server.ts115
-rw-r--r--src/routes/girls/+page.svelte14
-rw-r--r--src/routes/girls/[language]/+page.server.ts6
-rw-r--r--src/routes/girls/[language]/+page.svelte11
-rw-r--r--src/routes/graphql/+server.ts6
-rw-r--r--src/routes/hololive/[[stream]]/+page.server.ts6
-rw-r--r--src/routes/hololive/[[stream]]/+page.svelte76
-rw-r--r--src/routes/reader/+page.svelte21
-rw-r--r--src/routes/schedule/+page.svelte53
-rw-r--r--src/routes/settings/+page.svelte46
-rw-r--r--src/routes/tools/+page.svelte12
-rw-r--r--src/routes/tools/[tool]/+page.server.ts6
-rw-r--r--src/routes/tools/[tool]/+page.svelte57
-rw-r--r--src/routes/updates/+page.svelte118
-rw-r--r--src/routes/user/+page.svelte51
-rw-r--r--src/routes/user/[user]/+page.gql30
-rw-r--r--src/routes/user/[user]/+page.svelte334
-rw-r--r--src/routes/user/[user]/+page.ts32
-rw-r--r--src/routes/user/[user]/badges/+page.gql54
-rw-r--r--src/routes/user/[user]/badges/+page.svelte857
-rw-r--r--src/routes/user/[user]/badges/+page.ts34
-rw-r--r--src/routes/welcome/+page.svelte6
53 files changed, 2285 insertions, 1829 deletions
diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte
index f822d521..71482ffb 100644
--- a/src/routes/+error.svelte
+++ b/src/routes/+error.svelte
@@ -1,19 +1,19 @@
<script lang="ts">
- import { page } from '$app/stores';
- import { closest } from '$lib/Error/path';
- import Popup from '$lib/Layout/Popup.svelte';
+import { page } from "$app/stores";
+import { closest } from "$lib/Error/path";
+import Popup from "$lib/Layout/Popup.svelte";
- $: suggestion = closest($page.url.pathname.replace('/', ''), [
- 'birthdays',
- 'completed',
- 'schedule',
- 'hololive',
- 'settings',
- 'tools',
- 'updates',
- 'user',
- 'wrapped'
- ]);
+$: suggestion = closest($page.url.pathname.replace("/", ""), [
+ "birthdays",
+ "completed",
+ "schedule",
+ "hololive",
+ "settings",
+ "tools",
+ "updates",
+ "user",
+ "wrapped",
+]);
</script>
<Popup>
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
index 1b7bf69a..8357aa17 100644
--- a/src/routes/+layout.server.ts
+++ b/src/routes/+layout.server.ts
@@ -1,14 +1,16 @@
export const load = ({ locals, url, cookies }) => {
- const { user } = locals;
+ const { user } = locals;
- if (cookies.get('logout') === '1') {
- cookies.delete('user', { path: '/' });
- cookies.delete('logout', { path: '/' });
- }
+ if (cookies.get("logout") === "1") {
+ cookies.delete("user", { path: "/" });
+ cookies.delete("logout", { path: "/" });
+ }
- return {
- user,
- url: url.pathname,
- commit: process.env.VERCEL_GIT_COMMIT_SHA ?? 'ffffffffffffffffffffffffffffffffffffffff'
- };
+ return {
+ user,
+ url: url.pathname,
+ commit:
+ process.env.VERCEL_GIT_COMMIT_SHA ??
+ "ffffffffffffffffffffffffffffffffffffffff",
+ };
};
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 4d78b29f..39a2e40b 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,164 +1,198 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import type { SubsPleaseEpisode } from '$lib/Media/Anime/Airing/Subtitled/subsPlease';
- import { env } from '$env/dynamic/public';
- import { userIdentity as getUserIdentity } from '$lib/Data/AniList/identity';
- import { onDestroy, onMount } from 'svelte';
- import userIdentity from '$stores/identity';
- import settings from '$stores/settings';
- import { browser } from '$app/environment';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import '../app.css';
- import { readable, type Readable } from 'svelte/store';
- import { navigating } from '$app/stores';
- import NotificationsProvider from '$lib/Notification/NotificationsProvider.svelte';
- import Root from '$lib/Home/Root.svelte';
- import root from '$lib/Utility/root';
- import { addMessages, init, locale as i18nLocale, locales } from 'svelte-i18n';
- import english from '$lib/Locale/english';
- import japanese from '$lib/Locale/japanese';
- import type { LocaleDictionary } from '$lib/Locale/layout';
- import locale from '$stores/locale';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import subsPlease from '$stores/subsPlease';
- import Dropdown from '$lib/Layout/Dropdown.svelte';
- import { injectSpeedInsights } from '@vercel/speed-insights/sveltekit';
- import subtitles from '$lib/Data/Static/subtitles.json';
- import settingsSyncPulled from '$stores/settingsSyncPulled';
- import settingsSyncTimes from '$stores/settingsSyncTimes';
- import Announcement from '$lib/Announcement.svelte';
- import Message from '$lib/Loading/Message.svelte';
- import { requestNotifications } from '$lib/Utility/notifications';
- import { database as userDatabase } from '$lib/Database/IDB/user';
- import CommandPalette from '$lib/CommandPalette/CommandPalette.svelte';
- import { defaultActions } from '$lib/CommandPalette/actions';
- import { toolsAsCommandPaletteActions } from '$lib/Tools/tools';
- import localforage from 'localforage';
- import { dev } from '$app/environment';
- import { injectAnalytics } from '@vercel/analytics/sveltekit';
-
- injectSpeedInsights();
- injectAnalytics({ mode: dev ? 'development' : 'production' });
-
- export let data;
-
- let isHeaderVisible = true;
- let previousScrollPosition = 0;
- let notificationInterval: ReturnType<typeof setInterval> | undefined = undefined;
-
- addMessages('en', english as unknown as LocaleDictionary);
- addMessages('ja', japanese as unknown as LocaleDictionary);
- init({ fallbackLocale: 'en', initialLocale: $settings.displayLanguage });
-
- $: i18nLocale.set($settings.displayLanguage);
-
- const navigationOrder = ['/', '/completed', '/schedule', '/updates', '/tools', '/settings'];
- const previousPage: Readable<string | null> = readable(null, (set) => {
- const unsubscribe = navigating.subscribe(($navigating) => {
- if ($navigating && $navigating.from) set($navigating.from.url.pathname as unknown as null);
- });
-
- return () => unsubscribe();
- });
-
- $: way = data.url.includes('/user')
- ? 200
- : $previousPage && $previousPage.includes('/user')
- ? -200
- : navigationOrder.indexOf(data.url) > navigationOrder.indexOf($previousPage ?? '/')
- ? 200
- : -200;
-
- const handleScroll = () => {
- const currentScrollPosition = window.scrollY;
-
- isHeaderVisible =
- currentScrollPosition <= 100 || currentScrollPosition < previousScrollPosition;
- previousScrollPosition = currentScrollPosition;
- };
-
- onMount(async () => {
- if (browser) {
- if (await localforage.getItem('redirect')) {
- window.location.href = (await localforage.getItem('redirect')) ?? '/';
-
- await localforage.removeItem('redirect');
- }
-
- window.addEventListener('scroll', handleScroll);
-
- if ((await localforage.getItem('commit')) !== data.commit) {
- await localforage.removeItem('identity');
- await localforage.removeItem('anime');
- await localforage.removeItem('manga');
- await localforage.removeItem('anime');
- await localforage.removeItem('manga');
- await localforage.removeItem('lastPruneTimes');
- await localforage.setItem('commit', data.commit);
- }
- }
-
- settings.get();
-
- if (data.user !== undefined && $userIdentity.id === -2)
- getUserIdentity(data.user).then((h) => userIdentity.set(h));
-
- if ($settings.settingsSync && $userIdentity.id !== -2)
- fetch(root(`/api/configuration?id=${$userIdentity.id}`)).then((response) => {
- if (response.ok)
- response.json().then((data) => {
- if (data && data.configuration) {
- console.log('Pulled remote configuration');
- settings.set(data.configuration);
- settingsSyncPulled.set(true);
- settingsSyncTimes.set({
- lastPull: new Date(),
- lastPush: new Date(data.updated_at + 'Z')
- });
- }
- });
- });
-
- await userDatabase.users.where('id').below(0).delete();
-
- if (!(await userDatabase.users.get($userIdentity.id)) && $userIdentity.id > 0)
- await userDatabase.users.put({
- id: $userIdentity.id,
- user: data.user,
- lastNotificationID: null
- });
-
- if ($settings.displayAniListNotifications && data.user !== undefined)
- if ('Notification' in window && navigator.serviceWorker) requestNotifications();
- });
-
- onDestroy(() => {
- if (browser) window.removeEventListener('scroll', handleScroll);
-
- if (notificationInterval) clearInterval(notificationInterval);
- });
-
- $: {
- if ((data.url === '/' || data.url === '/completed' || data.url === '/schedule') && !$subsPlease)
- fetch(root(`/api/subsplease?tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}`))
- .then((r) => r.json())
- .then((r) => {
- for (const day in subtitles) {
- if (!r.schedule[day]) r.schedule[day] = [];
-
- (subtitles[day as keyof typeof subtitles] as SubsPleaseEpisode[]).forEach((episode) => {
- r.schedule[day].push({
- title: episode.title,
- page: episode.page || '',
- image_url: episode.image_url || '',
- time: episode.time
- });
- });
- }
-
- subsPlease.set(r);
- });
- }
+import Spacer from "$lib/Layout/Spacer.svelte";
+import type { SubsPleaseEpisode } from "$lib/Media/Anime/Airing/Subtitled/subsPlease";
+import { env } from "$env/dynamic/public";
+import { userIdentity as getUserIdentity } from "$lib/Data/AniList/identity";
+import { onDestroy, onMount } from "svelte";
+import userIdentity from "$stores/identity";
+import settings from "$stores/settings";
+import { browser } from "$app/environment";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import "../app.css";
+import { readable, type Readable } from "svelte/store";
+import { navigating } from "$app/stores";
+import NotificationsProvider from "$lib/Notification/NotificationsProvider.svelte";
+import Root from "$lib/Home/Root.svelte";
+import root from "$lib/Utility/root";
+import { addMessages, init, locale as i18nLocale, locales } from "svelte-i18n";
+import english from "$lib/Locale/english";
+import japanese from "$lib/Locale/japanese";
+import type { LocaleDictionary } from "$lib/Locale/layout";
+import locale from "$stores/locale";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import subsPlease from "$stores/subsPlease";
+import Dropdown from "$lib/Layout/Dropdown.svelte";
+import { injectSpeedInsights } from "@vercel/speed-insights/sveltekit";
+import subtitles from "$lib/Data/Static/subtitles.json";
+import settingsSyncPulled from "$stores/settingsSyncPulled";
+import settingsSyncTimes from "$stores/settingsSyncTimes";
+import Announcement from "$lib/Announcement.svelte";
+import Message from "$lib/Loading/Message.svelte";
+import { requestNotifications } from "$lib/Utility/notifications";
+import { database as userDatabase } from "$lib/Database/IDB/user";
+import CommandPalette from "$lib/CommandPalette/CommandPalette.svelte";
+import { defaultActions } from "$lib/CommandPalette/actions";
+import { toolsAsCommandPaletteActions } from "$lib/Tools/tools";
+import localforage from "localforage";
+import { dev } from "$app/environment";
+import { injectAnalytics } from "@vercel/analytics/sveltekit";
+import type { LayoutData } from "./$types";
+
+injectSpeedInsights();
+injectAnalytics({ mode: dev ? "development" : "production" });
+
+export let data: LayoutData;
+
+let isHeaderVisible = true;
+let previousScrollPosition = 0;
+let notificationInterval: ReturnType<typeof setInterval> | undefined =
+ undefined;
+
+addMessages("en", english as unknown as LocaleDictionary);
+addMessages("ja", japanese as unknown as LocaleDictionary);
+init({ fallbackLocale: "en", initialLocale: $settings.displayLanguage });
+
+$: i18nLocale.set($settings.displayLanguage);
+
+const navigationOrder = [
+ "/",
+ "/completed",
+ "/schedule",
+ "/updates",
+ "/tools",
+ "/settings",
+];
+const previousPage: Readable<string | null> = readable(null, (set) => {
+ const unsubscribe = navigating.subscribe(($navigating) => {
+ if ($navigating && $navigating.from)
+ set($navigating.from.url.pathname as unknown as null);
+ });
+
+ return () => unsubscribe();
+});
+
+$: way = data.url.includes("/user")
+ ? 200
+ : $previousPage && $previousPage.includes("/user")
+ ? -200
+ : navigationOrder.indexOf(data.url) >
+ navigationOrder.indexOf($previousPage ?? "/")
+ ? 200
+ : -200;
+
+const handleScroll = () => {
+ const currentScrollPosition = window.scrollY;
+
+ isHeaderVisible =
+ currentScrollPosition <= 100 ||
+ currentScrollPosition < previousScrollPosition;
+ previousScrollPosition = currentScrollPosition;
+};
+
+onMount(async () => {
+ if (browser) {
+ if (await localforage.getItem("redirect")) {
+ window.location.href = (await localforage.getItem("redirect")) ?? "/";
+
+ await localforage.removeItem("redirect");
+ }
+
+ window.addEventListener("scroll", handleScroll);
+
+ if ((await localforage.getItem("commit")) !== data.commit) {
+ await localforage.removeItem("identity");
+ await localforage.removeItem("anime");
+ await localforage.removeItem("manga");
+ await localforage.removeItem("anime");
+ await localforage.removeItem("manga");
+ await localforage.removeItem("lastPruneTimes");
+ await localforage.setItem("commit", data.commit);
+ }
+ }
+
+ settings.get();
+
+ const currentIdentity = data.user
+ ? await getUserIdentity(data.user)
+ : undefined;
+
+ if (currentIdentity) userIdentity.set(currentIdentity);
+ else if ($userIdentity.id !== -2) userIdentity.reset();
+
+ if ($settings.settingsSync && currentIdentity)
+ fetch(root(`/api/configuration?id=${currentIdentity.id}`)).then(
+ (response) => {
+ if (response.ok)
+ response.json().then((data) => {
+ if (data && data.configuration) {
+ console.log("Pulled remote configuration");
+ settings.set(data.configuration);
+ settingsSyncPulled.set(true);
+ settingsSyncTimes.set({
+ lastPull: new Date(),
+ lastPush: new Date(data.updated_at + "Z"),
+ });
+ }
+ });
+ },
+ );
+
+ await userDatabase.users.where("id").below(0).delete();
+
+ if (
+ currentIdentity &&
+ !(await userDatabase.users.get(currentIdentity.id)) &&
+ currentIdentity.id > 0
+ )
+ await userDatabase.users.put({
+ id: currentIdentity.id,
+ user: data.user,
+ lastNotificationID: null,
+ });
+
+ if ($settings.displayAniListNotifications && currentIdentity)
+ if ("Notification" in window && navigator.serviceWorker)
+ requestNotifications();
+});
+
+onDestroy(() => {
+ if (browser) window.removeEventListener("scroll", handleScroll);
+
+ if (notificationInterval) clearInterval(notificationInterval);
+});
+
+$: {
+ if (
+ (data.url === "/" ||
+ data.url === "/completed" ||
+ data.url === "/schedule") &&
+ !$subsPlease
+ )
+ fetch(
+ root(
+ `/api/subsplease?tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}`,
+ ),
+ )
+ .then((r) => r.json())
+ .then((r) => {
+ for (const day in subtitles) {
+ if (!r.schedule[day]) r.schedule[day] = [];
+
+ (
+ subtitles[day as keyof typeof subtitles] as SubsPleaseEpisode[]
+ ).forEach((episode) => {
+ r.schedule[day].push({
+ title: episode.title,
+ page: episode.page || "",
+ image_url: episode.image_url || "",
+ time: episode.time,
+ });
+ });
+ }
+
+ subsPlease.set(r);
+ });
+}
</script>
<HeadTitle />
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index fb80bbdd..ad3971d8 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,37 +1,185 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import { onDestroy, onMount } from 'svelte';
- import MangaListTemplate from '$lib/List/Manga/MangaListTemplate.svelte';
- import UpcomingAnimeList from '$lib/List/Anime/UpcomingAnimeList.svelte';
- import userIdentity from '$stores/identity.js';
- import settings from '$stores/settings';
- import ListTitle from '$lib/List/ListTitle.svelte';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import LastActivity from '$lib/Home/LastActivity.svelte';
- import { createHeightObserver } from '$lib/Utility/html.js';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import locale from '$stores/locale.js';
- import Landing from '$lib/Landing.svelte';
- import LandingHero from '$lib/LandingHero.svelte';
- import IndexColumn from '$lib/List/Anime/DueIndexColumn.svelte';
- import stateBin from '$stores/stateBin.js';
-
- export let data;
-
- let heightObserver: ReturnType<typeof setInterval>;
-
- onMount(() => {
- heightObserver = setInterval(() => createHeightObserver(), 0);
- $stateBin.upcomingAnimeListOpen ??= true;
- $stateBin.dueMangaListOpen ??= true;
- });
-
- onDestroy(() => clearInterval(heightObserver));
+import type { Component } from "svelte";
+import { browser } from "$app/environment";
+import Spacer from "$lib/Layout/Spacer.svelte";
+import { onDestroy, onMount } from "svelte";
+import userIdentity from "$stores/identity.js";
+import settings from "$stores/settings";
+import ListTitle from "$lib/List/ListTitle.svelte";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import { createHeightObserver } from "$lib/Utility/html.js";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import locale from "$stores/locale.js";
+import Landing from "$lib/Landing.svelte";
+import LandingHero from "$lib/LandingHero.svelte";
+import stateBin, { hydrateStateBin } from "$stores/stateBin.js";
+import type { PageData } from "./$types";
+
+let { data }: { data: PageData } = $props();
+
+type UserProp = PageData["user"];
+type LastActivitySvelteComponent = Component<{
+ user: UserProp;
+}>;
+type UpcomingAnimeListSvelteComponent = Component<{
+ user: UserProp;
+}>;
+type IndexColumnSvelteComponent = Component<{
+ user: UserProp;
+ userIdentity: { id: number };
+}>;
+type MangaListTemplateSvelteComponent = Component<{
+ user: UserProp;
+ displayUnresolved: boolean;
+ due: boolean;
+}>;
+
+let removeHeightObserver: (() => void) | undefined;
+let LastActivityComponent: LastActivitySvelteComponent | null = $state(null);
+let UpcomingAnimeListComponent: UpcomingAnimeListSvelteComponent | null =
+ $state(null);
+let IndexColumnComponent: IndexColumnSvelteComponent | null = $state(null);
+let MangaListTemplateComponent: MangaListTemplateSvelteComponent | null =
+ $state(null);
+let authenticatedHomeSurfaceImport: Promise<void> | null = null;
+let balancedListFlowElement: HTMLDivElement | undefined = $state();
+let balancedListResizeObserver: ResizeObserver | undefined;
+let balancedListMeasurementFrame = 0;
+const balancedListPanelElements = new Map<string, HTMLElement>();
+let balancedListPanelAssignments = $state<Record<string, "left" | "right">>({});
+
+const resetBalancedListLayout = () => {
+ balancedListPanelAssignments = {};
+};
+
+const updateBalancedListLayout = () => {
+ if (!balancedListFlowElement) return;
+
+ const balancedListFlowStyles = getComputedStyle(balancedListFlowElement);
+ const balancedListRowGap = parseFloat(
+ balancedListFlowStyles.getPropertyValue("row-gap"),
+ );
+ const balancedListColumnCount = balancedListFlowStyles
+ .getPropertyValue("grid-template-columns")
+ .split(" ")
+ .filter(Boolean).length;
+
+ if (balancedListColumnCount <= 1 || !Number.isFinite(balancedListRowGap)) {
+ balancedListPanelAssignments = Object.fromEntries(
+ Array.from(balancedListPanelElements.keys()).map((key) => [key, "left"]),
+ );
+
+ return;
+ }
+
+ const balancedListKeys = ["upcoming", "due", "manga"].filter((key) =>
+ balancedListPanelElements.has(key),
+ );
+ const balancedListHeights = { left: 0, right: 0 };
+ const nextBalancedListAssignments: Record<string, "left" | "right"> = {};
+
+ balancedListKeys.forEach((key) => {
+ const balancedListPanel = balancedListPanelElements.get(key);
+ if (!balancedListPanel) return;
+
+ const balancedListColumn =
+ balancedListHeights.left <= balancedListHeights.right ? "left" : "right";
+ const balancedListPanelHeight =
+ balancedListPanel.getBoundingClientRect().height;
+
+ nextBalancedListAssignments[key] = balancedListColumn;
+ balancedListHeights[balancedListColumn] +=
+ balancedListPanelHeight +
+ (balancedListHeights[balancedListColumn] > 0 ? balancedListRowGap : 0);
+ });
+
+ balancedListPanelAssignments = nextBalancedListAssignments;
+};
+
+const queueBalancedListLayout = () => {
+ if (!browser) return;
+
+ cancelAnimationFrame(balancedListMeasurementFrame);
+
+ balancedListMeasurementFrame = requestAnimationFrame(() => {
+ updateBalancedListLayout();
+ });
+};
+
+const observeBalancedListPanel = (panel: HTMLElement, key: string) => {
+ balancedListPanelElements.set(key, panel);
+ balancedListResizeObserver?.observe(panel);
+ queueBalancedListLayout();
+
+ return {
+ destroy: () => {
+ balancedListPanelElements.delete(key);
+ balancedListResizeObserver?.unobserve(panel);
+ queueBalancedListLayout();
+ },
+ };
+};
+
+const isBalancedListPanelInColumn = (key: string, column: "left" | "right") =>
+ (balancedListPanelAssignments[key] ?? "left") === column;
+
+const loadAuthenticatedHomeSurface = () => {
+ if (data.user === undefined) return null;
+ if (authenticatedHomeSurfaceImport) return authenticatedHomeSurfaceImport;
+
+ authenticatedHomeSurfaceImport = Promise.all([
+ import("$lib/Home/LastActivity.svelte"),
+ import("$lib/List/Anime/UpcomingAnimeList.svelte"),
+ import("$lib/List/Anime/DueIndexColumn.svelte"),
+ import("$lib/List/Manga/MangaListTemplate.svelte"),
+ ]).then(
+ ([lastActivity, upcomingAnimeList, indexColumn, mangaListTemplate]) => {
+ LastActivityComponent = lastActivity.default;
+ UpcomingAnimeListComponent = upcomingAnimeList.default;
+ IndexColumnComponent = indexColumn.default;
+ MangaListTemplateComponent = mangaListTemplate.default;
+ },
+ );
+
+ return authenticatedHomeSurfaceImport;
+};
+
+onMount(async () => {
+ removeHeightObserver = createHeightObserver();
+ await hydrateStateBin();
+ $stateBin.upcomingAnimeListOpen ??= true;
+ $stateBin.dueMangaListOpen ??= true;
+
+ balancedListResizeObserver = new ResizeObserver(() => {
+ queueBalancedListLayout();
+ });
+
+ balancedListPanelElements.forEach((panel) => {
+ balancedListResizeObserver?.observe(panel);
+ });
+
+ window.addEventListener("resize", queueBalancedListLayout);
+ void loadAuthenticatedHomeSurface();
+
+ queueBalancedListLayout();
+});
+
+onDestroy(() => {
+ removeHeightObserver?.();
+ balancedListResizeObserver?.disconnect();
+ if (browser) {
+ cancelAnimationFrame(balancedListMeasurementFrame);
+ window.removeEventListener("resize", queueBalancedListLayout);
+ }
+ resetBalancedListLayout();
+});
</script>
<HeadTitle />
-<LastActivity user={data.user} />
+{#if LastActivityComponent}
+ <LastActivityComponent user={data.user} />
+{/if}
{#if data.user === undefined}
<LandingHero />
@@ -40,57 +188,140 @@
<Landing />
{:else}
+ {@const balancedListColumnCount =
+ [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga]
+ .map(Number)
+ .reduce((a, b) => a + b) > 1
+ ? 2
+ : 1}
<div
- class="grid-container"
+ bind:this={balancedListFlowElement}
+ class="balanced-list-flow"
style={`
- grid-template-columns: ${
- [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga]
- .map(Number)
- .reduce((a, b) => a + b) > 1
- ? '1fr 1fr'
- : '1fr'
- }
+ --balanced-list-columns: ${balancedListColumnCount}
`}
>
- <div class="left-column">
- {#if !$settings.disableUpcomingAnime}
- <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming">
- {#if $userIdentity.id !== -2}
- <UpcomingAnimeList user={data.user} />
+ <div class="balanced-list-column">
+ {#if !$settings.disableUpcomingAnime && isBalancedListPanelInColumn("upcoming", "left")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"upcoming"}>
+ <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming">
+ {#if $userIdentity.id !== -2}
+ {#if UpcomingAnimeListComponent}
+ <UpcomingAnimeListComponent user={data.user} />
+ {:else}
+ <ListTitle title={$locale().lists.upcoming.episodes} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ {:else}
+ <ListTitle title={$locale().lists.upcoming.episodes} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ </details>
+ </div>
+ {/if}
+
+ {#if !$settings.disableAnime && isBalancedListPanelInColumn("due", "left")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"due"}>
+ {#if IndexColumnComponent}
+ <IndexColumnComponent user={data.user} userIdentity={$userIdentity} />
{:else}
- <ListTitle title={$locale().lists.upcoming.episodes} />
+ <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due">
+ <ListTitle title={$locale().lists.due.episodes} />
- <Skeleton card={false} count={5} height="0.9rem" list />
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ </details>
{/if}
- </details>
+ </div>
{/if}
- {#if !$settings.disableAnime && !$settings.disableManga}
- <IndexColumn user={data.user} userIdentity={$userIdentity} />
+ {#if !$settings.disableManga && isBalancedListPanelInColumn("manga", "left")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"manga"}>
+ <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga">
+ {#if $userIdentity.id !== -2}
+ {#if MangaListTemplateComponent}
+ <MangaListTemplateComponent
+ user={data.user}
+ displayUnresolved={$settings.displayUnresolved}
+ due={true}
+ />
+ {:else}
+ <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ {:else}
+ <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ </details>
+ </div>
{/if}
</div>
- <div class="right-column">
- {#if !$settings.disableAnime && $settings.disableManga}
- <IndexColumn user={data.user} userIdentity={$userIdentity} />
- {/if}
+ {#if balancedListColumnCount > 1}
+ <div class="balanced-list-column">
+ {#if !$settings.disableUpcomingAnime && isBalancedListPanelInColumn("upcoming", "right")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"upcoming"}>
+ <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming">
+ {#if $userIdentity.id !== -2}
+ {#if UpcomingAnimeListComponent}
+ <UpcomingAnimeListComponent user={data.user} />
+ {:else}
+ <ListTitle title={$locale().lists.upcoming.episodes} />
- {#if !$settings.disableManga}
- <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga">
- {#if $userIdentity.id !== -2}
- <MangaListTemplate
- user={data.user}
- displayUnresolved={$settings.displayUnresolved}
- due={true}
- />
- {:else}
- <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ {:else}
+ <ListTitle title={$locale().lists.upcoming.episodes} />
- <Skeleton card={false} count={5} height="0.9rem" list />
- {/if}
- </details>
- {/if}
- </div>
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ </details>
+ </div>
+ {/if}
+
+ {#if !$settings.disableAnime && isBalancedListPanelInColumn("due", "right")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"due"}>
+ {#if IndexColumnComponent}
+ <IndexColumnComponent user={data.user} userIdentity={$userIdentity} />
+ {:else}
+ <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due">
+ <ListTitle title={$locale().lists.due.episodes} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ </details>
+ {/if}
+ </div>
+ {/if}
+
+ {#if !$settings.disableManga && isBalancedListPanelInColumn("manga", "right")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"manga"}>
+ <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga">
+ {#if $userIdentity.id !== -2}
+ {#if MangaListTemplateComponent}
+ <MangaListTemplateComponent
+ user={data.user}
+ displayUnresolved={$settings.displayUnresolved}
+ due={true}
+ />
+ {:else}
+ <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ {:else}
+ <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ </details>
+ </div>
+ {/if}
+ </div>
+ {/if}
{#if $settings.disableUpcomingAnime && $settings.disableAnime && $settings.disableManga}
<video src="https://video.twimg.com/tweet_video/Do_eDPnX0AAKV9f.mp4" autoplay loop>
@@ -101,30 +332,32 @@
{/if}
<style>
- .grid-container {
+ .balanced-list-flow {
display: grid;
+ grid-template-columns: repeat(var(--balanced-list-columns), minmax(0, 1fr));
gap: 1rem;
+ align-items: start;
}
- .left-column {
+ .balanced-list-column {
display: grid;
gap: 1rem;
align-content: start;
+ min-width: 0;
}
- .right-column {
- align-self: start;
+ .balanced-list-panel {
+ min-width: 0;
}
.list {
overflow-y: auto;
- break-inside: avoid;
- page-break-inside: avoid;
+ margin: 0;
}
@media (max-width: 800px) {
- .grid-container {
- grid-template-columns: 1fr !important;
+ .balanced-list-flow {
+ grid-template-columns: 1fr;
}
}
</style>
diff --git a/src/routes/anilist-badges-easter-event-2025/+page.svelte b/src/routes/anilist-badges-easter-event-2025/+page.svelte
index d1a18967..b9c1295d 100644
--- a/src/routes/anilist-badges-easter-event-2025/+page.svelte
+++ b/src/routes/anilist-badges-easter-event-2025/+page.svelte
@@ -1,42 +1,42 @@
<script lang="ts">
- import { onMount } from 'svelte';
- import ClickableAreaPage from '$lib/Events/AniListBadges/EasterEvent2025/ClickableAreaPage.svelte';
- import MultipleChoicePage from '$lib/Events/AniListBadges/EasterEvent2025/MultipleChoicePage.svelte';
- import RiddlePage from '$lib/Events/AniListBadges/EasterEvent2025/RiddlePage.svelte';
- import '$lib/Events/AniListBadges/EasterEvent2025/event.css';
+import { onMount } from "svelte";
+import ClickableAreaPage from "$lib/Events/AniListBadges/EasterEvent2025/ClickableAreaPage.svelte";
+import MultipleChoicePage from "$lib/Events/AniListBadges/EasterEvent2025/MultipleChoicePage.svelte";
+import RiddlePage from "$lib/Events/AniListBadges/EasterEvent2025/RiddlePage.svelte";
+import "$lib/Events/AniListBadges/EasterEvent2025/event.css";
- const multipleChoiceAnswers = [
- 'The Beginning After the End',
- 'Domestic Girlfriend',
- 'Spy Classroom',
- 'Kaguya-sama: Love is War'
- ];
- const clickableAreaAnswers = [
- 'https://media1.tenor.com/m/RkgfXhcewvoAAAAC/rui-tachibana-tachibana-rui.gif',
- 'https://media1.tenor.com/m/fOBbcJOFZ-kAAAAC/hina-hina-tachibana.gif',
- 'https://media1.tenor.com/m/GQT7FHW-w-IAAAAC/anime-domestic-girlfriend.gif'
- ];
- let page = 0;
+const multipleChoiceAnswers = [
+ "The Beginning After the End",
+ "Domestic Girlfriend",
+ "Spy Classroom",
+ "Kaguya-sama: Love is War",
+];
+const clickableAreaAnswers = [
+ "https://media1.tenor.com/m/RkgfXhcewvoAAAAC/rui-tachibana-tachibana-rui.gif",
+ "https://media1.tenor.com/m/fOBbcJOFZ-kAAAAC/hina-hina-tachibana.gif",
+ "https://media1.tenor.com/m/GQT7FHW-w-IAAAAC/anime-domestic-girlfriend.gif",
+];
+let page = 0;
- onMount(() => {
- const urlParameters = new URLSearchParams(window.location.search);
- const pageParameter = urlParameters.get('page');
+onMount(() => {
+ const urlParameters = new URLSearchParams(window.location.search);
+ const pageParameter = urlParameters.get("page");
- if (pageParameter) page = parseInt(pageParameter) || 0;
- });
+ if (pageParameter) page = parseInt(pageParameter) || 0;
+});
- const updatePage = (to: number | undefined = undefined) => {
- const url = new URL(window.location.href);
+const updatePage = (to: number | undefined = undefined) => {
+ const url = new URL(window.location.href);
- if (to) {
- page = to;
- } else {
- page += 1;
- }
+ if (to) {
+ page = to;
+ } else {
+ page += 1;
+ }
- url.searchParams.set('page', page.toString());
- window.history.replaceState(null, '', url.toString());
- };
+ url.searchParams.set("page", page.toString());
+ window.history.replaceState(null, "", url.toString());
+};
</script>
<div class="card main-content">
diff --git a/src/routes/api/animeschedule/oauth/callback/+server.ts b/src/routes/api/animeschedule/oauth/callback/+server.ts
index 2b96ab81..294abc05 100644
--- a/src/routes/api/animeschedule/oauth/callback/+server.ts
+++ b/src/routes/api/animeschedule/oauth/callback/+server.ts
@@ -1,17 +1,17 @@
-import { callback } from '$lib/Utility/oauth.js';
-import { env } from '$env/dynamic/private';
-import { env as env2 } from '$env/dynamic/public';
+import { callback } from "$lib/Utility/oauth.js";
+import { env } from "$env/dynamic/private";
+import { env as env2 } from "$env/dynamic/public";
export const GET = async ({ url, cookies }) =>
- callback({
- url,
- cookies,
- cookie: 'animeschedule',
- authorise: 'https://animeschedule.net/api/v3/oauth2/token',
- redirect: '/settings',
- client: {
- id: env2.PUBLIC_ANIMESCHEDULE_CLIENT_ID,
- secret: env.ANIMESCHEDULE_CLIENT_SECRET,
- redirectURI: env2.PUBLIC_ANIMESCHEDULE_REDIRECT_URI
- }
- });
+ callback({
+ url,
+ cookies,
+ cookie: "animeschedule",
+ authorise: "https://animeschedule.net/api/v3/oauth2/token",
+ redirect: "/settings",
+ client: {
+ id: env2.PUBLIC_ANIMESCHEDULE_CLIENT_ID,
+ secret: env.ANIMESCHEDULE_CLIENT_SECRET,
+ redirectURI: env2.PUBLIC_ANIMESCHEDULE_REDIRECT_URI,
+ },
+ });
diff --git a/src/routes/api/authentication/log-out/+server.ts b/src/routes/api/authentication/log-out/+server.ts
index 26b5dd2c..c04fa5c5 100644
--- a/src/routes/api/authentication/log-out/+server.ts
+++ b/src/routes/api/authentication/log-out/+server.ts
@@ -1,15 +1,15 @@
-import root from '$lib/Utility/root.js';
-import { redirect } from '@sveltejs/kit';
+import root from "$lib/Utility/root.js";
+import { redirect } from "@sveltejs/kit";
export const GET = ({ cookies }) => {
- cookies.delete('user', { path: '/' });
- cookies.set('logout', '1', {
- path: '/',
- maxAge: 60 * 60 * 24 * 7,
- httpOnly: false,
- sameSite: 'lax',
- secure: false
- });
+ cookies.delete("user", { path: "/" });
+ cookies.set("logout", "1", {
+ path: "/",
+ maxAge: 60 * 60 * 24 * 7,
+ httpOnly: false,
+ sameSite: "lax",
+ secure: false,
+ });
- redirect(303, root('/'));
+ redirect(303, root("/"));
};
diff --git a/src/routes/api/badges/+server.ts b/src/routes/api/badges/+server.ts
index 35ed4512..46b98cbc 100644
--- a/src/routes/api/badges/+server.ts
+++ b/src/routes/api/badges/+server.ts
@@ -1,149 +1,179 @@
-import { userIdentity } from '$lib/Data/AniList/identity';
+import { safeUserIdentity } from "$lib/Data/AniList/identity";
+import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie";
+import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody";
import {
- removeAllUserBadges,
- removeUserBadge,
- updateUserBadge,
- getUserBadges,
- addUserBadge,
- type Badge,
- migrateCategory,
- setShadowHidden,
- setShadowHiddenBadge,
- incrementClickCount
-} from '$lib/Database/SB/User/badges';
-import privilegedUser from '$lib/Utility/privilegedUser';
-
-const unauthorised = new Response('Unauthorised', { status: 401 });
+ removeAllUserBadges,
+ removeUserBadge,
+ updateUserBadge,
+ getUserBadges,
+ addUserBadge,
+ type Badge,
+ type BadgeInput,
+ migrateCategory,
+ setShadowHidden,
+ setShadowHiddenBadge,
+ incrementClickCount,
+} from "$lib/Database/SB/User/badges";
+import { Schema } from "effect";
+import { appOrigin, appOriginHeaders } from "$lib/Utility/appOrigin";
+import privilegedUser from "$lib/Utility/privilegedUser";
+
+const unauthorised = () => new Response("Unauthorised", { status: 401 });
+const importedBadgeSchema = Schema.Record(Schema.String, Schema.Unknown);
const badges = async (id: number) =>
- Response.json(await getUserBadges(id), {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- });
+ Response.json(await getUserBadges(id), {
+ headers: appOriginHeaders(),
+ });
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 = decodeAuthCookieOrNull(userCookie);
- if ((url.searchParams.get('prune') || 0) === 'true') {
- await removeAllUserBadges(identity.id);
- } else {
- await removeUserBadge(identity.id, Number(url.searchParams.get('id')));
- }
+ if (!user) return unauthorised();
- return await badges(identity.id);
+ const identity = await safeUserIdentity(user);
+
+ if (!identity) return unauthorised();
+
+ 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);
};
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 = privilegedUser(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);
+ if (url.searchParams.get("incrementClickCount") || undefined) {
+ if (request.headers.get("origin") !== appOrigin()) return unauthorised();
+
+ await incrementClickCount(
+ Number(url.searchParams.get("incrementClickCount")),
+ );
+
+ return new Response("Incremented", { status: 200 });
+ }
+
+ const userCookie = cookies.get("user");
+
+ if (!userCookie) return unauthorised();
+
+ const user = decodeAuthCookieOrNull(userCookie);
+
+ if (!user) return unauthorised();
+
+ const identity = await safeUserIdentity(user);
+
+ if (!identity) return unauthorised();
+ const authorised = privilegedUser(identity.id);
+
+ if (url.searchParams.get("shadowHide"))
+ await setShadowHidden(
+ Number(url.searchParams.get("shadowHide")),
+ authorised,
+ );
+
+ if (url.searchParams.get("import") || undefined) {
+ const importedBadges = await decodeRequestJsonOrThrow(
+ request,
+ Schema.Array(importedBadgeSchema),
+ );
+
+ await Promise.all(
+ importedBadges.map(
+ async (badge) =>
+ await addUserBadge(identity.id, badge as unknown as BadgeInput),
+ ),
+ );
+
+ 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/api/birthdays/primary/+server.ts b/src/routes/api/birthdays/primary/+server.ts
index 774d47e1..109961f8 100644
--- a/src/routes/api/birthdays/primary/+server.ts
+++ b/src/routes/api/birthdays/primary/+server.ts
@@ -1,40 +1,40 @@
-import { JSDOM } from 'jsdom';
+import { JSDOM } from "jsdom";
+import { appOriginHeaders } from "$lib/Utility/appOrigin";
export const GET = async ({ url }: { url: URL }) => {
- const document = new JSDOM(
- await (
- await fetch(
- `https://www.anisearch.com/character/birthdays?month=${url.searchParams.get('month')}`
- )
- ).text()
- ).window.document;
- const section = document.querySelector(`#day-${url.searchParams.get('day')}`);
-
- if (!section) return Response.json([]);
-
- const ul = section.querySelector('ul.covers.simple');
-
- if (!ul) return Response.json([]);
-
- return Response.json(
- Array.from(ul.querySelectorAll('li')).map((li) => {
- const anchor = li.querySelector('a');
- const title = li.querySelector('.title');
-
- if (!anchor || !title) return { image: '', title: '' };
-
- const image = li.getElementsByClassName('item-cover')[0];
-
- return {
- image: image ? image.getAttribute('src') : '',
- name: title.textContent?.trim()
- };
- }),
- {
- headers: {
- 'Cache-Control': 'max-age=10800, s-maxage=10800',
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- }
- );
+ const document = new JSDOM(
+ await (
+ await fetch(
+ `https://www.anisearch.com/character/birthdays?month=${url.searchParams.get("month")}`,
+ )
+ ).text(),
+ ).window.document;
+ const section = document.querySelector(`#day-${url.searchParams.get("day")}`);
+
+ if (!section) return Response.json([]);
+
+ const ul = section.querySelector("ul.covers.simple");
+
+ if (!ul) return Response.json([]);
+
+ return Response.json(
+ Array.from(ul.querySelectorAll("li")).map((li) => {
+ const anchor = li.querySelector("a");
+ const title = li.querySelector(".title");
+
+ if (!anchor || !title) return { image: "", title: "" };
+
+ const image = li.getElementsByClassName("item-cover")[0];
+
+ return {
+ image: image ? image.getAttribute("src") : "",
+ name: title.textContent?.trim(),
+ };
+ }),
+ {
+ headers: appOriginHeaders({
+ "Cache-Control": "max-age=10800, s-maxage=10800",
+ }),
+ },
+ );
};
diff --git a/src/routes/api/birthdays/secondary/+server.ts b/src/routes/api/birthdays/secondary/+server.ts
index 5036622f..4619828f 100644
--- a/src/routes/api/birthdays/secondary/+server.ts
+++ b/src/routes/api/birthdays/secondary/+server.ts
@@ -1,28 +1,29 @@
-import { env } from '$env/dynamic/private';
+import { env } from "$env/dynamic/private";
+import { appOriginHeaders } from "$lib/Utility/appOrigin";
export const GET = async ({ url }: { url: URL }) => {
- return Response.json(
- await (
- await fetch(
- `https://www.animecharactersdatabase.com/api_series_characters.php?month=${url.searchParams.get(
- 'month'
- )}&day=${url.searchParams.get('day')}`,
- {
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0',
- 'Cache-Control': 'max-age=10, s-maxage=10',
- Cookie: `USTATS=${env.ACDB_COOKIE}`
- }
- }
- )
- ).json(),
- {
- headers: {
- 'Cache-Control': 'max-age=10800, s-maxage=10800',
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- }
- );
+ return Response.json(
+ await (
+ await fetch(
+ `https://www.animecharactersdatabase.com/api_series_characters.php?month=${url.searchParams.get(
+ "month",
+ )}&day=${url.searchParams.get("day")}`,
+ {
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ "User-Agent":
+ "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0",
+ "Cache-Control": "max-age=10, s-maxage=10",
+ Cookie: `USTATS=${env.ACDB_COOKIE}`,
+ },
+ },
+ )
+ ).json(),
+ {
+ headers: appOriginHeaders({
+ "Cache-Control": "max-age=10800, s-maxage=10800",
+ }),
+ },
+ );
};
diff --git a/src/routes/api/configuration/+server.ts b/src/routes/api/configuration/+server.ts
index 41a70f7b..306e1285 100644
--- a/src/routes/api/configuration/+server.ts
+++ b/src/routes/api/configuration/+server.ts
@@ -1,70 +1,65 @@
-import { userIdentity } from '$lib/Data/AniList/identity';
+import { Schema } from "effect";
+import { safeUserIdentity } from "$lib/Data/AniList/identity";
import {
- deleteUserConfiguration,
- getUserConfiguration,
- setUserConfiguration
-} from '$lib/Database/SB/User/configuration';
+ deleteUserConfiguration,
+ getUserConfiguration,
+ setUserConfiguration,
+} from "$lib/Database/SB/User/configuration";
+import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie";
+import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody";
+import { appOriginHeaders } from "$lib/Utility/appOrigin";
-const unauthorised = new Response('Unauthorised', { status: 401 });
+const unauthorised = new Response("Unauthorised", { status: 401 });
-export const GET = async ({ url }) =>
- Response.json(await getUserConfiguration(Number(url.searchParams.get('id') || 0)), {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- });
+const authenticatedUserId = async (cookies: {
+ get: (name: string) => string | undefined;
+}) => {
+ const userCookie = cookies.get("user");
-export const PUT = async ({ cookies, request }) => {
- const userCookie = cookies.get('user');
+ if (!userCookie) return null;
- if (!userCookie) return unauthorised;
+ const user = decodeAuthCookieOrNull(userCookie);
- const user = JSON.parse(userCookie);
+ if (!user) return null;
- return Response.json(
- await setUserConfiguration(
- (
- await userIdentity({
- tokenType: user['token_type'],
- expiresIn: user['expires_in'],
- accessToken: user['access_token'],
- refreshToken: user['refresh_token']
- })
- ).id,
- {
- configuration: await request.json()
- }
- ),
- {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- }
- );
+ return (await safeUserIdentity(user))?.id ?? null;
};
-export const DELETE = async ({ cookies }) => {
- const userCookie = cookies.get('user');
+export const GET = async ({ cookies, url }) => {
+ const userId = await authenticatedUserId(cookies);
+ const requestedUserId = Number(url.searchParams.get("id") || 0);
+
+ if (!userId || requestedUserId !== userId) return unauthorised;
- if (!userCookie) return unauthorised;
+ return Response.json(await getUserConfiguration(requestedUserId), {
+ headers: appOriginHeaders(),
+ });
+};
+
+export const PUT = async ({ cookies, request }) => {
+ const userId = await authenticatedUserId(cookies);
+
+ if (!userId) return unauthorised;
+
+ return Response.json(
+ await setUserConfiguration(userId, {
+ configuration: await decodeRequestJsonOrThrow(
+ request,
+ Schema.Record(Schema.String, Schema.Unknown),
+ ),
+ }),
+ {
+ headers: appOriginHeaders(),
+ },
+ );
+};
+
+export const DELETE = async ({ cookies }) => {
+ const userId = await authenticatedUserId(cookies);
- const user = JSON.parse(userCookie);
+ if (!userId) return unauthorised;
- return Response.json(
- await deleteUserConfiguration(
- (
- await userIdentity({
- tokenType: user['token_type'],
- expiresIn: user['expires_in'],
- accessToken: user['access_token'],
- refreshToken: user['refresh_token']
- })
- ).id
- ),
- {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- }
- );
+ return Response.json(await deleteUserConfiguration(userId), {
+ headers: appOriginHeaders(),
+ });
};
diff --git a/src/routes/api/events/+server.ts b/src/routes/api/events/+server.ts
index 22280394..f7fee7b8 100644
--- a/src/routes/api/events/+server.ts
+++ b/src/routes/api/events/+server.ts
@@ -1,8 +1,8 @@
-import { getEvents, getGroupEvents } from '$lib/Database/SB/events';
+import { getEvents, getGroupEvents } from "$lib/Database/SB/events";
export const GET = async ({ url }) =>
- Response.json(
- url.searchParams.get('group')
- ? await getGroupEvents(url.searchParams.get('group') || '')
- : await getEvents()
- );
+ Response.json(
+ url.searchParams.get("group")
+ ? await getGroupEvents(url.searchParams.get("group") || "")
+ : await getEvents(),
+ );
diff --git a/src/routes/api/events/group/+server.ts b/src/routes/api/events/group/+server.ts
index 9bd4c33a..b95db775 100644
--- a/src/routes/api/events/group/+server.ts
+++ b/src/routes/api/events/group/+server.ts
@@ -1,4 +1,4 @@
-import { getGroup } from '$lib/Database/SB/groups';
+import { getGroup } from "$lib/Database/SB/groups";
export const GET = async ({ url }) =>
- Response.json(await getGroup(url.searchParams.get('slug') || ''));
+ Response.json(await getGroup(url.searchParams.get("slug") || ""));
diff --git a/src/routes/api/events/groups/+server.ts b/src/routes/api/events/groups/+server.ts
index db50d0ad..d74b872c 100644
--- a/src/routes/api/events/groups/+server.ts
+++ b/src/routes/api/events/groups/+server.ts
@@ -1,3 +1,3 @@
-import { getGroups } from '$lib/Database/SB/groups';
+import { getGroups } from "$lib/Database/SB/groups";
export const GET = async () => Response.json(await getGroups());
diff --git a/src/routes/api/health/+server.ts b/src/routes/api/health/+server.ts
index 66a62024..9161b5c7 100644
--- a/src/routes/api/health/+server.ts
+++ b/src/routes/api/health/+server.ts
@@ -1 +1 @@
-export const GET = () => new Response('OK', { status: 200 });
+export const GET = () => new Response("OK", { status: 200 });
diff --git a/src/routes/api/myanimelist/oauth/callback/+server.ts b/src/routes/api/myanimelist/oauth/callback/+server.ts
index 0bb78a64..57a5fbe4 100644
--- a/src/routes/api/myanimelist/oauth/callback/+server.ts
+++ b/src/routes/api/myanimelist/oauth/callback/+server.ts
@@ -1,18 +1,18 @@
-import { callback } from '$lib/Utility/oauth.js';
-import { env } from '$env/dynamic/private';
-import { env as env2 } from '$env/dynamic/public';
+import { callback } from "$lib/Utility/oauth.js";
+import { env } from "$env/dynamic/private";
+import { env as env2 } from "$env/dynamic/public";
export const GET = async ({ url, cookies }) =>
- callback({
- url,
- cookies,
- cookie: 'myanimelist',
- authorise: 'https://myanimelist.net/v1/oauth2/token',
- redirect: '/settings',
- verifier: env.CODE_VERIFIER,
- client: {
- id: env2.PUBLIC_MYANIMELIST_CLIENT_ID,
- secret: env.MYANIMELIST_CLIENT_SECRET,
- redirectURI: env2.PUBLIC_MYANIMLIST_REDIRECT_URI
- }
- });
+ callback({
+ url,
+ cookies,
+ cookie: "myanimelist",
+ authorise: "https://myanimelist.net/v1/oauth2/token",
+ redirect: "/settings",
+ verifier: env.CODE_VERIFIER,
+ client: {
+ id: env2.PUBLIC_MYANIMELIST_CLIENT_ID,
+ secret: env.MYANIMELIST_CLIENT_SECRET,
+ redirectURI: env2.PUBLIC_MYANIMLIST_REDIRECT_URI,
+ },
+ });
diff --git a/src/routes/api/notifications/subscribe/+server.ts b/src/routes/api/notifications/subscribe/+server.ts
index d410dc9d..203470e0 100644
--- a/src/routes/api/notifications/subscribe/+server.ts
+++ b/src/routes/api/notifications/subscribe/+server.ts
@@ -1,27 +1,33 @@
-import { userIdentity } from '$lib/Data/AniList/identity';
-import { setUserSubscription } from '$lib/Database/SB/User/notifications';
+import { safeUserIdentity } from "$lib/Data/AniList/identity";
+import { setUserSubscription } from "$lib/Database/SB/User/notifications";
+import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie";
+import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody";
+import { Schema } from "effect";
-const unauthorised = new Response('Unauthorised', { status: 401 });
+const unauthorised = new Response("Unauthorised", { status: 401 });
export const POST = async ({ cookies, request, url }) => {
- const userCookie = cookies.get('user');
- const fingerprint = url.searchParams.get('p');
+ const userCookie = cookies.get("user");
+ const fingerprint = url.searchParams.get("p");
- if (!userCookie || !fingerprint) return unauthorised;
+ if (!userCookie || !fingerprint) 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;
+ const user = decodeAuthCookieOrNull(userCookie);
- if (!userId) return unauthorised;
+ if (!user) return unauthorised;
- await setUserSubscription(userId, await request.json(), fingerprint);
+ const userId = (await safeUserIdentity(user))?.id;
- return new Response(null, { status: 200 });
+ if (!userId) return unauthorised;
+
+ await setUserSubscription(
+ userId,
+ (await decodeRequestJsonOrThrow(
+ request,
+ Schema.Record(Schema.String, Schema.Unknown),
+ )) as unknown as JSON,
+ fingerprint,
+ );
+
+ return new Response(null, { status: 200 });
};
diff --git a/src/routes/api/notifications/unsubscribe/+server.ts b/src/routes/api/notifications/unsubscribe/+server.ts
index ded228f3..94bbd497 100644
--- a/src/routes/api/notifications/unsubscribe/+server.ts
+++ b/src/routes/api/notifications/unsubscribe/+server.ts
@@ -1,27 +1,24 @@
-import { userIdentity } from '$lib/Data/AniList/identity';
-import { deleteUserSubscription } from '$lib/Database/SB/User/notifications';
+import { safeUserIdentity } from "$lib/Data/AniList/identity";
+import { deleteUserSubscription } from "$lib/Database/SB/User/notifications";
+import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie";
-const unauthorised = new Response('Unauthorised', { status: 401 });
+const unauthorised = new Response("Unauthorised", { status: 401 });
export const POST = async ({ cookies, url }) => {
- const userCookie = cookies.get('user');
- const fingerprint = url.searchParams.get('p');
+ const userCookie = cookies.get("user");
+ const fingerprint = url.searchParams.get("p");
- if (!userCookie || !fingerprint) return unauthorised;
+ if (!userCookie || !fingerprint) 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;
+ const user = decodeAuthCookieOrNull(userCookie);
- if (!userId) return unauthorised;
+ if (!user) return unauthorised;
- await deleteUserSubscription(userId, fingerprint);
+ const userId = (await safeUserIdentity(user))?.id;
- return new Response(null, { status: 200 });
+ if (!userId) return unauthorised;
+
+ await deleteUserSubscription(userId, fingerprint);
+
+ return new Response(null, { status: 200 });
};
diff --git a/src/routes/api/oauth/callback/+server.ts b/src/routes/api/oauth/callback/+server.ts
index 986520f9..c5faa859 100644
--- a/src/routes/api/oauth/callback/+server.ts
+++ b/src/routes/api/oauth/callback/+server.ts
@@ -1,16 +1,16 @@
-import { callback } from '$lib/Utility/oauth.js';
-import { env } from '$env/dynamic/private';
-import { env as env2 } from '$env/dynamic/public';
+import { callback } from "$lib/Utility/oauth.js";
+import { env } from "$env/dynamic/private";
+import { env as env2 } from "$env/dynamic/public";
export const GET = async ({ url, cookies }) =>
- callback({
- url,
- cookies,
- cookie: 'user',
- authorise: 'https://anilist.co/api/v2/oauth/token',
- client: {
- id: env2.PUBLIC_ANILIST_CLIENT_ID,
- secret: env.ANILIST_CLIENT_SECRET,
- redirectURI: env2.PUBLIC_ANILIST_REDIRECT_URI
- }
- });
+ callback({
+ url,
+ cookies,
+ cookie: "user",
+ authorise: "https://anilist.co/api/v2/oauth/token",
+ client: {
+ id: env2.PUBLIC_ANILIST_CLIENT_ID,
+ secret: env.ANILIST_CLIENT_SECRET,
+ redirectURI: env2.PUBLIC_ANILIST_REDIRECT_URI,
+ },
+ });
diff --git a/src/routes/api/oauth/refresh/+server.ts b/src/routes/api/oauth/refresh/+server.ts
index 13f4400c..13e7ab09 100644
--- a/src/routes/api/oauth/refresh/+server.ts
+++ b/src/routes/api/oauth/refresh/+server.ts
@@ -1,30 +1,30 @@
-import { env } from '$env/dynamic/private';
-import { env as env2 } from '$env/dynamic/public';
-import { redirect } from '@sveltejs/kit';
+import { env } from "$env/dynamic/private";
+import { env as env2 } from "$env/dynamic/public";
+import { redirect } from "@sveltejs/kit";
export const GET = async ({ url, cookies }) => {
- const formData = new FormData();
+ const formData = new FormData();
- formData.append('grant_type', 'refresh_token');
- formData.append('client_id', env2.PUBLIC_ANILIST_CLIENT_ID as string);
- formData.append('client_secret', env.ANILIST_CLIENT_SECRET as string);
- formData.append('refresh_token', url.searchParams.get('token') || '');
+ formData.append("grant_type", "refresh_token");
+ formData.append("client_id", env2.PUBLIC_ANILIST_CLIENT_ID as string);
+ formData.append("client_secret", env.ANILIST_CLIENT_SECRET as string);
+ formData.append("refresh_token", url.searchParams.get("token") || "");
- const newUser = await (
- await fetch('https://anilist.co/api/v2/oauth/token', {
- method: 'POST',
- body: formData
- })
- ).json();
+ const newUser = await (
+ await fetch("https://anilist.co/api/v2/oauth/token", {
+ method: "POST",
+ body: formData,
+ })
+ ).json();
- cookies.set('user', JSON.stringify(newUser), {
- path: '/',
- maxAge: 60 * 60 * 24 * 7,
- httpOnly: false,
- sameSite: 'lax',
- secure: false
- });
+ cookies.set("user", JSON.stringify(newUser), {
+ path: "/",
+ maxAge: 60 * 60 * 24 * 7,
+ httpOnly: false,
+ sameSite: "lax",
+ secure: false,
+ });
- if (url.searchParams.get('redirect')) redirect(303, '/');
- else return Response.json(newUser);
+ if (url.searchParams.get("redirect")) redirect(303, "/");
+ else return Response.json(newUser);
};
diff --git a/src/routes/api/preferences/+server.ts b/src/routes/api/preferences/+server.ts
index 0fb91f45..47ce442b 100644
--- a/src/routes/api/preferences/+server.ts
+++ b/src/routes/api/preferences/+server.ts
@@ -1,89 +1,100 @@
-import { userIdentity } from '$lib/Data/AniList/identity';
+import { Schema } from "effect";
+import { safeUserIdentity } from "$lib/Data/AniList/identity";
import {
- getUserPreferences,
- toggleHideMissingBadges,
- setCSS,
- setBiography,
- toggleHideAWCBadges,
- togglePinnedBadgeWallCategory,
- setPinnedBadgeWallCategories
-} from '$lib/Database/SB/User/preferences';
+ getUserPreferences,
+ setBiography,
+ setCSS,
+ setPinnedBadgeWallCategories,
+ toggleHideAWCBadges,
+ toggleHideMissingBadges,
+ togglePinnedBadgeWallCategory,
+} from "$lib/Database/SB/User/preferences";
+import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie";
+import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody";
+import { appOriginHeaders } from "$lib/Utility/appOrigin";
-const unauthorised = new Response('Unauthorised', { status: 401 });
+const unauthorised = new Response("Unauthorised", { status: 401 });
+
+const authenticatedUserId = async (cookies: {
+ get: (name: string) => string | undefined;
+}) => {
+ const userCookie = cookies.get("user");
+
+ if (!userCookie) return null;
+
+ const user = decodeAuthCookieOrNull(userCookie);
+
+ if (!user) return null;
+
+ return (await safeUserIdentity(user))?.id ?? null;
+};
export const GET = async ({ url }) => {
- const preferences = await getUserPreferences(Number(url.searchParams.get('id') || 0));
+ const requestedUserId = Number(url.searchParams.get("id") || 0);
- return Response.json(preferences ? preferences : {}, {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- });
+ const preferences = await getUserPreferences(requestedUserId);
+
+ return Response.json(preferences ? preferences : {}, {
+ headers: appOriginHeaders(),
+ });
};
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(userId), {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- });
-
- if (url.searchParams.get('toggleHideAWCBadges') !== null)
- return Response.json(await toggleHideAWCBadges(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'
- }
- });
-
- if (url.searchParams.get('toggleCategory') !== null)
- return Response.json(
- await togglePinnedBadgeWallCategory(userId, url.searchParams.get('toggleCategory') || ''),
- {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- }
- );
-
- if (url.searchParams.get('setCategories') !== null)
- return Response.json(await setPinnedBadgeWallCategories(userId, await request.json()), {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- });
-
- if (url.searchParams.get('biography') !== null)
- return Response.json(await setBiography(userId, (await request.text()).slice(0, 3000)), {
- 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'
- }
- });
+ const userId = await authenticatedUserId(cookies);
+
+ if (!userId) return unauthorised;
+
+ if (url.searchParams.get("toggleHideMissingBadges") !== null)
+ return Response.json(await toggleHideMissingBadges(userId), {
+ headers: appOriginHeaders(),
+ });
+
+ if (url.searchParams.get("toggleHideAWCBadges") !== null)
+ return Response.json(await toggleHideAWCBadges(userId), {
+ headers: appOriginHeaders(),
+ });
+
+ if (url.searchParams.get("badgeWallCSS") !== null)
+ return Response.json(await setCSS(userId, await request.text()), {
+ headers: appOriginHeaders(),
+ });
+
+ if (url.searchParams.get("toggleCategory") !== null)
+ return Response.json(
+ await togglePinnedBadgeWallCategory(
+ userId,
+ url.searchParams.get("toggleCategory") || "",
+ ),
+ {
+ headers: appOriginHeaders(),
+ },
+ );
+
+ if (url.searchParams.get("setCategories") !== null)
+ return Response.json(
+ await setPinnedBadgeWallCategories(userId, [
+ ...(await decodeRequestJsonOrThrow(
+ request,
+ Schema.Array(Schema.String),
+ )),
+ ]),
+ {
+ headers: appOriginHeaders(),
+ },
+ );
+
+ if (url.searchParams.get("biography") !== null)
+ return Response.json(
+ await setBiography(userId, (await request.text()).slice(0, 3000)),
+ {
+ headers: appOriginHeaders(),
+ },
+ );
+
+ return Response.json(
+ await getUserPreferences(Number(url.searchParams.get("id") || 0)),
+ {
+ headers: appOriginHeaders(),
+ },
+ );
};
diff --git a/src/routes/api/preferences/pin/+server.ts b/src/routes/api/preferences/pin/+server.ts
index 28398cf0..b69a8142 100644
--- a/src/routes/api/preferences/pin/+server.ts
+++ b/src/routes/api/preferences/pin/+server.ts
@@ -1,32 +1,32 @@
-import { userIdentity } from '$lib/Data/AniList/identity';
-import { toggleHololiveStreamPinning } from '$lib/Database/SB/User/preferences';
+import { safeUserIdentity } from "$lib/Data/AniList/identity";
+import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie";
+import { toggleHololiveStreamPinning } from "$lib/Database/SB/User/preferences";
+import { appOriginHeaders } from "$lib/Utility/appOrigin";
-const unauthorised = new Response('Unauthorised', { status: 401 });
+const unauthorised = new Response("Unauthorised", { status: 401 });
export const PUT = async ({ cookies, url }) => {
- const userCookie = cookies.get('user');
-
- if (!userCookie) return unauthorised;
-
- const user = JSON.parse(userCookie);
-
- return Response.json(
- await toggleHololiveStreamPinning(
- (
- await userIdentity({
- tokenType: user['token_type'],
- expiresIn: user['expires_in'],
- accessToken: user['access_token'],
- refreshToken: user['refresh_token']
- })
- ).id,
- url.searchParams.get('stream') || ''
- ),
- {
- headers: {
- method: 'PUT',
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- }
- );
+ const userCookie = cookies.get("user");
+
+ if (!userCookie) return unauthorised;
+
+ const user = decodeAuthCookieOrNull(userCookie);
+
+ if (!user) return unauthorised;
+
+ const identity = await safeUserIdentity(user);
+
+ if (!identity) return unauthorised;
+
+ return Response.json(
+ await toggleHololiveStreamPinning(
+ identity.id,
+ url.searchParams.get("stream") || "",
+ ),
+ {
+ headers: appOriginHeaders({
+ method: "PUT",
+ }),
+ },
+ );
};
diff --git a/src/routes/api/subsplease/+server.ts b/src/routes/api/subsplease/+server.ts
index 93e734c2..1f678d8c 100644
--- a/src/routes/api/subsplease/+server.ts
+++ b/src/routes/api/subsplease/+server.ts
@@ -1,16 +1,18 @@
-export const GET = async ({ url }) =>
- Response.json(
- await (
- await fetch(
- `https://subsplease.org/api/?f=schedule&tz=${
- url.searchParams.get('tz') || 'America/Los_Angeles'
- }`
- )
- ).json(),
- {
- headers: {
- 'Cache-Control': 'max-age=86400, s-maxage=86400',
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- }
- );
+import { appOriginHeaders } from "$lib/Utility/appOrigin";
+
+export const GET = async ({ url }) => {
+ const timezone = url.searchParams.get("tz") || "America/Los_Angeles";
+
+ return Response.json(
+ await (
+ await fetch(
+ `https://subsplease.org/api/?f=schedule&tz=${encodeURIComponent(timezone)}`,
+ )
+ ).json(),
+ {
+ headers: appOriginHeaders({
+ "Cache-Control": "max-age=86400, s-maxage=86400",
+ }),
+ },
+ );
+};
diff --git a/src/routes/api/updates/all-novels/+server.ts b/src/routes/api/updates/all-novels/+server.ts
index 5dd2aaad..10a0a436 100644
--- a/src/routes/api/updates/all-novels/+server.ts
+++ b/src/routes/api/updates/all-novels/+server.ts
@@ -1,20 +1,24 @@
+import { appOriginHeaders } from "$lib/Utility/appOrigin";
+
export const GET = async ({ setHeaders }) => {
- setHeaders({
- 'Cache-Control': 'public, max-age=600, s-maxage=600'
- });
+ setHeaders({
+ "Cache-Control": "public, max-age=600, s-maxage=600",
+ });
- return Response.json(
- await (
- await fetch('https://www.wlnupdates.com/api', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Access-Control-Allow-Origin': 'https://due.moe'
- },
- body: JSON.stringify({
- mode: 'get-releases'
- })
- })
- ).json()
- );
+ return Response.json(
+ await (
+ await fetch("https://www.wlnupdates.com/api", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ mode: "get-releases",
+ }),
+ })
+ ).json(),
+ {
+ headers: appOriginHeaders(),
+ },
+ );
};
diff --git a/src/routes/api/updates/manga/+server.ts b/src/routes/api/updates/manga/+server.ts
index af078c05..f8f90c91 100644
--- a/src/routes/api/updates/manga/+server.ts
+++ b/src/routes/api/updates/manga/+server.ts
@@ -1,18 +1,17 @@
-import Parser from 'rss-parser';
+import Parser from "rss-parser";
+import { appOriginHeaders } from "$lib/Utility/appOrigin";
export const GET = async ({ setHeaders }) => {
- setHeaders({
- 'Cache-Control': 'public, max-age=600, s-maxage=600'
- });
+ setHeaders({
+ "Cache-Control": "public, max-age=600, s-maxage=600",
+ });
- return Response.json(
- await new Parser().parseString(
- await (await fetch('https://www.mangaupdates.com/rss.php')).text()
- ),
- {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- }
- );
+ return Response.json(
+ await new Parser().parseString(
+ await (await fetch("https://www.mangaupdates.com/rss.php")).text(),
+ ),
+ {
+ headers: appOriginHeaders(),
+ },
+ );
};
diff --git a/src/routes/api/updates/novels/+server.ts b/src/routes/api/updates/novels/+server.ts
index 7ca6ca43..5e33a2ff 100644
--- a/src/routes/api/updates/novels/+server.ts
+++ b/src/routes/api/updates/novels/+server.ts
@@ -1,18 +1,17 @@
-import Parser from 'rss-parser';
+import Parser from "rss-parser";
+import { appOriginHeaders } from "$lib/Utility/appOrigin";
export const GET = async ({ setHeaders }) => {
- setHeaders({
- 'Cache-Control': 'public, max-age=600, s-maxage=600'
- });
+ setHeaders({
+ "Cache-Control": "public, max-age=600, s-maxage=600",
+ });
- return Response.json(
- await new Parser().parseString(
- await (await fetch('https://api.syosetu.com/allnovel.Atom')).text()
- ),
- {
- headers: {
- 'Access-Control-Allow-Origin': 'https://due.moe'
- }
- }
- );
+ return Response.json(
+ await new Parser().parseString(
+ await (await fetch("https://api.syosetu.com/allnovel.Atom")).text(),
+ ),
+ {
+ headers: appOriginHeaders(),
+ },
+ );
};
diff --git a/src/routes/completed/+page.svelte b/src/routes/completed/+page.svelte
index 3c6b3b4d..bcad912b 100644
--- a/src/routes/completed/+page.svelte
+++ b/src/routes/completed/+page.svelte
@@ -1,35 +1,74 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import { onDestroy, onMount } from 'svelte';
- import userIdentity from '$stores/identity.js';
- import settings from '$stores/settings';
- import WatchingAnimeList from '$lib/List/Anime/CompletedAnimeList.svelte';
- import ListTitle from '$lib/List/ListTitle.svelte';
- import MangaListTemplate from '$lib/List/Manga/MangaListTemplate.svelte';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import LastActivity from '$lib/Home/LastActivity.svelte';
- import { createHeightObserver } from '$lib/Utility/html.js';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import locale from '$stores/locale.js';
- import Landing from '$lib/Landing.svelte';
- import stateBin from '$stores/stateBin.js';
-
- export let data;
-
- let heightObserver: ReturnType<typeof setInterval>;
-
- onMount(() => {
- heightObserver = setInterval(() => createHeightObserver(), 0);
- $stateBin.completedAnimeListOpen ??= true;
- $stateBin.completedMangaListOpen ??= true;
- });
-
- onDestroy(() => clearInterval(heightObserver));
+import type { Component } from "svelte";
+import Spacer from "$lib/Layout/Spacer.svelte";
+import { onDestroy, onMount } from "svelte";
+import userIdentity from "$stores/identity.js";
+import settings from "$stores/settings";
+import ListTitle from "$lib/List/ListTitle.svelte";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import { createHeightObserver } from "$lib/Utility/html.js";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import locale from "$stores/locale.js";
+import Landing from "$lib/Landing.svelte";
+import stateBin, { hydrateStateBin } from "$stores/stateBin.js";
+import type { PageData } from "./$types";
+
+let { data }: { data: PageData } = $props();
+
+type UserProp = PageData["user"];
+type LastActivitySvelteComponent = Component<{
+ user: UserProp;
+}>;
+type WatchingAnimeListSvelteComponent = Component<{
+ user: UserProp;
+}>;
+type MangaListTemplateSvelteComponent = Component<{
+ user: UserProp;
+ displayUnresolved: boolean;
+ due: boolean;
+}>;
+
+let removeHeightObserver: (() => void) | undefined;
+let LastActivityComponent: LastActivitySvelteComponent | null = $state(null);
+let WatchingAnimeListComponent: WatchingAnimeListSvelteComponent | null =
+ $state(null);
+let MangaListTemplateComponent: MangaListTemplateSvelteComponent | null =
+ $state(null);
+let completedSurfaceImport: Promise<void> | null = null;
+
+const loadCompletedSurface = () => {
+ if (data.user === undefined) return null;
+ if (completedSurfaceImport) return completedSurfaceImport;
+
+ completedSurfaceImport = Promise.all([
+ import("$lib/Home/LastActivity.svelte"),
+ import("$lib/List/Anime/CompletedAnimeList.svelte"),
+ import("$lib/List/Manga/MangaListTemplate.svelte"),
+ ]).then(([lastActivity, watchingAnimeList, mangaListTemplate]) => {
+ LastActivityComponent = lastActivity.default;
+ WatchingAnimeListComponent = watchingAnimeList.default;
+ MangaListTemplateComponent = mangaListTemplate.default;
+ });
+
+ return completedSurfaceImport;
+};
+
+onMount(async () => {
+ removeHeightObserver = createHeightObserver();
+ await hydrateStateBin();
+ $stateBin.completedAnimeListOpen ??= true;
+ $stateBin.completedMangaListOpen ??= true;
+ void loadCompletedSurface();
+});
+
+onDestroy(() => removeHeightObserver?.());
</script>
<HeadTitle route="Completed" path="/completed" />
-<LastActivity user={data.user} />
+{#if LastActivityComponent}
+ <LastActivityComponent user={data.user} />
+{/if}
{#if data.user === undefined}
<div class="card">Please log in to view completed media.</div>
@@ -42,7 +81,13 @@
{#if !$settings.displayFiltersIncludeCompleted || !$settings.disableAnime}
<details bind:open={$stateBin.completedAnimeListOpen} class="list">
{#if $userIdentity.id !== -2}
- <WatchingAnimeList user={data.user} />
+ {#if WatchingAnimeListComponent}
+ <WatchingAnimeListComponent user={data.user} />
+ {:else}
+ <ListTitle title={$locale().lists.completed.anime} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
{:else}
<ListTitle title={$locale().lists.completed.anime} />
@@ -54,11 +99,17 @@
{#if !$settings.displayFiltersIncludeCompleted || !$settings.disableManga}
<details bind:open={$stateBin.completedMangaListOpen} class="list">
{#if $userIdentity.id !== -2}
- <MangaListTemplate
- user={data.user}
- displayUnresolved={$settings.displayUnresolved}
- due={false}
- />
+ {#if MangaListTemplateComponent}
+ <MangaListTemplateComponent
+ user={data.user}
+ displayUnresolved={$settings.displayUnresolved}
+ due={false}
+ />
+ {:else}
+ <ListTitle title={$locale().lists.completed.mangaAndLightNovels} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
{:else}
<ListTitle title={$locale().lists.completed.mangaAndLightNovels} />
diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte
index fa833c1f..88a3da9e 100644
--- a/src/routes/events/+page.svelte
+++ b/src/routes/events/+page.svelte
@@ -1,9 +1,9 @@
<script>
- import Spacer from '$lib/Layout/Spacer.svelte';
- import Event from '$lib/Events/Event.svelte';
- import Message from '$lib/Loading/Message.svelte';
+import Spacer from "$lib/Layout/Spacer.svelte";
+import Event from "$lib/Events/Event.svelte";
+import Message from "$lib/Loading/Message.svelte";
- import root from '$lib/Utility/root';
+import root from "$lib/Utility/root";
</script>
{#await fetch(root(`/api/events`))}
diff --git a/src/routes/events/group/[group]/+page.server.ts b/src/routes/events/group/[group]/+page.server.ts
index 3df284fb..233aaab8 100644
--- a/src/routes/events/group/[group]/+page.server.ts
+++ b/src/routes/events/group/[group]/+page.server.ts
@@ -1,5 +1,5 @@
export const load = ({ params }) => {
- return {
- group: params.group
- };
+ return {
+ group: params.group,
+ };
};
diff --git a/src/routes/events/group/[group]/+page.svelte b/src/routes/events/group/[group]/+page.svelte
index 6265fb75..c02c1b51 100644
--- a/src/routes/events/group/[group]/+page.svelte
+++ b/src/routes/events/group/[group]/+page.svelte
@@ -1,24 +1,25 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import type { Group as GroupType } from '$lib/Database/SB/groups.js';
- import type { Event as EventType } from '$lib/Database/SB/events.js';
- import Message from '$lib/Loading/Message.svelte';
- import root from '$lib/Utility/root';
- import { onMount } from 'svelte';
- import Group from '$lib/Events/Group.svelte';
- import Event from '$lib/Events/Event.svelte';
+import Spacer from "$lib/Layout/Spacer.svelte";
+import type { Group as GroupType } from "$lib/Database/SB/groups.js";
+import type { Event as EventType } from "$lib/Database/SB/events.js";
+import Message from "$lib/Loading/Message.svelte";
+import root from "$lib/Utility/root";
+import { onMount } from "svelte";
+import Group from "$lib/Events/Group.svelte";
+import Event from "$lib/Events/Event.svelte";
+import type { PageData } from "./$types";
- export let data;
+export let data: PageData;
- let groupsResponse: Promise<Response>;
+let groupsResponse: Promise<Response>;
- onMount(async () => {
- groupsResponse = fetch(root(`/api/events/group?slug=${data.group}`));
- });
+onMount(async () => {
+ groupsResponse = fetch(root(`/api/events/group?slug=${data.group}`));
+});
- const asGroup = (group: unknown) => group as GroupType;
+const asGroup = (group: unknown) => group as GroupType;
- const asEvent = (event: unknown) => event as EventType;
+const asEvent = (event: unknown) => event as EventType;
</script>
{#await groupsResponse}
diff --git a/src/routes/events/groups/+page.svelte b/src/routes/events/groups/+page.svelte
index 685225be..b6181ad8 100644
--- a/src/routes/events/groups/+page.svelte
+++ b/src/routes/events/groups/+page.svelte
@@ -1,18 +1,18 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import type { Group as GroupType } from '$lib/Database/SB/groups';
- import Message from '$lib/Loading/Message.svelte';
- import root from '$lib/Utility/root';
- import { onMount } from 'svelte';
- import Group from '$lib/Events/Group.svelte';
+import Spacer from "$lib/Layout/Spacer.svelte";
+import type { Group as GroupType } from "$lib/Database/SB/groups";
+import Message from "$lib/Loading/Message.svelte";
+import root from "$lib/Utility/root";
+import { onMount } from "svelte";
+import Group from "$lib/Events/Group.svelte";
- let groupsResponse: Promise<Response>;
+let groupsResponse: Promise<Response>;
- onMount(async () => {
- groupsResponse = fetch(root('/api/events/groups'));
- });
+onMount(async () => {
+ groupsResponse = fetch(root("/api/events/groups"));
+});
- const asGroup = (group: unknown) => group as GroupType;
+const asGroup = (group: unknown) => group as GroupType;
</script>
{#await groupsResponse}
diff --git a/src/routes/feeds/activity-notifications/+server.ts b/src/routes/feeds/activity-notifications/+server.ts
index 37190255..70b24a20 100644
--- a/src/routes/feeds/activity-notifications/+server.ts
+++ b/src/routes/feeds/activity-notifications/+server.ts
@@ -1,54 +1,65 @@
-import { notifications, type Notification } from '$lib/Data/AniList/notifications';
-import root from '$lib/Utility/root';
+import {
+ notifications,
+ type Notification,
+} from "$lib/Data/AniList/notifications";
+import { siteUrl } from "$lib/Utility/appOrigin";
+import root from "$lib/Utility/root";
const htmlEncode = (input: string) => {
- return input.replace(/[\u00A0-\u9999<>&]/g, (i) => '&#' + i.charCodeAt(0) + ';');
+ return input.replace(
+ /[\u00A0-\u9999<>&]/g,
+ (i) => "&#" + i.charCodeAt(0) + ";",
+ );
};
-const render = (posts: Notification[] = []) => `<?xml version="1.0" encoding="UTF-8" ?>
+const render = (
+ posts: Notification[] = [],
+) => `<?xml version="1.0" encoding="UTF-8" ?>
<rss
version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
- xmlns:content="http://purl.org/rss/1.0/modules/content/"
- xmlns:snf="http://www.smartnews.be/snf"
- xmlns:media="http://search.yahoo.com/mrss/">
+xmlns:content="http://purl.org/rss/1.0/modules/content/"
+xmlns:snf="http://www.smartnews.be/snf"
+xmlns:media="http://search.yahoo.com/mrss/">
<channel>
- <atom:link href="https://due.moe/feeds/activity-notifications" rel="self" type="application/rss+xml" />
+ <atom:link href="${siteUrl("/feeds/activity-notifications")}" rel="self" type="application/rss+xml" />
<title>AniList Notifications • due.moe</title>
- <link>https://due.moe</link>
+ <link>${siteUrl("/")}</link>
<description>Instantly view your AniList notifications via RSS!</description>
<pubDate>${new Date().toUTCString()}</pubDate>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<language>en-US</language>
- <snf:logo><url>https://due.moe/favicon-196x196.png</url></snf:logo>
+ <snf:logo><url>${siteUrl("/favicon-196x196.png")}</url></snf:logo>
${posts
- .filter((notification: Notification) => notification.type !== undefined)
- .map((notification: Notification) => {
- let title = `${notification.user.name}${notification.context}`;
- let link = `https://anilist.co/user/${notification.user.name}`;
- const prettyType = notification.type
- .toString()
- .replace(/_/g, ' ')
- .toLowerCase()
- .replace(/\w\S*/g, (text) => {
- return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
- });
+ .filter((notification: Notification) => notification.type !== undefined)
+ .map((notification: Notification) => {
+ let title = `${notification.user.name}${notification.context}`;
+ let link = `https://anilist.co/user/${notification.user.name}`;
+ const prettyType = notification.type
+ .toString()
+ .replace(/_/g, " ")
+ .toLowerCase()
+ .replace(/\w\S*/g, (text) => {
+ return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
+ });
- try {
- if (
- !['FOLLOWING', 'ACTIVITY_MESSAGE'].includes(notification.type.toString()) &&
- !notification.type.toString().includes('THREAD')
- ) {
- link = `https://anilist.co/activity/${notification.activity.id}`;
- } else if (notification.type.toString().includes('THREAD')) {
- title += `${notification.thread.title}`;
- link = `https://anilist.co/forum/thread/${notification.thread.id}`;
- }
- } catch {
- return '';
- }
+ try {
+ if (
+ !["FOLLOWING", "ACTIVITY_MESSAGE"].includes(
+ notification.type.toString(),
+ ) &&
+ !notification.type.toString().includes("THREAD")
+ ) {
+ link = `https://anilist.co/activity/${notification.activity.id}`;
+ } else if (notification.type.toString().includes("THREAD")) {
+ title += `${notification.thread.title}`;
+ link = `https://anilist.co/forum/thread/${notification.thread.id}`;
+ }
+ } catch {
+ return "";
+ }
- return `<item>
+ return `<item>
<guid isPermaLink="false">${notification.id}</guid>
<title>${htmlEncode(title)}</title>
<link>${link}</link>
@@ -57,29 +68,29 @@ const render = (posts: Notification[] = []) => `<?xml version="1.0" encoding="UT
<category>${prettyType}</category>
<pubDate>${new Date(notification.createdAt * 1000).toUTCString()}</pubDate>
</item>`;
- })
- .join('')}
+ })
+ .join("")}
</channel>
</rss>
`;
export const GET = async ({ url }) => {
- let token = url.searchParams.get('token');
- const refresh = url.searchParams.get('refresh');
- let notification = await notifications(token || '');
+ let token = url.searchParams.get("token");
+ const refresh = url.searchParams.get("refresh");
+ let notification = await notifications(token || "");
- if (notification === null) {
- token = (await (await fetch(root(`/api/oauth/refresh?token=${refresh}`))).json())[
- 'access_token'
- ];
+ if (notification === null) {
+ token = (
+ await (await fetch(root(`/api/oauth/refresh?token=${refresh}`))).json()
+ )["access_token"];
- notification = await notifications(token as string);
- }
+ notification = await notifications(token as string);
+ }
- return new Response(token ? render(notification || []) : render(), {
- headers: {
- 'Cache-Control': `max-age=0`,
- 'Content-Type': 'application/xml'
- }
- });
+ return new Response(token ? render(notification || []) : render(), {
+ headers: {
+ "Cache-Control": `max-age=0`,
+ "Content-Type": "application/xml",
+ },
+ });
};
diff --git a/src/routes/girls/+page.svelte b/src/routes/girls/+page.svelte
index 0dcf75c4..88f06460 100644
--- a/src/routes/girls/+page.svelte
+++ b/src/routes/girls/+page.svelte
@@ -1,11 +1,11 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import Senpy from '$lib/Data/senpy';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import Message from '$lib/Loading/Message.svelte';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import root from '$lib/Utility/root';
- import '$styles/girls.scss';
+import Spacer from "$lib/Layout/Spacer.svelte";
+import Senpy from "$lib/Data/senpy";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import Message from "$lib/Loading/Message.svelte";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import root from "$lib/Utility/root";
+import "$styles/girls.scss";
</script>
<HeadTitle route="Anime Girls Holding Programming Books" path="/girls" />
diff --git a/src/routes/girls/[language]/+page.server.ts b/src/routes/girls/[language]/+page.server.ts
index 62fbb311..71595b9e 100644
--- a/src/routes/girls/[language]/+page.server.ts
+++ b/src/routes/girls/[language]/+page.server.ts
@@ -1,5 +1,5 @@
export const load = ({ params }) => {
- return {
- language: params.language
- };
+ return {
+ language: params.language,
+ };
};
diff --git a/src/routes/girls/[language]/+page.svelte b/src/routes/girls/[language]/+page.svelte
index 91b18628..2a97605f 100644
--- a/src/routes/girls/[language]/+page.svelte
+++ b/src/routes/girls/[language]/+page.svelte
@@ -1,10 +1,11 @@
<script lang="ts">
- import Senpy from '$lib/Data/senpy';
- import Message from '$lib/Loading/Message.svelte';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import '$styles/girls.scss';
+import Senpy from "$lib/Data/senpy";
+import Message from "$lib/Loading/Message.svelte";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import "$styles/girls.scss";
+import type { PageData } from "./$types";
- export let data;
+export let data: PageData;
</script>
<div class="card">
diff --git a/src/routes/graphql/+server.ts b/src/routes/graphql/+server.ts
index f8e47dc5..2b19b3ff 100644
--- a/src/routes/graphql/+server.ts
+++ b/src/routes/graphql/+server.ts
@@ -1 +1,5 @@
-export { default as GET, default as POST, default as OPTIONS } from '$graphql/server';
+export {
+ default as GET,
+ default as POST,
+ default as OPTIONS,
+} from "$graphql/server";
diff --git a/src/routes/hololive/[[stream]]/+page.server.ts b/src/routes/hololive/[[stream]]/+page.server.ts
index 6eb5dad1..04184eb3 100644
--- a/src/routes/hololive/[[stream]]/+page.server.ts
+++ b/src/routes/hololive/[[stream]]/+page.server.ts
@@ -1,5 +1,5 @@
export const load = ({ params }) => {
- return {
- stream: params.stream
- };
+ return {
+ stream: params.stream,
+ };
};
diff --git a/src/routes/hololive/[[stream]]/+page.svelte b/src/routes/hololive/[[stream]]/+page.svelte
index 15d2125c..573c16aa 100644
--- a/src/routes/hololive/[[stream]]/+page.svelte
+++ b/src/routes/hololive/[[stream]]/+page.svelte
@@ -1,50 +1,52 @@
<script lang="ts">
- import { onMount } from 'svelte';
- import Message from '$lib/Loading/Message.svelte';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import { parseScheduleHtml } from '$lib/Data/hololive';
- import proxy from '$lib/Utility/proxy';
- import locale from '$stores/locale';
- import root from '$lib/Utility/root';
- import identity from '$stores/identity';
- import Lives from '$lib/Hololive/Lives.svelte';
- import { typeSchedule } from '$lib/Hololive/hololive';
+import { onMount } from "svelte";
+import Message from "$lib/Loading/Message.svelte";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import { parseScheduleHtml } from "$lib/Data/hololive";
+import proxy from "$lib/Utility/proxy";
+import locale from "$stores/locale";
+import root from "$lib/Utility/root";
+import identity from "$stores/identity";
+import Lives from "$lib/Hololive/Lives.svelte";
+import { typeSchedule } from "$lib/Hololive/hololive";
+import type { PageData } from "./$types";
- export let data;
+export let data: PageData;
- let schedulePromise: Promise<Response>;
- let pinnedStreams: string[] = [];
+let schedulePromise: Promise<Response>;
+let pinnedStreams: string[] = [];
- onMount(() => getPinnedStreams());
+onMount(() => getPinnedStreams());
- const getPinnedStreams = () => {
- let streams: string[] = [];
+const getPinnedStreams = () => {
+ let streams: string[] = [];
- const setSchedule = () => {
- pinnedStreams = streams;
- schedulePromise = fetch(proxy('https://schedule.hololive.tv'), {
- headers: {
- Cookie: 'timezone=Asia/Tokyo'
- }
- });
- };
+ const setSchedule = () => {
+ pinnedStreams = streams;
+ schedulePromise = fetch(proxy("https://schedule.hololive.tv"), {
+ headers: {
+ Cookie: "timezone=Asia/Tokyo",
+ },
+ });
+ };
- if ($identity.id !== -2) {
- fetch(root(`/api/preferences?id=${$identity.id}`)).then((response) => {
- if (response.ok)
- response.json().then((data) => {
- if (data && data.pinned_hololive_streams) streams = data.pinned_hololive_streams;
+ if ($identity.id !== -2) {
+ fetch(root(`/api/preferences?id=${$identity.id}`)).then((response) => {
+ if (response.ok)
+ response.json().then((data) => {
+ if (data && data.pinned_hololive_streams)
+ streams = data.pinned_hololive_streams;
- setSchedule();
- });
- });
+ setSchedule();
+ });
+ });
- return;
- }
+ return;
+ }
- setSchedule();
- };
+ setSchedule();
+};
</script>
<HeadTitle route="hololive Schedule" path="/hololive" />
diff --git a/src/routes/reader/+page.svelte b/src/routes/reader/+page.svelte
index 775d3659..b6cce066 100644
--- a/src/routes/reader/+page.svelte
+++ b/src/routes/reader/+page.svelte
@@ -1,14 +1,19 @@
<script>
- import Notice from '$lib/Error/Notice.svelte';
- import Message from '$lib/Loading/Message.svelte';
- import MangaDexChapters from '$lib/Reader/Chapters/MangaDex.svelte';
- import RawkumaChapters from '$lib/Reader/Chapters/Rawkuma.svelte';
- import { decodeResource, fetchResource, identify, Resource } from '$lib/Reader/resource';
- import InputTemplate from '$lib/Tools/InputTemplate.svelte';
+import Notice from "$lib/Error/Notice.svelte";
+import Message from "$lib/Loading/Message.svelte";
+import MangaDexChapters from "$lib/Reader/Chapters/MangaDex.svelte";
+import RawkumaChapters from "$lib/Reader/Chapters/Rawkuma.svelte";
+import {
+ decodeResource,
+ fetchResource,
+ identify,
+ Resource,
+} from "$lib/Reader/resource";
+import InputTemplate from "$lib/Tools/InputTemplate.svelte";
- let submission = '';
+let submission = "";
- $: resourceIdentity = identify(submission);
+$: resourceIdentity = identify(submission);
</script>
<InputTemplate field="Manga URL" bind:submission submitText="Read" preserveCase>
diff --git a/src/routes/schedule/+page.svelte b/src/routes/schedule/+page.svelte
index 5ff212ef..a1674c8f 100644
--- a/src/routes/schedule/+page.svelte
+++ b/src/routes/schedule/+page.svelte
@@ -1,29 +1,36 @@
<script lang="ts">
- import Error from '$lib/Error/RateLimited.svelte';
- import { onMount } from 'svelte';
- import { parseOrDefault } from '$lib/Utility/parameters';
- import { browser } from '$app/environment';
- import type { Media } from '$lib/Data/AniList/media';
- import { scheduleMediaListCollection } from '$lib/Data/AniList/schedule';
- import { season } from '$lib/Media/Anime/season';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- // import Crunchyroll from '$lib/Schedule/Crunchyroll.svelte';
- import '$lib/Schedule/container.css';
- import Days from '$lib/Schedule/Days.svelte';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import Message from '$lib/Loading/Message.svelte';
- import subsPlease from '$stores/subsPlease';
+import RateLimitedError from "$lib/Error/RateLimited.svelte";
+import { onMount } from "svelte";
+import { parseOrDefault } from "$lib/Utility/parameters";
+import { browser } from "$app/environment";
+import type { Media } from "$lib/Data/AniList/media";
+import { scheduleMediaListCollection } from "$lib/Data/AniList/schedule";
+import { season } from "$lib/Media/Anime/season";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+// import Crunchyroll from '$lib/Schedule/Crunchyroll.svelte';
+import "$lib/Schedule/container.css";
+import Days from "$lib/Schedule/Days.svelte";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import Message from "$lib/Loading/Message.svelte";
+import subsPlease from "$stores/subsPlease";
+import type { PageData } from "./$types";
- export let data;
+export let data: PageData;
- let scheduledMediaPromise: Promise<Partial<Media[]>>;
- const urlParameters = browser ? new URLSearchParams(window.location.search) : null;
- // let crunchyrollExpanded = false;
- let forceListMode = parseOrDefault(urlParameters, 'list', false);
+let scheduledMediaPromise: Promise<Partial<Media[]>>;
+const urlParameters = browser
+ ? new URLSearchParams(window.location.search)
+ : null;
+// let crunchyrollExpanded = false;
+let forceListMode = parseOrDefault(urlParameters, "list", false);
- onMount(async () => {
- scheduledMediaPromise = scheduleMediaListCollection(new Date().getFullYear(), season(), true);
- });
+onMount(async () => {
+ scheduledMediaPromise = scheduleMediaListCollection(
+ new Date().getFullYear(),
+ season(),
+ true,
+ );
+});
</script>
<HeadTitle route="Schedule" path="/schedule" />
@@ -74,7 +81,7 @@
<Skeleton grid={true} count={7} height="15em" width="49.5%" />
{/if}
{:catch}
- <Error type="Media" loginSessionError={false} card list={false} />
+ <RateLimitedError type="Media" loginSessionError={false} card list={false} />
{/await}
{/if}
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte
index a6223b5f..33c7b096 100644
--- a/src/routes/settings/+page.svelte
+++ b/src/routes/settings/+page.svelte
@@ -1,31 +1,31 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- /* eslint svelte/no-at-html-tags: "off" */
+import Spacer from "$lib/Layout/Spacer.svelte";
- import Attributions from '$lib/Settings/Categories/Attributions.svelte';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import Display from '$lib/Settings/Categories/Display.svelte';
- import Calculation from '$lib/Settings/Categories/Calculation.svelte';
- import Debug from '$lib/Settings/Categories/Debug.svelte';
- import Cache from '$lib/Settings/Categories/Cache.svelte';
- import Category from '$lib/Settings/Category.svelte';
- import tooltip from '$lib/Tooltip/tooltip';
- import locale from '$stores/locale.js';
- import settings from '$stores/settings';
- import LogInRestricted from '$lib/Error/LogInRestricted.svelte';
- import SettingSync from '$lib/Settings/Categories/SettingSync.svelte';
- import RssFeeds from '$lib/Settings/Categories/RSSFeeds.svelte';
+import Attributions from "$lib/Settings/Categories/Attributions.svelte";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import Display from "$lib/Settings/Categories/Display.svelte";
+import Calculation from "$lib/Settings/Categories/Calculation.svelte";
+import Debug from "$lib/Settings/Categories/Debug.svelte";
+import Cache from "$lib/Settings/Categories/Cache.svelte";
+import Category from "$lib/Settings/Category.svelte";
+import tooltip from "$lib/Tooltip/tooltip";
+import locale from "$stores/locale.js";
+import settings from "$stores/settings";
+import LogInRestricted from "$lib/Error/LogInRestricted.svelte";
+import SettingSync from "$lib/Settings/Categories/SettingSync.svelte";
+import RssFeeds from "$lib/Settings/Categories/RSSFeeds.svelte";
+import type { PageData } from "./$types";
- export let data;
+export let data: PageData;
- // const pruneUnresolved = async () => {
- // const unresolved = await chapterDatabase.chapters.where('chapters').equals(-1).toArray();
- // const ids = unresolved.map((m) => m.id);
+// const pruneUnresolved = async () => {
+// const unresolved = await chapterDatabase.chapters.where('chapters').equals(-1).toArray();
+// const ids = unresolved.map((m) => m.id);
- // manga.set('');
- // anime.set('');
- // await chapterDatabase.chapters.bulkDelete(ids);
- // };
+// manga.set('');
+// anime.set('');
+// await chapterDatabase.chapters.bulkDelete(ids);
+// };
</script>
<HeadTitle route="Settings" path="/settings" />
diff --git a/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte
index 56ef8a10..572f4fc5 100644
--- a/src/routes/tools/+page.svelte
+++ b/src/routes/tools/+page.svelte
@@ -1,11 +1,11 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import Picker from '$lib/Tools/Picker.svelte';
- import { tools } from '$lib/Tools/tools.js';
- import root from '$lib/Utility/root';
+import Spacer from "$lib/Layout/Spacer.svelte";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import Picker from "$lib/Tools/Picker.svelte";
+import { tools } from "$lib/Tools/tools.js";
+import root from "$lib/Utility/root";
- let tool = 'default';
+let tool = "default";
</script>
<Picker {tool} />
diff --git a/src/routes/tools/[tool]/+page.server.ts b/src/routes/tools/[tool]/+page.server.ts
index 11ba6d15..c4941121 100644
--- a/src/routes/tools/[tool]/+page.server.ts
+++ b/src/routes/tools/[tool]/+page.server.ts
@@ -1,5 +1,5 @@
export const load = ({ params }) => {
- return {
- tool: params.tool
- };
+ return {
+ tool: params.tool,
+ };
};
diff --git a/src/routes/tools/[tool]/+page.svelte b/src/routes/tools/[tool]/+page.svelte
index 68eb0c07..ff764add 100644
--- a/src/routes/tools/[tool]/+page.svelte
+++ b/src/routes/tools/[tool]/+page.svelte
@@ -1,37 +1,38 @@
<script lang="ts">
- import Hayai from './../../../lib/Tools/Hayai.svelte';
- import UmaMusumeBirthdays from './../../../lib/Tools/UmaMusumeBirthdays.svelte';
- import ActivityHistory from '$lib/Tools/ActivityHistory/Tool.svelte';
- import Wrapped from '$lib/Tools/Wrapped/Tool.svelte';
- import EpisodeDiscussionCollector from '$lib/Tools/EpisodeDiscussionCollector.svelte';
- import CharacterBirthdays from '$lib/Tools/Birthdays.svelte';
- import SequelSpy from '$lib/Tools/SequelSpy/Tool.svelte';
- import { closest } from '$lib/Error/path';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import RandomFollower from '$lib/Tools/RandomFollower.svelte';
- // import DumpProfile from '$lib/Tools/DumpProfile.svelte';
- import { tools } from '$lib/Tools/tools.js';
- import { onMount } from 'svelte';
- import { goto } from '$app/navigation';
- import Picker from '$lib/Tools/Picker.svelte';
- import Likes from '$lib/Tools/Likes.svelte';
- import root from '$lib/Utility/root.js';
- import Popup from '$lib/Layout/Popup.svelte';
- import SequelCatcher from '$lib/Tools/SequelCatcher/Tool.svelte';
- import Tracker from '$lib/Tools/Tracker/Tool.svelte';
- import BirthdaysTemplate from '$lib/Tools/BirthdaysTemplate.svelte';
+import Hayai from "./../../../lib/Tools/Hayai.svelte";
+import UmaMusumeBirthdays from "./../../../lib/Tools/UmaMusumeBirthdays.svelte";
+import ActivityHistory from "$lib/Tools/ActivityHistory/Tool.svelte";
+import Wrapped from "$lib/Tools/Wrapped/Tool.svelte";
+import EpisodeDiscussionCollector from "$lib/Tools/EpisodeDiscussionCollector.svelte";
+import CharacterBirthdays from "$lib/Tools/Birthdays.svelte";
+import SequelSpy from "$lib/Tools/SequelSpy/Tool.svelte";
+import { closest } from "$lib/Error/path";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import RandomFollower from "$lib/Tools/RandomFollower.svelte";
+// import DumpProfile from '$lib/Tools/DumpProfile.svelte';
+import { tools } from "$lib/Tools/tools.js";
+import { onMount } from "svelte";
+import { goto } from "$app/navigation";
+import Picker from "$lib/Tools/Picker.svelte";
+import Likes from "$lib/Tools/Likes.svelte";
+import root from "$lib/Utility/root.js";
+import Popup from "$lib/Layout/Popup.svelte";
+import SequelCatcher from "$lib/Tools/SequelCatcher/Tool.svelte";
+import Tracker from "$lib/Tools/Tracker/Tool.svelte";
+import BirthdaysTemplate from "$lib/Tools/BirthdaysTemplate.svelte";
+import type { PageData } from "./$types";
- export let data;
+export let data: PageData;
- let tool = data.tool ?? 'default';
+let tool = data.tool ?? "default";
- onMount(() => {
- if (tool === 'default') goto(root('/tools'));
- });
+onMount(() => {
+ if (tool === "default") goto(root("/tools"));
+});
- $: suggestion = closest(tool, Object.keys(tools));
+$: suggestion = closest(tool, Object.keys(tools));
- $: if (tool == 'girls') goto(root('/girls'));
+$: if (tool === "girls") goto(root("/girls"));
</script>
<Picker bind:tool />
diff --git a/src/routes/updates/+page.svelte b/src/routes/updates/+page.svelte
index 22fcd3a7..7dc628ca 100644
--- a/src/routes/updates/+page.svelte
+++ b/src/routes/updates/+page.svelte
@@ -1,59 +1,67 @@
<script lang="ts">
- /* eslint svelte/no-at-html-tags: "off" */
-
- import { browser } from '$app/environment';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import { createHeightObserver } from '$lib/Utility/html';
- import root from '$lib/Utility/root';
- import { onDestroy, onMount } from 'svelte';
-
- let feed: { items: { title: string; link: string; content: string }[] } | null | undefined =
- undefined;
- let novelFeed:
- | {
- data: {
- items: { srcurl: string; postfix?: string; chapter: number; series: { name: string } }[];
- };
- }
- | undefined = undefined;
- let startTime: number;
- let mangaEndTime: number;
- let novelEndTime: number;
- let directLink = browser ? new URLSearchParams(window.location.search).has('d') : false;
- let heightObserver: ReturnType<typeof setInterval>;
-
- onMount(async () => {
- heightObserver = setInterval(() => createHeightObserver(false), 0);
-
- startTime = performance.now();
- novelFeed = await (await fetch(root('/api/updates/all-novels'))).json();
- novelEndTime = performance.now() - startTime;
- startTime = performance.now();
- feed = await (await fetch(root('/api/updates/manga'))).json();
- mangaEndTime = performance.now() - startTime;
- });
-
- onDestroy(() => clearInterval(heightObserver));
-
- const reformatChapter = (title: string) =>
- title
- .replace(/\[.*?\]\s/, '')
- .replace(/c\.Oneshot/, 'Oneshot')
- .replace(/c\.(\d+-\d+)/, 'Ch. $1')
- .replace(/v\.(\d+)\s/, 'Vol. $1 ')
- .replace(/c\.(\d+)/, 'Ch. $1');
-
- const clipTitle = (title: string) =>
- title
- .replace(/(Vol\. \d+ )?Ch\. \d+(-\d+(\.\d+)?)?$/, '')
- .replace(/\? ~.*$/, '')
- .trim();
-
- // const italicTitle = (title: string) =>
- // title.replace(/^(.*?) (Vol\.|Ch\.|\bOneshot\b)/, '<i>$1</i> $2');
-
- const chapterTitle = (title: string) => title.replace(/^(.*?) (Vol\.|Ch\.|\bOneshot\b)/, '$2');
+import { browser } from "$app/environment";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import { createHeightObserver } from "$lib/Utility/html";
+import root from "$lib/Utility/root";
+import { onDestroy, onMount } from "svelte";
+
+let feed:
+ | { items: { title: string; link: string; content: string }[] }
+ | null
+ | undefined = undefined;
+let novelFeed:
+ | {
+ data: {
+ items: {
+ srcurl: string;
+ postfix?: string;
+ chapter: number;
+ series: { name: string };
+ }[];
+ };
+ }
+ | undefined = undefined;
+let startTime: number;
+let mangaEndTime: number;
+let novelEndTime: number;
+let directLink = browser
+ ? new URLSearchParams(window.location.search).has("d")
+ : false;
+let removeHeightObserver: (() => void) | undefined;
+
+onMount(async () => {
+ removeHeightObserver = createHeightObserver(false);
+
+ startTime = performance.now();
+ novelFeed = await (await fetch(root("/api/updates/all-novels"))).json();
+ novelEndTime = performance.now() - startTime;
+ startTime = performance.now();
+ feed = await (await fetch(root("/api/updates/manga"))).json();
+ mangaEndTime = performance.now() - startTime;
+});
+
+onDestroy(() => removeHeightObserver?.());
+
+const reformatChapter = (title: string) =>
+ title
+ .replace(/\[.*?\]\s/, "")
+ .replace(/c\.Oneshot/, "Oneshot")
+ .replace(/c\.(\d+-\d+)/, "Ch. $1")
+ .replace(/v\.(\d+)\s/, "Vol. $1 ")
+ .replace(/c\.(\d+)/, "Ch. $1");
+
+const clipTitle = (title: string) =>
+ title
+ .replace(/(Vol\. \d+ )?Ch\. \d+(-\d+(\.\d+)?)?$/, "")
+ .replace(/\? ~.*$/, "")
+ .trim();
+
+// const italicTitle = (title: string) =>
+// title.replace(/^(.*?) (Vol\.|Ch\.|\bOneshot\b)/, '<i>$1</i> $2');
+
+const chapterTitle = (title: string) =>
+ title.replace(/^(.*?) (Vol\.|Ch\.|\bOneshot\b)/, "$2");
</script>
<HeadTitle route="Updates" path="/updates" />
diff --git a/src/routes/user/+page.svelte b/src/routes/user/+page.svelte
index eab089c6..20a8d390 100644
--- a/src/routes/user/+page.svelte
+++ b/src/routes/user/+page.svelte
@@ -1,31 +1,32 @@
<script lang="ts">
- import { browser } from '$app/environment';
- import { goto } from '$app/navigation';
- import type { UserIdentity } from '$lib/Data/AniList/identity';
- import { onMount } from 'svelte';
- import { env } from '$env/dynamic/public';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import root from '$lib/Utility/root';
- import { page } from '$app/stores';
- import localforage from 'localforage';
+import { browser } from "$app/environment";
+import { goto } from "$app/navigation";
+import type { UserIdentity } from "$lib/Data/AniList/identity";
+import { onMount } from "svelte";
+import { env } from "$env/dynamic/public";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import root from "$lib/Utility/root";
+import { page } from "$app/stores";
+import localforage from "localforage";
- onMount(async () => {
- if (browser) {
- const user = ((await localforage.getItem('identity')) as UserIdentity).name;
+onMount(async () => {
+ if (browser) {
+ const identity = await localforage.getItem<UserIdentity>("identity");
+ const user = identity?.name;
- if (user) {
- if (browser && $page.url.searchParams.get('badges') !== null) {
- goto(root(`/user/${user}/badges`));
- } else {
- goto(root(`/user/${user}`));
- }
- } else {
- goto(
- `https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code`
- );
- }
- }
- });
+ if (user) {
+ if (browser && $page.url.searchParams.get("badges") !== null) {
+ goto(root(`/user/${user}/badges`));
+ } else {
+ goto(root(`/user/${user}`));
+ }
+ } else {
+ goto(
+ `https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code`,
+ );
+ }
+ }
+});
</script>
<HeadTitle route="Profile" path="/user" />
diff --git a/src/routes/user/[user]/+page.gql b/src/routes/user/[user]/+page.gql
index 491290aa..fd31248b 100644
--- a/src/routes/user/[user]/+page.gql
+++ b/src/routes/user/[user]/+page.gql
@@ -1,18 +1,18 @@
query Profile($id: Int!) {
- User(id: $id) {
- id
- badgesCount
+ User(id: $id) {
+ id
+ badgesCount
- preferences {
- created_at
- updated_at
- user_id
- pinned_hololive_streams
- hide_missing_badges
- biography
- badge_wall_css
- hide_awc_badges
- pinned_badge_wall_categories
- }
- }
+ preferences {
+ created_at
+ updated_at
+ user_id
+ pinned_hololive_streams
+ hide_missing_badges
+ biography
+ badge_wall_css
+ hide_awc_badges
+ pinned_badge_wall_categories
+ }
+ }
}
diff --git a/src/routes/user/[user]/+page.svelte b/src/routes/user/[user]/+page.svelte
index 3c581c38..fd1e2d7e 100644
--- a/src/routes/user/[user]/+page.svelte
+++ b/src/routes/user/[user]/+page.svelte
@@ -1,36 +1,49 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import settings from '$stores/settings';
- import ParallaxImage from '../../../lib/Image/ParallaxImage.svelte';
- import { typeSchedule, type ParseResult } from '$lib/Hololive/hololive';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import Message from '$lib/Loading/Message.svelte';
- import { estimatedDayReading } from '$lib/Media/Manga/time';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import root from '$lib/Utility/root';
- import locale from '$stores/locale';
- import { onMount } from 'svelte';
- import authorisedUsers from '$lib/Data/Static/authorised.json';
- import tooltip from '$lib/Tooltip/tooltip';
- import AnimeRateLimited from '$lib/Error/AnimeRateLimited.svelte';
- import identity from '$stores/identity';
- import SettingHint from '$lib/Settings/SettingHint.svelte';
- import proxy from '$lib/Utility/proxy';
- import { parseScheduleHtml } from '$lib/Data/hololive';
- import type { Preferences } from '../../../graphql/$types';
- import SvelteMarkdown from '@humanspeak/svelte-markdown';
- import MarkdownLink from '$lib/MarkdownLink.svelte';
- import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte';
- import { graphql } from '$houdini';
-
- export let data;
-
- $: ({ Profile } = data);
- $: preferences = $Profile.fetching
- ? undefined
- : ($Profile.data?.User?.preferences as Preferences | undefined);
-
- const setCategoriesQuery = graphql(`
+import Spacer from "$lib/Layout/Spacer.svelte";
+import settings from "$stores/settings";
+import ParallaxImage from "../../../lib/Image/ParallaxImage.svelte";
+import { typeSchedule, type ParseResult } from "$lib/Hololive/hololive";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import Message from "$lib/Loading/Message.svelte";
+import { estimatedDayReading } from "$lib/Media/Manga/time";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import root from "$lib/Utility/root";
+import locale from "$stores/locale";
+import { onMount } from "svelte";
+import authorisedUsers from "$lib/Data/Static/authorised.json";
+import tooltip from "$lib/Tooltip/tooltip";
+import AnimeRateLimited from "$lib/Error/AnimeRateLimited.svelte";
+import identity from "$stores/identity";
+import SettingHint from "$lib/Settings/SettingHint.svelte";
+import proxy from "$lib/Utility/proxy";
+import { parseScheduleHtml } from "$lib/Data/hololive";
+import type { Preferences } from "../../../graphql/$types";
+import SvelteMarkdown from "@humanspeak/svelte-markdown";
+import MarkdownLink from "$lib/MarkdownLink.svelte";
+import LinkedTooltip from "$lib/Tooltip/LinkedTooltip.svelte";
+import { graphql } from "$houdini";
+import type { PageData } from "./$types";
+
+export let data: PageData;
+
+$: ({ Profile } = data);
+$: preferences = $Profile.fetching
+ ? undefined
+ : ($Profile.data?.User?.preferences as Preferences | undefined);
+$: isOwner = Boolean(userData && userData.id === $identity.id);
+$: ownerPreferences = preferences ?? {
+ created_at: "",
+ updated_at: "",
+ user_id: userData?.id ?? 0,
+ pinned_hololive_streams: [],
+ hide_missing_badges: false,
+ biography: null,
+ badge_wall_css: "",
+ hide_awc_badges: false,
+ pinned_badge_wall_categories: [],
+};
+
+const setCategoriesQuery = graphql(`
mutation SetCategories($categories: [String!]!) {
setPinnedBadgeWallCategories(categories: $categories) {
id
@@ -42,7 +55,7 @@
}
`);
- const toggleCategoryQuery = graphql(`
+const toggleCategoryQuery = graphql(`
mutation ToggleCategory($category: String!) {
togglePinnedBadgeWallCategory(category: $category) {
id
@@ -54,7 +67,7 @@
}
`);
- const toggleHideMissingBadgesQuery = graphql(`
+const toggleHideMissingBadgesQuery = graphql(`
mutation ToggleHideMissingBadges {
toggleHideMissingBadges {
id
@@ -66,7 +79,7 @@
}
`);
- const toggleHideAWCBadgesQuery = graphql(`
+const toggleHideAWCBadgesQuery = graphql(`
mutation ToggleHideAWCBadges {
toggleHideAWCBadges {
id
@@ -78,7 +91,7 @@
}
`);
- const setBiographyQuery = graphql(`
+const setBiographyQuery = graphql(`
mutation SetBiography($biography: String!) {
setBiography(biography: $biography) {
id
@@ -90,7 +103,7 @@
}
`);
- const setBadgeWallCSSQuery = graphql(`
+const setBadgeWallCSSQuery = graphql(`
mutation SetBadgeWallCSS($css: String!) {
setBadgeWallCSS(css: $css) {
id
@@ -102,121 +115,126 @@
}
`);
- $: userData = data.userData;
-
- let error = false;
- let schedule: ParseResult | undefined = undefined;
- let draggedCategory: string | null = null;
- let draggedOverCategory: string | null = null;
-
- $: displayBadges = (username: string, badges: number | string) =>
- $locale({
- values: {
- badges: badges,
- username
- }
- }).user.profile.badges;
-
- const handleDragStart = (
- event: DragEvent & { currentTarget: EventTarget & HTMLDivElement },
- category: string | null
- ) => {
- draggedCategory = category;
-
- if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
- };
-
- const handleDragOver = (event: DragEvent) => {
- event.preventDefault();
-
- if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
- };
-
- const handleDragEnter = (
- event: DragEvent & { currentTarget: EventTarget & HTMLDivElement },
- category: string | null
- ) => {
- event.preventDefault();
-
- if (draggedCategory !== category && preferences && draggedCategory) {
- draggedOverCategory = category;
-
- const categories = preferences.pinned_badge_wall_categories;
- const draggedIndex = categories.indexOf(draggedCategory);
- const targetIndex = categories.indexOf(category || '');
-
- categories.splice(draggedIndex, 1);
- categories.splice(targetIndex, 0, draggedCategory);
-
- preferences.pinned_badge_wall_categories = categories;
- }
- };
-
- const handleDragLeave = (
- event: DragEvent & { currentTarget: EventTarget & HTMLDivElement },
- category: string
- ) => {
- event.preventDefault();
-
- if (draggedOverCategory === category && preferences && draggedCategory) {
- draggedOverCategory = null;
-
- const categories = preferences.pinned_badge_wall_categories;
- const draggedIndex = categories.indexOf(draggedCategory);
-
- categories.splice(draggedIndex, 1);
- categories.splice(categories.indexOf(category) + 1, 0, draggedCategory);
-
- preferences.pinned_badge_wall_categories = categories;
- }
- };
-
- const handleDrop = (event: { preventDefault: () => void }) => {
- event.preventDefault();
-
- if (userData && preferences)
- setCategoriesQuery
- .mutate({
- categories: preferences.pinned_badge_wall_categories
- })
- .then();
-
- draggedCategory = null;
- draggedOverCategory = null;
- };
-
- onMount(async () => {
- schedule = typeSchedule(
- parseScheduleHtml(
- await (
- await fetch(proxy('https://schedule.hololive.tv'), {
- headers: {
- Cookie: 'timezone=Asia/Tokyo'
- }
- })
- ).text()
- )
- );
- });
-
- const getBadgeWallCSS = () =>
- (document.getElementById('badgeWallCSS') as HTMLTextAreaElement).value;
-
- const getBiography = () =>
- (document.getElementById('biography') as HTMLTextAreaElement).value.slice(0, 3000);
-
- const toggleCategory = () => {
- if (!userData) return;
-
- const categoryElement = document.getElementById('category') as HTMLInputElement;
- const category = categoryElement.value;
-
- toggleCategoryQuery.mutate({ category }).then();
-
- categoryElement.value = '';
- };
-
- // 8.5827814569536423841e0
+$: userData = data.userData;
+
+let error = false;
+let schedule: ParseResult | undefined = undefined;
+let draggedCategory: string | null = null;
+let draggedOverCategory: string | null = null;
+
+$: displayBadges = (username: string, badges: number | string) =>
+ $locale({
+ values: {
+ badges: badges,
+ username,
+ },
+ }).user.profile.badges;
+
+const handleDragStart = (
+ event: DragEvent & { currentTarget: EventTarget & HTMLDivElement },
+ category: string | null,
+) => {
+ draggedCategory = category;
+
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
+};
+
+const handleDragOver = (event: DragEvent) => {
+ event.preventDefault();
+
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
+};
+
+const handleDragEnter = (
+ event: DragEvent & { currentTarget: EventTarget & HTMLDivElement },
+ category: string | null,
+) => {
+ event.preventDefault();
+
+ if (draggedCategory !== category && draggedCategory) {
+ draggedOverCategory = category;
+
+ const categories = ownerPreferences.pinned_badge_wall_categories;
+ const draggedIndex = categories.indexOf(draggedCategory);
+ const targetIndex = categories.indexOf(category || "");
+
+ categories.splice(draggedIndex, 1);
+ categories.splice(targetIndex, 0, draggedCategory);
+
+ ownerPreferences.pinned_badge_wall_categories = categories;
+ }
+};
+
+const handleDragLeave = (
+ event: DragEvent & { currentTarget: EventTarget & HTMLDivElement },
+ category: string,
+) => {
+ event.preventDefault();
+
+ if (draggedOverCategory === category && draggedCategory) {
+ draggedOverCategory = null;
+
+ const categories = ownerPreferences.pinned_badge_wall_categories;
+ const draggedIndex = categories.indexOf(draggedCategory);
+
+ categories.splice(draggedIndex, 1);
+ categories.splice(categories.indexOf(category) + 1, 0, draggedCategory);
+
+ ownerPreferences.pinned_badge_wall_categories = categories;
+ }
+};
+
+const handleDrop = (event: { preventDefault: () => void }) => {
+ event.preventDefault();
+
+ if (userData)
+ setCategoriesQuery
+ .mutate({
+ categories: ownerPreferences.pinned_badge_wall_categories,
+ })
+ .then();
+
+ draggedCategory = null;
+ draggedOverCategory = null;
+};
+
+onMount(async () => {
+ schedule = typeSchedule(
+ parseScheduleHtml(
+ await (
+ await fetch(proxy("https://schedule.hololive.tv"), {
+ headers: {
+ Cookie: "timezone=Asia/Tokyo",
+ },
+ })
+ ).text(),
+ ),
+ );
+});
+
+const getBadgeWallCSS = () =>
+ (document.getElementById("badgeWallCSS") as HTMLTextAreaElement).value;
+
+const getBiography = () =>
+ (document.getElementById("biography") as HTMLTextAreaElement).value.slice(
+ 0,
+ 3000,
+ );
+
+const toggleCategory = () => {
+ if (!userData) return;
+
+ const categoryElement = document.getElementById(
+ "category",
+ ) as HTMLInputElement;
+ const category = categoryElement.value;
+
+ toggleCategoryQuery.mutate({ category }).then();
+
+ categoryElement.value = "";
+};
+
+// 8.5827814569536423841e0
</script>
<HeadTitle route={`${data.username}'s Profile`} path={`/user/${data.username}`} />
@@ -354,7 +372,7 @@
</div>
{/if}
- {#if preferences && userData && userData.id === $identity.id}
+ {#if isOwner}
<Spacer />
<details open>
@@ -365,7 +383,7 @@
onchange={() => {
if (userData) toggleHideMissingBadgesQuery.mutate(null).then();
}}
- checked={preferences.hide_missing_badges}
+ checked={ownerPreferences.hide_missing_badges}
/>
{$locale().user.preferences.hideMissingBadges.title}
<SettingHint lineBreak>{$locale().user.preferences.hideMissingBadges.hint}</SettingHint>
@@ -377,7 +395,7 @@
onchange={() => {
if (userData) toggleHideAWCBadgesQuery.mutate(null).then();
}}
- checked={preferences.hide_awc_badges}
+ checked={ownerPreferences.hide_awc_badges}
/>
{$locale().user.preferences.hideAWCBadges.title}
@@ -386,7 +404,7 @@
Pinned Categories
<div class="pinned-categories">
- {#each preferences.pinned_badge_wall_categories as category}
+ {#each ownerPreferences.pinned_badge_wall_categories as category}
<div
class="card card-small pinned-category"
draggable="true"
@@ -434,7 +452,7 @@
}}>Save</button
>
<textarea
- value={preferences.biography}
+ value={ownerPreferences.biography}
rows="5"
cols="100"
id="biography"
@@ -456,7 +474,7 @@
}}>Save</button
>
<textarea
- value={preferences.badge_wall_css}
+ value={ownerPreferences.badge_wall_css}
rows="10"
cols="100"
id="badgeWallCSS"
diff --git a/src/routes/user/[user]/+page.ts b/src/routes/user/[user]/+page.ts
index ca16077f..6ec3c845 100644
--- a/src/routes/user/[user]/+page.ts
+++ b/src/routes/user/[user]/+page.ts
@@ -1,21 +1,21 @@
-import { load_Profile } from '$houdini';
-import { user } from '$lib/Data/AniList/user';
-import type { LoadEvent } from '@sveltejs/kit';
+import { load_Profile } 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));
+ const username = event.params.user as string;
+ const userData = await user(username, /^\d+$/.test(username));
- if (!userData) throw new Error(`User not found: ${username}`);
+ if (!userData) throw new Error(`User not found: ${username}`);
- return {
- ...(await load_Profile({
- event,
- variables: {
- id: userData.id
- }
- })),
- username,
- userData
- };
+ return {
+ ...(await load_Profile({
+ event,
+ variables: {
+ id: userData.id,
+ },
+ })),
+ username,
+ userData,
+ };
};
diff --git a/src/routes/user/[user]/badges/+page.gql b/src/routes/user/[user]/badges/+page.gql
index 060b38e7..afdd797d 100644
--- a/src/routes/user/[user]/badges/+page.gql
+++ b/src/routes/user/[user]/badges/+page.gql
@@ -1,31 +1,31 @@
query BadgeWallUser($id: Int!) {
- User(id: $id) {
- id
+ User(id: $id) {
+ id
- badges {
- post
- image
- description
- id
- time
- category
- hidden
- source
- designer
- shadow_hidden
- click_count
- }
+ badges {
+ post
+ image
+ description
+ id
+ time
+ category
+ hidden
+ source
+ designer
+ shadow_hidden
+ click_count
+ }
- preferences {
- created_at
- updated_at
- user_id
- pinned_hololive_streams
- hide_missing_badges
- biography
- badge_wall_css
- hide_awc_badges
- pinned_badge_wall_categories
- }
- }
+ preferences {
+ created_at
+ updated_at
+ user_id
+ pinned_hololive_streams
+ hide_missing_badges
+ biography
+ badge_wall_css
+ hide_awc_badges
+ pinned_badge_wall_categories
+ }
+ }
}
diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte
index 386fe7b1..9ff81118 100644
--- a/src/routes/user/[user]/badges/+page.svelte
+++ b/src/routes/user/[user]/badges/+page.svelte
@@ -1,59 +1,64 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import AWC from './../../../../lib/User/BadgeWall/AWC.svelte';
- import { user, type User } from '$lib/Data/AniList/user';
- import type { Badge } from '../../../../graphql/$types';
- import { onDestroy, onMount } from 'svelte';
- import HeadTitle from '$lib/Home/HeadTitle.svelte';
- import { databaseTimeToDate, dateToInputTime, inputTimeToDatabaseTime } from '$lib/Utility/time';
- import proxy from '$lib/Utility/proxy';
- import locale from '$stores/locale';
- import Skeleton from '$lib/Loading/Skeleton.svelte';
- import Message from '$lib/Loading/Message.svelte';
- import Dropdown from '$lib/Layout/Dropdown.svelte';
- import { activityText } from '$lib/Data/AniList/activity';
- import SettingHint from '$lib/Settings/SettingHint.svelte';
- import Popup from '$lib/Layout/Popup.svelte';
- import { page } from '$app/stores';
- import { browser } from '$app/environment';
- 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';
- import type { Preferences } from '../../../../graphql/user/$types';
- import localforage from 'localforage';
-
- export let data;
-
- $: ({ BadgeWallUser } = data);
- $: preferences = $BadgeWallUser.fetching
- ? undefined
- : ($BadgeWallUser.data?.User?.preferences as Preferences | undefined);
-
- $: if (browser && preferences && 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);
- }
-
- const updateBadgeQuery = graphql(`
+import Spacer from "$lib/Layout/Spacer.svelte";
+import AWC from "./../../../../lib/User/BadgeWall/AWC.svelte";
+import { user, type User } from "$lib/Data/AniList/user";
+import type { Badge } from "../../../../graphql/$types";
+import { onDestroy, onMount } from "svelte";
+import HeadTitle from "$lib/Home/HeadTitle.svelte";
+import {
+ databaseTimeToDate,
+ dateToInputTime,
+ inputTimeToDatabaseTime,
+} from "$lib/Utility/time";
+import proxy from "$lib/Utility/proxy";
+import locale from "$stores/locale";
+import Skeleton from "$lib/Loading/Skeleton.svelte";
+import Message from "$lib/Loading/Message.svelte";
+import Dropdown from "$lib/Layout/Dropdown.svelte";
+import { activityText } from "$lib/Data/AniList/activity";
+import SettingHint from "$lib/Settings/SettingHint.svelte";
+import Popup from "$lib/Layout/Popup.svelte";
+import { page } from "$app/stores";
+import { browser } from "$app/environment";
+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";
+import type { Preferences } from "../../../../graphql/$types";
+import localforage from "localforage";
+import type { PageData } from "./$types";
+
+export let data: PageData;
+
+$: ({ BadgeWallUser } = data);
+$: preferences = $BadgeWallUser.fetching
+ ? undefined
+ : ($BadgeWallUser.data?.User?.preferences as Preferences | undefined);
+
+$: if (browser && preferences && 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);
+}
+
+const updateBadgeQuery = graphql(`
mutation UpdateBadge(
$id: Int
$post: String
@@ -95,7 +100,7 @@
}
`);
- const pruneBadgesQuery = graphql(`
+const pruneBadgesQuery = graphql(`
mutation PruneUserBadges {
pruneUserBadges {
id
@@ -117,7 +122,7 @@
}
`);
- const hideCategoryQuery = graphql(`
+const hideCategoryQuery = graphql(`
mutation HideCategory($category: String) {
hideBadge(category: $category) {
id
@@ -139,7 +144,7 @@
}
`);
- const deleteBadgeQuery = graphql(`
+const deleteBadgeQuery = graphql(`
mutation DeleteBadge($id: Int!) {
deleteBadge(id: $id) {
id
@@ -161,7 +166,7 @@
}
`);
- const shadowHideBadgeQuery = graphql(`
+const shadowHideBadgeQuery = graphql(`
mutation ShadowHideBadge($id: Int!, $state: Boolean) {
shadowHideBadge(id: $id, state: $state) {
id
@@ -173,359 +178,387 @@
}
`);
- interface ImportImage {
- link?: string;
- image: string;
- }
-
- let editMode = false;
- let importMode = false;
- let error: null | string;
- let awcPromise: Promise<Response>;
- let confirmDelete = 0;
- let confirmPrune = 0;
- let selectedBadge: IndexedBadge | undefined = undefined;
- let loadError: string | null = null;
- const isId = /^\d+$/.test(data.username);
- let importImages: ImportImage[] | undefined = undefined;
- let importLinks = false;
- let importCategory = '';
- let importReplies = false;
- let badger: Partial<User> | null;
- let migrateMode = false;
- let hideMode = false;
- const authorised = authorisedJson.includes($identity.id);
- let noticeDismissed = false;
-
- $: categoryFilter = new URLSearchParams($page.url.searchParams).get('category');
- $: loadQueryParameter = new URLSearchParams($page.url.searchParams).get('load');
-
- type GroupedBadges = { [key: string]: IndexedBadge[] };
-
- const setShadowHide = () => {
- if (!badger) {
- loadError = 'Something went wrong. Try refreshing.';
-
- return;
- }
-
- shadowHideBadgeQuery.mutate({
- id: badger.id as number
- });
- };
-
- onMount(async () => {
- if (browser && (await localforage.getItem('badgeWallNoticeDismissed'))) noticeDismissed = true;
-
- badger = isId
- ? {
- id: parseInt(data.username),
- name: 'User'
- }
- : await user(data.username);
-
- if (!badger) {
- loadError = "Couldn't find this user.";
-
- return;
- }
-
- awcPromise = fetch(proxy(`https://awc.moe/challenger/${badger.name}`));
- });
-
- onDestroy(() => {
- if (browser)
- Array.from(document.head.querySelectorAll('style')).forEach((style) => {
- if (style.dataset.badgeWall) style.remove();
- });
- });
-
- const submitBadge = () => {
- const imageURL = document.querySelector('input[name="image_url"]') as HTMLInputElement;
- const activityURL = document.querySelector('input[name="activity_url"]') as HTMLInputElement;
- const description = document.querySelector('input[name="description"]') as HTMLInputElement;
- const time = document.querySelector('input[type="datetime-local"]') as HTMLInputElement;
- const category = document.querySelector('input[name="category"]') as HTMLInputElement;
- const hidden = document.querySelector('input[name="hidden"]') as HTMLInputElement;
- const source = document.querySelector('input[name="source"]') as HTMLInputElement;
- const designer = document.querySelector('input[name="designer"]') as HTMLInputElement;
-
- if (!imageURL.value) {
- error = 'Image URL cannot be empty.';
-
- return;
- }
-
- if (
- !imageURL.value.startsWith('http') ||
- (activityURL.value.length > 0 && !activityURL.value.startsWith('http'))
- ) {
- error = 'URLs must start with http or https.';
-
- return;
- }
-
- updateBadgeQuery
- .mutate({
- id: selectedBadge?.id,
- image: imageURL.value,
- post: activityURL.value || '#',
- description: description.value,
- category: category.value,
- time: time.value ? inputTimeToDatabaseTime(new Date(time.value)) : 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;
- source.value = '';
- designer.value = '';
- });
- };
-
- const removeAllBadges = () => {
- if (confirmPrune === 2) {
- confirmPrune = 0;
- } else if (confirmPrune === 0) {
- confirmPrune = 1;
-
- return;
- } else {
- confirmPrune = 2;
-
- return;
- }
-
- selectedBadge = undefined;
-
- pruneBadgesQuery.mutate(null).then();
- };
-
- const removeBadge = (badge: Badge) => {
- if (!badge.id) return;
-
- if (confirmDelete === badge.id * 2) {
- confirmDelete = 0;
- } else if (confirmDelete / 4 === badge.id) {
- confirmDelete = badge.id * 2;
-
- return;
- } else {
- confirmDelete = badge.id * 2;
-
- return;
- }
-
- selectedBadge = undefined;
-
- deleteBadgeQuery
- .mutate({
- id: badge.id
- })
- .then();
- };
-
- const groupBadges = (badges: IndexedBadge[]) => {
- const groupedBadges: GroupedBadges = {};
-
- badges.forEach((badge) => {
- if (!badge.category) badge.category = 'Uncategorised';
-
- if (!groupedBadges[badge.category]) groupedBadges[badge.category] = [];
-
- groupedBadges[badge.category].push(badge);
- });
-
- Object.entries(groupedBadges).forEach(([_categoryKey, badges]) => {
- badges.forEach((badge, index) => {
- badge.index = index;
- });
- });
-
- return Object.entries(groupedBadges)
- .sort((a, b) => a[1].length - b[1].length)
- .sort((a, b) => {
- const pinnedCategories =
- preferences && preferences.pinned_badge_wall_categories
- ? preferences.pinned_badge_wall_categories
- : ([] as string[]);
- const aIndex = pinnedCategories.indexOf(a[0]);
- const bIndex = pinnedCategories.indexOf(b[0]);
-
- if (aIndex === -1 && bIndex === -1) return 0;
- if (aIndex === -1) return 1;
- if (bIndex === -1) return -1;
-
- return aIndex - bIndex;
- })
- .reduce((set: GroupedBadges, [key, value]) => {
- set[key] = value;
-
- return set;
- }, {});
- };
-
- const parsePost = async () => {
- if (importImages && importImages.length > 0) importImages = undefined;
-
- const link = (document.querySelector('#import_activity_url') as HTMLInputElement).value;
- const type = link.replace(/.*\/(activity|thread)\/(\d+).*/, '$1');
- const id = link.replace(/.*\/(activity|thread)\/(\d+).*/, '$2');
-
- if (type !== 'activity') return null;
-
- let text = await activityText(parseInt(id), importReplies);
-
- const images: ImportImage[] = [];
-
- if (importLinks) {
- Array.from(new DOMParser().parseFromString(text, 'text/html').querySelectorAll('a')).forEach(
- (a) => {
- const anchor = a as HTMLAnchorElement;
-
- if (anchor.querySelector('img')) {
- images.push({
- link: anchor.href,
- image: (anchor.querySelector('img') as HTMLImageElement).src
- });
- }
- }
- );
-
- text = text.replace(/<a.*?>.*?<img.*?>.*?<\/a>/g, '');
-
- Array.from(
- new DOMParser().parseFromString(text, 'text/html').querySelectorAll('img')
- ).forEach((img) => {
- const image = img as HTMLImageElement;
-
- images.push({
- image: image.src
- });
- });
- } else {
- Array.from(
- new DOMParser().parseFromString(text, 'text/html').querySelectorAll('img')
- ).forEach((img) => {
- const image = img as HTMLImageElement;
-
- images.push({
- image: image.src
- });
- });
- }
-
- importImages = images;
- };
-
- const importBadges = () =>
- fetch(
- `/api/badges?import=true
- ${importCategory.length > 0 ? `&category=${encodeURIComponent(importCategory)}` : ''}
+interface ImportImage {
+ link?: string;
+ image: string;
+}
+
+let editMode = false;
+let importMode = false;
+let error: null | string;
+let awcPromise: Promise<Response>;
+let confirmDelete = 0;
+let confirmPrune = 0;
+let selectedBadge: IndexedBadge | undefined = undefined;
+let loadError: string | null = null;
+const isId = /^\d+$/.test(data.username);
+let importImages: ImportImage[] | undefined = undefined;
+let importLinks = false;
+let importCategory = "";
+let importReplies = false;
+let badger: Partial<User> | null;
+let migrateMode = false;
+let hideMode = false;
+const authorised = authorisedJson.includes($identity.id);
+let noticeDismissed = false;
+
+$: categoryFilter = new URLSearchParams($page.url.searchParams).get("category");
+$: loadQueryParameter = new URLSearchParams($page.url.searchParams).get("load");
+
+type GroupedBadges = { [key: string]: IndexedBadge[] };
+
+const setShadowHide = () => {
+ if (!badger) {
+ loadError = "Something went wrong. Try refreshing.";
+
+ return;
+ }
+
+ shadowHideBadgeQuery.mutate({
+ id: badger.id as number,
+ });
+};
+
+onMount(async () => {
+ if (browser && (await localforage.getItem("badgeWallNoticeDismissed")))
+ noticeDismissed = true;
+
+ badger = isId
+ ? {
+ id: parseInt(data.username),
+ name: "User",
+ }
+ : await user(data.username);
+
+ if (!badger) {
+ loadError = "Couldn't find this user.";
+
+ return;
+ }
+
+ awcPromise = fetch(proxy(`https://awc.moe/challenger/${badger.name}`));
+});
+
+onDestroy(() => {
+ if (browser)
+ Array.from(document.head.querySelectorAll("style")).forEach((style) => {
+ if (style.dataset.badgeWall) style.remove();
+ });
+});
+
+const submitBadge = () => {
+ const imageURL = document.querySelector(
+ 'input[name="image_url"]',
+ ) as HTMLInputElement;
+ const activityURL = document.querySelector(
+ 'input[name="activity_url"]',
+ ) as HTMLInputElement;
+ const description = document.querySelector(
+ 'input[name="description"]',
+ ) as HTMLInputElement;
+ const time = document.querySelector(
+ 'input[type="datetime-local"]',
+ ) as HTMLInputElement;
+ const category = document.querySelector(
+ 'input[name="category"]',
+ ) as HTMLInputElement;
+ const hidden = document.querySelector(
+ 'input[name="hidden"]',
+ ) as HTMLInputElement;
+ const source = document.querySelector(
+ 'input[name="source"]',
+ ) as HTMLInputElement;
+ const designer = document.querySelector(
+ 'input[name="designer"]',
+ ) as HTMLInputElement;
+
+ if (!imageURL.value) {
+ error = "Image URL cannot be empty.";
+
+ return;
+ }
+
+ if (
+ !imageURL.value.startsWith("http") ||
+ (activityURL.value.length > 0 && !activityURL.value.startsWith("http"))
+ ) {
+ error = "URLs must start with http or https.";
+
+ return;
+ }
+
+ updateBadgeQuery
+ .mutate({
+ id: selectedBadge?.id,
+ image: imageURL.value,
+ post: activityURL.value || "#",
+ description: description.value,
+ category: category.value,
+ time: time.value
+ ? inputTimeToDatabaseTime(new Date(time.value))
+ : 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;
+ source.value = "";
+ designer.value = "";
+ });
+};
+
+const removeAllBadges = () => {
+ if (confirmPrune === 2) {
+ confirmPrune = 0;
+ } else if (confirmPrune === 0) {
+ confirmPrune = 1;
+
+ return;
+ } else {
+ confirmPrune = 2;
+
+ return;
+ }
+
+ selectedBadge = undefined;
+
+ pruneBadgesQuery.mutate(null).then();
+};
+
+const removeBadge = (badge: Badge) => {
+ if (!badge.id) return;
+
+ if (confirmDelete === badge.id * 2) {
+ confirmDelete = 0;
+ } else if (confirmDelete / 4 === badge.id) {
+ confirmDelete = badge.id * 2;
+
+ return;
+ } else {
+ confirmDelete = badge.id * 2;
+
+ return;
+ }
+
+ selectedBadge = undefined;
+
+ deleteBadgeQuery
+ .mutate({
+ id: badge.id,
+ })
+ .then();
+};
+
+const groupBadges = (badges: IndexedBadge[]) => {
+ const groupedBadges: GroupedBadges = {};
+
+ badges.forEach((badge) => {
+ if (!badge.category) badge.category = "Uncategorised";
+
+ if (!groupedBadges[badge.category]) groupedBadges[badge.category] = [];
+
+ groupedBadges[badge.category].push(badge);
+ });
+
+ Object.entries(groupedBadges).forEach(([_categoryKey, badges]) => {
+ badges.forEach((badge, index) => {
+ badge.index = index;
+ });
+ });
+
+ return Object.entries(groupedBadges)
+ .sort((a, b) => a[1].length - b[1].length)
+ .sort((a, b) => {
+ const pinnedCategories =
+ preferences && preferences.pinned_badge_wall_categories
+ ? preferences.pinned_badge_wall_categories
+ : ([] as string[]);
+ const aIndex = pinnedCategories.indexOf(a[0]);
+ const bIndex = pinnedCategories.indexOf(b[0]);
+
+ if (aIndex === -1 && bIndex === -1) return 0;
+ if (aIndex === -1) return 1;
+ if (bIndex === -1) return -1;
+
+ return aIndex - bIndex;
+ })
+ .reduce((set: GroupedBadges, [key, value]) => {
+ set[key] = value;
+
+ return set;
+ }, {});
+};
+
+const parsePost = async () => {
+ if (importImages && importImages.length > 0) importImages = undefined;
+
+ const link = (
+ document.querySelector("#import_activity_url") as HTMLInputElement
+ ).value;
+ const type = link.replace(/.*\/(activity|thread)\/(\d+).*/, "$1");
+ const id = link.replace(/.*\/(activity|thread)\/(\d+).*/, "$2");
+
+ if (type !== "activity") return null;
+
+ let text = await activityText(parseInt(id), importReplies);
+
+ const images: ImportImage[] = [];
+
+ if (importLinks) {
+ Array.from(
+ new DOMParser().parseFromString(text, "text/html").querySelectorAll("a"),
+ ).forEach((a) => {
+ const anchor = a as HTMLAnchorElement;
+
+ if (anchor.querySelector("img")) {
+ images.push({
+ link: anchor.href,
+ image: (anchor.querySelector("img") as HTMLImageElement).src,
+ });
+ }
+ });
+
+ text = text.replace(/<a.*?>.*?<img.*?>.*?<\/a>/g, "");
+
+ Array.from(
+ new DOMParser()
+ .parseFromString(text, "text/html")
+ .querySelectorAll("img"),
+ ).forEach((img) => {
+ const image = img as HTMLImageElement;
+
+ images.push({
+ image: image.src,
+ });
+ });
+ } else {
+ Array.from(
+ new DOMParser()
+ .parseFromString(text, "text/html")
+ .querySelectorAll("img"),
+ ).forEach((img) => {
+ const image = img as HTMLImageElement;
+
+ images.push({
+ image: image.src,
+ });
+ });
+ }
+
+ importImages = images;
+};
+
+const importBadges = () =>
+ fetch(
+ `/api/badges?import=true
+ ${importCategory.length > 0 ? `&category=${encodeURIComponent(importCategory)}` : ""}
`,
- {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(
- importImages?.map((image) => ({
- image: image.image,
- post: image.link || '#',
- category: importCategory
- }))
- )
- }
- ).then(() => {
- importMode = false;
- importImages = undefined;
- });
-
- const migrateCategory = () => {
- fetch(
- `/api/badges?migrate=true&original=${encodeURIComponent(
- (document.querySelector('#migrate_original') as HTMLInputElement).value
- )}&new=${encodeURIComponent(
- (document.querySelector('#migrate_new') as HTMLInputElement).value
- )}`,
- {
- method: 'PUT'
- }
- ).then(() => (migrateMode = false));
- };
-
- const hideCategory = () => {
- hideCategoryQuery
- .mutate({
- category: (document.querySelector('#category_hide') as HTMLInputElement).value
- })
- .then(() => (hideMode = false));
- };
-
- const removeHiddenBadges = (isOwner: boolean, badges: IndexedBadge[]) =>
- isOwner || authorised ? badges : badges.filter((b) => !b.hidden && !b.shadow_hidden);
-
- const setAdjacentCursor = (badges: IndexedBadge[], direction: number) => {
- const currentCategory = selectedBadge?.category || 'Uncategorised';
- const currentBadge = selectedBadge?.index;
- const categoryBadges = groupBadges(badges)[currentCategory];
-
- if (!currentCategory || currentBadge === undefined) return;
-
- let previousBadge = categoryBadges[currentBadge + direction];
-
- while (previousBadge && (previousBadge.hidden || previousBadge.shadow_hidden))
- previousBadge = categoryBadges[previousBadge.index + direction];
-
- if (previousBadge) selectedBadge = previousBadge;
- };
-
- const adjacentBadgeExists = (
- selectedBadge: IndexedBadge | undefined,
- badges: IndexedBadge[],
- direction: number
- ) => {
- const currentCategory = selectedBadge?.category || 'Uncategorised';
- const currentBadge = selectedBadge?.index;
- const categoryBadges = groupBadges(badges)[currentCategory];
-
- if (!currentCategory || currentBadge === undefined || !categoryBadges) return;
-
- let previousBadge = categoryBadges[currentBadge + direction];
-
- while (previousBadge && (previousBadge.hidden || previousBadge.shadow_hidden))
- previousBadge = categoryBadges[previousBadge.index + direction];
-
- return previousBadge;
- };
-
- const castAsStringArray = (array: unknown[]) => array as string[];
-
- const castBadgesToIndexedBadges = (array: unknown[]) => array as IndexedBadge[];
-
- const shadowHideBadge = () => {
- if (!selectedBadge && !authorised) return;
-
- if (!badger) {
- loadError = 'Something went wrong. Try refreshing.';
-
- return;
- }
-
- shadowHideBadgeQuery
- .mutate({
- id: badger.id as number,
- state: selectedBadge?.shadow_hidden as boolean
- })
- .then();
- };
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(
+ importImages?.map((image) => ({
+ image: image.image,
+ post: image.link || "#",
+ category: importCategory,
+ })),
+ ),
+ },
+ ).then(() => {
+ importMode = false;
+ importImages = undefined;
+ });
+
+const migrateCategory = () => {
+ fetch(
+ `/api/badges?migrate=true&original=${encodeURIComponent(
+ (document.querySelector("#migrate_original") as HTMLInputElement).value,
+ )}&new=${encodeURIComponent(
+ (document.querySelector("#migrate_new") as HTMLInputElement).value,
+ )}`,
+ {
+ method: "PUT",
+ },
+ ).then(() => (migrateMode = false));
+};
+
+const hideCategory = () => {
+ hideCategoryQuery
+ .mutate({
+ category: (document.querySelector("#category_hide") as HTMLInputElement)
+ .value,
+ })
+ .then(() => (hideMode = false));
+};
+
+const removeHiddenBadges = (isOwner: boolean, badges: IndexedBadge[]) =>
+ isOwner || authorised
+ ? badges
+ : badges.filter((b) => !b.hidden && !b.shadow_hidden);
+
+const setAdjacentCursor = (badges: IndexedBadge[], direction: number) => {
+ const currentCategory = selectedBadge?.category || "Uncategorised";
+ const currentBadge = selectedBadge?.index;
+ const categoryBadges = groupBadges(badges)[currentCategory];
+
+ if (!currentCategory || currentBadge === undefined) return;
+
+ let previousBadge = categoryBadges[currentBadge + direction];
+
+ while (previousBadge && (previousBadge.hidden || previousBadge.shadow_hidden))
+ previousBadge = categoryBadges[previousBadge.index + direction];
+
+ if (previousBadge) selectedBadge = previousBadge;
+};
+
+const adjacentBadgeExists = (
+ selectedBadge: IndexedBadge | undefined,
+ badges: IndexedBadge[],
+ direction: number,
+) => {
+ const currentCategory = selectedBadge?.category || "Uncategorised";
+ const currentBadge = selectedBadge?.index;
+ const categoryBadges = groupBadges(badges)[currentCategory];
+
+ if (!currentCategory || currentBadge === undefined || !categoryBadges) return;
+
+ let previousBadge = categoryBadges[currentBadge + direction];
+
+ while (previousBadge && (previousBadge.hidden || previousBadge.shadow_hidden))
+ previousBadge = categoryBadges[previousBadge.index + direction];
+
+ return previousBadge;
+};
+
+const castAsStringArray = (array: unknown[]) => array as string[];
+
+const castBadgesToIndexedBadges = (array: unknown[]) => array as IndexedBadge[];
+
+const shadowHideBadge = () => {
+ if (!selectedBadge && !authorised) return;
+
+ if (!badger) {
+ loadError = "Something went wrong. Try refreshing.";
+
+ return;
+ }
+
+ shadowHideBadgeQuery
+ .mutate({
+ id: badger.id as number,
+ state: selectedBadge?.shadow_hidden as boolean,
+ })
+ .then();
+};
</script>
<HeadTitle route={`${data.username}'s Badge Wall`} path={`/user/${data.username}`} />
@@ -580,7 +613,7 @@
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}
+ {:else if false && !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
diff --git a/src/routes/user/[user]/badges/+page.ts b/src/routes/user/[user]/badges/+page.ts
index db70d16c..a14c0a6a 100644
--- a/src/routes/user/[user]/badges/+page.ts
+++ b/src/routes/user/[user]/badges/+page.ts
@@ -1,22 +1,22 @@
-import { load_BadgeWallUser } from '$houdini';
-import { user } from '$lib/Data/AniList/user';
-import type { LoadEvent } from '@sveltejs/kit';
+import { load_BadgeWallUser } 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));
+ const username = event.params.user as string;
+ const userData = await user(username, /^\d+$/.test(username));
- if (!userData) throw new Error(`User not found: ${username}`);
+ if (!userData) throw new Error(`User not found: ${username}`);
- return {
- ...(await load_BadgeWallUser({
- event,
- variables: {
- id: userData.id
- }
- })),
- username,
- userData,
- event
- };
+ return {
+ ...(await load_BadgeWallUser({
+ event,
+ variables: {
+ id: userData.id,
+ },
+ })),
+ username,
+ userData,
+ event,
+ };
};
diff --git a/src/routes/welcome/+page.svelte b/src/routes/welcome/+page.svelte
index 3e15ab26..b4fd1fe0 100644
--- a/src/routes/welcome/+page.svelte
+++ b/src/routes/welcome/+page.svelte
@@ -1,7 +1,7 @@
<script>
- import Landing from '$lib/Landing.svelte';
- import LandingHero from '$lib/LandingHero.svelte';
- import Spacer from '$lib/Layout/Spacer.svelte';
+import Landing from "$lib/Landing.svelte";
+import LandingHero from "$lib/LandingHero.svelte";
+import Spacer from "$lib/Layout/Spacer.svelte";
</script>
<LandingHero />