diff --git a/prisma/migrations/20230917122645_pasen/migration.sql b/prisma/migrations/20230917122645_pasen/migration.sql new file mode 100644 index 00000000..4e6d95ba --- /dev/null +++ b/prisma/migrations/20230917122645_pasen/migration.sql @@ -0,0 +1,26 @@ +-- CreateTable +CREATE TABLE `Egg` ( + `id` VARCHAR(191) NOT NULL, + `img` VARCHAR(191) NOT NULL DEFAULT 'egg1.png', + `name` VARCHAR(191) NOT NULL DEFAULT 'Egg', + `found` TINYBLOB NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateIndex +CREATE UNIQUE INDEX `Egg_name_key` ON `Egg`(`name`); + +INSERT IGNORE INTO `Egg` (`id`, `name`, `found`) +VALUES ('HX1aAqX25B4lo1FQgNUeKjPEGH53r5byWc0Id6gmwkPeevvG7N', 'urEI', 0x0), + ('UWn89YWEeurHH6NfeUFYBaewEu88uhl2iWTCh4OP18VguRs078', 'spEItify', 0x0), + ('ZX0cAYFXqIyMQQz4cVmgVI7vRZfx3Fb9EH1lHZUAHPp4qVlSNN', 'strEIbos', 0x0), + ('xkzR2iH1Ut4kwoT8G53vlxFoRQqfZLrKESLO7lFygpSMiPfDk7', 'Markdown', 0x0), + ('FDaR8VJklWZdZX9qYMabhAJlR9wWWU7gPgagGfZG8AbckOY27T', 'profEIl', 0x0), + ('3BoXkqmZITrIkVQ2WzaFXTROlx8VxjRKinjWcz6H1LLdVfZnLP', 'tutorEIal', 0x0), + ('HnSF3i41PdqTzUpvmO7S0RwiB7RA6el5nfRUBAY7IhJVAK0DkT', 'Menu Ei', 0x0), + ('wCZXlGQDecVPXIrQyz8lmR8BLpe730hTPu0mEVcFetBPjWXxwH', '100 Kliks', 0x0), + ('Zask8Z6FdyZwxFQkwwUrPAUd53he7ydiLW1lJezA1mg0m7zm7O', 'instEIlingen', 0x0); + + +INSERT IGNORE INTO `Settings` (`name`, `description`, `value`) VALUES ('EGGHUNT_ENABLED', 'Of het paasei zoeken aan staat', '0'); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 667abe69..09b97ef1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -757,3 +757,10 @@ model Comment { activity Activity? @relation(fields: [activityId], references: [id], onDelete: SetNull) activityId Int? } + +model Egg { + id String @id + img String @default("egg1.png") + name String @unique @default("Egg") + found Bytes @db.TinyBlob +} diff --git a/src/lib/components/breadcrumps.svelte b/src/lib/components/breadcrumps.svelte index 44793bfd..4852ce06 100644 --- a/src/lib/components/breadcrumps.svelte +++ b/src/lib/components/breadcrumps.svelte @@ -11,10 +11,10 @@ {#each $breadcrumbStore as c, i} {#if c !== null} {#if i == $breadcrumbStore.length - 1} - {@html markdown(c.label)} + {@html markdown(c.label, true)} {:else} - {@html markdown(c.label)} + {@html markdown(c.label, true)} {/if} diff --git a/src/routes/(app)/activiteit/[slug]/[id]/_user-card.svelte b/src/lib/components/user-card.svelte similarity index 84% rename from src/routes/(app)/activiteit/[slug]/[id]/_user-card.svelte rename to src/lib/components/user-card.svelte index 4c5b8ee0..dfc3b7b2 100644 --- a/src/routes/(app)/activiteit/[slug]/[id]/_user-card.svelte +++ b/src/lib/components/user-card.svelte @@ -1,9 +1,13 @@
@@ -15,9 +19,11 @@ name={user.firstName + ' ' + user.lastName} /> -
-
-
+ {#if status} +
+
+
+ {/if}
diff --git a/src/lib/oudlogo.png b/src/lib/oudlogo.png new file mode 100644 index 00000000..4ec767a7 Binary files /dev/null and b/src/lib/oudlogo.png differ diff --git a/src/lib/server/egghunt.ts b/src/lib/server/egghunt.ts new file mode 100644 index 00000000..3c7a6997 --- /dev/null +++ b/src/lib/server/egghunt.ts @@ -0,0 +1,34 @@ +import db from './db'; + +export interface Egg { + show: boolean; + id: string; + img: string | undefined; + name: string | undefined; +} + +export const shouldShowEgg = async (eggId: string, userId: number): Promise => { + const enabled = await db.settings.findUnique({ + where: { + name: 'EGGHUNT_ENABLED' + } + }); + if (enabled?.value != '1') + return { + show: false, + img: undefined, + name: undefined, + id: eggId + }; + + const egg = await db.egg.findUnique({ + where: { id: eggId } + }); + + return { + show: !!(egg && !([...egg.found][Math.round((userId - 1) / 8)] & (1 << (userId - 1) % 8))), + img: egg?.img, + name: egg?.name, + id: eggId + }; +}; diff --git a/src/lib/server/spotify.ts b/src/lib/server/spotify.ts index 0e1af3ee..b4f1fdb0 100644 --- a/src/lib/server/spotify.ts +++ b/src/lib/server/spotify.ts @@ -32,4 +32,31 @@ export const refreshToken = async () => { spotify.setAccessToken(accessToken); }; +export const getLikedTracks = async (locals: App.Locals) => { + return ( + await db.trackReaction.findMany({ + where: { + userId: locals.user.id, + liked: true + }, + select: { + trackId: true + } + }) + ).map((r) => r.trackId); +}; + +export const getPlaylist = async () => { + return ( + await db.track.findMany({ + where: { + inPlaylist: true + }, + select: { + id: true + } + }) + ).map((track) => track.id); +}; + export default spotify; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e2e8ead8..69e4e612 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -8,35 +8,40 @@ import markdownItSup from 'markdown-it-sup'; import markdownItIns from 'markdown-it-ins'; import markdownItEmojis from 'markdown-it-emoji'; // @ts-expect-error Geen types -import markdownItArrow from 'markdown-it-smartarrows' +import markdownItArrow from 'markdown-it-smartarrows'; import markdownItKbd from 'markdown-it-kbd'; -import markdownItPlainText from 'markdown-it-plain-text' - -import xss from 'xss' +import markdownItPlainText from 'markdown-it-plain-text'; +import xss from 'xss'; const md = new markdownIt({ linkify: true, breaks: true }) - .use(markdownItSub) - .use(markdownItSup) - .use(markdownItIns) - .use(markdownItEmojis) - .use(markdownItArrow) - .use(markdownItKbd) - .use(markdownItPlainText) - .disable(['image']); - -export function markdown(text: string | null | undefined): string | null { - if (text === null || text === undefined) return null; - return xss(md.renderInline(text)) + .use(markdownItSub) + .use(markdownItSup) + .use(markdownItIns) + .use(markdownItEmojis) + .use(markdownItArrow) + .use(markdownItKbd) + .use(markdownItPlainText) + .disable(['image']); + +export function markdown(text: string | null | undefined, disableEgg = false): string | null { + if (text === null || text === undefined) return null; + let html = md.renderInline(text); + if (!disableEgg) + html = html.replace( + /paasei/gi, + 'paasei' + ); + return xss(html); } export function stripMarkdown(text: string | undefined) { - if (text === null || text === undefined) return null; - md.render(text) - return (md as any).plainText + if (text === null || text === undefined) return null; + md.render(text); + return (md as any).plainText; } // Currently in dark mode? diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts index c4e736d2..cf70f926 100644 --- a/src/routes/(app)/+layout.server.ts +++ b/src/routes/(app)/+layout.server.ts @@ -1,42 +1,52 @@ import type { Committee } from '@prisma/client'; import type { LayoutServerLoad } from './$types'; import { LDAP_IDS } from '$lib/constants'; +import { shouldShowEgg } from '$lib/server/egghunt'; export const load = (async ({ locals }) => { - return { - topRole: getTopRole(locals.committees), - }; + return { + topRole: getTopRole(locals.committees), + menuEgg: shouldShowEgg('HnSF3i41PdqTzUpvmO7S0RwiB7RA6el5nfRUBAY7IhJVAK0DkT', locals.user.id), + profileEgg: shouldShowEgg('FDaR8VJklWZdZX9qYMabhAJlR9wWWU7gPgagGfZG8AbckOY27T', locals.user.id) + }; }) satisfies LayoutServerLoad; -const ranking = [LDAP_IDS.FEUTEN, LDAP_IDS.SENAAT, LDAP_IDS.ADMINS, LDAP_IDS.FINANCIE, LDAP_IDS.COLOSSEUM, LDAP_IDS.MEMBERS] +const ranking = [ + LDAP_IDS.FEUTEN, + LDAP_IDS.SENAAT, + LDAP_IDS.ADMINS, + LDAP_IDS.FINANCIE, + LDAP_IDS.COLOSSEUM, + LDAP_IDS.MEMBERS +]; function getTopRole(committees: Committee[]) { - // Get the best committee where ldapId is lowest in the ranking - let topIdx = ranking.length - 1 - let topCommittee = committees[topIdx] - for (const c of committees) { - const index = ranking.indexOf(c.ldapId) - if (index === -1) continue - if (index < topIdx) { - topIdx = index - topCommittee = c - } - } + // Get the best committee where ldapId is lowest in the ranking + let topIdx = ranking.length - 1; + let topCommittee = committees[topIdx]; + for (const c of committees) { + const index = ranking.indexOf(c.ldapId); + if (index === -1) continue; + if (index < topIdx) { + topIdx = index; + topCommittee = c; + } + } - switch (topCommittee?.ldapId) { - case LDAP_IDS.FEUTEN: - return 'Feut' - case LDAP_IDS.SENAAT: - return 'Senaat' - case LDAP_IDS.ADMINS: - return 'Admin' - case LDAP_IDS.FINANCIE: - return 'FinanCie' - case LDAP_IDS.COLOSSEUM: - return 'Colosseum-bewoner' - case LDAP_IDS.MEMBERS: - return 'Lid' - default: - return 'Lid' - } -} \ No newline at end of file + switch (topCommittee?.ldapId) { + case LDAP_IDS.FEUTEN: + return 'Feut'; + case LDAP_IDS.SENAAT: + return 'Senaat'; + case LDAP_IDS.ADMINS: + return 'Admin'; + case LDAP_IDS.FINANCIE: + return 'FinanCie'; + case LDAP_IDS.COLOSSEUM: + return 'Colosseum-bewoner'; + case LDAP_IDS.MEMBERS: + return 'Lid'; + default: + return 'Lid'; + } +} diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 856a6515..1734c21b 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -10,6 +10,7 @@ import { afterNavigate } from '$app/navigation'; import { Modals, closeModal } from 'svelte-modals'; import MobileMenu from './_mobile-menu.svelte'; + import type { LayoutData } from './$types'; afterNavigate(() => { // Reset scroll position on layout--container-slot @@ -22,10 +23,12 @@ let open = false; const openMenu = () => (open = !open); + + export let data: LayoutData;
- + {#if open}
@@ -40,7 +43,7 @@
- + {#if !open}
diff --git a/src/routes/(app)/+page.server.ts b/src/routes/(app)/+page.server.ts index 96ce13b9..d0412ebc 100644 --- a/src/routes/(app)/+page.server.ts +++ b/src/routes/(app)/+page.server.ts @@ -1,133 +1,146 @@ -import db from '$lib/server/db' +import db from '$lib/server/db'; import type { PageServerLoad } from './$types'; import { getNextBirthdayInLine } from '$lib/server/birthdays'; import { LDAP_IDS } from '$lib/constants'; +import { shouldShowEgg } from '$lib/server/egghunt'; export const load = (async ({ locals }) => { - - // Deze methode faalt niet, ook als je 0 sessies hebt - NR - const getTotalClicks = async () => { - return await db.clickSession.aggregate({ - _sum: { - amount: true - }, - where: { - userId: locals.user.id - } - }); - } - - const getTopClicker = async () => { - let q = await db.$queryRaw` + // Deze methode faalt niet, ook als je 0 sessies hebt - NR + const getTotalClicks = async () => { + return await db.clickSession.aggregate({ + _sum: { + amount: true + }, + where: { + userId: locals.user.id + } + }); + }; + + const getTopClicker = async () => { + let q = (await db.$queryRaw` SELECT u.firstName, SUM(c.amount) AS amount FROM User AS u, ClickSession AS c WHERE u.id = c.userId GROUP BY c.userId ORDER BY amount DESC - LIMIT 1` as { firstName: string, amount: number }[] - - q = q.map((e) => { return { ...e, amount: Number(e.amount) } }); - - return q[0]; - } - - const getGreeting = () => { - let word = 'Goedenavond'; - - const hour = new Date().getHours(); - if (hour < 6) { - word = 'Goedenacht'; - } else if (hour < 12) { - word = 'Goedemorgen'; - } else if (hour < 18) { - word = 'Goedemiddag'; - } - - return `${word}, ${locals.user.firstName}!`; - } - - const getQuote = async () => { - let obj - - // 1 in 2000 chance to get a quote from IBS - if (Math.floor(Math.random() * 2000) === 1234) { - obj = { - quote: '"Wie dit leest trekt een bak" - IBS (1 op 2000 kans)' - } - } else { - try { - obj = await fetch(process.env.QUOTE_API_URL!, { headers: { - 'Authorization': `${process.env.QUOTE_API_TOKEN}` - }}).then((res) => res.json()) - } catch (err) { - obj = { - quote: '"De quote module is stukkie wukkie" - IBS' - } - } - } - - let message = obj.quote - - // Replace all "{string}" with "*{string}*" - message = message.replace(/"([^"]*)"/g, '*“$1”*') - - return message - } - - const getStrafbakken = () => { - // Count the amount of strafbakken locals.user.id has - return db.strafbak.count({ - where: { - receiverId: locals.user.id, - dateDeleted: null - } - }) - } - - const getFirstActivity = () => { - const today = new Date() - - const member = locals.committees.filter((c) => c.ldapId === LDAP_IDS.MEMBERS)[0] - - if (member) { - return db.activity.findFirst({ - orderBy: [{ - endTime: 'asc' - }], - where: { - endTime: { - gte: today - }, - }, - include: { - photo: true - } - }) - } else { - return db.activity.findFirst({ - orderBy: [{ - endTime: 'asc' - }], - where: { - endTime: { - gte: today - }, - membersOnly: false - }, - include: { - photo: true - } - }) - } - } - - return { - clicks: getTotalClicks(), - topclicker: getTopClicker(), - greeting: getGreeting(), - quote: getQuote(), - activity: getFirstActivity(), - strafbakken: getStrafbakken(), - nextBirthday: getNextBirthdayInLine() - } -}) satisfies PageServerLoad; \ No newline at end of file + LIMIT 1`) as { firstName: string; amount: number }[]; + + q = q.map((e) => { + return { ...e, amount: Number(e.amount) }; + }); + + return q[0]; + }; + + const getGreeting = () => { + let word = 'Goedenavond'; + + const hour = new Date().getHours(); + if (hour < 6) { + word = 'Goedenacht'; + } else if (hour < 12) { + word = 'Goedemorgen'; + } else if (hour < 18) { + word = 'Goedemiddag'; + } + + return `${word}, ${locals.user.firstName}!`; + }; + + const getQuote = async () => { + let obj; + + // 1 in 2000 chance to get a quote from IBS + if (Math.floor(Math.random() * 2000) === 1234) { + obj = { + quote: '"Wie dit leest trekt een bak" - IBS (1 op 2000 kans)' + }; + } else { + try { + obj = await fetch(process.env.QUOTE_API_URL!, { + headers: { + Authorization: `${process.env.QUOTE_API_TOKEN}` + } + }).then((res) => res.json()); + } catch (err) { + obj = { + quote: '"De quote module is stukkie wukkie" - IBS' + }; + } + } + + let message = obj.quote; + + // Replace all "{string}" with "*{string}*" + message = message.replace(/"([^"]*)"/g, '*“$1”*'); + + return message; + }; + + const getStrafbakken = () => { + // Count the amount of strafbakken locals.user.id has + return db.strafbak.count({ + where: { + receiverId: locals.user.id, + dateDeleted: null + } + }); + }; + + const getFirstActivity = () => { + const today = new Date(); + + const member = locals.committees.filter((c) => c.ldapId === LDAP_IDS.MEMBERS)[0]; + + if (member) { + return db.activity.findFirst({ + orderBy: [ + { + endTime: 'asc' + } + ], + where: { + endTime: { + gte: today + } + }, + include: { + photo: true + } + }); + } else { + return db.activity.findFirst({ + orderBy: [ + { + endTime: 'asc' + } + ], + where: { + endTime: { + gte: today + }, + membersOnly: false + }, + include: { + photo: true + } + }); + } + }; + + return { + clicks: getTotalClicks(), + topclicker: getTopClicker(), + greeting: getGreeting(), + quote: getQuote(), + activity: getFirstActivity(), + strafbakken: getStrafbakken(), + nextBirthday: getNextBirthdayInLine(), + tutorialEgg: shouldShowEgg( + '3BoXkqmZITrIkVQ2WzaFXTROlx8VxjRKinjWcz6H1LLdVfZnLP', + locals.user.id + ), + knoppersEgg: shouldShowEgg('wCZXlGQDecVPXIrQyz8lmR8BLpe730hTPu0mEVcFetBPjWXxwH', locals.user.id) + }; +}) satisfies PageServerLoad; diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index fc38bc08..fc66f70d 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -84,6 +84,11 @@ await endSession(startTime, sessionClicks, endTime); }); + let hasHit100 = false; + $: if (sessionClicks >= 100) hasHit100 = true; + + /* Other */ + function activityImage(resize: boolean) { let link = ''; @@ -228,6 +233,12 @@ {/if} Meer informatie
+ + {#if data.knoppersEgg.show && hasHit100} + + Paasei + + {/if}
@@ -264,6 +275,12 @@
+{#if data.tutorialEgg.show} + + paasei + +{/if} + diff --git a/src/routes/(app)/_navbar.svelte b/src/routes/(app)/_navbar.svelte index 3b9f35f0..edb44023 100644 --- a/src/routes/(app)/_navbar.svelte +++ b/src/routes/(app)/_navbar.svelte @@ -17,8 +17,11 @@ import Menu from '~icons/tabler/menu-2'; import X from '~icons/tabler/x'; + import type { Egg } from '$lib/server/egghunt'; + export let openMenu: () => void; export let open: boolean; + export let egg: Egg;