feat(Nextjs): HMR support added & websocker added
This commit is contained in:
parent
3ab3756fa5
commit
8eee6d55a2
15 changed files with 482 additions and 118 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<Link
|
||||
href="/dashboard"
|
||||
prefetch={false}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ export default function MarkdownEditor({ content, onChange }: { content: string;
|
|||
});
|
||||
|
||||
// Update editor content when the content prop changes (for streaming)
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<div className="relative">
|
||||
|
|
|
|||
|
|
@ -66,16 +66,11 @@ export function OutlineItem({
|
|||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}
|
||||
|
||||
|
||||
const handleSlideDelete = () => {
|
||||
if (isStreaming) return;
|
||||
dispatch(deleteSlideOutline({ index: index - 1 }))
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="mb-2 bg-[#F9F9F9]">
|
||||
{/* Main Title Row */}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const page = () => {
|
|||
<div className='relative'>
|
||||
<Header />
|
||||
<div className='flex flex-col items-center justify-center py-8'>
|
||||
<h1 className='text-3xl font-semibold font-instrument_sans'>Create Presentation </h1>
|
||||
<h1 className='text-3xl font-semibold font-instrument_sans'>Create Presentation </h1>
|
||||
{/* <p className='text-sm text-gray-500'>We will generate a presentation for you</p> */}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<p className="text-gray-600 mt-2">
|
||||
{layoutGroup.layouts.length} layout{layoutGroup.layouts.length !== 1 ? 's' : ''} • {layoutGroup.settings.description}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -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<string, LayoutGroup>()
|
||||
const loadingGroupsCache = new Set<string>()
|
||||
|
||||
export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => {
|
||||
const [layoutGroup, setLayoutGroup] = useState<LayoutGroup | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string | null>(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
|
||||
}
|
||||
}
|
||||
133
servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts
Normal file
133
servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts
Normal file
|
|
@ -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<WebSocket | null>(null)
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = () => {
|
|||
<p className="text-gray-600 mt-2">
|
||||
{layoutGroups.length} groups • {layouts.length} layouts
|
||||
</p>
|
||||
|
||||
{/* Development Mode Status */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div className="mt-4 flex items-center justify-center gap-2">
|
||||
<div className={`flex items-center gap-2 px-3 py-1 rounded-full text-sm font-medium ${isWatcherConnected
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-orange-100 text-orange-800'
|
||||
}`}>
|
||||
{isWatcherConnected ? (
|
||||
<>
|
||||
<Wifi className="w-4 h-4" />
|
||||
Hot Reload Active
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOff className="w-4 h-4" />
|
||||
Hot Reload Disconnected
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={retry}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<Type1SlideLayoutProps> = ({ data: slideData }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className=" w-full rounded-sm max-w-[1280px] shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] max-h-[720px] flex items-center aspect-video bg-white relative z-20 mx-auto"
|
||||
|
|
@ -40,12 +39,12 @@ const Type1SlideLayout: React.FC<Type1SlideLayoutProps> = ({ data: slideData })
|
|||
<div className="flex flex-col w-full items-start justify-center space-y-1 md:space-y-2 lg:space-y-6">
|
||||
{/* Title */}
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
|
||||
{slideData?.title || 'Sample Title'}
|
||||
{slideData?.title || ' This is the title of slide'}
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-base sm:text-lg lg:text-xl text-gray-700 leading-relaxed">
|
||||
{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.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
174
servers/nextjs/scripts/layout-watcher.cjs
Normal file
174
servers/nextjs/scripts/layout-watcher.cjs
Normal file
|
|
@ -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.');
|
||||
Loading…
Add table
Reference in a new issue