Files
app/src/utils/tldraw/ui-overrides/components/shared/navigation/CCNodeSnapshotPanel.tsx
T
CC Worker 765573163d feat(ts): R4 TypeScript hardening H1-H6 — reduce errors 177→152
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)
2026-06-01 02:29:38 +00:00

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>
);
};