mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat(optimizations): adds memoization for performance optimization
Add currentUserName to video API responses and session for display in comments. Also adds memoization for performance optimization and implements Page Visibility API to pause comment polling when tab is hidden.
This commit is contained in:
@@ -70,6 +70,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
|||||||
...video,
|
...video,
|
||||||
isAuthenticated: !!session?.user?.id,
|
isAuthenticated: !!session?.user?.id,
|
||||||
currentUserId: session?.user?.id || null,
|
currentUserId: session?.user?.id || null,
|
||||||
|
currentUserName: session?.user?.name || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return withCacheControl(response, 'private, no-cache');
|
return withCacheControl(response, 'private, no-cache');
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
|||||||
},
|
},
|
||||||
isAuthenticated: !!session?.user?.id,
|
isAuthenticated: !!session?.user?.id,
|
||||||
currentUserId: session?.user?.id || null,
|
currentUserId: session?.user?.id || null,
|
||||||
|
currentUserName: session?.user?.name || null,
|
||||||
canComment: access.hasAccess,
|
canComment: access.hasAccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"@base-ui/react": "^1.1.0",
|
"@base-ui/react": "^1.1.0",
|
||||||
"@prisma/adapter-pg": "^7.3.0",
|
"@prisma/adapter-pg": "^7.3.0",
|
||||||
"@prisma/client": "^7.3.0",
|
"@prisma/client": "^7.3.0",
|
||||||
|
"@types/react-window": "^2.0.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -25,7 +26,7 @@
|
|||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"shadcn": "^3.8.3",
|
"react-window": "^2.2.7",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
|
"shadcn": "^3.8.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
},
|
},
|
||||||
@@ -697,6 +699,8 @@
|
|||||||
|
|
||||||
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@types/react-window": ["@types/[email protected]", "", { "dependencies": { "react-window": "*" } }, "sha512-E8hMDtImEpMk1SjswSvqoSmYvk7GEtyVaTa/GJV++FdDNuMVVEzpAClyJ0nqeKYBrMkGiyH6M1+rPLM0Nu1exQ=="],
|
||||||
|
|
||||||
"@types/statuses": ["@types/[email protected]", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
|
"@types/statuses": ["@types/[email protected]", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
|
||||||
|
|
||||||
"@types/validate-npm-package-name": ["@types/[email protected]", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
|
"@types/validate-npm-package-name": ["@types/[email protected]", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
|
||||||
@@ -1163,7 +1167,7 @@
|
|||||||
|
|
||||||
"hermes-parser": ["[email protected]", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
"hermes-parser": ["[email protected]", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||||
|
|
||||||
"hono": ["[email protected].4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
|
"hono": ["[email protected].7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
||||||
|
|
||||||
"http-errors": ["[email protected]", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
"http-errors": ["[email protected]", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
|
|
||||||
@@ -1577,6 +1581,8 @@
|
|||||||
|
|
||||||
"react-style-singleton": ["[email protected]", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["[email protected]", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
|
"react-window": ["[email protected]", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w=="],
|
||||||
|
|
||||||
"readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
"recast": ["[email protected]", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
"recast": ["[email protected]", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||||
@@ -1863,10 +1869,10 @@
|
|||||||
|
|
||||||
"@modelcontextprotocol/sdk/ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
"@modelcontextprotocol/sdk/ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/hono": ["[email protected]", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
|
||||||
|
|
||||||
"@next/eslint-plugin-next/fast-glob": ["[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
"@next/eslint-plugin-next/fast-glob": ["[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
||||||
|
|
||||||
|
"@prisma/dev/hono": ["[email protected]", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
|
||||||
|
|
||||||
"@prisma/engines/@prisma/get-platform": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="],
|
"@prisma/engines/@prisma/get-platform": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="],
|
||||||
|
|
||||||
"@prisma/fetch-engine/@prisma/get-platform": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="],
|
"@prisma/fetch-engine/@prisma/get-platform": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { List } from 'react-window';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -127,6 +128,7 @@ interface VideoData {
|
|||||||
versions: (Version & { comments: Comment[] })[];
|
versions: (Version & { comments: Comment[] })[];
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
currentUserId: string | null;
|
currentUserId: string | null;
|
||||||
|
currentUserName: string | null;
|
||||||
canComment?: boolean;
|
canComment?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +243,16 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
const projectId = propProjectId || video?.projectId;
|
const projectId = propProjectId || video?.projectId;
|
||||||
|
|
||||||
// Cursor idle detection: hide overlay when cursor idle for 3s while playing
|
// Cursor idle detection: hide overlay when cursor idle for 3s while playing
|
||||||
|
// Memoize version selection handler to prevent recreating on each render
|
||||||
|
const handleVersionSelect = useCallback((versionId: string) => {
|
||||||
|
setActiveVersionId(versionId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Memoize toggle show resolved handler
|
||||||
|
const handleToggleShowResolved = useCallback(() => {
|
||||||
|
setShowResolved(prev => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleVideoMouseMove = useCallback(() => {
|
const handleVideoMouseMove = useCallback(() => {
|
||||||
setCursorIdle(false);
|
setCursorIdle(false);
|
||||||
if (cursorIdleTimerRef.current) clearTimeout(cursorIdleTimerRef.current);
|
if (cursorIdleTimerRef.current) clearTimeout(cursorIdleTimerRef.current);
|
||||||
@@ -260,8 +272,9 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Determine current user ID for permission checks
|
// Determine current user info for permission checks and comment display
|
||||||
const currentUserId = video?.currentUserId || null;
|
const currentUserId = video?.currentUserId || null;
|
||||||
|
const currentUserName = video?.currentUserName || null;
|
||||||
|
|
||||||
const apiBasePath = mode === 'dashboard'
|
const apiBasePath = mode === 'dashboard'
|
||||||
? `/api/projects/${propProjectId}/videos/${videoId}`
|
? `/api/projects/${propProjectId}/videos/${videoId}`
|
||||||
@@ -295,12 +308,52 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
fetchVideo();
|
fetchVideo();
|
||||||
}, [apiBasePath, mode]);
|
}, [apiBasePath, mode]);
|
||||||
|
|
||||||
const activeVersion = video?.versions?.find((v) => v.id === activeVersionId) ||
|
// Memoize active version lookup to avoid recalculating on every render
|
||||||
video?.versions?.find((v) => v.isActive) ||
|
const activeVersion = useMemo(() => {
|
||||||
video?.versions?.[0];
|
return video?.versions?.find((v) => v.id === activeVersionId) ||
|
||||||
const comments = activeVersion?.comments || [];
|
video?.versions?.find((v) => v.isActive) ||
|
||||||
const filteredComments = comments.filter((c) => showResolved || !c.isResolved);
|
video?.versions?.[0];
|
||||||
const duration = videoDuration || activeVersion?.duration || 0;
|
}, [video?.versions, activeVersionId]);
|
||||||
|
|
||||||
|
// Memoize comments array
|
||||||
|
const comments = useMemo(() => {
|
||||||
|
return activeVersion?.comments || [];
|
||||||
|
}, [activeVersion]);
|
||||||
|
|
||||||
|
// Memoize filtered comments to avoid filtering on every render
|
||||||
|
const filteredComments = useMemo(() => {
|
||||||
|
return comments.filter((c) => showResolved || !c.isResolved);
|
||||||
|
}, [comments, showResolved]);
|
||||||
|
|
||||||
|
// Memoize sorted comments to avoid sorting on every render
|
||||||
|
const sortedComments = useMemo(() => {
|
||||||
|
return [...filteredComments].sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
}, [filteredComments]);
|
||||||
|
|
||||||
|
// Memoize duration computation
|
||||||
|
const duration = useMemo(() => {
|
||||||
|
return videoDuration || activeVersion?.duration || 0;
|
||||||
|
}, [videoDuration, activeVersion?.duration]);
|
||||||
|
|
||||||
|
// Memoize embed URL calculation to avoid recalculating on every render
|
||||||
|
const embedUrl = useMemo(() => {
|
||||||
|
if (!activeVersion) return '';
|
||||||
|
if (activeVersion.providerId === 'youtube') {
|
||||||
|
return `https://www.youtube.com/embed/${activeVersion.videoId}?enablejsapi=1&rel=0&modestbranding=1&controls=0&showinfo=0&iv_load_policy=3&disablekb=1`;
|
||||||
|
}
|
||||||
|
if (activeVersion.providerId === 'vimeo') {
|
||||||
|
return `https://player.vimeo.com/video/${activeVersion.videoId}`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(activeVersion.originalUrl);
|
||||||
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return activeVersion.originalUrl;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, [activeVersion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
@@ -734,7 +787,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
voiceDuration: voiceData?.duration ?? null,
|
voiceDuration: voiceData?.duration ?? null,
|
||||||
isResolved: false,
|
isResolved: false,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
author: isGuest ? null : { id: 'current-user', name: null, image: null },
|
author: isGuest ? null : { id: 'current-user', name: currentUserName, image: null },
|
||||||
guestName: isGuest ? guestName : null,
|
guestName: isGuest ? guestName : null,
|
||||||
tag: availableTags.find(t => t.id === selectedTagId) || null,
|
tag: availableTags.find(t => t.id === selectedTagId) || null,
|
||||||
replies: [],
|
replies: [],
|
||||||
@@ -1085,7 +1138,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
voiceUrl: voiceData?.url ?? null,
|
voiceUrl: voiceData?.url ?? null,
|
||||||
voiceDuration: voiceData?.duration ?? null,
|
voiceDuration: voiceData?.duration ?? null,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
author: isGuest ? null : { id: 'current-user', name: null, image: null },
|
author: isGuest ? null : { id: 'current-user', name: currentUserName, image: null },
|
||||||
guestName: isGuest ? guestName : null,
|
guestName: isGuest ? guestName : null,
|
||||||
tag: null,
|
tag: null,
|
||||||
};
|
};
|
||||||
@@ -1352,11 +1405,16 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
}
|
}
|
||||||
}, [activeVersionId, video]);
|
}, [activeVersionId, video]);
|
||||||
|
|
||||||
|
// Comment polling with Page Visibility API to pause when tab is hidden
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeVersion) return;
|
if (!activeVersion) return;
|
||||||
const interval = setInterval(async () => {
|
|
||||||
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let isPageVisible = true;
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
try {
|
try {
|
||||||
if (isMutatingRef.current) return;
|
if (isMutatingRef.current || !isPageVisible) return;
|
||||||
|
|
||||||
const res = await fetch(apiBasePath, { cache: 'no-store' });
|
const res = await fetch(apiBasePath, { cache: 'no-store' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -1366,8 +1424,22 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
}, 10000);
|
};
|
||||||
return () => clearInterval(interval);
|
|
||||||
|
// Start polling
|
||||||
|
intervalId = setInterval(poll, 10000);
|
||||||
|
|
||||||
|
// Handle page visibility change
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
isPageVisible = document.visibilityState === 'visible';
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
}, [activeVersion, apiBasePath]);
|
}, [activeVersion, apiBasePath]);
|
||||||
|
|
||||||
const handleNewVersionUrlChange = (url: string) => {
|
const handleNewVersionUrlChange = (url: string) => {
|
||||||
@@ -1472,23 +1544,6 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEmbedUrl = (version: Version) => {
|
|
||||||
if (version.providerId === 'youtube') {
|
|
||||||
return `https://www.youtube.com/embed/${version.videoId}?enablejsapi=1&rel=0&modestbranding=1&controls=0&showinfo=0&iv_load_policy=3&disablekb=1`;
|
|
||||||
}
|
|
||||||
if (version.providerId === 'vimeo') {
|
|
||||||
return `https://player.vimeo.com/video/${version.videoId}`;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const url = new URL(version.originalUrl);
|
|
||||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return version.originalUrl;
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerHeight = 'h-screen';
|
const containerHeight = 'h-screen';
|
||||||
const backHref = mode === 'dashboard'
|
const backHref = mode === 'dashboard'
|
||||||
@@ -1630,8 +1685,6 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const embedUrl = getEmbedUrl(activeVersion);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(containerHeight, 'flex flex-col bg-background overflow-hidden')}
|
className={cn(containerHeight, 'flex flex-col bg-background overflow-hidden')}
|
||||||
@@ -1671,7 +1724,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
{video.versions.map((version) => (
|
{video.versions.map((version) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={version.id}
|
key={version.id}
|
||||||
onClick={() => setActiveVersionId(version.id)}
|
onClick={() => handleVersionSelect(version.id)}
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
variant={version.id === activeVersionId ? 'default' : 'secondary'}
|
variant={version.id === activeVersionId ? 'default' : 'secondary'}
|
||||||
@@ -1988,7 +2041,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
<span className="font-medium">Comments</span>
|
<span className="font-medium">Comments</span>
|
||||||
<Badge variant="secondary">{comments.length}</Badge>
|
<Badge variant="secondary">{comments.length}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" onClick={() => setShowResolved(!showResolved)}>
|
<Button variant="ghost" size="sm" onClick={handleToggleShowResolved}>
|
||||||
{showResolved ? 'Hide' : 'Show'} Resolved
|
{showResolved ? 'Hide' : 'Show'} Resolved
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2001,9 +2054,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
<p className="text-sm">Be the first to leave feedback!</p>
|
<p className="text-sm">Be the first to leave feedback!</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredComments
|
sortedComments.map((comment) => {
|
||||||
.sort((a, b) => a.timestamp - b.timestamp)
|
|
||||||
.map((comment) => {
|
|
||||||
const authorName =
|
const authorName =
|
||||||
comment.author?.name || comment.guestName || 'Anonymous';
|
comment.author?.name || comment.guestName || 'Anonymous';
|
||||||
const isEditing = editingCommentId === comment.id;
|
const isEditing = editingCommentId === comment.id;
|
||||||
|
|||||||
@@ -62,12 +62,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (token.sub && session.user) {
|
if (token.sub && session.user) {
|
||||||
session.user.id = token.sub;
|
session.user.id = token.sub;
|
||||||
|
session.user.name = token.name || null;
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
if (user) {
|
if (user) {
|
||||||
token.sub = user.id;
|
token.sub = user.id;
|
||||||
|
token.name = user.name;
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@base-ui/react": "^1.1.0",
|
"@base-ui/react": "^1.1.0",
|
||||||
"@prisma/adapter-pg": "^7.3.0",
|
"@prisma/adapter-pg": "^7.3.0",
|
||||||
"@prisma/client": "^7.3.0",
|
"@prisma/client": "^7.3.0",
|
||||||
|
"@types/react-window": "^2.0.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
"react-window": "^2.2.7",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user