Fix TypeScript build errors and improve navigation consistency

- Replace hardcoded navigation routes with history-based navigation (navigate(-1))
- Convert presentation deletion to use shared ConfirmDialog/AlertDialog components
- Consolidate button components throughout the application
- Fix TypeScript unused import errors from button component consolidation
- Convert enums to const assertions to fix erasableSyntaxOnly compatibility
- Hide theme browsing features with "coming soon" comments
- Improve AppHeader routing with regex patterns instead of manual parsing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-08-21 12:34:11 -05:00
parent 5739bdf4a9
commit 3d0f6dd25e
11 changed files with 206 additions and 159 deletions

View File

@ -12,5 +12,5 @@
"hasMasterSlide": true "hasMasterSlide": true
} }
}, },
"generated": "2025-08-21T16:40:45.572Z" "generated": "2025-08-21T17:31:29.998Z"
} }

0
public/themes/CLAUDE.md Normal file
View File

View File

@ -1,99 +1,63 @@
import React from 'react'; import React from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation, useParams } from 'react-router-dom';
export const AppHeader: React.FC = () => { export const AppHeader: React.FC = () => {
const location = useLocation(); const location = useLocation();
const params = useParams();
const getPageTitle = () => { const getPageTitle = () => {
if (location.pathname === '/') { const { pathname } = location;
return 'Welcome to Slideshare';
} // Route patterns with titles (cleaner than manual parsing)
if (location.pathname === '/presentations') { const routes = [
return 'My Presentations'; { pattern: /^\/$/, title: 'Welcome to Slideshare' },
} { pattern: /^\/presentations$/, title: 'My Presentations' },
if (location.pathname === '/presentations/new') { { pattern: /^\/presentations\/new$/, title: 'Create New Presentation' },
return 'Create New Presentation'; { pattern: /^\/presentations\/[^/]+\/edit\/slides\/(\d+)$/, title: () => `Editing Slide ${params.slideNumber}` },
} { pattern: /^\/presentations\/[^/]+\/view\/slides\/(\d+)$/, title: () => `Viewing Slide ${params.slideNumber}` },
if (location.pathname.includes('/slide/') && location.pathname.includes('/edit')) { { pattern: /^\/presentations\/[^/]+\/present\/(\d+)$/, title: () => `Presenting Slide ${params.slideNumber}` },
const segments = location.pathname.split('/'); { pattern: /^\/presentations\/[^/]+\/slide\/new\/edit$/, title: 'Add New Slide' },
const slideId = segments[4]; { pattern: /^\/presentations\/[^/]+\/slide\/[^/]+\/edit$/, title: 'Edit Slide' },
if (slideId === 'new') { { pattern: /^\/themes$/, title: 'Theme Browser' },
return 'Add New Slide'; { pattern: /^\/themes\/([^/]+)$/, title: () => `Theme: ${params.themeId}` },
} else { { pattern: /^\/themes\/[^/]+\/layouts\/([^/]+)$/, title: () => `Layout: ${params.layoutId}` },
return 'Edit Slide'; { pattern: /^\/themes\/[^/]+\/layouts\/[^/]+\/preview$/, title: () => `Preview Layout` },
} ];
}
if (location.pathname.includes('/presentations/') && location.pathname.includes('/slides/')) { for (const route of routes) {
const segments = location.pathname.split('/'); if (route.pattern.test(pathname)) {
if (segments.length === 6 && segments[1] === 'presentations') { return typeof route.title === 'function' ? route.title() : route.title;
const mode = segments[3]; // 'edit' or 'view'
const slideNumber = segments[5];
if (mode === 'edit') {
return `Editing Slide ${slideNumber}`;
} else if (mode === 'view') {
return `Viewing Slide ${slideNumber}`;
}
return `Presentation Slide ${slideNumber}`;
}
}
if (location.pathname === '/themes') {
return 'Theme Browser';
}
if (location.pathname.includes('/themes/')) {
const segments = location.pathname.split('/');
if (segments.length === 3) {
return `Theme: ${segments[2]}`;
}
if (segments.length === 5 && segments[3] === 'layouts') {
return `Layout: ${segments[4]}`;
} }
} }
return 'Slideshare'; return 'Slideshare';
}; };
const getPageDescription = () => { const getPageDescription = () => {
if (location.pathname === '/') { const { pathname } = location;
return 'Create beautiful presentations with customizable themes';
} // Route patterns with descriptions
if (location.pathname === '/presentations') { const routes = [
return 'View and manage all your presentations'; { pattern: /^\/$/, description: 'Create beautiful presentations with customizable themes' },
} { pattern: /^\/presentations$/, description: 'View and manage all your presentations' },
if (location.pathname === '/presentations/new') { { pattern: /^\/presentations\/new$/, description: 'Enter details for your new presentation' },
return 'Select a theme and enter details for your new presentation'; { pattern: /^\/presentations\/[^/]+\/edit\/slides\/(\d+)$/, description: 'Edit slide content, add notes, and manage your presentation' },
} { pattern: /^\/presentations\/[^/]+\/view\/slides\/(\d+)$/, description: 'View your presentation slides in read-only mode' },
if (location.pathname.includes('/slide/') && location.pathname.includes('/edit')) { { pattern: /^\/presentations\/[^/]+\/present\/(\d+)$/, description: 'Present your slides in full screen mode' },
const segments = location.pathname.split('/'); { pattern: /^\/presentations\/[^/]+\/slide\/new\/edit$/, description: 'Choose a layout and add content for your new slide' },
const slideId = segments[4]; { pattern: /^\/presentations\/[^/]+\/slide\/[^/]+\/edit$/, description: 'Edit slide content and layout' },
if (slideId === 'new') { { pattern: /^\/themes$/, description: 'Browse and select themes for your presentations' },
return 'Choose a layout and add content for your new slide'; { pattern: /^\/themes\/([^/]+)$/, description: 'View theme details, layouts, and color palette' },
} else { { pattern: /^\/themes\/[^/]+\/layouts\/([^/]+)$/, description: 'View layout template, slots, and configuration' },
return 'Edit slide content and layout'; { pattern: /^\/themes\/[^/]+\/layouts\/[^/]+\/preview$/, description: 'Preview layout with sample content' },
} ];
}
if (location.pathname.includes('/presentations/') && location.pathname.includes('/slides/')) { for (const route of routes) {
const segments = location.pathname.split('/'); if (route.pattern.test(pathname)) {
if (segments.length === 6 && segments[1] === 'presentations') { return route.description;
const mode = segments[3];
if (mode === 'edit') {
return 'Edit slide content, add notes, and manage your presentation';
} else if (mode === 'view') {
return 'View your presentation slides in read-only mode';
}
}
return 'View and manage your presentation slides';
}
if (location.pathname === '/themes') {
return 'Browse and select themes for your presentations';
}
if (location.pathname.includes('/themes/')) {
const segments = location.pathname.split('/');
if (segments.length === 3) {
return 'View theme details, layouts, and color palette';
}
if (segments.length === 5 && segments[3] === 'layouts') {
return 'View layout template, slots, and configuration';
} }
} }
return 'Slide authoring and presentation tool'; return 'Slide authoring and presentation tool';
}; };
@ -110,10 +74,13 @@ export const AppHeader: React.FC = () => {
<Link to="/presentations/new" className="nav-button primary"> <Link to="/presentations/new" className="nav-button primary">
Create Presentation Create Presentation
</Link> </Link>
{/* Theme browsing - Coming soon */}
<div style={{ display: 'none' }}>
<Link to="/themes" className="nav-link"> <Link to="/themes" className="nav-link">
Themes Themes
</Link> </Link>
</div> </div>
</div>
</nav> </nav>
<div className="page-title-section"> <div className="page-title-section">
<h1 className="page-title">{getPageTitle()}</h1> <h1 className="page-title">{getPageTitle()}</h1>

View File

@ -1,7 +1,10 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Button } from './ui/buttons/Button.tsx';
export const Welcome: React.FC = () => { export const Welcome: React.FC = () => {
const navigate = useNavigate();
return ( return (
<div className="welcome-page"> <div className="welcome-page">
<section className="hero-section"> <section className="hero-section">
@ -10,9 +13,13 @@ export const Welcome: React.FC = () => {
Create beautiful presentations with customizable themes and layouts Create beautiful presentations with customizable themes and layouts
</p> </p>
<div className="hero-actions"> <div className="hero-actions">
<Link to="/presentations/new" className="primary-button"> <Button
variant="primary"
size="large"
onClick={() => navigate('/presentations/new')}
>
Get started with a new presentation Get started with a new presentation
</Link> </Button>
</div> </div>
</section> </section>
@ -66,7 +73,8 @@ export const Welcome: React.FC = () => {
<section className="getting-started-section"> <section className="getting-started-section">
<h2 className="section-title">Getting Started</h2> <h2 className="section-title">Getting Started</h2>
<div className="steps-list"> <div className="steps-list">
<div className="step-item"> {/* Theme browsing step - Coming soon */}
<div className="step-item" style={{ display: 'none' }}>
<div className="step-number">1</div> <div className="step-number">1</div>
<div className="step-content"> <div className="step-content">
<h3 className="step-title">Browse Themes</h3> <h3 className="step-title">Browse Themes</h3>
@ -77,17 +85,17 @@ export const Welcome: React.FC = () => {
</div> </div>
<div className="step-item"> <div className="step-item">
<div className="step-number">2</div> <div className="step-number">1</div>
<div className="step-content"> <div className="step-content">
<h3 className="step-title">Create Presentation</h3> <h3 className="step-title">Create Presentation</h3>
<p className="step-description"> <p className="step-description">
Start a new presentation using your chosen theme Start a new presentation with the default theme
</p> </p>
</div> </div>
</div> </div>
<div className="step-item"> <div className="step-item">
<div className="step-number">3</div> <div className="step-number">2</div>
<div className="step-content"> <div className="step-content">
<h3 className="step-title">Add Content</h3> <h3 className="step-title">Add Content</h3>
<p className="step-description"> <p className="step-description">
@ -97,7 +105,7 @@ export const Welcome: React.FC = () => {
</div> </div>
<div className="step-item"> <div className="step-item">
<div className="step-number">4</div> <div className="step-number">3</div>
<div className="step-content"> <div className="step-content">
<h3 className="step-title">Present</h3> <h3 className="step-title">Present</h3>
<p className="step-description"> <p className="step-description">
@ -114,9 +122,13 @@ export const Welcome: React.FC = () => {
<p className="cta-description"> <p className="cta-description">
Start building your presentation with our theme collection Start building your presentation with our theme collection
</p> </p>
<Link to="/presentations/new" className="primary-button"> <Button
variant="primary"
size="large"
onClick={() => navigate('/presentations/new')}
>
Create Your First Presentation Create Your First Presentation
</Link> </Button>
</div> </div>
</section> </section>
</div> </div>

View File

@ -107,8 +107,8 @@ export const NewPresentationPage: React.FC = () => {
<div className="error-content"> <div className="error-content">
<h2>Error Loading Themes</h2> <h2>Error Loading Themes</h2>
<p>{error}</p> <p>{error}</p>
<button onClick={() => navigate('/themes')} className="button secondary"> <button onClick={() => navigate(-1)} className="button secondary">
Back to Themes Back
</button> </button>
</div> </div>
</div> </div>
@ -119,11 +119,11 @@ export const NewPresentationPage: React.FC = () => {
<div className="new-presentation-page"> <div className="new-presentation-page">
<header className="page-header"> <header className="page-header">
<button <button
onClick={() => navigate('/themes')} onClick={() => navigate(-1)}
className="back-button" className="back-button"
type="button" type="button"
> >
Back to Themes Back
</button> </button>
<div className="header-content"> <div className="header-content">
<h1>Create New Presentation</h1> <h1>Create New Presentation</h1>
@ -160,7 +160,7 @@ export const NewPresentationPage: React.FC = () => {
error={error} error={error}
creating={creating} creating={creating}
presentationTitle={presentationTitle} presentationTitle={presentationTitle}
onCancel={() => navigate('/themes')} onCancel={() => navigate(-1)}
onCreate={handleCreatePresentation} onCreate={handleCreatePresentation}
/> />
</div> </div>

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom'; import { useParams, useNavigate, Link } from 'react-router-dom';
import type { Presentation } from '../../types/presentation.ts'; import type { Presentation } from '../../types/presentation.ts';
import type { Theme } from '../../types/theme.ts'; import type { Theme } from '../../types/theme.ts';
import { getPresentationById, updatePresentation } from '../../utils/presentationStorage.ts'; import { getPresentationById } from '../../utils/presentationStorage.ts';
import { loadTheme } from '../../utils/themeLoader.ts'; import { loadTheme } from '../../utils/themeLoader.ts';
import { useDialog } from '../../hooks/useDialog.ts'; import { useDialog } from '../../hooks/useDialog.ts';
import { AlertDialog } from '../ui/AlertDialog.tsx'; import { AlertDialog } from '../ui/AlertDialog.tsx';

View File

@ -156,7 +156,8 @@ export const PresentationMode: React.FC = () => {
}, [presentationId]); }, [presentationId]);
const exitPresentationMode = () => { const exitPresentationMode = () => {
navigate(`/presentations/${presentationId}/view/slides/${currentSlideIndex + 1}`); // Navigate back to the previous page in history
navigate(-1);
}; };
const renderSlideContent = (slide: SlideContent): string => { const renderSlideContent = (slide: SlideContent): string => {

View File

@ -1,8 +1,11 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import type { Presentation } from '../../types/presentation.ts'; import type { Presentation } from '../../types/presentation.ts';
import { getAllPresentations, deletePresentation } from '../../utils/presentationStorage.ts'; import { getAllPresentations, deletePresentation } from '../../utils/presentationStorage.ts';
import { loggers } from '../../utils/logger.ts'; import { loggers } from '../../utils/logger.ts';
import { ConfirmDialog } from '../ui/ConfirmDialog.tsx';
import { AlertDialog } from '../ui/AlertDialog.tsx';
import { Button } from '../ui/buttons/Button.tsx';
import './PresentationsList.css'; import './PresentationsList.css';
export const PresentationsList: React.FC = () => { export const PresentationsList: React.FC = () => {
@ -12,6 +15,19 @@ export const PresentationsList: React.FC = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [deleting, setDeleting] = useState<string | null>(null); const [deleting, setDeleting] = useState<string | null>(null);
// Dialog states
const [confirmDialog, setConfirmDialog] = useState<{
isOpen: boolean;
presentationId: string;
presentationName: string;
}>({ isOpen: false, presentationId: '', presentationName: '' });
const [alertDialog, setAlertDialog] = useState<{
isOpen: boolean;
message: string;
type?: 'error' | 'success';
}>({ isOpen: false, message: '' });
useEffect(() => { useEffect(() => {
loadPresentations(); loadPresentations();
}, []); }, []);
@ -29,18 +45,34 @@ export const PresentationsList: React.FC = () => {
} }
}; };
const handleDeletePresentation = async (id: string, name: string) => { const handleDeletePresentation = (id: string, name: string) => {
if (!confirm(`Are you sure you want to delete "${name}"? This action cannot be undone.`)) { setConfirmDialog({
return; isOpen: true,
} presentationId: id,
presentationName: name
});
};
const confirmDeletePresentation = async () => {
const { presentationId } = confirmDialog;
setConfirmDialog({ isOpen: false, presentationId: '', presentationName: '' });
try { try {
setDeleting(id); setDeleting(presentationId);
await deletePresentation(id); await deletePresentation(presentationId);
setPresentations(prev => prev.filter(p => p.metadata.id !== id)); setPresentations(prev => prev.filter(p => p.metadata.id !== presentationId));
setAlertDialog({
isOpen: true,
message: 'Presentation deleted successfully',
type: 'success'
});
} catch (err) { } catch (err) {
loggers.presentation.error('Failed to delete presentation', err instanceof Error ? err : new Error(String(err))); loggers.presentation.error('Failed to delete presentation', err instanceof Error ? err : new Error(String(err)));
alert('Failed to delete presentation. Please try again.'); setAlertDialog({
isOpen: true,
message: 'Failed to delete presentation. Please try again.',
type: 'error'
});
} finally { } finally {
setDeleting(null); setDeleting(null);
} }
@ -100,14 +132,6 @@ export const PresentationsList: React.FC = () => {
<h1>My Presentations</h1> <h1>My Presentations</h1>
<p>Manage and organize your presentation library</p> <p>Manage and organize your presentation library</p>
</div> </div>
<div className="header-actions">
<Link
to="/presentations/new"
className="action-button primary"
>
Create New Presentation
</Link>
</div>
</header> </header>
<main className="list-content"> <main className="list-content">
@ -116,12 +140,13 @@ export const PresentationsList: React.FC = () => {
<div className="empty-content"> <div className="empty-content">
<h2>No presentations yet</h2> <h2>No presentations yet</h2>
<p>Create your first presentation to get started</p> <p>Create your first presentation to get started</p>
<Link <Button
to="/presentations/new" variant="primary"
className="action-button primary large" size="large"
onClick={() => navigate('/presentations/new')}
> >
Create Your First Presentation Create Your First Presentation
</Link> </Button>
</div> </div>
</div> </div>
) : ( ) : (
@ -233,22 +258,36 @@ export const PresentationsList: React.FC = () => {
</p> </p>
</div> </div>
<div className="footer-actions"> <div className="footer-actions">
<button <Button
onClick={loadPresentations} variant="primary"
className="action-button secondary" onClick={() => navigate('/presentations/new')}
>
Refresh
</button>
<Link
to="/presentations/new"
className="action-button primary"
> >
Create New Create New
</Link> </Button>
</div> </div>
</div> </div>
)} )}
</main> </main>
{/* Confirmation Dialog for Deletion */}
<ConfirmDialog
isOpen={confirmDialog.isOpen}
onClose={() => setConfirmDialog({ isOpen: false, presentationId: '', presentationName: '' })}
onConfirm={confirmDeletePresentation}
title="Delete Presentation"
message={`Are you sure you want to delete "${confirmDialog.presentationName}"? This action cannot be undone.`}
type="danger"
confirmText="Delete"
isDestructive={true}
/>
{/* Alert Dialog for Success/Error Messages */}
<AlertDialog
isOpen={alertDialog.isOpen}
onClose={() => setAlertDialog({ isOpen: false, message: '' })}
message={alertDialog.message}
type={alertDialog.type}
/>
</div> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link, useNavigate } from 'react-router-dom';
import { SlideEditorErrorBoundary } from './SlideEditorErrorBoundary.tsx'; import { SlideEditorErrorBoundary } from './SlideEditorErrorBoundary.tsx';
import { LoadingState } from './LoadingState.tsx'; import { LoadingState } from './LoadingState.tsx';
import { ErrorState } from './ErrorState.tsx'; import { ErrorState } from './ErrorState.tsx';
@ -11,6 +11,7 @@ import { useSlideEditor } from './useSlideEditor.ts';
import './SlideEditor.css'; import './SlideEditor.css';
export const SlideEditor: React.FC = () => { export const SlideEditor: React.FC = () => {
const navigate = useNavigate();
const { presentationId, slideId } = useParams<{ const { presentationId, slideId } = useParams<{
presentationId: string; presentationId: string;
slideId: string; slideId: string;
@ -43,6 +44,21 @@ export const SlideEditor: React.FC = () => {
cancelEditing, cancelEditing,
} = useSlideEditor({ presentationId, slideId }); } = useSlideEditor({ presentationId, slideId });
// Helper function to get current slide number for presentation
const getCurrentSlideNumber = (): number => {
if (!presentation || !slideId || slideId === 'new') {
return 1; // Default to first slide for new slides
}
const slideIndex = presentation.slides.findIndex(slide => slide.id === slideId);
return slideIndex !== -1 ? slideIndex + 1 : 1;
};
const handlePresentFromHere = () => {
const slideNumber = getCurrentSlideNumber();
navigate(`/presentations/${presentationId}/present/${slideNumber}`);
};
if (loading) { if (loading) {
return <LoadingState presentationId={presentationId} />; return <LoadingState presentationId={presentationId} />;
} }
@ -76,12 +92,20 @@ export const SlideEditor: React.FC = () => {
<div className="editor-actions"> <div className="editor-actions">
{currentStep === 'content' && selectedLayout && ( {currentStep === 'content' && selectedLayout && (
<>
<ActionButton <ActionButton
variant="secondary" variant="secondary"
onClick={() => setShowPreview(true)} onClick={() => setShowPreview(true)}
> >
Full Preview Full Preview
</ActionButton> </ActionButton>
<ActionButton
variant="primary"
onClick={handlePresentFromHere}
>
Present from here
</ActionButton>
</>
)} )}
</div> </div>
</header> </header>

View File

@ -17,7 +17,7 @@ export function themeWatcherPlugin(): Plugin {
const watcher = fs.watch( const watcher = fs.watch(
publicThemesPath, publicThemesPath,
{ recursive: true }, { recursive: true },
(eventType: string, filename: string | null) => { (_eventType: string, filename: string | null) => {
if (filename && (filename.endsWith('.css') || filename.endsWith('.html'))) { if (filename && (filename.endsWith('.css') || filename.endsWith('.html'))) {
console.log(`Theme file changed: ${filename}`); console.log(`Theme file changed: ${filename}`);

View File

@ -5,25 +5,29 @@ import log from 'loglevel';
*/ */
// Define log levels for different parts of the application // Define log levels for different parts of the application
export enum LogLevel { export const LogLevel = {
TRACE = 0, TRACE: 0,
DEBUG = 1, DEBUG: 1,
INFO = 2, INFO: 2,
WARN = 3, WARN: 3,
ERROR = 4, ERROR: 4,
SILENT = 5, SILENT: 5,
} } as const;
export type LogLevel = typeof LogLevel[keyof typeof LogLevel];
// Log categories for better organization // Log categories for better organization
export enum LogCategory { export const LogCategory = {
PRESENTATION = 'presentation', PRESENTATION: 'presentation',
THEME = 'theme', THEME: 'theme',
STORAGE = 'storage', STORAGE: 'storage',
UI = 'ui', UI: 'ui',
SECURITY = 'security', SECURITY: 'security',
PERFORMANCE = 'performance', PERFORMANCE: 'performance',
GENERAL = 'general', GENERAL: 'general',
} } as const;
export type LogCategory = typeof LogCategory[keyof typeof LogCategory];
/** /**
* Configure logging based on environment * Configure logging based on environment