From 8eee6d55a2d9f24ed2ac6fc68fce9f3771b1b693 Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Sun, 20 Jul 2025 19:50:57 +0545 Subject: [PATCH 1/4] feat(Nextjs): HMR support added & websocker added --- nginx.conf | 4 + .../components/HeaderNab.tsx | 3 +- .../components/MarkdownEditor.tsx | 10 +- .../outline/components/OutlineItem.tsx | 5 - .../(presentation-generator)/upload/page.tsx | 2 +- servers/nextjs/app/api/layouts/route.ts | 1 - .../nextjs/app/layout-preview/[slug]/page.tsx | 5 +- .../hooks/useGroupLayoutLoader.ts | 94 ++++------ .../layout-preview/hooks/useLayoutLoader.ts | 69 ++++--- .../layout-preview/hooks/useLayoutWatcher.ts | 133 +++++++++++++ servers/nextjs/app/layout-preview/page.tsx | 38 +++- servers/nextjs/next.config.mjs | 45 +++++ servers/nextjs/package.json | 8 +- .../default/Type1SlideLayout.tsx | 9 +- servers/nextjs/scripts/layout-watcher.cjs | 174 ++++++++++++++++++ 15 files changed, 482 insertions(+), 118 deletions(-) create mode 100644 servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts create mode 100644 servers/nextjs/scripts/layout-watcher.cjs diff --git a/nginx.conf b/nginx.conf index 9d4764aa..7198143e 100644 --- a/nginx.conf +++ b/nginx.conf @@ -16,6 +16,10 @@ http { location / { proxy_pass http://localhost:3000; + proxy_http_version 1.1; # Required for WebSocket + proxy_set_header Upgrade $http_upgrade; # WebSocket header + proxy_set_header Connection "upgrade"; # WebSocket header + proxy_set_header Host $host; proxy_read_timeout 30m; proxy_connect_timeout 30m; } diff --git a/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx index 5f0fbe2e..d960ad8d 100644 --- a/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx @@ -1,5 +1,5 @@ "use client"; -import { LayoutDashboard, Settings } from "lucide-react"; +import { LayoutDashboard, Settings, Upload } from "lucide-react"; import React from "react"; import Link from "next/link"; import { RootState } from "@/store/store"; @@ -11,6 +11,7 @@ const HeaderNav = () => { return (
+ { - if (editor && content !== editor.storage.markdown.getMarkdown()) { - editor.commands.setContent(content); - } - }, [content, editor]); + // useEffect(() => { + // if (editor && content !== editor.storage.markdown.getMarkdown()) { + // editor.commands.setContent(content); + // } + // }, [content, editor]); return (
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx index 936835f0..40002f3b 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx @@ -66,16 +66,11 @@ export function OutlineItem({ transform: CSS.Transform.toString(transform), transition, } - - const handleSlideDelete = () => { if (isStreaming) return; dispatch(deleteSlideOutline({ index: index - 1 })) } - - - return (
{/* Main Title Row */} diff --git a/servers/nextjs/app/(presentation-generator)/upload/page.tsx b/servers/nextjs/app/(presentation-generator)/upload/page.tsx index 13d3c028..0866a9a8 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/page.tsx @@ -43,7 +43,7 @@ const page = () => {
-

Create Presentation

+

Create Presentation

{/*

We will generate a presentation for you

*/}
diff --git a/servers/nextjs/app/api/layouts/route.ts b/servers/nextjs/app/api/layouts/route.ts index 9ffc3ce1..a179122e 100644 --- a/servers/nextjs/app/api/layouts/route.ts +++ b/servers/nextjs/app/api/layouts/route.ts @@ -3,7 +3,6 @@ import { promises as fs } from 'fs' import path from 'path' import { GroupSetting } from '@/app/layout-preview/types' - export async function GET() { try { // Get the path to the presentation-layouts directory diff --git a/servers/nextjs/app/layout-preview/[slug]/page.tsx b/servers/nextjs/app/layout-preview/[slug]/page.tsx index af64dd0d..f6ee9e27 100644 --- a/servers/nextjs/app/layout-preview/[slug]/page.tsx +++ b/servers/nextjs/app/layout-preview/[slug]/page.tsx @@ -5,14 +5,14 @@ import { useGroupLayoutLoader } from '../hooks/useGroupLayoutLoader' import LoadingStates from '../components/LoadingStates' import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' -import { ArrowLeft, Home } from 'lucide-react' +import { ArrowLeft, Home, Wifi, WifiOff, RefreshCw } from 'lucide-react' const GroupLayoutPreview = () => { const params = useParams() const router = useRouter() const slug = params.slug as string - const { layoutGroup, loading, error, retry } = useGroupLayoutLoader(slug) + const { layoutGroup, loading, error, retry, isWatcherConnected } = useGroupLayoutLoader(slug) // Handle loading state if (loading) { @@ -63,6 +63,7 @@ const GroupLayoutPreview = () => {

{layoutGroup.layouts.length} layout{layoutGroup.layouts.length !== 1 ? 's' : ''} β€’ {layoutGroup.settings.description}

+
diff --git a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts index 00a14e2a..42596224 100644 --- a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useRef } from 'react' - +import { useState, useEffect, useRef, useCallback } from 'react' +import { useLayoutWatcher } from './useLayoutWatcher' import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' import { toast } from 'sonner' @@ -9,44 +9,33 @@ interface UseGroupLayoutLoaderReturn { loading: boolean error: string | null retry: () => void + isWatcherConnected: boolean } -// Global cache to store layout groups and avoid re-fetching -const layoutGroupCache = new Map() -const loadingGroupsCache = new Set() - export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => { const [layoutGroup, setLayoutGroup] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const hasMountedRef = useRef(false) - const loadGroupLayouts = async () => { - // Check cache first - if (layoutGroupCache.has(groupSlug)) { - setLayoutGroup(layoutGroupCache.get(groupSlug)!) - setLoading(false) - setError(null) - return - } - - // Prevent multiple simultaneous requests for the same group - if (loadingGroupsCache.has(groupSlug)) { - return - } - + const loadGroupLayouts = useCallback(async () => { try { setLoading(true) setError(null) - loadingGroupsCache.add(groupSlug) - const response = await fetch('/api/layouts') + const response = await fetch('/api/layouts', { + cache: 'no-cache', + headers: { 'Cache-Control': 'no-cache' } + }) + if (!response.ok) { toast.error('Error loading layouts', { description: response.statusText, }) return } + const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json() // Find the specific group by slug @@ -71,6 +60,8 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet for (const fileName of targetGroupData.files) { try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + + // Import the module to get exports const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) if (!module.default) { @@ -92,7 +83,9 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet // Use empty object to let schema apply its default values const sampleData = module.Schema.parse({}) const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') - + + + const layoutInfo: LayoutInfo = { name: layoutName, component: module.default, @@ -107,31 +100,6 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet } catch (importError) { console.error(`Failed to import ${fileName} from ${targetGroupData.groupName}:`, importError) - - // Try alternative import path - try { - const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) - - if (module.default && module.Schema) { - const sampleData = module.Schema.parse({}) - const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') - const layoutInfo: LayoutInfo = { - name: layoutName, - component: module.default, - schema: module.Schema, - sampleData, - fileName, - groupName: targetGroupData.groupName, - layoutId - } - groupLayouts.push(layoutInfo) - } else { - console.error(`${layoutName} is missing required exports (default component or Schema)`) - } - } catch (altError) { - console.error(`Alternative import also failed for ${fileName} from ${targetGroupData.groupName}:`, altError) - } } } @@ -147,8 +115,6 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet settings: groupSettings } - // Cache the result - layoutGroupCache.set(groupSlug, group) setLayoutGroup(group) setError(null) } @@ -158,27 +124,39 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet setError(error instanceof Error ? error.message : 'Failed to load group layouts') } finally { setLoading(false) - loadingGroupsCache.delete(groupSlug) } - } + }, [groupSlug]) - const retry = () => { - // Clear cache for this group to force reload - layoutGroupCache.delete(groupSlug) + const handleLayoutChange = useCallback(() => { + console.log(`πŸ”„ Layout change detected for group: ${groupSlug}, reloading...`) + + setTimeout(() => { + loadGroupLayouts() + }, 150) + }, [loadGroupLayouts]) + + // Setup file watcher for development + const { isConnected: isWatcherConnected } = useLayoutWatcher({ + onLayoutChange: handleLayoutChange, + enabled: process.env.NODE_ENV === 'development' + }) + + const retry = useCallback(() => { loadGroupLayouts() - } + }, [loadGroupLayouts]) useEffect(() => { if (groupSlug && !hasMountedRef.current) { hasMountedRef.current = true loadGroupLayouts() } - }, [groupSlug]) + }, [groupSlug, loadGroupLayouts]) return { layoutGroup, loading, error, - retry + retry, + isWatcherConnected } } \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts index 3d4e3a2d..004fc1f0 100644 --- a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect } from 'react' - +import { useState, useEffect, useCallback } from 'react' +import { useLayoutWatcher } from './useLayoutWatcher' import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' import { toast } from 'sonner' @@ -10,6 +10,7 @@ interface UseLayoutLoaderReturn { loading: boolean error: string | null retry: () => void + isWatcherConnected: boolean } export const useLayoutLoader = (): UseLayoutLoaderReturn => { @@ -18,12 +19,17 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const loadAllLayouts = async () => { + const loadAllLayouts = useCallback(async () => { try { setLoading(true) setError(null) - const response = await fetch('/api/layouts') + // Always fetch fresh data for layout preview + const response = await fetch('/api/layouts', { + cache: 'no-cache', + headers: { 'Cache-Control': 'no-cache' } + }) + if (!response.ok) { toast.error('Error loading layouts', { description: response.statusText, @@ -46,6 +52,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { for (const fileName of groupData.files) { try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + + // Always fresh import for hot reloading - no caching const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) if (!module.default) { @@ -65,8 +73,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } // Use empty object to let schema apply its default values - // User will need to provide actual data when using the layouts const sampleData = module.Schema.parse({}) + console.log('πŸ” Sample data:', sampleData) const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') const layoutInfo: LayoutInfo = { @@ -84,33 +92,6 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } catch (importError) { console.error(`Failed to import ${fileName} from ${groupData.groupName}:`, importError) - - // Try alternative import path - try { - const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) - - if (module.default && module.Schema) { - // Use empty object to let schema apply its default values - const sampleData = module.Schema.parse({}) - const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') - const layoutInfo: LayoutInfo = { - name: layoutName, - component: module.default, - schema: module.Schema, - sampleData, - fileName, - groupName: groupData.groupName, - layoutId - } - groupLayouts.push(layoutInfo) - allLayouts.push(layoutInfo) - } else { - console.error(`${layoutName} is missing required exports (default component or Schema)`) - } - } catch (altError) { - console.error(`Alternative import also failed for ${fileName} from ${groupData.groupName}:`, altError) - } } } @@ -140,21 +121,35 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } finally { setLoading(false) } - } + }, []) - const retry = () => { + const handleLayoutChange = () => { + console.log('πŸ”„ Layout change detected, reloading...') + setTimeout(() => { + loadAllLayouts() + }, 150) + }; + + // Setup file watcher for development + const { isConnected: isWatcherConnected } = useLayoutWatcher({ + onLayoutChange: handleLayoutChange, + enabled: process.env.NODE_ENV === 'development' + }) + + const retry = useCallback(() => { loadAllLayouts() - } + }, [loadAllLayouts]) useEffect(() => { loadAllLayouts() - }, []) + }, [loadAllLayouts]) return { layoutGroups, layouts, loading, error, - retry + retry, + isWatcherConnected } } \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts new file mode 100644 index 00000000..4e3b57a8 --- /dev/null +++ b/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts @@ -0,0 +1,133 @@ +'use client' +import { useEffect, useRef, useState } from 'react' +import { toast } from 'sonner' + +interface UseLayoutWatcherProps { + onLayoutChange: () => void + enabled?: boolean +} + +export const useLayoutWatcher = ({ onLayoutChange, enabled = true }: UseLayoutWatcherProps) => { + const wsRef = useRef(null) + const reconnectTimeoutRef = useRef(null) + const [isConnected, setIsConnected] = useState(false) + + const connectWebSocket = () => { + if (!enabled || typeof window === 'undefined') return + + try { + // Get the current host (works in Docker and local development) + const host = window.location.hostname + const wsUrl = `ws://${host}:3001` + + console.log('πŸ”„ Connecting to layout watcher:', wsUrl) + + // Create WebSocket connection to our layout watcher endpoint + const ws = new WebSocket(wsUrl) + wsRef.current = ws + + ws.onopen = () => { + console.log('βœ… Layout watcher connected successfully') + setIsConnected(true) + + // Clear any existing reconnect timeout + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current) + reconnectTimeoutRef.current = null + } + } + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + console.log('πŸ“¨ WebSocket message received:', data) + + if (data.type === 'layout-change') { + console.log('πŸ”₯ Layout file changed:', data.file) + console.log('πŸ“ Group:', data.groupName, 'File:', data.fileName) + + // Show toast notification + toast.success('Layout updated!', { + description: `${data.file} has been reloaded`, + duration: 2000, + }) + + // Trigger reload + console.log('πŸš€ Triggering layout reload...') + onLayoutChange() + } else if (data.type === 'connected') { + console.log('βœ… Layout watcher handshake complete') + } + } catch (error) { + console.error('❌ Error parsing WebSocket message:', error) + } + } + + ws.onclose = (event) => { + console.log('πŸ”Œ Layout watcher disconnected. Code:', event.code, 'Reason:', event.reason) + setIsConnected(false) + wsRef.current = null + + // Attempt to reconnect after 3 seconds if enabled + if (enabled) { + console.log('⏰ Will attempt to reconnect in 3 seconds...') + reconnectTimeoutRef.current = setTimeout(() => { + console.log('πŸ”„ Attempting to reconnect layout watcher...') + connectWebSocket() + }, 3000) + } + } + + ws.onerror = (error) => { + console.error('❌ Layout watcher WebSocket error:', error) + setIsConnected(false) + ws.close() + } + + } catch (error) { + console.error('❌ Failed to create WebSocket connection:', error) + setIsConnected(false) + } + } + + useEffect(() => { + if (enabled) { + console.log('🎯 Layout watcher hook enabled, connecting...') + connectWebSocket() + } else { + console.log('⏸️ Layout watcher hook disabled') + } + + return () => { + if (wsRef.current) { + console.log('🧹 Cleaning up WebSocket connection') + wsRef.current.close() + wsRef.current = null + } + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current) + reconnectTimeoutRef.current = null + } + setIsConnected(false) + } + }, [enabled]) + + // Cleanup on component unmount + useEffect(() => { + return () => { + console.log('🧹 Cleaning up WebSocket connection') + if (wsRef.current) { + wsRef.current.close() + } + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current) + } + setIsConnected(false) + } + }, []) + + return { + isConnected, + reconnect: connectWebSocket + } +} \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/page.tsx b/servers/nextjs/app/layout-preview/page.tsx index 91406dca..904a94ef 100644 --- a/servers/nextjs/app/layout-preview/page.tsx +++ b/servers/nextjs/app/layout-preview/page.tsx @@ -5,10 +5,10 @@ import { useLayoutLoader } from './hooks/useLayoutLoader' import LoadingStates from './components/LoadingStates' import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' -import { ExternalLink } from 'lucide-react' +import { ExternalLink, Wifi, WifiOff, RefreshCw } from 'lucide-react' const LayoutPreview = () => { - const { layoutGroups, layouts, loading, error, retry } = useLayoutLoader() + const { layoutGroups, layouts, loading, error, retry, isWatcherConnected } = useLayoutLoader() const router = useRouter() // Handle loading state @@ -35,6 +35,40 @@ const LayoutPreview = () => {

{layoutGroups.length} groups β€’ {layouts.length} layouts

+ + {/* Development Mode Status */} + {process.env.NODE_ENV === 'development' && ( +
+
+ {isWatcherConnected ? ( + <> + + Hot Reload Active + + ) : ( + <> + + Hot Reload Disconnected + + )} +
+ + +
+ )} + +
diff --git a/servers/nextjs/next.config.mjs b/servers/nextjs/next.config.mjs index 5338685f..ec366315 100644 --- a/servers/nextjs/next.config.mjs +++ b/servers/nextjs/next.config.mjs @@ -1,6 +1,51 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); + + /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, + + // Enable experimental features for better HMR + experimental: { + optimizePackageImports: ['presentation-layouts'], + }, + + // Webpack configuration for better hot reloading + webpack: (config, { dev, isServer }) => { + if (dev && !isServer) { + // Enable hot reloading for presentation layouts + config.watchOptions = { + ...config.watchOptions, + ignored: /node_modules/, + poll: 1000, + aggregateTimeout: 300, + }; + + // Add alias for presentation layouts + config.resolve.alias = { + ...config.resolve.alias, + '@/presentation-layouts': path.join(process.cwd(), 'presentation-layouts'), + }; + + // Configure module resolution for better hot reloading + config.resolve.symlinks = false; + config.resolve.cache = false; + + // Optimize for faster rebuilds + config.cache = { + type: 'filesystem', + buildDependencies: { + config: [__filename], + }, + cacheDirectory: path.join(process.cwd(), '.next/cache/webpack'), + }; + } + + return config; + }, images: { remotePatterns: [ diff --git a/servers/nextjs/package.json b/servers/nextjs/package.json index f8d58538..632b9ae1 100644 --- a/servers/nextjs/package.json +++ b/servers/nextjs/package.json @@ -4,7 +4,9 @@ "private": true, "type": "module", "scripts": { - "dev": "next dev", + "dev": "WATCHPACK_POLLING=true next dev", + "dev:with-watcher": "concurrently \"npm run dev\" \"npm run layout-watcher\"", + "layout-watcher": "node scripts/layout-watcher.cjs", "build": "next build", "start": "next start", "lint": "next lint", @@ -38,9 +40,11 @@ "@tiptap/extension-underline": "^2.0.0", "@tiptap/react": "^2.11.5", "@tiptap/starter-kit": "^2.11.5", + "chokidar": "^4.0.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "concurrently": "^9.1.0", "jsonrepair": "^3.12.0", "lucide-react": "^0.447.0", "marked": "^15.0.11", @@ -57,6 +61,7 @@ "tailwind-scrollbar-hide": "^2.0.0", "tailwindcss-animate": "^1.0.7", "tiptap-markdown": "^0.8.10", + "ws": "^8.18.0", "zod": "^4.0.5" }, "devDependencies": { @@ -67,6 +72,7 @@ "@types/react-dom": "^18", "@types/sharp": "^0.32.0", "@types/uuid": "^10.0.0", + "@types/ws": "^8.5.13", "cypress": "^14.3.3", "tailwindcss": "^3.4.1", "typescript": "^5" diff --git a/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx b/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx index 888adb91..6694a02e 100644 --- a/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx +++ b/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx @@ -7,10 +7,10 @@ export const layoutName = 'Type1 Slide' export const layoutDescription = 'A clean two-column layout with title and description on the left and a featured image on the right.' const type1SlideSchema = z.object({ - title: z.string().min(3).max(100).default('Sample Title').meta({ + title: z.string().min(3).max(100).default('Hot NOT Reload Working!').meta({ description: "Main title of the slide", }), - description: z.string().min(10).max(500).default('Your description content goes here. This layout provides a clean and professional way to present content with supporting imagery.').meta({ + description: z.string().min(10).max(500).default('This is a test of the hot reload system! If you can see this text, hot reload is working perfectly. Changes should appear instantly without page refresh.').meta({ description: "Main description text", }), image: ImageSchema.default({ @@ -30,7 +30,6 @@ interface Type1SlideLayoutProps { } const Type1SlideLayout: React.FC = ({ data: slideData }) => { - return (
= ({ data: slideData })
{/* Title */}

- {slideData?.title || 'Sample Title'} + {slideData?.title || ' This is the title of slide'}

{/* Description */}

- {slideData?.description || 'Your description content goes here. This layout provides a clean and professional way to present content with supporting imagery.'} + {slideData?.description || 'This is a test of the hot reload system! If you can see this text, hot reload is working perfectly. Changes should appear instantly without page refresh.'}

diff --git a/servers/nextjs/scripts/layout-watcher.cjs b/servers/nextjs/scripts/layout-watcher.cjs new file mode 100644 index 00000000..49acdebf --- /dev/null +++ b/servers/nextjs/scripts/layout-watcher.cjs @@ -0,0 +1,174 @@ +const WebSocket = require('ws'); +const chokidar = require('chokidar'); +const path = require('path'); + +const PORT = 3001; +const LAYOUTS_DIR = path.join(process.cwd(), 'presentation-layouts'); + +console.log('πŸ” Starting Layout Watcher...'); +console.log('πŸ“ Watching directory:', LAYOUTS_DIR); + +// Create WebSocket server (without path since ws doesn't handle paths like HTTP) +const wss = new WebSocket.Server({ + port: PORT +}); + +console.log(`πŸ”Œ WebSocket server running on ws://localhost:${PORT}`); + +// Track connected clients +let connectedClients = new Set(); + +wss.on('connection', (ws) => { + console.log('πŸ‘‹ Client connected to layout watcher'); + connectedClients.add(ws); + + ws.on('close', () => { + console.log('πŸ‘‹ Client disconnected from layout watcher'); + connectedClients.delete(ws); + }); + + ws.on('error', (error) => { + console.error('❌ WebSocket error:', error); + connectedClients.delete(ws); + }); + + // Send initial connection confirmation + ws.send(JSON.stringify({ + type: 'connected', + message: 'Layout watcher connected', + timestamp: Date.now() + })); +}); + +// File watcher setup +const watcher = chokidar.watch(LAYOUTS_DIR, { + ignored: [ + /(^|[\/\\])\../, // ignore dotfiles + /node_modules/, + /\.git/, + /\.next/, + /\.DS_Store/, + /thumbs\.db/i + ], + persistent: true, + ignoreInitial: true, // Don't fire events for initial scan + followSymlinks: false, + depth: 3, // Limit depth to avoid deep nested directories + awaitWriteFinish: { + stabilityThreshold: 100, // Wait 100ms after last write + pollInterval: 50 + } +}); + +// Debounce function to prevent rapid fire events +const debounce = (func, wait) => { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + +// Broadcast changes to all connected clients +const broadcastChange = debounce((eventType, filePath) => { + if (connectedClients.size === 0) return; + + const relativePath = path.relative(LAYOUTS_DIR, filePath); + const pathParts = relativePath.split(path.sep); + + // Only process .tsx and .ts files in group directories + if (pathParts.length >= 2 && (filePath.endsWith('.tsx') || filePath.endsWith('.ts'))) { + const groupName = pathParts[0]; + const fileName = pathParts[pathParts.length - 1]; + + // Skip non-layout files + if (fileName.startsWith('.') || fileName.includes('.test.') || fileName.includes('.spec.')) { + return; + } + + const changeData = { + type: 'layout-change', + eventType, + file: relativePath, + groupName, + fileName, + fullPath: filePath, + timestamp: Date.now() + }; + + console.log(`πŸ”₯ Broadcasting ${eventType}:`, relativePath); + + // Send to all connected clients + connectedClients.forEach(ws => { + if (ws.readyState === WebSocket.OPEN) { + try { + ws.send(JSON.stringify(changeData)); + } catch (error) { + console.error('Error sending WebSocket message:', error); + connectedClients.delete(ws); + } + } else { + // Remove disconnected clients + connectedClients.delete(ws); + } + }); + } +}, 200); // Debounce for 200ms + +// Watch for file changes +watcher + .on('change', (filePath) => { + console.log('πŸ“ File changed:', path.relative(LAYOUTS_DIR, filePath)); + broadcastChange('change', filePath); + }) + .on('add', (filePath) => { + console.log('βž• File added:', path.relative(LAYOUTS_DIR, filePath)); + broadcastChange('add', filePath); + }) + .on('unlink', (filePath) => { + console.log('πŸ—‘οΈ File removed:', path.relative(LAYOUTS_DIR, filePath)); + broadcastChange('unlink', filePath); + }) + .on('addDir', (dirPath) => { + console.log('πŸ“ Directory added:', path.relative(LAYOUTS_DIR, dirPath)); + broadcastChange('addDir', dirPath); + }) + .on('unlinkDir', (dirPath) => { + console.log('πŸ“ Directory removed:', path.relative(LAYOUTS_DIR, dirPath)); + broadcastChange('unlinkDir', dirPath); + }) + .on('error', (error) => { + console.error('❌ Watcher error:', error); + }) + .on('ready', () => { + console.log('βœ… Initial scan complete. Ready for changes.'); + console.log(`πŸ“Š Watching ${Object.keys(watcher.getWatched()).length} directories`); + }); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\nπŸ›‘ Shutting down layout watcher...'); + watcher.close().then(() => { + console.log('πŸ“ File watcher closed'); + wss.close(() => { + console.log('πŸ”Œ WebSocket server closed'); + process.exit(0); + }); + }); +}); + +process.on('SIGTERM', () => { + console.log('\nπŸ›‘ Received SIGTERM, shutting down...'); + watcher.close().then(() => { + wss.close(() => { + process.exit(0); + }); + }); +}); + +// Keep the process alive +console.log('πŸš€ Layout watcher is running. Press Ctrl+C to stop.'); \ No newline at end of file From 44a3751545a9daa54b31fa47498b01232b7bc125 Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Sun, 20 Jul 2025 20:29:02 +0545 Subject: [PATCH 2/4] refactor(Nexjs): websocket removed --- .../nextjs/app/layout-preview/[slug]/page.tsx | 2 +- .../hooks/useGroupLayoutLoader.ts | 94 ++++++---- .../layout-preview/hooks/useLayoutLoader.ts | 69 +++---- .../layout-preview/hooks/useLayoutWatcher.ts | 133 ------------- servers/nextjs/app/layout-preview/page.tsx | 35 +--- servers/nextjs/next.config.mjs | 37 ---- servers/nextjs/package.json | 13 +- .../default/Type1SlideLayout.tsx | 2 +- servers/nextjs/scripts/layout-watcher.cjs | 174 ------------------ 9 files changed, 103 insertions(+), 456 deletions(-) delete mode 100644 servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts delete mode 100644 servers/nextjs/scripts/layout-watcher.cjs diff --git a/servers/nextjs/app/layout-preview/[slug]/page.tsx b/servers/nextjs/app/layout-preview/[slug]/page.tsx index f6ee9e27..00dee152 100644 --- a/servers/nextjs/app/layout-preview/[slug]/page.tsx +++ b/servers/nextjs/app/layout-preview/[slug]/page.tsx @@ -12,7 +12,7 @@ const GroupLayoutPreview = () => { const router = useRouter() const slug = params.slug as string - const { layoutGroup, loading, error, retry, isWatcherConnected } = useGroupLayoutLoader(slug) + const { layoutGroup, loading, error, retry } = useGroupLayoutLoader(slug) // Handle loading state if (loading) { diff --git a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts index 42596224..00a14e2a 100644 --- a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useRef, useCallback } from 'react' -import { useLayoutWatcher } from './useLayoutWatcher' +import { useState, useEffect, useRef } from 'react' + import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' import { toast } from 'sonner' @@ -9,33 +9,44 @@ interface UseGroupLayoutLoaderReturn { loading: boolean error: string | null retry: () => void - isWatcherConnected: boolean } +// Global cache to store layout groups and avoid re-fetching +const layoutGroupCache = new Map() +const loadingGroupsCache = new Set() + export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => { const [layoutGroup, setLayoutGroup] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const hasMountedRef = useRef(false) - const loadGroupLayouts = useCallback(async () => { + const loadGroupLayouts = async () => { + // Check cache first + if (layoutGroupCache.has(groupSlug)) { + setLayoutGroup(layoutGroupCache.get(groupSlug)!) + setLoading(false) + setError(null) + return + } + + // Prevent multiple simultaneous requests for the same group + if (loadingGroupsCache.has(groupSlug)) { + return + } + try { setLoading(true) setError(null) + loadingGroupsCache.add(groupSlug) - const response = await fetch('/api/layouts', { - cache: 'no-cache', - headers: { 'Cache-Control': 'no-cache' } - }) - + const response = await fetch('/api/layouts') if (!response.ok) { toast.error('Error loading layouts', { description: response.statusText, }) return } - const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json() // Find the specific group by slug @@ -60,8 +71,6 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet for (const fileName of targetGroupData.files) { try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - - // Import the module to get exports const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) if (!module.default) { @@ -83,9 +92,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet // Use empty object to let schema apply its default values const sampleData = module.Schema.parse({}) const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') - - - + const layoutInfo: LayoutInfo = { name: layoutName, component: module.default, @@ -100,6 +107,31 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet } catch (importError) { console.error(`Failed to import ${fileName} from ${targetGroupData.groupName}:`, importError) + + // Try alternative import path + try { + const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) + + if (module.default && module.Schema) { + const sampleData = module.Schema.parse({}) + const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') + const layoutInfo: LayoutInfo = { + name: layoutName, + component: module.default, + schema: module.Schema, + sampleData, + fileName, + groupName: targetGroupData.groupName, + layoutId + } + groupLayouts.push(layoutInfo) + } else { + console.error(`${layoutName} is missing required exports (default component or Schema)`) + } + } catch (altError) { + console.error(`Alternative import also failed for ${fileName} from ${targetGroupData.groupName}:`, altError) + } } } @@ -115,6 +147,8 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet settings: groupSettings } + // Cache the result + layoutGroupCache.set(groupSlug, group) setLayoutGroup(group) setError(null) } @@ -124,39 +158,27 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet setError(error instanceof Error ? error.message : 'Failed to load group layouts') } finally { setLoading(false) + loadingGroupsCache.delete(groupSlug) } - }, [groupSlug]) + } - const handleLayoutChange = useCallback(() => { - console.log(`πŸ”„ Layout change detected for group: ${groupSlug}, reloading...`) - - setTimeout(() => { - loadGroupLayouts() - }, 150) - }, [loadGroupLayouts]) - - // Setup file watcher for development - const { isConnected: isWatcherConnected } = useLayoutWatcher({ - onLayoutChange: handleLayoutChange, - enabled: process.env.NODE_ENV === 'development' - }) - - const retry = useCallback(() => { + const retry = () => { + // Clear cache for this group to force reload + layoutGroupCache.delete(groupSlug) loadGroupLayouts() - }, [loadGroupLayouts]) + } useEffect(() => { if (groupSlug && !hasMountedRef.current) { hasMountedRef.current = true loadGroupLayouts() } - }, [groupSlug, loadGroupLayouts]) + }, [groupSlug]) return { layoutGroup, loading, error, - retry, - isWatcherConnected + retry } } \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts index 004fc1f0..3d4e3a2d 100644 --- a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useCallback } from 'react' -import { useLayoutWatcher } from './useLayoutWatcher' +import { useState, useEffect } from 'react' + import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' import { toast } from 'sonner' @@ -10,7 +10,6 @@ interface UseLayoutLoaderReturn { loading: boolean error: string | null retry: () => void - isWatcherConnected: boolean } export const useLayoutLoader = (): UseLayoutLoaderReturn => { @@ -19,17 +18,12 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const loadAllLayouts = useCallback(async () => { + const loadAllLayouts = async () => { try { setLoading(true) setError(null) - // Always fetch fresh data for layout preview - const response = await fetch('/api/layouts', { - cache: 'no-cache', - headers: { 'Cache-Control': 'no-cache' } - }) - + const response = await fetch('/api/layouts') if (!response.ok) { toast.error('Error loading layouts', { description: response.statusText, @@ -52,8 +46,6 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { for (const fileName of groupData.files) { try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - - // Always fresh import for hot reloading - no caching const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) if (!module.default) { @@ -73,8 +65,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } // Use empty object to let schema apply its default values + // User will need to provide actual data when using the layouts const sampleData = module.Schema.parse({}) - console.log('πŸ” Sample data:', sampleData) const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') const layoutInfo: LayoutInfo = { @@ -92,6 +84,33 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } catch (importError) { console.error(`Failed to import ${fileName} from ${groupData.groupName}:`, importError) + + // Try alternative import path + try { + const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) + + if (module.default && module.Schema) { + // Use empty object to let schema apply its default values + const sampleData = module.Schema.parse({}) + const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') + const layoutInfo: LayoutInfo = { + name: layoutName, + component: module.default, + schema: module.Schema, + sampleData, + fileName, + groupName: groupData.groupName, + layoutId + } + groupLayouts.push(layoutInfo) + allLayouts.push(layoutInfo) + } else { + console.error(`${layoutName} is missing required exports (default component or Schema)`) + } + } catch (altError) { + console.error(`Alternative import also failed for ${fileName} from ${groupData.groupName}:`, altError) + } } } @@ -121,35 +140,21 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } finally { setLoading(false) } - }, []) + } - const handleLayoutChange = () => { - console.log('πŸ”„ Layout change detected, reloading...') - setTimeout(() => { - loadAllLayouts() - }, 150) - }; - - // Setup file watcher for development - const { isConnected: isWatcherConnected } = useLayoutWatcher({ - onLayoutChange: handleLayoutChange, - enabled: process.env.NODE_ENV === 'development' - }) - - const retry = useCallback(() => { + const retry = () => { loadAllLayouts() - }, [loadAllLayouts]) + } useEffect(() => { loadAllLayouts() - }, [loadAllLayouts]) + }, []) return { layoutGroups, layouts, loading, error, - retry, - isWatcherConnected + retry } } \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts deleted file mode 100644 index 4e3b57a8..00000000 --- a/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts +++ /dev/null @@ -1,133 +0,0 @@ -'use client' -import { useEffect, useRef, useState } from 'react' -import { toast } from 'sonner' - -interface UseLayoutWatcherProps { - onLayoutChange: () => void - enabled?: boolean -} - -export const useLayoutWatcher = ({ onLayoutChange, enabled = true }: UseLayoutWatcherProps) => { - const wsRef = useRef(null) - const reconnectTimeoutRef = useRef(null) - const [isConnected, setIsConnected] = useState(false) - - const connectWebSocket = () => { - if (!enabled || typeof window === 'undefined') return - - try { - // Get the current host (works in Docker and local development) - const host = window.location.hostname - const wsUrl = `ws://${host}:3001` - - console.log('πŸ”„ Connecting to layout watcher:', wsUrl) - - // Create WebSocket connection to our layout watcher endpoint - const ws = new WebSocket(wsUrl) - wsRef.current = ws - - ws.onopen = () => { - console.log('βœ… Layout watcher connected successfully') - setIsConnected(true) - - // Clear any existing reconnect timeout - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current) - reconnectTimeoutRef.current = null - } - } - - ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data) - console.log('πŸ“¨ WebSocket message received:', data) - - if (data.type === 'layout-change') { - console.log('πŸ”₯ Layout file changed:', data.file) - console.log('πŸ“ Group:', data.groupName, 'File:', data.fileName) - - // Show toast notification - toast.success('Layout updated!', { - description: `${data.file} has been reloaded`, - duration: 2000, - }) - - // Trigger reload - console.log('πŸš€ Triggering layout reload...') - onLayoutChange() - } else if (data.type === 'connected') { - console.log('βœ… Layout watcher handshake complete') - } - } catch (error) { - console.error('❌ Error parsing WebSocket message:', error) - } - } - - ws.onclose = (event) => { - console.log('πŸ”Œ Layout watcher disconnected. Code:', event.code, 'Reason:', event.reason) - setIsConnected(false) - wsRef.current = null - - // Attempt to reconnect after 3 seconds if enabled - if (enabled) { - console.log('⏰ Will attempt to reconnect in 3 seconds...') - reconnectTimeoutRef.current = setTimeout(() => { - console.log('πŸ”„ Attempting to reconnect layout watcher...') - connectWebSocket() - }, 3000) - } - } - - ws.onerror = (error) => { - console.error('❌ Layout watcher WebSocket error:', error) - setIsConnected(false) - ws.close() - } - - } catch (error) { - console.error('❌ Failed to create WebSocket connection:', error) - setIsConnected(false) - } - } - - useEffect(() => { - if (enabled) { - console.log('🎯 Layout watcher hook enabled, connecting...') - connectWebSocket() - } else { - console.log('⏸️ Layout watcher hook disabled') - } - - return () => { - if (wsRef.current) { - console.log('🧹 Cleaning up WebSocket connection') - wsRef.current.close() - wsRef.current = null - } - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current) - reconnectTimeoutRef.current = null - } - setIsConnected(false) - } - }, [enabled]) - - // Cleanup on component unmount - useEffect(() => { - return () => { - console.log('🧹 Cleaning up WebSocket connection') - if (wsRef.current) { - wsRef.current.close() - } - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current) - } - setIsConnected(false) - } - }, []) - - return { - isConnected, - reconnect: connectWebSocket - } -} \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/page.tsx b/servers/nextjs/app/layout-preview/page.tsx index 904a94ef..a7186109 100644 --- a/servers/nextjs/app/layout-preview/page.tsx +++ b/servers/nextjs/app/layout-preview/page.tsx @@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button' import { ExternalLink, Wifi, WifiOff, RefreshCw } from 'lucide-react' const LayoutPreview = () => { - const { layoutGroups, layouts, loading, error, retry, isWatcherConnected } = useLayoutLoader() + const { layoutGroups, layouts, loading, error, retry } = useLayoutLoader() const router = useRouter() // Handle loading state @@ -36,39 +36,6 @@ const LayoutPreview = () => { {layoutGroups.length} groups β€’ {layouts.length} layouts

- {/* Development Mode Status */} - {process.env.NODE_ENV === 'development' && ( -
-
- {isWatcherConnected ? ( - <> - - Hot Reload Active - - ) : ( - <> - - Hot Reload Disconnected - - )} -
- - -
- )} - -
diff --git a/servers/nextjs/next.config.mjs b/servers/nextjs/next.config.mjs index ec366315..e19c31de 100644 --- a/servers/nextjs/next.config.mjs +++ b/servers/nextjs/next.config.mjs @@ -8,44 +8,7 @@ const __filename = fileURLToPath(import.meta.url); const nextConfig = { reactStrictMode: false, - // Enable experimental features for better HMR - experimental: { - optimizePackageImports: ['presentation-layouts'], - }, - // Webpack configuration for better hot reloading - webpack: (config, { dev, isServer }) => { - if (dev && !isServer) { - // Enable hot reloading for presentation layouts - config.watchOptions = { - ...config.watchOptions, - ignored: /node_modules/, - poll: 1000, - aggregateTimeout: 300, - }; - - // Add alias for presentation layouts - config.resolve.alias = { - ...config.resolve.alias, - '@/presentation-layouts': path.join(process.cwd(), 'presentation-layouts'), - }; - - // Configure module resolution for better hot reloading - config.resolve.symlinks = false; - config.resolve.cache = false; - - // Optimize for faster rebuilds - config.cache = { - type: 'filesystem', - buildDependencies: { - config: [__filename], - }, - cacheDirectory: path.join(process.cwd(), '.next/cache/webpack'), - }; - } - - return config; - }, images: { remotePatterns: [ diff --git a/servers/nextjs/package.json b/servers/nextjs/package.json index 632b9ae1..9e80adf7 100644 --- a/servers/nextjs/package.json +++ b/servers/nextjs/package.json @@ -4,14 +4,11 @@ "private": true, "type": "module", "scripts": { - "dev": "WATCHPACK_POLLING=true next dev", - "dev:with-watcher": "concurrently \"npm run dev\" \"npm run layout-watcher\"", - "layout-watcher": "node scripts/layout-watcher.cjs", + "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", - "get:version": "next --version" - }, + "lint": "next lint" + }, "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", @@ -40,11 +37,11 @@ "@tiptap/extension-underline": "^2.0.0", "@tiptap/react": "^2.11.5", "@tiptap/starter-kit": "^2.11.5", - "chokidar": "^4.0.1", + "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", - "concurrently": "^9.1.0", + "jsonrepair": "^3.12.0", "lucide-react": "^0.447.0", "marked": "^15.0.11", diff --git a/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx b/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx index 6694a02e..79fd6e66 100644 --- a/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx +++ b/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx @@ -39,7 +39,7 @@ const Type1SlideLayout: React.FC = ({ data: slideData })
{/* Title */}

- {slideData?.title || ' This is the title of slide'} + nice {slideData?.title || ' This is the title of slide'}

{/* Description */} diff --git a/servers/nextjs/scripts/layout-watcher.cjs b/servers/nextjs/scripts/layout-watcher.cjs deleted file mode 100644 index 49acdebf..00000000 --- a/servers/nextjs/scripts/layout-watcher.cjs +++ /dev/null @@ -1,174 +0,0 @@ -const WebSocket = require('ws'); -const chokidar = require('chokidar'); -const path = require('path'); - -const PORT = 3001; -const LAYOUTS_DIR = path.join(process.cwd(), 'presentation-layouts'); - -console.log('πŸ” Starting Layout Watcher...'); -console.log('πŸ“ Watching directory:', LAYOUTS_DIR); - -// Create WebSocket server (without path since ws doesn't handle paths like HTTP) -const wss = new WebSocket.Server({ - port: PORT -}); - -console.log(`πŸ”Œ WebSocket server running on ws://localhost:${PORT}`); - -// Track connected clients -let connectedClients = new Set(); - -wss.on('connection', (ws) => { - console.log('πŸ‘‹ Client connected to layout watcher'); - connectedClients.add(ws); - - ws.on('close', () => { - console.log('πŸ‘‹ Client disconnected from layout watcher'); - connectedClients.delete(ws); - }); - - ws.on('error', (error) => { - console.error('❌ WebSocket error:', error); - connectedClients.delete(ws); - }); - - // Send initial connection confirmation - ws.send(JSON.stringify({ - type: 'connected', - message: 'Layout watcher connected', - timestamp: Date.now() - })); -}); - -// File watcher setup -const watcher = chokidar.watch(LAYOUTS_DIR, { - ignored: [ - /(^|[\/\\])\../, // ignore dotfiles - /node_modules/, - /\.git/, - /\.next/, - /\.DS_Store/, - /thumbs\.db/i - ], - persistent: true, - ignoreInitial: true, // Don't fire events for initial scan - followSymlinks: false, - depth: 3, // Limit depth to avoid deep nested directories - awaitWriteFinish: { - stabilityThreshold: 100, // Wait 100ms after last write - pollInterval: 50 - } -}); - -// Debounce function to prevent rapid fire events -const debounce = (func, wait) => { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -}; - -// Broadcast changes to all connected clients -const broadcastChange = debounce((eventType, filePath) => { - if (connectedClients.size === 0) return; - - const relativePath = path.relative(LAYOUTS_DIR, filePath); - const pathParts = relativePath.split(path.sep); - - // Only process .tsx and .ts files in group directories - if (pathParts.length >= 2 && (filePath.endsWith('.tsx') || filePath.endsWith('.ts'))) { - const groupName = pathParts[0]; - const fileName = pathParts[pathParts.length - 1]; - - // Skip non-layout files - if (fileName.startsWith('.') || fileName.includes('.test.') || fileName.includes('.spec.')) { - return; - } - - const changeData = { - type: 'layout-change', - eventType, - file: relativePath, - groupName, - fileName, - fullPath: filePath, - timestamp: Date.now() - }; - - console.log(`πŸ”₯ Broadcasting ${eventType}:`, relativePath); - - // Send to all connected clients - connectedClients.forEach(ws => { - if (ws.readyState === WebSocket.OPEN) { - try { - ws.send(JSON.stringify(changeData)); - } catch (error) { - console.error('Error sending WebSocket message:', error); - connectedClients.delete(ws); - } - } else { - // Remove disconnected clients - connectedClients.delete(ws); - } - }); - } -}, 200); // Debounce for 200ms - -// Watch for file changes -watcher - .on('change', (filePath) => { - console.log('πŸ“ File changed:', path.relative(LAYOUTS_DIR, filePath)); - broadcastChange('change', filePath); - }) - .on('add', (filePath) => { - console.log('βž• File added:', path.relative(LAYOUTS_DIR, filePath)); - broadcastChange('add', filePath); - }) - .on('unlink', (filePath) => { - console.log('πŸ—‘οΈ File removed:', path.relative(LAYOUTS_DIR, filePath)); - broadcastChange('unlink', filePath); - }) - .on('addDir', (dirPath) => { - console.log('πŸ“ Directory added:', path.relative(LAYOUTS_DIR, dirPath)); - broadcastChange('addDir', dirPath); - }) - .on('unlinkDir', (dirPath) => { - console.log('πŸ“ Directory removed:', path.relative(LAYOUTS_DIR, dirPath)); - broadcastChange('unlinkDir', dirPath); - }) - .on('error', (error) => { - console.error('❌ Watcher error:', error); - }) - .on('ready', () => { - console.log('βœ… Initial scan complete. Ready for changes.'); - console.log(`πŸ“Š Watching ${Object.keys(watcher.getWatched()).length} directories`); - }); - -// Graceful shutdown -process.on('SIGINT', () => { - console.log('\nπŸ›‘ Shutting down layout watcher...'); - watcher.close().then(() => { - console.log('πŸ“ File watcher closed'); - wss.close(() => { - console.log('πŸ”Œ WebSocket server closed'); - process.exit(0); - }); - }); -}); - -process.on('SIGTERM', () => { - console.log('\nπŸ›‘ Received SIGTERM, shutting down...'); - watcher.close().then(() => { - wss.close(() => { - process.exit(0); - }); - }); -}); - -// Keep the process alive -console.log('πŸš€ Layout watcher is running. Press Ctrl+C to stop.'); \ No newline at end of file From 25654df08f9b6f5b1b805ec5677000c1a737b29e Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Sun, 20 Jul 2025 21:47:08 +0545 Subject: [PATCH 3/4] feat(Nextjs): Default data previewd in layout group selection --- .../outline/components/GroupLayouts.tsx | 65 +++++++++++++ .../outline/components/LayoutSelection.tsx | 97 +++---------------- .../outline/components/OutlinePage.tsx | 2 +- .../hooks/usePresentationGeneration.ts | 17 +--- .../outline/types/index.ts | 2 +- .../hooks/useGroupLayoutLoader.ts | 6 +- servers/nextjs/package.json | 3 - 7 files changed, 88 insertions(+), 104 deletions(-) create mode 100644 servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx new file mode 100644 index 00000000..99c95d9e --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx @@ -0,0 +1,65 @@ +import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader'; +import { CheckCircle } from 'lucide-react'; +import React from 'react'; +import { LayoutGroup } from "../types/index"; + +interface GroupLayoutsProps { + group: LayoutGroup; + onSelectLayoutGroup: (group: LayoutGroup) => void; + selectedLayoutGroup: LayoutGroup | null; +} + +const GroupLayouts: React.FC = ({ group, onSelectLayoutGroup, selectedLayoutGroup }) => { + const { layoutGroup } = useGroupLayoutLoader(group.id); + return ( +
onSelectLayoutGroup(group)} + className={`relative p-4 rounded-lg border cursor-pointer transition-all duration-200 ${selectedLayoutGroup?.id === group.id + ? 'border-blue-500 bg-blue-50 shadow-md' + : 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm' + }`} + > + {selectedLayoutGroup?.id === group.id && ( +
+ +
+ )} + +
+
+ {group.name} +
+

+ {group.description} +

+
+ + {/* Layout previews */} +
+ {layoutGroup && layoutGroup?.layouts.slice(0, 4).map((layout: any, index: number) => { + const { component: LayoutComponent, sampleData, layoutId } = layout + return ( +
+
+
+ +
+
+ ) + })} +
+ +
+ {layoutGroup?.layouts.length} layouts + + {group.ordered ? 'Structured' : 'Flexible'} + +
+
+ ); +}; + +export default GroupLayouts; diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx index 1c2b4bd5..04db37d3 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx @@ -1,17 +1,9 @@ "use client"; import React, { useEffect } from "react"; import { useLayout } from "../../context/LayoutContext"; -import { CheckCircle } from "lucide-react"; - -interface LayoutGroup { - id: string; - name: string; - description: string; - ordered: boolean; - isDefault?: boolean; - slides: string[]; -} +import GroupLayouts from "./GroupLayouts"; +import { LayoutGroup } from "../types/index"; interface LayoutSelectionProps { selectedLayoutGroup: LayoutGroup | null; onSelectLayoutGroup: (group: LayoutGroup) => void; @@ -25,17 +17,15 @@ const LayoutSelection: React.FC = ({ getLayoutsByGroup, getGroupSetting, getAllGroups, - getLayout, loading } = useLayout(); const layoutGroups: LayoutGroup[] = React.useMemo(() => { const groups = getAllGroups(); - if (groups.length === 0) return []; const Groups: LayoutGroup[] = groups.map(groupName => { - const layouts = getLayoutsByGroup(groupName); + const settings = getGroupSetting(groupName); return { id: groupName, @@ -43,7 +33,6 @@ const LayoutSelection: React.FC = ({ description: settings?.description || `${groupName} presentation layouts`, ordered: settings?.ordered || false, isDefault: settings?.isDefault || false, - slides: layouts.map(layout => layout.id) }; }); @@ -63,32 +52,6 @@ const LayoutSelection: React.FC = ({ } }, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]); - const renderLayoutPreview = (layoutId: string) => { - const Layout = getLayout(layoutId); - if (!Layout) { - return ( -
- Preview unavailable -
- ); - } - - // Sample data for preview - const sampleData = { - title: "Sample Title", - description: "This is a preview of the layout", - subtitle: "Sample subtitle", - }; - - return ( -
-
- -
-
- ); - }; - if (loading) { return (
@@ -124,52 +87,24 @@ const LayoutSelection: React.FC = ({ ); } + const handleLayoutGroupSelection = (group: LayoutGroup) => { + const slides = getLayoutsByGroup(group.id); + onSelectLayoutGroup({ + ...group, + slides: slides, + }); + } + return (
{layoutGroups.map((group) => ( -
onSelectLayoutGroup(group)} - className={`relative p-4 rounded-lg border cursor-pointer transition-all duration-200 ${selectedLayoutGroup?.id === group.id - ? 'border-blue-500 bg-blue-50 shadow-md' - : 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm' - }`} - > - {selectedLayoutGroup?.id === group.id && ( -
- -
- )} - -
-
- {group.name} -
-

- {group.description} -

-
- - {/* Layout previews */} -
- {group.slides.slice(0, 6).map((layoutId, index) => ( -
- {renderLayoutPreview(layoutId)} -
- ))} -
- -
- {group.slides.length} layouts - - {group.ordered ? 'Structured' : 'Flexible'} - -
-
+ group={group} + onSelectLayoutGroup={handleLayoutGroupSelection} + selectedLayoutGroup={selectedLayoutGroup} + /> ))}
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index 9c615eda..fa9c0ee4 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -24,7 +24,6 @@ const OutlinePage: React.FC = () => { const [activeTab, setActiveTab] = useState(TABS.OUTLINE); const [selectedLayoutGroup, setSelectedLayoutGroup] = useState(null); - // Custom hooks const streamState = useOutlineStreaming(presentation_id); const { handleDragEnd, handleAddSlide } = useOutlineManagement(outlines); @@ -39,6 +38,7 @@ const OutlinePage: React.FC = () => { return ; } + return ( { if (!selectedLayoutGroup) return null; - const groupLayoutSchemas = selectedLayoutGroup.slides - .map(slideId => { - const layout = getLayoutById(slideId); - return layout ? { - id: layout.id, - name: layout.name, - description: layout.description, - json_schema: layout.json_schema - } : null; - }) - .filter(schema => schema !== null); + return { name: selectedLayoutGroup.name, ordered: selectedLayoutGroup.ordered, - slides: groupLayoutSchemas + slides: selectedLayoutGroup.slides }; }, [selectedLayoutGroup, getLayoutById]); @@ -84,7 +74,6 @@ export const usePresentationGeneration = ( try { const layoutData = prepareLayoutData(); if (!layoutData) return; - const response = await PresentationGenerationApi.presentationPrepare({ presentation_id: presentationId, outlines: outlines, diff --git a/servers/nextjs/app/(presentation-generator)/outline/types/index.ts b/servers/nextjs/app/(presentation-generator)/outline/types/index.ts index 9965d2f4..a311f447 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/types/index.ts +++ b/servers/nextjs/app/(presentation-generator)/outline/types/index.ts @@ -4,7 +4,7 @@ export interface LayoutGroup { description: string; ordered: boolean; isDefault?: boolean; - slides: string[]; + slides?: any } export interface LoadingState { diff --git a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts index 00a14e2a..278607f5 100644 --- a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts @@ -67,7 +67,6 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet ordered: false, isDefault: false } - for (const fileName of targetGroupData.files) { try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') @@ -163,8 +162,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet } const retry = () => { - // Clear cache for this group to force reload - layoutGroupCache.delete(groupSlug) + hasMountedRef.current = false loadGroupLayouts() } @@ -179,6 +177,6 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet layoutGroup, loading, error, - retry + retry, } } \ No newline at end of file diff --git a/servers/nextjs/package.json b/servers/nextjs/package.json index 109e3de8..5c769cc1 100644 --- a/servers/nextjs/package.json +++ b/servers/nextjs/package.json @@ -37,11 +37,9 @@ "@tiptap/extension-underline": "^2.0.0", "@tiptap/react": "^2.11.5", "@tiptap/starter-kit": "^2.11.5", - "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", - "jsonrepair": "^3.12.0", "lucide-react": "^0.447.0", "marked": "^15.0.11", @@ -57,7 +55,6 @@ "tailwind-scrollbar-hide": "^2.0.0", "tailwindcss-animate": "^1.0.7", "tiptap-markdown": "^0.8.10", - "ws": "^8.18.0", "uuid": "^11.1.0", "zod": "^4.0.5" }, From 516c9bade08d0a34ab07076e9e04de8ba9c8fa1e Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Sun, 20 Jul 2025 22:55:00 +0545 Subject: [PATCH 4/4] refactor(Nextjs/outline): Remove markdwon editor in streaming & increase export_as_pdf navigation timeout --- .../components/SlideFooter.tsx | 666 ------------------ .../context/footerContext.tsx | 111 --- .../outline/components/OutlineItem.tsx | 10 +- .../pdf-maker/page.tsx | 5 +- .../presentation/components/Help.tsx | 13 +- .../presentation/components/SlideContent.tsx | 3 - .../presentation/page.tsx | 1 - .../services/footerService.ts | 85 --- .../services/themeService.ts | 67 -- servers/nextjs/app/api/export-as-pdf/route.ts | 2 +- servers/nextjs/app/layout.tsx | 7 +- .../analytics/setting.json | 3 +- .../default/Type1SlideLayout.tsx | 2 +- 13 files changed, 18 insertions(+), 957 deletions(-) delete mode 100644 servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx delete mode 100644 servers/nextjs/app/(presentation-generator)/context/footerContext.tsx delete mode 100644 servers/nextjs/app/(presentation-generator)/services/footerService.ts delete mode 100644 servers/nextjs/app/(presentation-generator)/services/themeService.ts diff --git a/servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx b/servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx deleted file mode 100644 index 6905aaca..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx +++ /dev/null @@ -1,666 +0,0 @@ -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet"; -import { Slider } from "@/components/ui/slider"; -import { Switch } from "@/components/ui/switch"; -import { Textarea } from "@/components/ui/textarea"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import React, { useRef, useState, useEffect } from "react"; -import { Camera, Loader2, Plus } from "lucide-react"; -import { useSelector } from "react-redux"; -import { RootState } from "@/store/store"; -import { getStaticFileUrl, isDarkColor } from "../../utils/others"; -import { defaultFooterProperties, useFooterContext } from "../../context/footerContext"; -import { FooterProperties } from "../../services/footerService"; -import { toast } from "sonner"; - -const SlideFooter: React.FC = () => { - const [showEditor, setShowEditor] = useState(false); - const [isUploading, setIsUploading] = useState({ - white: false, - dark: false, - }); - const { currentColors } = useSelector((state: RootState) => state.theme); - const isDark = isDarkColor(currentColors.slideBg); - - const whiteLogoRef = useRef(null); - const darkLogoRef = useRef(null); - - const { footerProperties, setFooterProperties, saveFooterProperties, resetFooterProperties, isPropertyChanged, setIsPropertyChanged } = useFooterContext(); - - - const handleSave = async () => { - await saveFooterProperties(footerProperties); - setIsPropertyChanged(false); - toast.success("Footer properties saved successfully"); - }; - - const handleReset = async () => { - await resetFooterProperties(); - setFooterProperties(defaultFooterProperties); - toast.success("Footer properties reset to default"); - }; - - const updateProperty = (path: string, value: any): void => { - setIsPropertyChanged(true); - const keys = path.split("."); - // Security: Validate path to prevent prototype pollution - const dangerousKeys = ['__proto__', 'constructor', 'prototype']; - if (keys.some(key => dangerousKeys.includes(key))) { - console.warn('Attempted prototype pollution with path:', path); - return; - } - - const allowedPaths = [ - 'logoProperties.showLogo', - 'logoProperties.logoPosition', - 'logoProperties.opacity', - 'logoProperties.logoImage.light', - 'logoProperties.logoImage.dark', - 'logoScale', - 'logoOffset.x', - 'logoOffset.y', - 'footerMessage.showMessage', - 'footerMessage.message', - 'footerMessage.fontSize', - 'footerMessage.opacity' - ] - - if (!allowedPaths.includes(path)) { - console.error(`Invalid path: ${path}`); - return; - } - - setFooterProperties((prevProps: FooterProperties) => { - const newProps = { ...prevProps }; - let current: any = newProps; - - for (let i = 0; i < keys.length - 1; i++) { - if (dangerousKeys.includes(keys[i])) { - console.warn('Attempted prototype pollution with path:', path); - return prevProps; - } - current[keys[i]] = { ...current[keys[i]] }; - current = current[keys[i]]; - } - - const finalKey = keys[keys.length - 1]; - if (dangerousKeys.includes(finalKey)) { - console.warn('Dangerous final key detected:', finalKey); - return prevProps; - } - current[keys[keys.length - 1]] = value; - return newProps; - }); - }; - - const handleSwitchChange = - (path: string) => - (checked: boolean): void => { - updateProperty(path, checked); - }; - - const handleTextChange = - (path: string) => - (e: React.ChangeEvent): void => { - updateProperty(path, e.target.value); - }; - - const handleSelectChange = - (path: string) => - (value: string): void => { - updateProperty(path, value); - }; - - const handleSliderChange = - (path: string) => - (value: number[]): void => { - updateProperty(path, value[0]); - }; - - const getLogoPositionClass = (): string => { - const { logoPosition } = footerProperties.logoProperties; - return logoPosition === "left" - ? "justify-start" - : logoPosition === "center" - ? "justify-center" - : "justify-end"; - }; - - const getLogoStyle = (): React.CSSProperties => { - const { opacity } = footerProperties.logoProperties; - const { logoScale, logoOffset } = footerProperties; - return { - opacity: opacity, - transform: `scale(${logoScale}) translate(${logoOffset.x}px, ${logoOffset.y}px)`, - }; - }; - - const getMessageStyle = (): React.CSSProperties => { - const { fontSize, opacity } = footerProperties.footerMessage; - return { - opacity: opacity, - fontSize: `${fontSize}px`, - fontFamily: currentColors.fontFamily || "Inter, sans-serif", - }; - }; - - const getLogoImageSrc = (): string => { - - if (isDark) { - return footerProperties.logoProperties.logoImage.dark; - } else { - return footerProperties.logoProperties.logoImage.light; - } - }; - - const convertToBase64 = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => resolve(reader.result as string); - reader.onerror = (error) => reject(error); - }); - }; - - const handleWhiteLogoUpload = async ( - event: React.ChangeEvent - ) => { - setIsPropertyChanged(true); - const file = event.target.files?.[0]; - if (!file) return; - - if (!file.type.startsWith("image/")) { - toast.error("Please Upload An Image File"); - return; - } - - try { - setIsUploading({ ...isUploading, white: true }); - const base64String = await convertToBase64(file); - - setFooterProperties((prev: FooterProperties) => ({ - ...prev, - logoProperties: { - ...prev.logoProperties, - logoImage: { - ...prev.logoProperties.logoImage, - light: base64String, - }, - }, - })); - } catch (error) { - console.error("Error converting image:", error); - toast.error("Error uploading image"); - } finally { - setIsUploading({ ...isUploading, white: false }); - } - }; - - const handleDarkLogoUpload = async ( - event: React.ChangeEvent - ) => { - setIsPropertyChanged(true); - const file = event.target.files?.[0]; - if (!file) return; - - if (!file.type.startsWith("image/")) { - toast.error("Please Upload An Image File"); - return; - } - - try { - setIsUploading({ ...isUploading, dark: true }); - const base64String = await convertToBase64(file); - - setFooterProperties((prev: FooterProperties) => ({ - ...prev, - logoProperties: { - ...prev.logoProperties, - logoImage: { - ...prev.logoProperties.logoImage, - dark: base64String, - }, - }, - })); - } catch (error) { - console.error("Error converting image:", error); - toast.error("Error uploading image"); - } finally { - setIsUploading({ ...isUploading, dark: false }); - } - }; - - const getLocalImageUrl = (filePath: string) => { - if (!filePath) return ""; - if (filePath.startsWith('data:image')) return filePath; - return getStaticFileUrl(filePath); - }; - - const handleEditor = () => { - setShowEditor(!showEditor); - return; - }; - - const handleUploadClick = (isWhite: boolean) => { - if (isWhite) { - whiteLogoRef.current?.click(); - } else { - darkLogoRef.current?.click(); - } - }; - - const handleSheetClose = () => { - if (isPropertyChanged) { - toast.error("Unsaved Changes", { - description: "Please save changes before closing the editor", - }); - return; - } - setShowEditor(false); - }; - - return ( - <> - - - - e.preventDefault()} - className="sm:max-w-[500px] overflow-y-auto" - > - - Configure Footer -

- These changes will apply to all slides. -

-
- -
-
-

Logo Settings

- -
-
- -
-
-
-
handleUploadClick(true)} - className="h-28 border relative overflow-hidden flex justify-center items-center cursor-pointer group group-hover:border-blue-500" - > - - {isUploading.white ? ( -
- -
- ) : footerProperties.logoProperties.logoImage.light ? ( -
- brand white logo -
- -
-
- ) : ( - - )} -
-

Logo on Light

-
-
-
handleUploadClick(false)} - className="h-28 flex bg-black justify-center items-center border relative cursor-pointer group" - > - - {footerProperties.logoProperties.logoImage.dark && ( - brand white logo - )} - {isUploading.dark ? ( -
- -
- ) : footerProperties.logoProperties.logoImage.dark ? ( -
- brand white logo -
- -
-
- ) : ( - - )} -
-

Logo on Dark/Color

-
-
- - {footerProperties.logoProperties.showLogo && ( -
- - -
- )} - -
-
- - - {footerProperties.logoProperties.opacity} - -
- -
- -
-
- - - {footerProperties.logoScale} - -
- -
- -
-
- - - {footerProperties.logoOffset.x}px - -
- -
- -
-
- - - {footerProperties.logoOffset.y * -1}px - -
- -
-
-
- -
-

Footer Message

- -
-
- - -
-
- -