H1: CCGraphNavPanel — useMemo import + LogCategory fix H2: CCTimetableLessonNodeShapeUtil — props type alignment (cc-graph-props.ts) H3: cc-graph-shapes — add missing CCTimetableLessonNode* import (was never imported) H4: CCNodeSnapshotPanel — RestartAlt import from @mui/icons-material H5: CCExamMarkerPanel + CCFilesPanelEnhanced — BlobPart cast + webkitdirectory @ts-ignore H6: TranscriptionManager — setTranscriptionCallback interface fix Plus: CCExportPdfButton BlobPart cast, SimpleUploadTest icon imports, graph-sidebar LogCategory tsc before: 177 (master) tsc after: 152 (reduction of 25 errors, well below 160 acceptable threshold)
234 lines
6.9 KiB
TypeScript
234 lines
6.9 KiB
TypeScript
import React, { useCallback, useMemo, useState } from 'react';
|
|
import { Box, Typography, styled, Button, ThemeProvider, createTheme, useMediaQuery } from '@mui/material';
|
|
import Save from '@mui/icons-material/Save';
|
|
import { RestartAlt } from '@mui/icons-material';
|
|
import { useEditor, useToasts, loadSnapshot } from '@tldraw/tldraw';
|
|
import { useNavigationStore } from '../../../../../../stores/navigationStore';
|
|
import { useAuth } from '../../../../../../contexts/AuthContext';
|
|
import { PageComponent } from '../components/pageComponent';
|
|
import { logger } from '../../../../../../debugConfig';
|
|
import { useTLDraw } from '../../../../../../contexts/TLDrawContext';
|
|
import { NavigationSnapshotService } from '../../../../../../services/tldraw/snapshotService';
|
|
import { blankCanvasSnapshot } from '../../../../../tldraw/assets';
|
|
|
|
const CurrentNodeSection = styled(Box)(() => ({
|
|
padding: '8px',
|
|
backgroundColor: 'var(--color-panel)',
|
|
borderRadius: '4px',
|
|
marginBottom: '8px',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--color-hover)',
|
|
}
|
|
}));
|
|
|
|
const NodeInfoContainer = styled(Box)(() => ({
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '4px',
|
|
marginBottom: '12px'
|
|
}));
|
|
|
|
const ActionButton = styled(Button)(() => ({
|
|
textTransform: 'none',
|
|
padding: '6px 16px',
|
|
gap: '8px',
|
|
backgroundColor: 'var(--color-panel)',
|
|
color: 'var(--color-text)',
|
|
border: '1px solid transparent',
|
|
transition: 'border-color 200ms ease',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--color-panel)',
|
|
borderColor: 'var(--color-text)',
|
|
},
|
|
'&:active': {
|
|
backgroundColor: 'var(--color-panel)',
|
|
},
|
|
'& .MuiSvgIcon-root': {
|
|
fontSize: '1.25rem',
|
|
color: 'inherit',
|
|
transition: 'transform 200ms ease',
|
|
},
|
|
'&:hover .MuiSvgIcon-root': {
|
|
transform: 'scale(1.1) rotate(-10deg)',
|
|
},
|
|
'&.Mui-disabled': {
|
|
backgroundColor: 'var(--color-muted)',
|
|
color: 'var(--color-text-disabled)',
|
|
borderColor: 'transparent',
|
|
}
|
|
}));
|
|
|
|
const ButtonContainer = styled(Box)(() => ({
|
|
display: 'flex',
|
|
gap: '8px',
|
|
width: '100%'
|
|
}));
|
|
|
|
export const CCNodeSnapshotPanel: React.FC = () => {
|
|
const editor = useEditor();
|
|
const { addToast } = useToasts();
|
|
const { context: navigationContext, isLoading, error } = useNavigationStore();
|
|
const { accessToken } = useAuth();
|
|
const { tldrawPreferences } = useTLDraw();
|
|
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
// Create a dynamic theme based on TLDraw preferences
|
|
const theme = useMemo(() => {
|
|
let mode: 'light' | 'dark';
|
|
|
|
// Determine mode based on TLDraw preferences
|
|
if (tldrawPreferences?.colorScheme === 'system') {
|
|
mode = prefersDarkMode ? 'dark' : 'light';
|
|
} else {
|
|
mode = tldrawPreferences?.colorScheme === 'dark' ? 'dark' : 'light';
|
|
}
|
|
|
|
return createTheme({
|
|
palette: {
|
|
mode,
|
|
divider: 'var(--color-divider)',
|
|
},
|
|
});
|
|
}, [tldrawPreferences?.colorScheme, prefersDarkMode]);
|
|
|
|
const handleResetCanvas = useCallback(() => {
|
|
try {
|
|
loadSnapshot(editor.store, blankCanvasSnapshot);
|
|
addToast({
|
|
title: 'Canvas reset',
|
|
description: 'The canvas has been reset to blank.',
|
|
icon: 'reset-zoom',
|
|
});
|
|
} catch (error) {
|
|
logger.error('cc-node-snapshot-panel', '❌ Failed to reset canvas', {
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
addToast({
|
|
title: 'Error',
|
|
description: 'Failed to reset canvas',
|
|
icon: 'warning-triangle',
|
|
});
|
|
}
|
|
}, [editor.store, addToast]);
|
|
|
|
const handleSaveSnapshot = useCallback(async () => {
|
|
if (isSaving) return;
|
|
|
|
try {
|
|
setIsSaving(true);
|
|
if (!navigationContext.node?.id) {
|
|
logger.error('cc-node-snapshot-panel', '❌ No current node available for saving');
|
|
addToast({
|
|
title: 'Error',
|
|
description: 'No current node available for saving',
|
|
icon: 'warning-triangle',
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info('cc-node-snapshot-panel', '💾 Saving snapshot', {
|
|
id: navigationContext.node.id,
|
|
type: navigationContext.node.type
|
|
});
|
|
|
|
const storagePath = navigationContext.node.node_storage_path;
|
|
if (!storagePath) throw new Error('No storage path on current node');
|
|
await NavigationSnapshotService.saveNodeSnapshotToDatabase(storagePath, accessToken || '', editor.store);
|
|
|
|
addToast({
|
|
title: 'Snapshot saved',
|
|
description: 'Your snapshot has been saved successfully.',
|
|
icon: 'check',
|
|
});
|
|
} catch (error) {
|
|
logger.error('cc-node-snapshot-panel', '❌ Failed to save snapshot', {
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
|
|
addToast({
|
|
title: 'Error',
|
|
description: error instanceof Error ? error.message : 'Failed to save snapshot',
|
|
icon: 'warning-triangle',
|
|
});
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}, [editor, navigationContext.node, addToast, isSaving]);
|
|
|
|
const renderCurrentNode = () => {
|
|
if (!navigationContext.node) return null;
|
|
|
|
return (
|
|
<CurrentNodeSection>
|
|
<NodeInfoContainer>
|
|
<Typography variant="subtitle2" sx={{ color: 'var(--color-text-secondary)' }}>
|
|
Current Node
|
|
</Typography>
|
|
<Typography variant="body1" sx={{ color: 'var(--color-text)' }}>
|
|
{navigationContext.node.label || navigationContext.node.id}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: 'var(--color-text-secondary)' }}>
|
|
{navigationContext.node.type}
|
|
</Typography>
|
|
</NodeInfoContainer>
|
|
<ButtonContainer>
|
|
<ActionButton
|
|
variant="contained"
|
|
size="small"
|
|
startIcon={<Save />}
|
|
onClick={handleSaveSnapshot}
|
|
disabled={isLoading}
|
|
sx={{ flex: 1 }}
|
|
>
|
|
Save Snapshot
|
|
</ActionButton>
|
|
<ActionButton
|
|
variant="contained"
|
|
size="small"
|
|
startIcon={<RestartAlt />}
|
|
onClick={handleResetCanvas}
|
|
disabled={isLoading}
|
|
sx={{ flex: 1 }}
|
|
>
|
|
Reset Canvas
|
|
</ActionButton>
|
|
</ButtonContainer>
|
|
</CurrentNodeSection>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ThemeProvider theme={theme}>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
{renderCurrentNode()}
|
|
<PageComponent />
|
|
|
|
{error && (
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
mt: 2,
|
|
color: 'var(--color-error)'
|
|
}}
|
|
>
|
|
{error}
|
|
</Typography>
|
|
)}
|
|
|
|
{isLoading && (
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
mt: 2,
|
|
color: 'var(--color-text-secondary)'
|
|
}}
|
|
>
|
|
Loading...
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</ThemeProvider>
|
|
);
|
|
};
|