Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 56 additions & 88 deletions package-lock.json

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions src/app/api/bookmarks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { NextResponse } from 'next/server';

type PersistedVideoBookmark = {
id: string;
time: number;
title: string;
note?: string;
createdAt: string; // ISO
updatedAt: string; // ISO
};

const bookmarksStore = new Map<string, PersistedVideoBookmark[]>();

const keyFor = (userId: string | undefined, lessonId: string) => {
const safeUserId = encodeURIComponent(userId ?? 'anon');
return `${safeUserId}::${encodeURIComponent(lessonId)}`;
};

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const lessonId = searchParams.get('lessonId');
const userId = searchParams.get('userId') ?? undefined;

if (!lessonId) {
return NextResponse.json({ success: false, message: 'lessonId is required' }, { status: 400 });
}

return NextResponse.json({
data: bookmarksStore.get(keyFor(userId, lessonId)) ?? [],
success: true,
});
}

export async function POST(request: Request) {
const body = (await request.json()) as {
userId?: string;
lessonId: string;
bookmark: { id?: string; time: number; title: string; note?: string };
};

if (!body?.lessonId || !body?.bookmark?.time || !body?.bookmark?.title) {
return NextResponse.json({ success: false, message: 'Invalid payload' }, { status: 400 });
}

const now = new Date().toISOString();
const id = body.bookmark.id ?? `bookmark-${Date.now()}`;

const persisted: PersistedVideoBookmark = {
id,
time: Math.max(0, body.bookmark.time),
title: body.bookmark.title.trim(),
note: body.bookmark.note?.trim() ? body.bookmark.note.trim() : undefined,
createdAt: now,
updatedAt: now,
};

const key = keyFor(body.userId, body.lessonId);
const prev = bookmarksStore.get(key) ?? [];
const next = [persisted, ...prev.filter((b) => b.id !== persisted.id)];
bookmarksStore.set(key, next);

return NextResponse.json({ success: true, data: persisted });
}

export async function PATCH(request: Request) {
const body = (await request.json()) as {
userId?: string;
lessonId: string;
id: string;
title: string;
note?: string;
time?: number;
};

if (!body?.lessonId || !body?.id || !body?.title) {
return NextResponse.json({ success: false, message: 'Invalid payload' }, { status: 400 });
}

const key = keyFor(body.userId, body.lessonId);
const prev = bookmarksStore.get(key) ?? [];
const now = new Date().toISOString();

const next = prev.map((b) =>
b.id === body.id
? {
...b,
title: body.title.trim(),
note: body.note?.trim() ? body.note.trim() : undefined,
time: typeof body.time === 'number' ? Math.max(0, body.time) : b.time,
updatedAt: now,
}
: b,
);

bookmarksStore.set(key, next);
return NextResponse.json({ success: true });
}

export async function DELETE(request: Request) {
const body = (await request.json()) as { userId?: string; lessonId: string; id: string };
if (!body?.lessonId || !body?.id) {
return NextResponse.json({ success: false, message: 'Invalid payload' }, { status: 400 });
}

const key = keyFor(body.userId, body.lessonId);
const prev = bookmarksStore.get(key) ?? [];
bookmarksStore.set(
key,
prev.filter((b) => b.id !== body.id),
);

return NextResponse.json({ success: true });
}
108 changes: 108 additions & 0 deletions src/app/api/notes/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { NextResponse } from 'next/server';

type PersistedVideoNote = {
id: string;
time: number;
text: string;
createdAt: string; // ISO
updatedAt: string; // ISO
};

const notesStore = new Map<string, PersistedVideoNote[]>();

const keyFor = (userId: string | undefined, lessonId: string) => {
const safeUserId = encodeURIComponent(userId ?? 'anon');
return `${safeUserId}::${encodeURIComponent(lessonId)}`;
};

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const lessonId = searchParams.get('lessonId');
const userId = searchParams.get('userId') ?? undefined;

if (!lessonId) {
return NextResponse.json({ success: false, message: 'lessonId is required' }, { status: 400 });
}

return NextResponse.json({
data: notesStore.get(keyFor(userId, lessonId)) ?? [],
success: true,
});
}

export async function POST(request: Request) {
const body = (await request.json()) as {
userId?: string;
lessonId: string;
note: { id?: string; time: number; text: string };
};

if (!body?.lessonId || !body?.note?.time || !body?.note?.text) {
return NextResponse.json({ success: false, message: 'Invalid payload' }, { status: 400 });
}

const now = new Date().toISOString();
const id = body.note.id ?? `note-${Date.now()}`;
const persisted: PersistedVideoNote = {
id,
time: Math.max(0, body.note.time),
text: body.note.text.trim(),
createdAt: now,
updatedAt: now,
};

const key = keyFor(body.userId, body.lessonId);
const prev = notesStore.get(key) ?? [];
const next = [persisted, ...prev.filter((n) => n.id !== persisted.id)];
notesStore.set(key, next);

return NextResponse.json({ success: true, data: persisted });
}

export async function PATCH(request: Request) {
const body = (await request.json()) as {
userId?: string;
lessonId: string;
id: string;
text: string;
time?: number;
};

if (!body?.lessonId || !body?.id || !body?.text) {
return NextResponse.json({ success: false, message: 'Invalid payload' }, { status: 400 });
}

const key = keyFor(body.userId, body.lessonId);
const prev = notesStore.get(key) ?? [];
const now = new Date().toISOString();

const next = prev.map((n) =>
n.id === body.id
? {
...n,
text: body.text.trim(),
time: typeof body.time === 'number' ? Math.max(0, body.time) : n.time,
updatedAt: now,
}
: n,
);

notesStore.set(key, next);
return NextResponse.json({ success: true });
}

export async function DELETE(request: Request) {
const body = (await request.json()) as { userId?: string; lessonId: string; id: string };
if (!body?.lessonId || !body?.id) {
return NextResponse.json({ success: false, message: 'Invalid payload' }, { status: 400 });
}

const key = keyFor(body.userId, body.lessonId);
const prev = notesStore.get(key) ?? [];
notesStore.set(
key,
prev.filter((n) => n.id !== body.id),
);

return NextResponse.json({ success: true });
}
41 changes: 41 additions & 0 deletions src/app/api/video-analytics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server';

type AnalyticsEvent = {
userId?: string;
lessonId: string;
eventType: string;
payload: Record<string, unknown>;
};

const analyticsStore = new Map<string, AnalyticsEvent[]>();

const keyFor = (userId: string | undefined, lessonId: string) => {
const safeUserId = encodeURIComponent(userId ?? 'anon');
return `${safeUserId}::${encodeURIComponent(lessonId)}`;
};

export async function POST(request: Request) {
const body = (await request.json()) as {
userId?: string;
lessonId: string;
eventType: string;
payload?: Record<string, unknown>;
};

if (!body?.lessonId || !body?.eventType) {
return NextResponse.json({ success: false, message: 'Invalid payload' }, { status: 400 });
}

const event: AnalyticsEvent = {
userId: body.userId,
lessonId: body.lessonId,
eventType: body.eventType,
payload: body.payload ?? {},
};

const key = keyFor(body.userId, body.lessonId);
const prev = analyticsStore.get(key) ?? [];
analyticsStore.set(key, [event, ...prev].slice(0, 1000)); // cap for memory safety

return NextResponse.json({ success: true });
}
Loading
Loading