first working demo
This commit is contained in:
256
src/routes/participate/audio/[id]/+page.server.js
Normal file
256
src/routes/participate/audio/[id]/+page.server.js
Normal file
@@ -0,0 +1,256 @@
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { audioFile, rating, participantProgress, participant } 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' };
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user