Files
compor-academy/services/db.ts
Cauê Faleiros 701788ea08
Some checks failed
CI / Validate frontend and API (push) Failing after 7m27s
first commit
2026-08-31 12:38:34 -03:00

167 lines
5.9 KiB
TypeScript

import localforage from 'localforage';
import { Attachment, AttachmentType, Comment, Course, Lesson } from '../types';
import { apiRequest } from './api';
const COMMENTS_KEY = 'compor_hub_comments_v1';
export const initDB = async (): Promise<void> => undefined;
type ApiAsset = {
id: string;
name: string;
kind: 'document' | 'spreadsheet' | 'archive' | 'image' | 'link';
url: string;
description: string;
};
type ApiCourse = {
id: string;
title: string;
description: string;
category: string;
coverImageUrl: string | null;
instructor: { name: string };
lessons: Array<{
id: string;
title: string;
description: string;
durationSeconds: number | null;
accessLevel: 'public' | 'enrolled';
media: Array<{ provider: string; playbackUrl: string | null; embedUrl: string | null }>;
assets: ApiAsset[];
}>;
assets: ApiAsset[];
};
const toAttachmentType = (kind: ApiAsset['kind']): AttachmentType => {
if (kind === 'document') return 'doc';
if (kind === 'archive') return 'zip';
return kind;
};
const toAttachment = (asset: ApiAsset): Attachment => ({
id: asset.id,
name: asset.name,
type: toAttachmentType(asset.kind),
url: asset.url,
description: asset.description,
});
const toDuration = (seconds: number | null) => {
if (!seconds) return '—';
const minutes = Math.round(seconds / 60);
return minutes >= 60 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${minutes}m`;
};
const toCourse = (course: ApiCourse): Course => ({
id: course.id,
title: course.title,
description: course.description,
category: course.category,
thumbnail: course.coverImageUrl || 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80',
instructor: course.instructor.name,
duration: `${course.lessons.length} Aulas`,
lessons: course.lessons.map((lesson): Lesson => ({
id: lesson.id,
title: lesson.title,
description: lesson.description,
duration: toDuration(lesson.durationSeconds),
isFree: lesson.accessLevel === 'public',
videoUrl: lesson.media[0]?.playbackUrl || lesson.media[0]?.embedUrl || '',
mediaProvider: lesson.media[0]?.provider,
attachments: lesson.assets.map(toAttachment),
})),
attachments: course.assets.map(toAttachment),
});
const toSeconds = (duration: string) => {
const hours = Number(duration.match(/(\d+)h/)?.[1] || 0);
const minutes = Number(duration.match(/(\d+)m/)?.[1] || 0);
const seconds = hours * 3600 + minutes * 60;
return seconds || null;
};
const toAssetKind = (type: AttachmentType): ApiAsset['kind'] => {
if (type === 'doc' || type === 'pdf') return 'document';
if (type === 'zip') return 'archive';
if (type === 'video') return 'link';
return type;
};
const toAssetPayload = (attachment: Attachment) => ({
name: attachment.name,
kind: toAssetKind(attachment.type),
url: attachment.url,
description: attachment.description || '',
accessLevel: 'enrolled' as const,
});
const toCoursePayload = (course: Course) => ({
title: course.title,
description: course.description,
category: course.category,
coverImageUrl: course.thumbnail && !course.thumbnail.startsWith('blob:') ? course.thumbnail : null,
status: 'published' as const,
assets: (course.attachments || []).filter((attachment) => attachment.url && attachment.url !== '#').map(toAssetPayload),
lessons: course.lessons.map((lesson) => ({
title: lesson.title,
description: lesson.description || '',
durationSeconds: toSeconds(lesson.duration),
accessLevel: lesson.isFree ? 'public' as const : 'enrolled' as const,
media: lesson.videoUrl && !lesson.videoUrl.startsWith('blob:')
? [{ provider: lesson.mediaProvider || 'external', externalId: lesson.videoUrl, playbackUrl: lesson.videoUrl, status: 'ready' as const }]
: [],
assets: (lesson.attachments || []).filter((attachment) => attachment.url && attachment.url !== '#').map(toAssetPayload),
})),
});
export const getCourses = async (): Promise<Course[]> => {
const response = await apiRequest<{ data: ApiCourse[] }>('/courses');
return response.data.map(toCourse);
};
export const getManagedCourses = async (): Promise<Course[]> => {
const response = await apiRequest<{ data: ApiCourse[] }>('/manage/courses');
return response.data.map(toCourse);
};
export const getCourseById = async (id: string): Promise<Course | null> => {
try {
const response = await apiRequest<{ data: ApiCourse }>(`/courses/${id}`);
return toCourse(response.data);
} catch {
return null;
}
};
export const saveCourse = async (course: Course): Promise<void> => {
const payload = toCoursePayload(course);
const isExisting = /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(course.id);
if (isExisting) {
await apiRequest(`/manage/courses/${course.id}`, { method: 'PATCH', body: JSON.stringify(payload) });
} else {
await apiRequest('/manage/courses', { method: 'POST', body: JSON.stringify(payload) });
}
};
export const deleteCourse = async (id: string): Promise<void> => {
await apiRequest(`/manage/courses/${id}`, { method: 'DELETE' });
};
// Comments and learner progress move to PostgreSQL in the next milestone.
export const getComments = async (courseId: string): Promise<Comment[]> => {
const allComments = (await localforage.getItem<Comment[]>(COMMENTS_KEY)) || [];
return allComments.filter((comment) => comment.courseId === courseId);
};
export const saveComment = async (comment: Comment): Promise<void> => {
const allComments = (await localforage.getItem<Comment[]>(COMMENTS_KEY)) || [];
const index = allComments.findIndex((item) => item.id === comment.id);
if (index >= 0) allComments[index] = comment;
else allComments.unshift(comment);
await localforage.setItem(COMMENTS_KEY, allComments);
};
export const saveProgress = async (_courseId: string, _progress: number): Promise<void> => undefined;
export const incrementViews = async (_courseId: string): Promise<void> => undefined;