Expand website and game trackers
Some checks failed
Test and deploy staging / staging (push) Failing after 5m29s
Some checks failed
Test and deploy staging / staging (push) Failing after 5m29s
This commit is contained in:
133
src/routes/projects/game-tracker/osu/+page.server.ts
Normal file
133
src/routes/projects/game-tracker/osu/+page.server.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { and, desc, eq } from 'drizzle-orm';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { osuEntries } from '$lib/server/db/schema';
|
||||
import { allowedOsuUrl, boundedInteger, validDate } from '$lib/game-tracker/journal';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => ({
|
||||
entries: locals.user
|
||||
? await db
|
||||
.select()
|
||||
.from(osuEntries)
|
||||
.where(eq(osuEntries.userId, locals.user.id))
|
||||
.orderBy(desc(osuEntries.playedOn), desc(osuEntries.id))
|
||||
: [],
|
||||
seo: {
|
||||
title: 'osu! training · Game Tracker',
|
||||
description:
|
||||
'A personal journal for memorable scores, beatmap goals and speed, stamina, aim and reading practice.'
|
||||
}
|
||||
});
|
||||
export const actions = {
|
||||
save: async ({ request, locals }) => {
|
||||
if (!locals.user) return fail(401, { message: 'Sign in to save training.' });
|
||||
const data = await request.formData();
|
||||
const t = (k: string) => String(data.get(k) ?? '').trim();
|
||||
const id = t('id') ? boundedInteger(data.get('id'), 1, 2147483647) : null;
|
||||
const kind = t('kind');
|
||||
const mode = t('mode');
|
||||
const goal = t('goal') || 'pass';
|
||||
const skill = t('skill') || 'stamina';
|
||||
if (
|
||||
(t('id') && !id) ||
|
||||
!['score', 'goal', 'training'].includes(kind) ||
|
||||
!['osu', 'taiko', 'fruits', 'mania'].includes(mode) ||
|
||||
!['pass', 'fc', 'accuracy', 'rank', 'custom'].includes(goal) ||
|
||||
!['stamina', 'speed', 'aim', 'reading', 'rhythm', 'finger-control', 'consistency'].includes(
|
||||
skill
|
||||
) ||
|
||||
!t('title') ||
|
||||
t('title').length > 250 ||
|
||||
t('notes').length > 5000 ||
|
||||
t('mods').length > 80 ||
|
||||
!validDate(t('playedOn'))
|
||||
)
|
||||
return fail(400, { message: 'Check the title, date and training details.' });
|
||||
if (!allowedOsuUrl(t('beatmapUrl'), 'beatmap') || !allowedOsuUrl(t('scoreUrl'), 'score'))
|
||||
return fail(400, { message: 'Use an https://osu.ppy.sh beatmap or score link.' });
|
||||
const optionalInt = (key: string, min: number, max: number) =>
|
||||
t(key) ? boundedInteger(data.get(key), min, max) : null;
|
||||
const bpm = optionalInt('bpm', 1, 2000),
|
||||
duration = optionalInt('duration', 1, 86400),
|
||||
combo = optionalInt('combo', 0, 1000000),
|
||||
misses = optionalInt('misses', 0, 1000000);
|
||||
const decimal = (key: string, min: number, max: number) =>
|
||||
!t(key) ||
|
||||
(/^\d+(\.\d{1,4})?$/.test(t(key)) && Number(t(key)) >= min && Number(t(key)) <= max);
|
||||
if (
|
||||
(t('bpm') && bpm === null) ||
|
||||
(t('duration') && duration === null) ||
|
||||
(t('combo') && combo === null) ||
|
||||
(t('misses') && misses === null) ||
|
||||
!decimal('accuracy', 0, 100) ||
|
||||
!decimal('pp', 0, 100000) ||
|
||||
!t('rate') ||
|
||||
!decimal('rate', 0.1, 4) ||
|
||||
(kind === 'training' && (!bpm || !duration))
|
||||
)
|
||||
return fail(400, {
|
||||
message:
|
||||
'Training needs BPM and duration. Check numeric fields, accuracy (0–100) and rate (0.1–4).'
|
||||
});
|
||||
const values = {
|
||||
userId: locals.user.id,
|
||||
kind: kind as 'score' | 'goal' | 'training',
|
||||
mode,
|
||||
goal,
|
||||
skill,
|
||||
title: t('title'),
|
||||
notes: t('notes'),
|
||||
bpm,
|
||||
duration,
|
||||
combo,
|
||||
misses,
|
||||
accuracy: t('accuracy') || null,
|
||||
pp: t('pp') || null,
|
||||
rate: t('rate'),
|
||||
mods: t('mods') || 'NM',
|
||||
beatmapUrl: t('beatmapUrl') || null,
|
||||
scoreUrl: t('scoreUrl') || null,
|
||||
playedOn: t('playedOn'),
|
||||
achieved: data.get('achieved') === 'on'
|
||||
};
|
||||
if (id) {
|
||||
const updated = await db
|
||||
.update(osuEntries)
|
||||
.set(values)
|
||||
.where(and(eq(osuEntries.id, id), eq(osuEntries.userId, locals.user.id)))
|
||||
.returning({ id: osuEntries.id });
|
||||
if (!updated.length) return fail(404, { message: 'Entry not found.' });
|
||||
} else await db.insert(osuEntries).values(values);
|
||||
return { saved: true };
|
||||
},
|
||||
toggleGoal: async ({ request, locals }) => {
|
||||
if (!locals.user) return fail(401, { message: 'Sign in to update goals.' });
|
||||
const data = await request.formData();
|
||||
const id = boundedInteger(data.get('id'), 1, 2147483647);
|
||||
if (!id) return fail(400, { message: 'Invalid goal.' });
|
||||
const updated = await db
|
||||
.update(osuEntries)
|
||||
.set({ achieved: data.get('achieved') === 'on' })
|
||||
.where(
|
||||
and(
|
||||
eq(osuEntries.id, id),
|
||||
eq(osuEntries.userId, locals.user.id),
|
||||
eq(osuEntries.kind, 'goal')
|
||||
)
|
||||
)
|
||||
.returning({ id: osuEntries.id });
|
||||
return updated.length ? { saved: true } : fail(404, { message: 'Goal not found.' });
|
||||
},
|
||||
delete: async ({ request, locals }) => {
|
||||
if (!locals.user) return fail(401, { message: 'Sign in to remove entries.' });
|
||||
const data = await request.formData();
|
||||
const id = boundedInteger(data.get('id'), 1, 2147483647);
|
||||
if (!id) return fail(400, { message: 'Invalid entry.' });
|
||||
const removed = await db
|
||||
.delete(osuEntries)
|
||||
.where(and(eq(osuEntries.id, id), eq(osuEntries.userId, locals.user.id)))
|
||||
.returning({ id: osuEntries.id });
|
||||
return removed.length ? { saved: true } : fail(404, { message: 'Entry not found.' });
|
||||
}
|
||||
} satisfies Actions;
|
||||
Reference in New Issue
Block a user