Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
- Removed manual 'images' metadata overrides. - This allows Next.js to use built-in automatic discovery. - Ensures metadata uses the dynamic metadataBase from the environment. - Refined Traefik public router regex for sub-routes. - Restored and verified imports in modified page.tsx files.
179 lines
5.1 KiB
TypeScript
179 lines
5.1 KiB
TypeScript
'use client';
|
|
|
|
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
|
import { RecordEvent, RecordingSession } from '@/types/record-mode';
|
|
|
|
interface RecordModeContextType {
|
|
isActive: boolean;
|
|
setIsActive: (active: boolean) => void;
|
|
isRecording: boolean;
|
|
startRecording: () => void;
|
|
stopRecording: () => void;
|
|
events: RecordEvent[];
|
|
addEvent: (event: Omit<RecordEvent, 'id' | 'timestamp'>) => void;
|
|
updateEvent: (id: string, event: Partial<RecordEvent>) => void;
|
|
removeEvent: (id: string) => void;
|
|
clearEvents: () => void;
|
|
isPlaying: boolean;
|
|
playEvents: () => void;
|
|
stopPlayback: () => void;
|
|
currentSession: RecordingSession | null;
|
|
saveSession: (name: string) => void;
|
|
}
|
|
|
|
const RecordModeContext = createContext<RecordModeContextType | null>(null);
|
|
|
|
export function useRecordMode() {
|
|
const context = useContext(RecordModeContext);
|
|
if (!context) {
|
|
throw new Error('useRecordMode must be used within a RecordModeProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export function RecordModeProvider({ children }: { children: React.ReactNode }) {
|
|
const [isActive, setIsActive] = useState(false);
|
|
const [isRecording, setIsRecording] = useState(false);
|
|
const [events, setEvents] = useState<RecordEvent[]>([]);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [currentSession, setCurrentSession] = useState<RecordingSession | null>(null);
|
|
const startTimeRef = useRef<number>(0);
|
|
|
|
const startRecording = () => {
|
|
setIsRecording(true);
|
|
setEvents([]);
|
|
startTimeRef.current = Date.now();
|
|
};
|
|
|
|
const stopRecording = () => {
|
|
setIsRecording(false);
|
|
};
|
|
|
|
const addEvent = (eventData: Omit<RecordEvent, 'id' | 'timestamp'>) => {
|
|
const timestamp = Date.now() - startTimeRef.current;
|
|
const newEvent: RecordEvent = {
|
|
id: crypto.randomUUID(),
|
|
timestamp,
|
|
...eventData,
|
|
};
|
|
setEvents((prev) => [...prev, newEvent]);
|
|
};
|
|
|
|
const updateEvent = (id: string, updates: Partial<RecordEvent>) => {
|
|
setEvents((prev) => prev.map((e) => (e.id === id ? { ...e, ...updates } : e)));
|
|
};
|
|
|
|
const removeEvent = (id: string) => {
|
|
setEvents((prev) => prev.filter((e) => e.id !== id));
|
|
};
|
|
|
|
const clearEvents = () => {
|
|
setEvents([]);
|
|
};
|
|
|
|
const playEvents = async () => {
|
|
if (events.length === 0) return;
|
|
setIsPlaying(true);
|
|
|
|
// Simple playback logic mostly for preview
|
|
const startPlayTime = Date.now();
|
|
|
|
// Sort events by timestamp just in case
|
|
const sortedEvents = [...events].sort((a, b) => a.timestamp - b.timestamp);
|
|
|
|
for (const event of sortedEvents) {
|
|
if (!isPlaying) break; // Check if stopped
|
|
|
|
const targetTime = startPlayTime + event.timestamp;
|
|
const now = Date.now();
|
|
const delay = targetTime - now;
|
|
|
|
if (delay > 0) {
|
|
await new Promise((r) => setTimeout(r, delay));
|
|
}
|
|
|
|
// Execute event visual feedback
|
|
if (document) {
|
|
if (event.selector) {
|
|
const el = document.querySelector(event.selector);
|
|
if (el) {
|
|
// Highlight or scroll to element
|
|
if (event.type === 'scroll') {
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
} else if (event.type === 'click') {
|
|
// Visualize click
|
|
const rect = el.getBoundingClientRect();
|
|
const clickMarker = document.createElement('div');
|
|
clickMarker.style.position = 'fixed';
|
|
clickMarker.style.left = `${rect.left + rect.width / 2}px`;
|
|
clickMarker.style.top = `${rect.top + rect.height / 2}px`;
|
|
clickMarker.style.width = '20px';
|
|
clickMarker.style.height = '20px';
|
|
clickMarker.style.borderRadius = '50%';
|
|
clickMarker.style.backgroundColor = 'rgba(255, 0, 0, 0.5)';
|
|
clickMarker.style.transform = 'translate(-50%, -50%)';
|
|
clickMarker.style.zIndex = '99999';
|
|
document.body.appendChild(clickMarker);
|
|
setTimeout(() => clickMarker.remove(), 500);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
setIsPlaying(false);
|
|
};
|
|
|
|
const stopPlayback = () => {
|
|
setIsPlaying(false);
|
|
};
|
|
|
|
const saveSession = (name: string) => {
|
|
const session: RecordingSession = {
|
|
id: crypto.randomUUID(),
|
|
name,
|
|
events,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
setCurrentSession(session);
|
|
// Ideally save to local storage or API
|
|
localStorage.setItem('klz-record-session', JSON.stringify(session));
|
|
};
|
|
|
|
// Load session on mount
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem('klz-record-session');
|
|
if (saved) {
|
|
try {
|
|
setCurrentSession(JSON.parse(saved));
|
|
} catch (e) {
|
|
console.error('Failed to load session', e);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<RecordModeContext.Provider
|
|
value={{
|
|
isActive,
|
|
setIsActive,
|
|
isRecording,
|
|
startRecording,
|
|
stopRecording,
|
|
events,
|
|
addEvent,
|
|
updateEvent,
|
|
removeEvent,
|
|
clearEvents,
|
|
isPlaying,
|
|
playEvents,
|
|
stopPlayback,
|
|
currentSession,
|
|
saveSession,
|
|
}}
|
|
>
|
|
{children}
|
|
</RecordModeContext.Provider>
|
|
);
|
|
}
|