From 1c7ee70815051b22e4958ef52c3ad873f7ac42fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Sat, 14 Feb 2026 16:21:52 +0300 Subject: [PATCH] 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. --- .../[projectId]/videos/[videoId]/route.ts | 1 + app/api/watch/[videoId]/route.ts | 1 + bun.lock | 14 +- components/video-page-content.tsx | 127 ++++++++++++------ lib/auth.ts | 2 + package.json | 2 + 6 files changed, 105 insertions(+), 42 deletions(-) diff --git a/app/api/projects/[projectId]/videos/[videoId]/route.ts b/app/api/projects/[projectId]/videos/[videoId]/route.ts index 4e24e90..0d616b3 100644 --- a/app/api/projects/[projectId]/videos/[videoId]/route.ts +++ b/app/api/projects/[projectId]/videos/[videoId]/route.ts @@ -70,6 +70,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { ...video, isAuthenticated: !!session?.user?.id, currentUserId: session?.user?.id || null, + currentUserName: session?.user?.name || null, }); return withCacheControl(response, 'private, no-cache'); diff --git a/app/api/watch/[videoId]/route.ts b/app/api/watch/[videoId]/route.ts index e94f238..6f13ebe 100644 --- a/app/api/watch/[videoId]/route.ts +++ b/app/api/watch/[videoId]/route.ts @@ -76,6 +76,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { }, isAuthenticated: !!session?.user?.id, currentUserId: session?.user?.id || null, + currentUserName: session?.user?.name || null, canComment: access.hasAccess, }); diff --git a/bun.lock b/bun.lock index 59ed60c..c605971 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@base-ui/react": "^1.1.0", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", + "@types/react-window": "^2.0.0", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -25,7 +26,7 @@ "radix-ui": "^1.4.3", "react": "19.2.3", "react-dom": "19.2.3", - "shadcn": "^3.8.3", + "react-window": "^2.2.7", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", @@ -41,6 +42,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.1.6", + "shadcn": "^3.8.3", "tailwindcss": "^4", "typescript": "^5", }, @@ -697,6 +699,8 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-window": ["@types/react-window@2.0.0", "", { "dependencies": { "react-window": "*" } }, "sha512-E8hMDtImEpMk1SjswSvqoSmYvk7GEtyVaTa/GJV++FdDNuMVVEzpAClyJ0nqeKYBrMkGiyH6M1+rPLM0Nu1exQ=="], + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], @@ -1163,7 +1167,7 @@ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], "http-errors": ["http-errors@2.0.1", "", { "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": ["react-style-singleton@2.2.3", "", { "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": ["react-window@2.2.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "recast": ["recast@0.23.11", "", { "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": ["ajv@8.17.1", "", { "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": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], - "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "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": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="], "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="], diff --git a/components/video-page-content.tsx b/components/video-page-content.tsx index ddb22a3..457292a 100644 --- a/components/video-page-content.tsx +++ b/components/video-page-content.tsx @@ -1,6 +1,7 @@ '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 { usePathname } from 'next/navigation'; import { toast } from 'sonner'; @@ -127,6 +128,7 @@ interface VideoData { versions: (Version & { comments: Comment[] })[]; isAuthenticated: boolean; currentUserId: string | null; + currentUserName: string | null; canComment?: boolean; } @@ -241,6 +243,16 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi const projectId = propProjectId || video?.projectId; // 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(() => { setCursorIdle(false); 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 currentUserName = video?.currentUserName || null; const apiBasePath = mode === 'dashboard' ? `/api/projects/${propProjectId}/videos/${videoId}` @@ -295,12 +308,52 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi fetchVideo(); }, [apiBasePath, mode]); - const activeVersion = video?.versions?.find((v) => v.id === activeVersionId) || - video?.versions?.find((v) => v.isActive) || - video?.versions?.[0]; - const comments = activeVersion?.comments || []; - const filteredComments = comments.filter((c) => showResolved || !c.isResolved); - const duration = videoDuration || activeVersion?.duration || 0; + // Memoize active version lookup to avoid recalculating on every render + const activeVersion = useMemo(() => { + return video?.versions?.find((v) => v.id === activeVersionId) || + video?.versions?.find((v) => v.isActive) || + video?.versions?.[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(() => { if (!projectId) return; @@ -734,7 +787,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi voiceDuration: voiceData?.duration ?? null, isResolved: false, 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, tag: availableTags.find(t => t.id === selectedTagId) || null, replies: [], @@ -1085,7 +1138,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi voiceUrl: voiceData?.url ?? null, voiceDuration: voiceData?.duration ?? null, 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, tag: null, }; @@ -1352,11 +1405,16 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi } }, [activeVersionId, video]); + // Comment polling with Page Visibility API to pause when tab is hidden useEffect(() => { if (!activeVersion) return; - const interval = setInterval(async () => { + + let intervalId: ReturnType | null = null; + let isPageVisible = true; + + const poll = async () => { try { - if (isMutatingRef.current) return; + if (isMutatingRef.current || !isPageVisible) return; const res = await fetch(apiBasePath, { cache: 'no-store' }); if (res.ok) { @@ -1366,8 +1424,22 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi } } } 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]); 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 backHref = mode === 'dashboard' @@ -1630,8 +1685,6 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi ); } - const embedUrl = getEmbedUrl(activeVersion); - return (
( setActiveVersionId(version.id)} + onClick={() => handleVersionSelect(version.id)} > Comments {comments.length}
- @@ -2001,9 +2054,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi

Be the first to leave feedback!

) : ( - filteredComments - .sort((a, b) => a.timestamp - b.timestamp) - .map((comment) => { + sortedComments.map((comment) => { const authorName = comment.author?.name || comment.guestName || 'Anonymous'; const isEditing = editingCommentId === comment.id; diff --git a/lib/auth.ts b/lib/auth.ts index 8e8afa4..ed7e232 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -62,12 +62,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ async session({ session, token }) { if (token.sub && session.user) { session.user.id = token.sub; + session.user.name = token.name || null; } return session; }, async jwt({ token, user }) { if (user) { token.sub = user.id; + token.name = user.name; } return token; }, diff --git a/package.json b/package.json index 5fa4f77..4ad52a5 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@base-ui/react": "^1.1.0", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", + "@types/react-window": "^2.0.0", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -38,6 +39,7 @@ "radix-ui": "^1.4.3", "react": "19.2.3", "react-dom": "19.2.3", + "react-window": "^2.2.7", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0",