diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 49c92ef..781b983 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5099,9 +5099,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { diff --git a/frontend/src/App.css b/frontend/src/App.css index 18e0f3d..8974299 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -236,6 +236,33 @@ [data-theme="neon"] .class-btn.active { box-shadow: 0 0 10px rgba(0,245,255,0.5); } + /* Galaxy Theme */ +[data-theme="galaxy"] { + --bg: #0a0614; + --text: #e8e0f5; + --text-muted: #8b7aa8; + --panel-bg: #110d1f; + --border: #2d1f4a; + --border-subtle: #1a1230; + --primary: #7c3aed; + --primary-hover: #6d28d9; + --card-bg: #110d1f; + --box-bg: #0d0a1a; + --input-bg: #080512; + --input-border: #3d2566; + --input-text: #e8e0f5; + --btn-primary: #7c3aed; + --btn-primary-hover: #6d28d9; + --btn-download: #5b21b6; + --btn-download-hover: #4c1d95; + --btn-clear: #db2777; + --btn-clear-hover: #be185d; + --shadow-sm: 0 1px 2px rgba(0,0,0,0.5); + --shadow-md: 0 4px 12px rgba(124,58,237,0.25); + --shadow-lg: 0 8px 24px rgba(124,58,237,0.3); + --shadow-inset: inset 0 1px 3px rgba(0,0,0,0.6); +} + /* ========================================================================== BASE STYLES ========================================================================== */ @@ -3019,3 +3046,103 @@ three - column responsive justify-content: center; } } + +/* ── Animated Compile Button ── */ +.btn-compile { + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + transition: all var(--transition-base); +} + +.btn-compile-icon { + display: inline-block; + transition: transform 0.3s ease; +} + +.btn-compile:hover:not(:disabled) .btn-compile-icon { + transform: scale(1.3) rotate(-10deg); +} + +/* Pulse ring on click */ +.btn-compile::after { + content: ''; + position: absolute; + inset: 0; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.15); + opacity: 0; + transform: scale(0.8); + transition: none; +} + +.btn-compile:active::after { + opacity: 1; + transform: scale(1); + transition: transform 0.15s ease, opacity 0.15s ease; +} + +/* Compiling state — shimmer sweep */ +.btn-compile.is-compiling { + cursor: not-allowed; + background: linear-gradient( + 90deg, + var(--btn-primary) 0%, + #6ba3f9 40%, + #a5c8ff 50%, + #6ba3f9 60%, + var(--btn-primary) 100% + ); + background-size: 200% 100%; + animation: compile-shimmer 1.4s linear infinite; +} + +@keyframes compile-shimmer { + 0% { background-position: 200% center; } + 100% { background-position: -200% center; } +} + +/* Compiling state — spinning icon */ +.btn-compile.is-compiling .btn-compile-icon { + animation: compile-spin 0.8s linear infinite; +} + +@keyframes compile-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Success flash — add/remove a class via JS after compile */ +.btn-compile.compile-success { + background: linear-gradient(180deg, #34d399 0%, var(--btn-download) 100%); + animation: compile-success-flash 0.6s ease forwards; +} + +@keyframes compile-success-flash { + 0% { transform: scale(1); } + 40% { transform: scale(1.04); } + 70% { transform: scale(0.97); } + 100% { transform: scale(1); } +} + +.save-status { + font-size: 12px; + opacity: 0.75; + margin-left: 10px; + transition: opacity 0.2s ease; +} + +.save-status.saving { + color: #888; +} + +.save-status.offline { + color: #d97706; +} + +.save-status.saved { + color: #16a34a; +} \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0b249c0..d3e32e2 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -146,7 +146,8 @@ const THEMES = [ { id: 'miami', label: '🌴 Miami 🐬' }, { id: 'forest', label: '🌲 Forest' }, { id: 'coolGrey', label: '❄️ Cool Grey'}, - { id: 'neon', label: '🩵 neon'} + { id: 'neon', label: '🩵 neon'}, + { id: 'galaxy', label: '🌌 Galaxy' }, ]; function App() { diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index 1d51c53..5fbd2b6 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -234,7 +234,7 @@ function FormulaReorderPanel({ groupedFormulas, onReorderClass, onReorderFormula useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); - const [expandedGroups, setExpandedGroups] = React.useState({}); + const [expandedGroups, setExpandedGroups] = useState({}); const handleDragEnd = (event) => { const { active, over } = event; @@ -267,6 +267,7 @@ function FormulaReorderPanel({ groupedFormulas, onReorderClass, onReorderFormula } }; + const toggleGroup = (className) => { setExpandedGroups(prev => ({ ...prev, [className]: !prev[className] })); }; @@ -683,7 +684,7 @@ const LatexEditor = ({ content, onChange, isModified, compileError }) => { value={content} onChange={(e) => onChange(e.target.value)} onScroll={handleScroll} - placeholder='Select classes and categories above, then click "Compile PDF" to see the LaTeX code here.' + placeholder='Select classes and categories above, then click "GET CHEAT SHEET" to see the LaTeX code here.' className={`textarea-field ${isModified ? 'modified' : ''}`} rows={15} spellCheck="false" @@ -1035,6 +1036,8 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS const [rightPanelVisible, setRightPanelVisible] = useState(true); const [panelLayout, setPanelLayout] = useState(() => loadPanelLayout()); const [videoSearchRequest, setVideoSearchRequest] = useState(null); + const [saveStatus, setSaveStatus] = useState('idle'); + const [lastSavedAt, setLastSavedAt] = useState(null); const [classesCollapseSignal, setClassesCollapseSignal] = useState(0); const pendingPanelLayoutRef = useRef(panelLayout); const hasCollapsedLeftPanelOnceRef = useRef(false); @@ -1043,6 +1046,7 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS const modalDialogRef = useRef(null); const appBodyRef = useRef(null); const centerPanelRef = useRef(null); + const compileBtnRef = useRef(null); const snapshots = useMemo(() => [...(initialData?.compileHistory || [])].reverse(), [initialData?.compileHistory]); const selectedClassNames = useMemo( () => classesData.filter((cls) => selectedClasses[cls.name]).map((cls) => cls.name), @@ -1094,6 +1098,31 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS } }, []); + const getSaveStatusText = () => { + if (saveStatus === 'saving') return 'Saving...'; + if (saveStatus === 'offline') return 'Offline changes pending' + if (saveStatus === 'saved' && lastSavedAt) { + const diff = Date.now() - lastSavedAt; + const minutes = Math.floor(diff / 60000); + if (minutes < 1) return 'Saved just now'; + if (minutes === 1) return 'Saved 1 min ago'; + return `Saved ${minutes} min ago`; + } + return ''; + }; + + useEffect(() => { + if(!initialData) return + if(initialData.title) setTitle(initialData.title); + if (initialData.content){ + handleContentChange(initialData.content); + } + if (initialData.columns) setColumns(initialData.columns); + if(initialData.fontSize) setFontSize(initialData.fontSize); + if (initialData.spacing) setSpacing(initialData.spacing); + if (initialData.margins) setMargins(initialData.margins); + }, [initialData]); + useEffect(() => { const hasCompiledBefore = Boolean(initialData?.compileHistory?.length || pdfBlob || content.trim()); if (hasCompiledBefore) return; @@ -1194,7 +1223,8 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS } lastAutoSavedPdfRef.current = pdfBlob; - + setSaveStatus('saving'); + setLastSavedAt(Date.now()); onSave({ title, content, @@ -1215,11 +1245,18 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS selectedFormulas: getSelectedFormulasList(), compiledAt: new Date().toISOString(), }, - }, false).catch((error) => { + }, false) + .then(() => { + setSaveStatus('saved'); + setLastSavedAt(Date.now()); + }).catch((error) => { console.error('Failed to autosave compiled sheet', error); + setSaveStatus('offline'); }); }, [columns, compileError, content, contentSource, fontSize, getSelectedFormulasList, margins, onSave, pdfBlob, spacing, title]); + + const startResize = useCallback((panel) => (event) => { event.preventDefault(); @@ -1300,7 +1337,16 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS const workspaceSplitTemplate = `minmax(${LATEX_PANEL_MIN_WIDTH}px, ${panelLayout.latexWidth}px) 10px minmax(${MIN_PREVIEW_WIDTH}px, 1fr)`; const previewLayoutSignature = `${appBodyGridTemplate}|${workspaceSplitTemplate}|${leftPanelVisible}|${rightPanelVisible}|${showLatex}`; - + useEffect(() => { + if (!pdfBlob || isCompiling) return; + const btn = compileBtnRef.current; + if (!btn) return; + btn.classList.add('compile-success'); + const timer = setTimeout(() => { + btn.classList.remove('compile-success'); + }, 600); + return () => clearTimeout(timer); + }, [pdfBlob, isCompiling]); const handleCompileClick = () => { if (!hasCollapsedLeftPanelOnceRef.current) { // First compile: keep controls reachable while reclaiming preview space. @@ -1328,7 +1374,7 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS }; const handleSave = async (e) => { - e.preventDefault(); + e?.preventDefault?.(); await onSave({ title, content, @@ -1405,12 +1451,17 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS {/* Footer buttons */}
@@ -1502,6 +1553,9 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
+ + {getSaveStatusText()} +