feat: simplify materials library and use real metadata
This commit is contained in:
@@ -395,13 +395,23 @@ const CourseModal: React.FC<{
|
|||||||
alert("Informe o nome do arquivo para download.");
|
alert("Informe o nome do arquivo para download.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!newAttUrl.trim()) {
|
||||||
|
alert('Informe a URL do material.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new URL(newAttUrl.trim());
|
||||||
|
} catch {
|
||||||
|
alert('Informe uma URL válida para o material.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const att: Attachment = {
|
const att: Attachment = {
|
||||||
id: `att-${Date.now()}`,
|
id: `att-${Date.now()}`,
|
||||||
name: newAttName.trim(),
|
name: newAttName.trim(),
|
||||||
type: newAttType,
|
type: newAttType,
|
||||||
url: newAttUrl.trim() || '#',
|
url: newAttUrl.trim(),
|
||||||
size: newAttSize.trim() || '1.5 MB',
|
size: newAttSize.trim() || undefined,
|
||||||
description: newAttDesc.trim()
|
description: newAttDesc.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -679,12 +689,21 @@ const CourseModal: React.FC<{
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={newAttUrl}
|
||||||
|
onChange={(e) => setNewAttUrl(e.target.value)}
|
||||||
|
placeholder="URL do material (https://...)"
|
||||||
|
className="min-w-0 flex-1 bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newAttSize}
|
value={newAttSize}
|
||||||
onChange={(e) => setNewAttSize(e.target.value)}
|
onChange={(e) => setNewAttSize(e.target.value)}
|
||||||
placeholder="Ex.: 2,4 MB"
|
placeholder="Tamanho (opcional)"
|
||||||
className="w-28 bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white focus:outline-none"
|
className="w-28 bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,8 @@ export const courseSelect = (publicOnly = false, includeUnreadyMedia = false) =>
|
|||||||
'name', a.name,
|
'name', a.name,
|
||||||
'kind', a.kind,
|
'kind', a.kind,
|
||||||
'url', a.url,
|
'url', a.url,
|
||||||
|
'sizeBytes', a.size_bytes,
|
||||||
|
'createdAt', a.created_at,
|
||||||
'description', a.description,
|
'description', a.description,
|
||||||
'accessLevel', a.access_level
|
'accessLevel', a.access_level
|
||||||
) order by a.created_at)
|
) order by a.created_at)
|
||||||
@@ -81,6 +83,8 @@ export const courseSelect = (publicOnly = false, includeUnreadyMedia = false) =>
|
|||||||
'name', a.name,
|
'name', a.name,
|
||||||
'kind', a.kind,
|
'kind', a.kind,
|
||||||
'url', a.url,
|
'url', a.url,
|
||||||
|
'sizeBytes', a.size_bytes,
|
||||||
|
'createdAt', a.created_at,
|
||||||
'description', a.description,
|
'description', a.description,
|
||||||
'accessLevel', a.access_level
|
'accessLevel', a.access_level
|
||||||
) order by a.created_at)
|
) order by a.created_at)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const assetSchema = z.object({
|
|||||||
name: z.string().trim().min(1).max(255),
|
name: z.string().trim().min(1).max(255),
|
||||||
kind: z.enum(['document', 'spreadsheet', 'archive', 'image', 'link']),
|
kind: z.enum(['document', 'spreadsheet', 'archive', 'image', 'link']),
|
||||||
url: z.string().url(),
|
url: z.string().url(),
|
||||||
|
sizeBytes: z.number().int().nonnegative().nullable().optional(),
|
||||||
description: z.string().max(2000).default(''),
|
description: z.string().max(2000).default(''),
|
||||||
accessLevel: z.enum(['public', 'enrolled']).default('enrolled'),
|
accessLevel: z.enum(['public', 'enrolled']).default('enrolled'),
|
||||||
});
|
});
|
||||||
@@ -85,9 +86,9 @@ async function uniqueSlug(client: PoolClient, requestedSlug: string | undefined,
|
|||||||
async function insertAssets(client: PoolClient, parent: { courseId?: string; lessonId?: string }, assets: CourseInput['assets']) {
|
async function insertAssets(client: PoolClient, parent: { courseId?: string; lessonId?: string }, assets: CourseInput['assets']) {
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
await client.query(
|
await client.query(
|
||||||
`insert into assets (course_id, lesson_id, name, kind, url, description, access_level)
|
`insert into assets (course_id, lesson_id, name, kind, url, size_bytes, description, access_level)
|
||||||
values ($1, $2, $3, $4, $5, $6, $7)`,
|
values ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||||
[parent.courseId ?? null, parent.lessonId ?? null, asset.name, asset.kind, asset.url, asset.description, asset.accessLevel],
|
[parent.courseId ?? null, parent.lessonId ?? null, asset.name, asset.kind, asset.url, asset.sizeBytes ?? null, asset.description, asset.accessLevel],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ type ApiAsset = {
|
|||||||
name: string;
|
name: string;
|
||||||
kind: 'document' | 'spreadsheet' | 'archive' | 'image' | 'link';
|
kind: 'document' | 'spreadsheet' | 'archive' | 'image' | 'link';
|
||||||
url: string;
|
url: string;
|
||||||
|
sizeBytes: number | null;
|
||||||
|
createdAt: string;
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,9 +90,19 @@ const toAttachment = (asset: ApiAsset): Attachment => ({
|
|||||||
name: asset.name,
|
name: asset.name,
|
||||||
type: toAttachmentType(asset.kind),
|
type: toAttachmentType(asset.kind),
|
||||||
url: asset.url,
|
url: asset.url,
|
||||||
|
size: asset.sizeBytes === null ? undefined : formatBytes(asset.sizeBytes),
|
||||||
|
sizeBytes: asset.sizeBytes ?? undefined,
|
||||||
|
createdAt: asset.createdAt,
|
||||||
description: asset.description,
|
description: asset.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number) => {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
const units = ['KB', 'MB', 'GB'];
|
||||||
|
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)) - 1, units.length - 1);
|
||||||
|
return `${new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 1 }).format(bytes / 1024 ** (index + 1))} ${units[index]}`;
|
||||||
|
};
|
||||||
|
|
||||||
const toDuration = (seconds: number | null) => {
|
const toDuration = (seconds: number | null) => {
|
||||||
if (!seconds || seconds < 1) return '—';
|
if (!seconds || seconds < 1) return '—';
|
||||||
const totalSeconds = Math.round(seconds);
|
const totalSeconds = Math.round(seconds);
|
||||||
@@ -166,10 +178,19 @@ const toAssetPayload = (attachment: Attachment) => ({
|
|||||||
name: attachment.name,
|
name: attachment.name,
|
||||||
kind: toAssetKind(attachment.type),
|
kind: toAssetKind(attachment.type),
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
|
sizeBytes: attachment.sizeBytes ?? parseSizeBytes(attachment.size),
|
||||||
description: attachment.description || '',
|
description: attachment.description || '',
|
||||||
accessLevel: 'enrolled' as const,
|
accessLevel: 'enrolled' as const,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const parseSizeBytes = (value?: string) => {
|
||||||
|
if (!value) return null;
|
||||||
|
const match = value.trim().replace(',', '.').match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)$/i);
|
||||||
|
if (!match) return null;
|
||||||
|
const units: Record<string, number> = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 };
|
||||||
|
return Math.round(Number(match[1]) * units[match[2].toUpperCase()]);
|
||||||
|
};
|
||||||
|
|
||||||
const toCoursePayload = (course: Course) => ({
|
const toCoursePayload = (course: Course) => ({
|
||||||
title: course.title,
|
title: course.title,
|
||||||
description: course.description,
|
description: course.description,
|
||||||
|
|||||||
2
types.ts
2
types.ts
@@ -8,6 +8,8 @@ export interface Attachment {
|
|||||||
type: AttachmentType;
|
type: AttachmentType;
|
||||||
url: string;
|
url: string;
|
||||||
size?: string; // e.g. "4.2 MB"
|
size?: string; // e.g. "4.2 MB"
|
||||||
|
sizeBytes?: number;
|
||||||
|
createdAt?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
lessonId?: string; // Associated lesson or global course asset
|
lessonId?: string; // Associated lesson or global course asset
|
||||||
downloadCount?: number;
|
downloadCount?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user