mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat: add video watch progress tracking with resume functionality
Implements a complete watch progress system that allows users to: - Save playback position automatically every 5 seconds while watching - Resume from last position when returning to a video - Save progress on page leave using sendBeacon for reliability Also includes: - Optimized slug generation in projects and workspaces APIs (single query vs loop) - Added isActive filter to version queries across all video endpoints - Added pagination support for video and comment queries - Enhanced database pool management with connection limits and graceful shutdown - New WatchProgress Prisma model with user-version relations
This commit is contained in:
@@ -52,6 +52,11 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const session = await auth();
|
||||
const { projectId } = await params;
|
||||
|
||||
// Parse pagination params
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100);
|
||||
const offset = parseInt(searchParams.get('offset') || '0');
|
||||
|
||||
const project = await db.project.findUnique({
|
||||
where: { id: projectId },
|
||||
@@ -64,10 +69,20 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
},
|
||||
videos: {
|
||||
orderBy: { position: 'asc' },
|
||||
skip: offset,
|
||||
take: limit,
|
||||
include: {
|
||||
versions: {
|
||||
where: { isActive: true },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
take: 1,
|
||||
select: {
|
||||
id: true,
|
||||
thumbnailUrl: true,
|
||||
duration: true,
|
||||
versionNumber: true,
|
||||
_count: { select: { comments: true } },
|
||||
},
|
||||
},
|
||||
_count: { select: { versions: true } },
|
||||
},
|
||||
|
||||
@@ -15,25 +15,37 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
const session = await auth();
|
||||
const { projectId, videoId } = await params;
|
||||
|
||||
// Parse query params for pagination and options
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const commentLimit = Math.min(parseInt(searchParams.get('commentLimit') || '50'), 100);
|
||||
const commentOffset = parseInt(searchParams.get('commentOffset') || '0');
|
||||
const includeReplies = searchParams.get('includeReplies') === 'true';
|
||||
|
||||
const video = await db.video.findFirst({
|
||||
where: { id: videoId, projectId },
|
||||
include: {
|
||||
project: true,
|
||||
versions: {
|
||||
where: { isActive: true },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
take: 1,
|
||||
include: {
|
||||
comments: {
|
||||
orderBy: { timestamp: 'asc' },
|
||||
skip: commentOffset,
|
||||
take: commentLimit,
|
||||
include: {
|
||||
author: { select: { id: true, name: true, image: true } },
|
||||
tag: { select: { id: true, name: true, color: true } },
|
||||
replies: {
|
||||
orderBy: { createdAt: 'asc' },
|
||||
include: {
|
||||
author: { select: { id: true, name: true, image: true } },
|
||||
tag: { select: { id: true, name: true, color: true } },
|
||||
...(includeReplies ? {
|
||||
replies: {
|
||||
orderBy: { createdAt: 'asc' },
|
||||
include: {
|
||||
author: { select: { id: true, name: true, image: true } },
|
||||
tag: { select: { id: true, name: true, color: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} : {}),
|
||||
},
|
||||
where: { parentId: null }, // Only top-level comments
|
||||
},
|
||||
|
||||
@@ -38,8 +38,14 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
orderBy: { position: 'asc' },
|
||||
include: {
|
||||
versions: {
|
||||
where: { isActive: true },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
include: {
|
||||
take: 1,
|
||||
select: {
|
||||
id: true,
|
||||
thumbnailUrl: true,
|
||||
duration: true,
|
||||
versionNumber: true,
|
||||
_count: { select: { comments: true } },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -105,14 +105,19 @@ export async function POST(request: NextRequest) {
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-');
|
||||
|
||||
// Ensure uniqueness by appending random suffix if needed
|
||||
// Find all existing slugs with the same prefix in a single query
|
||||
const existingProjects = await db.project.findMany({
|
||||
where: { slug: { startsWith: baseSlug } },
|
||||
select: { slug: true },
|
||||
});
|
||||
|
||||
// Generate unique slug from the results
|
||||
const usedSlugs = new Set(existingProjects.map(p => p.slug));
|
||||
let slug = baseSlug;
|
||||
let attempts = 0;
|
||||
while (attempts < 10) {
|
||||
const existing = await db.project.findUnique({ where: { slug } });
|
||||
if (!existing) break;
|
||||
slug = `${baseSlug}-${Math.random().toString(36).substring(2, 6)}`;
|
||||
attempts++;
|
||||
let counter = 1;
|
||||
while (usedSlugs.has(slug)) {
|
||||
slug = `${baseSlug}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
// Verify user has access to the workspace
|
||||
|
||||
Reference in New Issue
Block a user