Files
taptapp/src/routes/participate/audio/[id]/+page.server.js
2025-07-24 22:38:28 +02:00

303 lines
8.2 KiB
JavaScript

import { redirect, error } from '@sveltejs/kit';
import { db } from '$lib/server/db/index.js';
import { audioFile, rating, participantProgress, participant, overallRating } from '$lib/server/db/schema.js';
import { eq, and, isNull } from 'drizzle-orm';
export async function load({ params, url, cookies }) {
const audioId = params.id;
const token = url.searchParams.get('token');
if (!token) {
throw error(400, 'Invalid or missing invite token');
}
const participantId = cookies.get(`participant-${token}`);
if (!participantId) {
redirect(302, `/participate?token=${token}`);
}
// Verify participant exists and isn't soft-deleted
const participants = await db
.select()
.from(participant)
.where(
and(
eq(participant.id, participantId),
isNull(participant.deletedAt)
)
);
if (participants.length === 0) {
redirect(302, `/participate?token=${token}`);
}
const audioFiles = await db.select({
id: audioFile.id,
filename: audioFile.filename,
contentType: audioFile.contentType,
duration: audioFile.duration,
fileSize: audioFile.fileSize,
createdAt: audioFile.createdAt
})
.from(audioFile)
.where(and(
eq(audioFile.id, audioId),
isNull(audioFile.deletedAt) // Only show active audio files
));
if (audioFiles.length === 0) {
throw error(404, 'Audio file not found');
}
const progressData = await db
.select({
id: participantProgress.id,
isCompleted: participantProgress.isCompleted,
lastPosition: participantProgress.lastPosition,
maxReachedTime: participantProgress.maxReachedTime,
updatedAt: participantProgress.updatedAt
})
.from(participantProgress)
.where(
and(
eq(participantProgress.participantId, participantId),
eq(participantProgress.audioFileId, audioId)
)
);
const progress = progressData.length > 0 ? progressData[0] : null;
const existingRatings = await db
.select()
.from(rating)
.where(and(
eq(rating.participantId, participantId),
eq(rating.audioFileId, audioId),
isNull(rating.deletedAt)
));
return {
audioFile: audioFiles[0],
participantId,
token,
progress,
existingRatings
};
}
export const actions = {
saveRating: async ({ request }) => {
const data = await request.formData();
const participantId = data.get('participantId');
const audioFileId = data.get('audioFileId');
const ratingHistoryStr = data.get('ratingHistory');
const finalValue = parseFloat(data.get('finalValue'));
const maxReachedTime = parseFloat(data.get('maxReachedTime')) || 0;
const currentPosition = parseFloat(data.get('currentPosition')) || 0;
if (!participantId || !audioFileId || !ratingHistoryStr || isNaN(finalValue)) {
return { error: 'Invalid rating data' };
}
try {
const ratingHistory = JSON.parse(ratingHistoryStr);
console.log('Rating history length:', ratingHistory.length);
console.log('Rating history:', ratingHistory);
// Use a transaction to ensure atomicity
await db.transaction(async (tx) => {
// Soft delete any existing ratings for this participant and audio file (for redo functionality)
await tx.update(rating)
.set({ deletedAt: new Date() })
.where(
and(
eq(rating.participantId, participantId),
eq(rating.audioFileId, audioFileId),
isNull(rating.deletedAt)
)
);
// Save single completed rating record with entire timeseries as JSON
const ratingId = `${participantId}-${audioFileId}-${Date.now()}`;
const finalTimestamp = ratingHistory[ratingHistory.length - 1]?.timestamp || 0;
await tx.insert(rating).values({
id: ratingId,
participantId,
audioFileId,
timestamp: finalTimestamp,
value: finalValue,
isCompleted: true,
timeseriesData: JSON.stringify(ratingHistory), // Store entire timeseries as JSON
createdAt: new Date(),
deletedAt: null // Active rating
});
});
// Save listening progress (only when rating is saved)
const progressId = `${participantId}-${audioFileId}`;
const existingProgress = await db
.select()
.from(participantProgress)
.where(
and(
eq(participantProgress.participantId, participantId),
eq(participantProgress.audioFileId, audioFileId)
)
);
if (existingProgress.length > 0) {
await db
.update(participantProgress)
.set({
lastPosition: currentPosition,
maxReachedTime: maxReachedTime,
isCompleted: true, // Mark as completed when rating is saved
updatedAt: new Date()
})
.where(eq(participantProgress.id, existingProgress[0].id));
} else {
await db.insert(participantProgress).values({
id: progressId,
participantId,
audioFileId,
lastPosition: currentPosition,
maxReachedTime: maxReachedTime,
isCompleted: true,
updatedAt: new Date()
});
}
return { success: true };
} catch (error) {
console.error('Error saving rating:', error);
return { error: 'Failed to save rating' };
}
},
deleteRating: async ({ request }) => {
const data = await request.formData();
const participantId = data.get('participantId');
const audioFileId = data.get('audioFileId');
if (!participantId || !audioFileId) {
return { error: 'Invalid request data' };
}
try {
// Soft delete the active rating for this participant and audio file
await db.update(rating)
.set({ deletedAt: new Date() })
.where(
and(
eq(rating.participantId, participantId),
eq(rating.audioFileId, audioFileId),
isNull(rating.deletedAt)
)
);
return { success: true };
} catch (error) {
console.error('Error deleting rating:', error);
return { error: 'Failed to delete rating' };
}
},
updateProgress: async ({ request }) => {
const data = await request.formData();
const participantId = data.get('participantId');
const audioFileId = data.get('audioFileId');
const lastPosition = parseFloat(data.get('lastPosition'));
const isCompleted = data.get('isCompleted') === 'true';
if (!participantId || !audioFileId || isNaN(lastPosition)) {
return { error: 'Invalid progress data' };
}
try {
const progressId = `${participantId}-${audioFileId}`;
const existingProgress = await db
.select()
.from(participantProgress)
.where(
and(
eq(participantProgress.participantId, participantId),
eq(participantProgress.audioFileId, audioFileId)
)
);
if (existingProgress.length > 0) {
await db
.update(participantProgress)
.set({
lastPosition,
isCompleted,
updatedAt: new Date()
})
.where(eq(participantProgress.id, existingProgress[0].id));
} else {
await db.insert(participantProgress).values({
id: progressId,
participantId,
audioFileId,
isCompleted,
lastPosition,
updatedAt: new Date()
});
}
return { success: true };
} catch (error) {
console.error('Error updating progress:', error);
return { error: 'Failed to update progress' };
}
},
saveOverallRating: async ({ request }) => {
const data = await request.formData();
const participantId = data.get('participantId');
const audioFileId = data.get('audioFileId');
const overallRatingValue = parseFloat(data.get('overallRating'));
if (!participantId || !audioFileId || isNaN(overallRatingValue)) {
return { error: 'Invalid overall rating data' };
}
try {
const overallRatingId = `${participantId}-${audioFileId}-overall-${Date.now()}`;
const now = new Date();
// Use a transaction to ensure atomicity (same pattern as saveRating)
await db.transaction(async (tx) => {
// Soft delete any existing overall ratings for this participant and audio file (for redo functionality)
await tx.update(overallRating)
.set({ deletedAt: now })
.where(
and(
eq(overallRating.participantId, participantId),
eq(overallRating.audioFileId, audioFileId),
isNull(overallRating.deletedAt)
)
);
// Always insert new overall rating record
await tx.insert(overallRating).values({
id: overallRatingId,
participantId,
audioFileId,
value: overallRatingValue,
createdAt: now,
updatedAt: now,
deletedAt: null
});
});
return { success: true };
} catch (error) {
console.error('Error saving overall rating:', error);
return { error: 'Failed to save overall rating' };
}
}
};