diff options
| author | Fuwn <[email protected]> | 2023-10-24 17:12:54 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2023-10-24 17:12:54 -0700 |
| commit | 861c03b85160972431ca9b262345d15edecf9acb (patch) | |
| tree | 48bf0a18cebf3c8fb20b774adac7f9c85373a3a2 | |
| parent | fix(settings): round down chapters hint (diff) | |
| download | due.moe-861c03b85160972431ca9b262345d15edecf9acb.tar.xz due.moe-861c03b85160972431ca9b262345d15edecf9acb.zip | |
feat: badge wall
| -rw-r--r-- | .dockerignore | 1 | ||||
| -rw-r--r-- | .eslintignore | 1 | ||||
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | .prettierignore | 1 | ||||
| -rw-r--r-- | src/lib/userBadgesDatabase.ts | 57 | ||||
| -rw-r--r-- | src/routes/api/badges/add/+server.ts | 23 | ||||
| -rw-r--r-- | src/routes/api/badges/remove/+server.ts | 22 | ||||
| -rw-r--r-- | src/routes/user/[user]/+page.server.ts | 5 | ||||
| -rw-r--r-- | src/routes/user/[user]/+page.svelte | 40 | ||||
| -rw-r--r-- | src/routes/user/[user]/badges/+page.server.ts | 11 | ||||
| -rw-r--r-- | src/routes/user/[user]/badges/+page.svelte | 301 |
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 @@ -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> |