move next to web-front
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import 'server-only';
|
||||
|
||||
import { unstable_cache } from 'next/cache';
|
||||
import { DB_MEMORY_TTL_MS, NEXT_CACHE_REVALIDATE_SECONDS } from '@/shared/config/env';
|
||||
import { calculateVideoStats } from '@/entities/video/model/video';
|
||||
import { listVideos } from './videoRepository';
|
||||
|
||||
export const VIDEO_COLLECTION_TAG = 'video-collection';
|
||||
|
||||
export const getCachedVideoDashboard = unstable_cache(
|
||||
async () => {
|
||||
const videos = await listVideos();
|
||||
|
||||
return {
|
||||
videos,
|
||||
stats: calculateVideoStats(videos),
|
||||
cache: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
nextRevalidateSeconds: NEXT_CACHE_REVALIDATE_SECONDS,
|
||||
memoryTtlMs: DB_MEMORY_TTL_MS,
|
||||
},
|
||||
};
|
||||
},
|
||||
['video-dashboard-v1'],
|
||||
{
|
||||
revalidate: NEXT_CACHE_REVALIDATE_SECONDS,
|
||||
tags: [VIDEO_COLLECTION_TAG],
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'server-only';
|
||||
|
||||
import { createVideo } from '@/entities/video/model/video';
|
||||
import type { VideoDraft } from '@/entities/video/model/types';
|
||||
import { readPrimitiveDb, writePrimitiveDb } from '@/shared/db/primitiveJsonDb';
|
||||
|
||||
export async function listVideos() {
|
||||
const db = await readPrimitiveDb();
|
||||
return [...db.videos].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
||||
}
|
||||
|
||||
export async function addVideo(draft: VideoDraft) {
|
||||
const video = createVideo(draft);
|
||||
|
||||
await writePrimitiveDb((current) => ({
|
||||
...current,
|
||||
videos: [video, ...current.videos],
|
||||
}));
|
||||
|
||||
return video;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export const videoStatuses = ['draft', 'published', 'archived'] as const;
|
||||
|
||||
export type VideoStatus = (typeof videoStatuses)[number];
|
||||
|
||||
export type Video = {
|
||||
id: string;
|
||||
title: string;
|
||||
owner: string;
|
||||
sourceUrl: string;
|
||||
status: VideoStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type VideoDraft = {
|
||||
title: string;
|
||||
owner: string;
|
||||
sourceUrl: string;
|
||||
status: VideoStatus;
|
||||
};
|
||||
|
||||
export type VideoDashboardStats = {
|
||||
total: number;
|
||||
draft: number;
|
||||
published: number;
|
||||
archived: number;
|
||||
};
|
||||
|
||||
export type VideoDashboardData = {
|
||||
videos: Video[];
|
||||
stats: VideoDashboardStats;
|
||||
cache: {
|
||||
generatedAt: string;
|
||||
nextRevalidateSeconds: number;
|
||||
memoryTtlMs: number;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { calculateVideoStats, normalizeVideoDraft } from './video';
|
||||
import type { Video } from './types';
|
||||
|
||||
describe('video model', () => {
|
||||
it('normalizes sparse form input', () => {
|
||||
expect(
|
||||
normalizeVideoDraft({
|
||||
title: ' Demo reel ',
|
||||
owner: '',
|
||||
sourceUrl: 'not a url',
|
||||
status: 'unexpected',
|
||||
}),
|
||||
).toEqual({
|
||||
title: 'Demo reel',
|
||||
owner: 'Unassigned',
|
||||
sourceUrl: 'https://example.com/video',
|
||||
status: 'draft',
|
||||
});
|
||||
});
|
||||
|
||||
it('counts videos by status', () => {
|
||||
const base = {
|
||||
id: 'id',
|
||||
title: 'Title',
|
||||
owner: 'Ops',
|
||||
sourceUrl: 'https://example.com/video',
|
||||
createdAt: '2026-05-16T00:00:00.000Z',
|
||||
updatedAt: '2026-05-16T00:00:00.000Z',
|
||||
};
|
||||
const videos: Video[] = [
|
||||
{ ...base, id: 'draft', status: 'draft' },
|
||||
{ ...base, id: 'published', status: 'published' },
|
||||
{ ...base, id: 'archived', status: 'archived' },
|
||||
{ ...base, id: 'published-2', status: 'published' },
|
||||
];
|
||||
|
||||
expect(calculateVideoStats(videos)).toEqual({
|
||||
total: 4,
|
||||
draft: 1,
|
||||
published: 2,
|
||||
archived: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { videoStatuses, type Video, type VideoDashboardStats, type VideoDraft, type VideoStatus } from './types';
|
||||
|
||||
const fallbackUrl = 'https://example.com/video';
|
||||
|
||||
function normalizeText(value: string, fallback: string) {
|
||||
const normalized = value.trim().replace(/\s+/g, ' ');
|
||||
return normalized.length > 0 ? normalized : fallback;
|
||||
}
|
||||
|
||||
function normalizeUrl(value: string) {
|
||||
const normalized = value.trim();
|
||||
if (normalized.length === 0) return fallbackUrl;
|
||||
|
||||
try {
|
||||
return new URL(normalized).toString();
|
||||
} catch {
|
||||
return fallbackUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export function isVideoStatus(value: string): value is VideoStatus {
|
||||
return videoStatuses.includes(value as VideoStatus);
|
||||
}
|
||||
|
||||
export function normalizeVideoDraft(input: {
|
||||
title?: string;
|
||||
owner?: string;
|
||||
sourceUrl?: string;
|
||||
status?: string;
|
||||
}): VideoDraft {
|
||||
return {
|
||||
title: normalizeText(input.title ?? '', 'Untitled video'),
|
||||
owner: normalizeText(input.owner ?? '', 'Unassigned'),
|
||||
sourceUrl: normalizeUrl(input.sourceUrl ?? ''),
|
||||
status: input.status && isVideoStatus(input.status) ? input.status : 'draft',
|
||||
};
|
||||
}
|
||||
|
||||
export function createVideoId(now = new Date()) {
|
||||
const suffix = Math.random().toString(36).slice(2, 8);
|
||||
return `video_${now.getTime().toString(36)}_${suffix}`;
|
||||
}
|
||||
|
||||
export function createVideo(draft: VideoDraft, now = new Date()): Video {
|
||||
const timestamp = now.toISOString();
|
||||
return {
|
||||
...draft,
|
||||
id: createVideoId(now),
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateVideoStats(videos: Video[]): VideoDashboardStats {
|
||||
return videos.reduce<VideoDashboardStats>(
|
||||
(stats, video) => ({
|
||||
...stats,
|
||||
total: stats.total + 1,
|
||||
[video.status]: stats[video.status] + 1,
|
||||
}),
|
||||
{
|
||||
total: 0,
|
||||
draft: 0,
|
||||
published: 0,
|
||||
archived: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user