aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2023-10-24 17:12:54 -0700
committerFuwn <[email protected]>2023-10-24 17:12:54 -0700
commit861c03b85160972431ca9b262345d15edecf9acb (patch)
tree48bf0a18cebf3c8fb20b774adac7f9c85373a3a2
parentfix(settings): round down chapters hint (diff)
downloaddue.moe-861c03b85160972431ca9b262345d15edecf9acb.tar.xz
due.moe-861c03b85160972431ca9b262345d15edecf9acb.zip
feat: badge wall
-rw-r--r--.dockerignore1
-rw-r--r--.eslintignore1
-rw-r--r--.gitignore1
-rw-r--r--.prettierignore1
-rw-r--r--src/lib/userBadgesDatabase.ts57
-rw-r--r--src/routes/api/badges/add/+server.ts23
-rw-r--r--src/routes/api/badges/remove/+server.ts22
-rw-r--r--src/routes/user/[user]/+page.server.ts5
-rw-r--r--src/routes/user/[user]/+page.svelte40
-rw-r--r--src/routes/user/[user]/badges/+page.server.ts11
-rw-r--r--src/routes/user/[user]/badges/+page.svelte301
11 files changed, 463 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore
index ced480f7..f1e46035 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -13,6 +13,7 @@ node_modules
.env
.env.*
!.env.example
+*.sqlite3
# PNPM
pnpm-lock.yaml
diff --git a/.eslintignore b/.eslintignore
index ced480f7..f1e46035 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -13,6 +13,7 @@ node_modules
.env
.env.*
!.env.example
+*.sqlite3
# PNPM
pnpm-lock.yaml
diff --git a/.gitignore b/.gitignore
index ced480f7..f1e46035 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ node_modules
.env
.env.*
!.env.example
+*.sqlite3
# PNPM
pnpm-lock.yaml
diff --git a/.prettierignore b/.prettierignore
index ced480f7..f1e46035 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -13,6 +13,7 @@ node_modules
.env
.env.*
!.env.example
+*.sqlite3
# PNPM
pnpm-lock.yaml
diff --git a/src/lib/userBadgesDatabase.ts b/src/lib/userBadgesDatabase.ts
new file mode 100644
index 00000000..c129770e
--- /dev/null
+++ b/src/lib/userBadgesDatabase.ts
@@ -0,0 +1,57 @@
+import { dev } from '$app/environment';
+import Database from 'better-sqlite3';
+
+export interface Badge {
+ post: string;
+ image: string;
+ description?: string;
+ id?: number;
+}
+
+const database = new Database('./due_moe.sqlite3', {
+ verbose: dev ? console.log : undefined
+});
+
+database.exec(`create table if not exists user_badges (
+ id integer primary key,
+ user_id integer not null,
+ post text not null,
+ image text not null,
+ description text default null,
+ time timestamp default current_timestamp
+)`);
+
+export const getUserBadges = (userId: number): Badge[] => {
+ return database
+ .prepare('select * from user_badges where user_id = ?')
+ .all(String(userId)) as Badge[];
+};
+
+export const addUserBadge = (userId: number, badge: Badge) => {
+ const { post, image, description } = badge;
+ const statement = database.prepare(`
+ insert into user_badges (user_id, post, image, description) values (?, ?, ?, ?)
+ `);
+
+ statement.run(userId, post, image, description);
+};
+
+export const addUserBadges = (userId: number, badges: Badge[]) => {
+ const statement = database.prepare(`
+ insert into user_badges (user_id, post, image, description) values (?, ?, ?, ?)
+ `);
+
+ for (const badge of badges) {
+ const { post, image, description } = badge;
+
+ statement.run(userId, post, image, description);
+ }
+};
+
+export const removeUserBadge = (userId: number, id: number) => {
+ if (!isNaN(id)) {
+ const statement = database.prepare('delete from user_badges where user_id = ? and id = ?');
+
+ statement.run(userId, id);
+ }
+};
diff --git a/src/routes/api/badges/add/+server.ts b/src/routes/api/badges/add/+server.ts
new file mode 100644
index 00000000..6ce9421e
--- /dev/null
+++ b/src/routes/api/badges/add/+server.ts
@@ -0,0 +1,23 @@
+import { userIdentity } from '$lib/AniList/identity.js';
+import { addUserBadges } from '$lib/userBadgesDatabase.js';
+
+export const POST = async ({ cookies, request }) => {
+ const userCookie = cookies.get('user');
+
+ if (!userCookie) {
+ return new Response('Unauthenticated', { status: 401 });
+ }
+
+ 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 formData = await request.json();
+
+ addUserBadges(identity.id, formData);
+
+ return Response.json({});
+};
diff --git a/src/routes/api/badges/remove/+server.ts b/src/routes/api/badges/remove/+server.ts
new file mode 100644
index 00000000..8b05369a
--- /dev/null
+++ b/src/routes/api/badges/remove/+server.ts
@@ -0,0 +1,22 @@
+import { userIdentity } from '$lib/AniList/identity.js';
+import { removeUserBadge } from '$lib/userBadgesDatabase.js';
+
+export const POST = async ({ url, cookies }) => {
+ const userCookie = cookies.get('user');
+
+ if (!userCookie) {
+ return new Response('Unauthenticated', { status: 401 });
+ }
+
+ 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']
+ });
+
+ removeUserBadge(identity.id, Number(url.searchParams.get('id')));
+
+ return Response.json({});
+};
diff --git a/src/routes/user/[user]/+page.server.ts b/src/routes/user/[user]/+page.server.ts
new file mode 100644
index 00000000..76d2d889
--- /dev/null
+++ b/src/routes/user/[user]/+page.server.ts
@@ -0,0 +1,5 @@
+export const load = ({ params }) => {
+ return {
+ username: params.user
+ };
+};
diff --git a/src/routes/user/[user]/+page.svelte b/src/routes/user/[user]/+page.svelte
new file mode 100644
index 00000000..227ba252
--- /dev/null
+++ b/src/routes/user/[user]/+page.svelte
@@ -0,0 +1,40 @@
+<script lang="ts">
+ import { user, type User } from '$lib/AniList/user';
+ import { onMount } from 'svelte';
+
+ export let data;
+
+ let userData: User | undefined = undefined;
+
+ onMount(() => {
+ user(data.username).then((profile) => {
+ userData = profile;
+ });
+ });
+
+ // 8.5827814569536423841e0
+</script>
+
+{#if userData === null}
+ Could not load user profile for <a
+ href={`https://anilist.co/user/${data.username}`}
+ target="_blank">@{data.username}</a
+ >.
+
+ <p />
+
+ Does this user exist?
+{:else if userData === undefined}
+ Loading ...
+{:else}
+ <p>
+ <a href={`https://anilist.co/user/${userData.name}`} target="_blank" title={String(userData.id)}
+ >@{userData.name}</a
+ >
+ • <a href={`/user/${userData.name}/badges`}>Badge Wall</a>
+ </p>
+
+ This user has watched {(userData.statistics.anime.minutesWatched / 60 / 24).toFixed(1)} days of anime
+ and read
+ {((userData.statistics.manga.chaptersRead * 8.58) / 60 / 24).toFixed(1)} days of manga.
+{/if}
diff --git a/src/routes/user/[user]/badges/+page.server.ts b/src/routes/user/[user]/badges/+page.server.ts
new file mode 100644
index 00000000..4be5bcd2
--- /dev/null
+++ b/src/routes/user/[user]/badges/+page.server.ts
@@ -0,0 +1,11 @@
+import { user } from '$lib/AniList/user.js';
+import { getUserBadges } from '$lib/userBadgesDatabase.js';
+
+export const load = async ({ params }) => {
+ const badges = getUserBadges((await user(params.user)).id);
+
+ return {
+ username: params.user,
+ badges
+ };
+};
diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte
new file mode 100644
index 00000000..276bf1ca
--- /dev/null
+++ b/src/routes/user/[user]/badges/+page.svelte
@@ -0,0 +1,301 @@
+<script lang="ts">
+ import { userIdentity } from '$lib/AniList/identity.js';
+ import type { Badge } from '$lib/userBadgesDatabase.js';
+ import { onMount } from 'svelte';
+
+ export let data;
+
+ let editMode = false;
+ let currentUserIdentity: ReturnType<typeof userIdentity>;
+
+ // const badges: Badge[] = [
+ // {
+ // post: 'https://anilist.co/activity/611973592',
+ // image: 'https://files.catbox.moe/6tvw17.png'
+ // },
+ // { post: 'https://anilist.co/activity/611972285', image: 'https://files.catbox.moe/rn5qr5.png' },
+ // { post: 'https://anilist.co/activity/611977824', image: 'https://i.imgur.com/DFkT4zB.png' },
+ // {
+ // post: 'https://anilist.co/activity/612036793',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1144851101414326333/Badge2_26-08-23.png'
+ // },
+ // {
+ // post: 'https://anilist.co/activity/612273794',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1144773312468234351/DOGDAY_5_v2.png'
+ // },
+ // { post: 'https://anilist.co/activity/613961295', image: 'https://files.catbox.moe/6rebg8.png' },
+ // { post: 'https://anilist.co/activity/614793182', image: 'https://imgur.com/QhJbw4l.png' },
+ // { post: 'https://anilist.co/activity/615002857', image: 'https://files.catbox.moe/oc8g02.png' },
+ // { post: 'https://anilist.co/activity/615426233', image: 'https://files.catbox.moe/4z226e.png' },
+ // { post: 'https://anilist.co/activity/615427328', image: 'https://files.catbox.moe/tqcltp.png' },
+ // { post: 'https://anilist.co/activity/615920191', image: 'https://files.catbox.moe/frw5p5.png' },
+ // { post: 'https://anilist.co/activity/616629257', image: 'https://files.catbox.moe/15st7d.png' },
+ // { post: 'https://anilist.co/activity/617442391', image: 'https://i.imgur.com/aHkSRCz.gif' },
+ // {
+ // post: 'https://anilist.co/activity/617445099',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1148663790452346961/chondyunbday.gif'
+ // },
+ // {
+ // post: 'https://anilist.co/activity/617616590',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1148678438551568444/Badge_3.png'
+ // },
+ // { post: 'https://anilist.co/activity/617842237', image: 'https://i.imgur.com/Zx4uiAz.gif' },
+ // { post: 'https://anilist.co/activity/618296369', image: 'https://i.imgur.com/V6UsqYI.gif' },
+ // { post: 'https://anilist.co/activity/618664650', image: 'https://imgur.com/x98vT7p.png' },
+ // { post: 'https://anilist.co/activity/619306471', image: 'https://i.imgur.com/GppbpqE.png' },
+ // { post: 'https://anilist.co/activity/619657632', image: 'https://files.catbox.moe/barla6.png' },
+ // { post: 'https://anilist.co/activity/619659847', image: 'https://i.imgur.com/e81dgSB.gif' },
+ // { post: 'https://anilist.co/activity/619661657', image: 'https://i.imgur.com/S0fSeD4.gif' },
+ // { post: 'https://anilist.co/activity/619664832', image: 'https://i.imgur.com/EXNQE3n.gif' },
+ // {
+ // post: 'https://anilist.co/activity/619764622',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1151314632942817290/persona_5_3.png'
+ // },
+ // { post: 'https://anilist.co/activity/620025361', image: 'https://i.imgur.com/DmEl13g.gif' },
+ // { post: 'https://anilist.co/activity/620125206', image: 'https://i.imgur.com/SmzhGyu.gif' },
+ // { post: 'https://anilist.co/activity/620125762', image: 'https://i.imgur.com/38I5gUM.gif' },
+ // { post: 'https://anilist.co/activity/620126356', image: 'https://i.imgur.com/9I7Xggm.gif' },
+ // { post: 'https://anilist.co/activity/620600819', image: 'https://i.imgur.com/nHREaUc.png' },
+ // { post: 'https://anilist.co/activity/620989269', image: 'https://imgur.com/XjhyOHU.png' },
+ // {
+ // post: 'https://anilist.co/activity/621253410',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1139717993845239849/1147701375707381760/0028HLA.png'
+ // },
+ // { post: 'https://anilist.co/activity/621787546', image: 'https://i.imgur.com/tn5yVsk.gif' },
+ // {
+ // post: 'https://anilist.co/activity/621789551',
+ // image: 'https://i.postimg.cc/Z5325GDx/ota-day-otaku-academia-ittle-witch-academia.png'
+ // },
+ // { post: 'https://anilist.co/activity/622236894', image: 'https://i.imgur.com/vicrIfS.png' },
+ // { post: 'https://anilist.co/activity/622237728', image: 'https://i.imgur.com/TLSC65A.jpg' },
+ // { post: 'https://anilist.co/activity/623156563', image: 'https://files.catbox.moe/ujf0ym.png' },
+ // { post: 'https://anilist.co/activity/623990926', image: 'https://files.catbox.moe/gkalwm.png' },
+ // {
+ // post: 'https://anilist.co/activity/623995806',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1154888665638649916/monikabday.png'
+ // },
+ // { post: 'https://anilist.co/activity/624542383', image: 'https://files.catbox.moe/9tzs66.png' },
+ // {
+ // post: 'https://anilist.co/activity/624542383',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1154540564671377459/EMILIA_BADGE_2.png'
+ // },
+ // { post: 'https://anilist.co/activity/624543474', image: 'https://imgur.com/WQuXh6g.png' },
+ // {
+ // post: 'https://anilist.co/activity/624544489',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1154736156093726870/fsdfwefewfwf.png'
+ // },
+ // {
+ // post: 'https://anilist.co/activity/624545233',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1153606849464111134/katoubadge1.png'
+ // },
+ // { post: 'https://anilist.co/activity/624548754', image: 'https://imgur.com/j5aqX5w.png' },
+ // {
+ // post: 'https://anilist.co/activity/624549956',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1152962059126972417/1154745849465811015/Day_of_Mid_2.png'
+ // },
+ // { post: 'https://anilist.co/activity/626483669', image: 'https://files.catbox.moe/lz0r48.png' },
+ // {
+ // post: 'https://anilist.co/activity/626483669',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1156396779332456538/jojobdayb.png'
+ // },
+ // {
+ // post: 'https://anilist.co/activity/626770819',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1156582649779994664/kikurihiroi.png'
+ // },
+ // { post: 'https://anilist.co/activity/626772329', image: 'https://i.imgur.com/P09v438.gif' },
+ // {
+ // post: 'https://anilist.co/activity/627283326',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1157124224197083226/coffeeday2.png'
+ // },
+ // {
+ // post: 'https://anilist.co/activity/628202238',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1156472801457340506/rinshima1.png'
+ // },
+ // {
+ // post: 'https://anilist.co/activity/628202913',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1157574135069806592/Badge-1_-_01_10_23.png'
+ // },
+ // {
+ // post: 'https://anilist.co/activity/628305048',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1157650582094479370/SakeDay2.png'
+ // },
+ // { post: 'https://anilist.co/activity/629168789', image: 'https://files.catbox.moe/0mwudd.png' },
+ // { post: 'https://anilist.co/activity/629592629', image: 'https://files.catbox.moe/pyjy0z.png' },
+ // { post: 'https://anilist.co/activity/629593251', image: 'https://files.catbox.moe/e9xx50.png' },
+ // { post: 'https://anilist.co/activity/630084060', image: 'https://i.imgur.com/zVU0gie.gif' },
+ // { post: 'https://anilist.co/activity/630462423', image: 'https://files.catbox.moe/b63wxi.png' },
+ // {
+ // post: 'https://anilist.co/activity/630464366',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1158527481486266490/codegeass1.png'
+ // },
+ // { post: 'https://anilist.co/activity/630996180', image: 'https://files.catbox.moe/ap15dx.png' },
+ // { post: 'https://anilist.co/activity/631494022', image: 'https://files.catbox.moe/fw4rqx.png' },
+ // {
+ // post: 'https://anilist.co/activity/631503062',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1158787682231660684/rize1.png'
+ // },
+ // {
+ // post: 'https://anilist.co/activity/632259051',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1154438205731524638/1158943217459412992/Luna_Bday2023_Axel5.png'
+ // },
+ // { post: 'https://anilist.co/activity/632260829', image: 'https://files.catbox.moe/ighico.png' },
+ // { post: 'https://anilist.co/activity/632311940', image: 'https://files.catbox.moe/ukv6tv.png' },
+ // {
+ // post: 'https://anilist.co/activity/632311940',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1160915207711895593/Gintoki_2.png'
+ // },
+ // {
+ // post: 'https://anilist.co/activity/632407688',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1136989514653519924/1159856849735127110/Nishinoya_1.png'
+ // },
+ // { post: 'https://anilist.co/activity/632832412', image: 'https://files.catbox.moe/9lk6s1.png' },
+ // { post: 'https://anilist.co/activity/633710355', image: 'https://i.imgur.com/JmpriDr.gif' },
+ // { post: 'https://anilist.co/activity/633710743', image: 'https://files.catbox.moe/it9d7q.png' },
+ // {
+ // post: 'https://anilist.co/activity/633711260',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1118627570074800264/1160674827670134804/Sonia1.png'
+ // },
+ // { post: 'https://anilist.co/activity/634118108', image: 'https://files.catbox.moe/tzudpj.png' },
+ // {
+ // post: 'https://anilist.co/activity/634119722',
+ // image:
+ // 'https://cdn.discordapp.com/attachments/1085425937933418578/1162583650840354846/Mystery_Day_badge_4.png'
+ // }
+ // ];
+
+ // onMount(async () => {
+ // const id = (await user(data.username)).id;
+
+ // for (const badge of badges) {
+ // await fetch(`/api/badges-add?id=${id}`, {
+ // method: 'POST',
+ // body: JSON.stringify(badge)
+ // });
+ // }
+ // });
+
+ onMount(async () => {
+ if (data.user) {
+ currentUserIdentity = userIdentity(data.user);
+ } else {
+ currentUserIdentity = new Promise((resolve) =>
+ resolve({
+ name: 'Guest',
+ id: -1
+ })
+ );
+ }
+ });
+
+ 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;
+
+ fetch(`/api/badges/add`, {
+ method: 'POST',
+ body: JSON.stringify([
+ { image: imageURL.value, post: activityURL.value, description: description.value }
+ ])
+ });
+
+ console.log(imageURL.value, activityURL.value, description.value);
+
+ imageURL.value = '';
+ activityURL.value = '';
+ description.value = '';
+ };
+
+ const removeBadge = (badge: Badge) => {
+ fetch(`/api/badges/remove?id=${badge.id}`, {
+ method: 'POST'
+ });
+ (document.querySelector(`#badge-${badge.id}`) as HTMLAnchorElement).style.display = 'none';
+ };
+</script>
+
+{#await currentUserIdentity}
+ Loading ...
+{:then identity}
+ {@const isOwner = identity && identity.name === data.username}
+ <p>
+ <a href={`/user/${data.username}`}>Back to Profile</a>
+ {#if isOwner}
+ •
+ <a href={`#`} on:click={() => (editMode = !editMode)}
+ >{editMode ? 'Disable' : 'Enable'} Edit Mode</a
+ >
+ {/if}
+ </p>
+
+ {#if editMode && isOwner}
+ <p>
+ Delete mode is enabled. Click on an image to delete it. There is no confirmation, so be
+ careful!
+ </p>
+
+ <p>
+ <input type="text" placeholder="Image URL" name="image_url" />
+ <input type="text" placeholder="Activity URL" name="activity_url" />
+ <input type="text" placeholder="Description (Optional)" name="description" />
+ <a href={`#`} on:click={submitBadge}>Add Badge</a>
+ </p>
+ {/if}
+
+ <div id="badges">
+ {#each data.badges as badge}
+ {#if editMode}
+ <a href={`#`} on:click={() => removeBadge(badge)} id={`badge-${badge.id}`}>
+ <img src={badge.image} alt={badge.description} />
+ </a>
+ {:else}
+ <a href={badge.post} target="_blank" id={`badge-${badge.id}`}>
+ <img src={badge.image} alt={badge.description} />
+ </a>
+ {/if}
+ {/each}
+ </div>
+{/await}
+
+<style>
+ /* body {
+ margin: 0;
+ padding: 0;
+ text-align: center;
+ background-color: #151f2e;
+ } */
+
+ img {
+ width: 100%;
+ height: auto;
+ }
+
+ #badges {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(8%, 1fr));
+ grid-gap: 0;
+ }
+</style>