223 lines
7.3 KiB
TypeScript
223 lines
7.3 KiB
TypeScript
import { error, fail } from '@sveltejs/kit';
|
|
import type { Actions, PageServerLoad } from './$types';
|
|
import { requireUser } from '#lib/server/auth.js';
|
|
import { getTaxonomy, listPieces } from '#lib/server/content.js';
|
|
import {
|
|
addPieceUsage,
|
|
addShowSection,
|
|
getShow,
|
|
movePieceUsage,
|
|
moveShowSection,
|
|
removePieceUsage,
|
|
removeShowSection,
|
|
saveShowMetadata,
|
|
updatePieceUsage,
|
|
updateShowSection,
|
|
type EditableShowStatus,
|
|
type UsageStatus
|
|
} from '#lib/server/shows.js';
|
|
|
|
function text(data: FormData, name: string) {
|
|
return String(data.get(name) || '');
|
|
}
|
|
|
|
function nullableDuration(data: FormData) {
|
|
const value = text(data, 'explicitDurationSeconds').trim();
|
|
return value === '' ? null : Number(value);
|
|
}
|
|
|
|
function pieceDurationInSeconds(data: FormData) {
|
|
return Math.round(Number(data.get('durationMinutes')) * 60);
|
|
}
|
|
|
|
function berlinLocalToUtc(value: string): string | null {
|
|
if (!value) return null;
|
|
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(value);
|
|
if (!match) throw new Error('INVALID_TIMESTAMP');
|
|
const parts = match.slice(1).map(Number);
|
|
const localMilliseconds = Date.UTC(parts[0], parts[1] - 1, parts[2], parts[3], parts[4]);
|
|
let candidate = localMilliseconds;
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
const formatted = new Intl.DateTimeFormat('en-CA', {
|
|
timeZone: 'Europe/Berlin',
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hourCycle: 'h23'
|
|
}).formatToParts(new Date(candidate));
|
|
const read = (type: Intl.DateTimeFormatPartTypes) =>
|
|
Number(formatted.find((part) => part.type === type)?.value);
|
|
candidate +=
|
|
localMilliseconds -
|
|
Date.UTC(read('year'), read('month') - 1, read('day'), read('hour'), read('minute'));
|
|
}
|
|
return new Date(candidate).toISOString();
|
|
}
|
|
|
|
function berlinInput(value: string | null) {
|
|
if (!value) return '';
|
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
timeZone: 'Europe/Berlin',
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hourCycle: 'h23'
|
|
}).formatToParts(new Date(value));
|
|
const read = (type: Intl.DateTimeFormatPartTypes) =>
|
|
parts.find((part) => part.type === type)?.value ?? '';
|
|
return `${read('year')}-${read('month')}-${read('day')}T${read('hour')}:${read('minute')}`;
|
|
}
|
|
|
|
function actionError(cause: unknown) {
|
|
const code = cause instanceof Error ? cause.message : '';
|
|
if (code === 'CONFLICT')
|
|
return {
|
|
status: 409,
|
|
error: 'Die Sendung wurde inzwischen geändert. Bitte neu laden.',
|
|
conflict: true
|
|
};
|
|
if (code === 'SHOW_COMPLETED')
|
|
return { status: 409, error: 'Abgeschlossene Sendungen können nicht bearbeitet werden.' };
|
|
if (code === 'PIECE_NOT_READY')
|
|
return { status: 400, error: 'Nur sendefertige Pieces können eingeplant werden.' };
|
|
if (code === 'NOT_FOUND') return { status: 404, error: 'Der Eintrag wurde nicht gefunden.' };
|
|
if (code === 'INVALID_DURATION')
|
|
return { status: 400, error: 'Piece-Dauern müssen zwischen 0 und 1.440 Minuten liegen.' };
|
|
if (code.startsWith('INVALID_'))
|
|
return { status: 400, error: 'Mindestens ein Feld ist ungültig oder zu lang.' };
|
|
return { status: 500, error: 'Die Änderung konnte nicht gespeichert werden.' };
|
|
}
|
|
|
|
function attempt(intent: string, operation: () => unknown) {
|
|
try {
|
|
return { intent, success: 'Änderung gespeichert.', result: operation() };
|
|
} catch (cause) {
|
|
return fail(actionError(cause).status, { intent, ...actionError(cause) });
|
|
}
|
|
}
|
|
|
|
export const load: PageServerLoad = ({ locals, params, url }) => {
|
|
requireUser(locals.user);
|
|
const show = getShow(params.id);
|
|
if (!show) error(404, 'Sendung nicht gefunden');
|
|
return {
|
|
show,
|
|
startsAtLocal: berlinInput(show.startsAtUtc),
|
|
pieces: listPieces({
|
|
statuses: ['ready'],
|
|
query: url.searchParams.get('q') ?? '',
|
|
categoryId: url.searchParams.get('category') ?? ''
|
|
}),
|
|
taxonomy: getTaxonomy(),
|
|
filters: {
|
|
query: url.searchParams.get('q') ?? '',
|
|
categoryId: url.searchParams.get('category') ?? ''
|
|
}
|
|
};
|
|
};
|
|
|
|
export const actions = {
|
|
saveMetadata: async ({ locals, params, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('saveMetadata', () =>
|
|
saveShowMetadata(params.id, Number(data.get('revision')), {
|
|
title: text(data, 'title'),
|
|
startsAtUtc: berlinLocalToUtc(text(data, 'startsAt')),
|
|
twitchTitle: text(data, 'twitchTitle'),
|
|
announcementText: text(data, 'announcementText'),
|
|
status: text(data, 'status') as EditableShowStatus
|
|
})
|
|
);
|
|
},
|
|
addSection: async ({ locals, params, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('addSection', () =>
|
|
addShowSection(params.id, Number(data.get('revision')), {
|
|
name: text(data, 'name'),
|
|
categoryId: text(data, 'categoryId') || null,
|
|
explicitDurationSeconds: nullableDuration(data)
|
|
})
|
|
);
|
|
},
|
|
updateSection: async ({ locals, params, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('updateSection', () =>
|
|
updateShowSection(params.id, text(data, 'sectionId'), Number(data.get('revision')), {
|
|
name: text(data, 'name'),
|
|
categoryId: text(data, 'categoryId') || null,
|
|
explicitDurationSeconds: nullableDuration(data)
|
|
})
|
|
);
|
|
},
|
|
removeSection: async ({ locals, params, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('removeSection', () =>
|
|
removeShowSection(params.id, text(data, 'sectionId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
moveSection: async ({ locals, params, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('moveSection', () =>
|
|
moveShowSection(
|
|
params.id,
|
|
text(data, 'sectionId'),
|
|
Number(data.get('position')),
|
|
Number(data.get('revision'))
|
|
)
|
|
);
|
|
},
|
|
addUsage: async ({ locals, params, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('addUsage', () =>
|
|
addPieceUsage(params.id, Number(data.get('revision')), {
|
|
pieceId: text(data, 'pieceId'),
|
|
sectionId: text(data, 'sectionId') || null
|
|
})
|
|
);
|
|
},
|
|
updateUsage: async ({ locals, params, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('updateUsage', () =>
|
|
updatePieceUsage(params.id, text(data, 'usageId'), Number(data.get('revision')), {
|
|
title: text(data, 'title'),
|
|
durationSeconds: pieceDurationInSeconds(data),
|
|
ownNotes: text(data, 'ownNotes'),
|
|
intro: text(data, 'intro'),
|
|
research: text(data, 'research'),
|
|
moderation: text(data, 'moderation'),
|
|
transition: text(data, 'transition'),
|
|
presentation: text(data, 'presentation'),
|
|
status: text(data, 'status') as UsageStatus
|
|
})
|
|
);
|
|
},
|
|
removeUsage: async ({ locals, params, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('removeUsage', () =>
|
|
removePieceUsage(params.id, text(data, 'usageId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
moveUsage: async ({ locals, params, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('moveUsage', () =>
|
|
movePieceUsage(params.id, text(data, 'usageId'), Number(data.get('revision')), {
|
|
sectionId: text(data, 'sectionId') || null,
|
|
position: Number(data.get('position'))
|
|
})
|
|
);
|
|
}
|
|
} satisfies Actions;
|