## New Feature: Full Screen Slide Preview - Add SlidePreviewModal component for full screen slide preview in SlideEditor - ESC key support and temporary hint for user guidance - Proper aspect ratio handling with theme CSS inheritance - Modal follows existing UI patterns for consistency ## Import Standards Compliance (31 files updated) - Fix all imports to use explicit .tsx/.ts extensions per IMPORT_STANDARDS.md - Eliminate barrel imports in App.tsx for better Vite tree shaking - Add direct imports with explicit paths across entire codebase - Preserve CSS imports and external library imports unchanged ## Code Architecture Improvements - Add comprehensive CSS & Component Architecture Guidelines to CLAUDE.md - Document modal patterns, aspect ratio handling, and CSS reuse principles - Reference all coding standards files for consistent development workflow - Prevent future CSS overcomplication and component duplication ## Performance Optimizations - Enable Vite tree shaking with proper import structure - Improve module resolution speed with explicit extensions - Optimize build performance through direct imports ## Files Changed - 31 TypeScript/React files with import fixes - 2 new SlidePreviewModal files (component + CSS) - Updated project documentation and coding guidelines - Fixed aspect ratio CSS patterns across components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
257 lines
9.2 KiB
TypeScript
257 lines
9.2 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { Link, useNavigate } from 'react-router-dom';
|
||
import type { Presentation } from '../../types/presentation.ts';
|
||
import { getAllPresentations, deletePresentation } from '../../utils/presentationStorage.ts';
|
||
import { loggers } from '../../utils/logger.ts';
|
||
import './PresentationsList.css';
|
||
|
||
export const PresentationsList: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const [presentations, setPresentations] = useState<Presentation[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [deleting, setDeleting] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
loadPresentations();
|
||
}, []);
|
||
|
||
const loadPresentations = async () => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
const allPresentations = await getAllPresentations();
|
||
setPresentations(allPresentations);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Failed to load presentations');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleDeletePresentation = async (id: string, name: string) => {
|
||
if (!confirm(`Are you sure you want to delete "${name}"? This action cannot be undone.`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setDeleting(id);
|
||
await deletePresentation(id);
|
||
setPresentations(prev => prev.filter(p => p.metadata.id !== id));
|
||
} catch (err) {
|
||
loggers.presentation.error('Failed to delete presentation', err instanceof Error ? err : new Error(String(err)));
|
||
alert('Failed to delete presentation. Please try again.');
|
||
} finally {
|
||
setDeleting(null);
|
||
}
|
||
};
|
||
|
||
const handleEditPresentation = (id: string, slideCount: number) => {
|
||
const slideNumber = slideCount > 0 ? 1 : 1; // Always go to slide 1, or empty state
|
||
navigate(`/presentations/${id}/edit/slides/${slideNumber}`);
|
||
};
|
||
|
||
const handleViewPresentation = (id: string, slideCount: number) => {
|
||
const slideNumber = slideCount > 0 ? 1 : 1; // Always go to slide 1, or empty state
|
||
navigate(`/presentations/${id}/view/slides/${slideNumber}`);
|
||
};
|
||
|
||
const formatDate = (dateString: string) => {
|
||
return new Date(dateString).toLocaleDateString('en-US', {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="presentations-list">
|
||
<div className="loading-content">
|
||
<div className="loading-spinner">Loading presentations...</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="presentations-list">
|
||
<div className="error-content">
|
||
<h2>Error Loading Presentations</h2>
|
||
<p>{error}</p>
|
||
<button
|
||
onClick={loadPresentations}
|
||
className="action-button secondary"
|
||
>
|
||
Try Again
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="presentations-list">
|
||
<header className="list-header">
|
||
<div className="header-content">
|
||
<h1>My Presentations</h1>
|
||
<p>Manage and organize your presentation library</p>
|
||
</div>
|
||
<div className="header-actions">
|
||
<Link
|
||
to="/presentations/new"
|
||
className="action-button primary"
|
||
>
|
||
Create New Presentation
|
||
</Link>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="list-content">
|
||
{presentations.length === 0 ? (
|
||
<div className="empty-state">
|
||
<div className="empty-content">
|
||
<h2>No presentations yet</h2>
|
||
<p>Create your first presentation to get started</p>
|
||
<Link
|
||
to="/presentations/new"
|
||
className="action-button primary large"
|
||
>
|
||
Create Your First Presentation
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="presentations-grid">
|
||
{presentations.map((presentation) => (
|
||
<div key={presentation.metadata.id} className="presentation-card">
|
||
<div className="card-header">
|
||
<div className="presentation-info">
|
||
<h3 className="presentation-name">{presentation.metadata.name}</h3>
|
||
{presentation.metadata.description && (
|
||
<p className="presentation-description">
|
||
{presentation.metadata.description}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<div className="presentation-actions">
|
||
<button
|
||
type="button"
|
||
className="action-icon"
|
||
onClick={() => handleEditPresentation(presentation.metadata.id, presentation.slides.length)}
|
||
title="Edit presentation"
|
||
>
|
||
✏️
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="action-icon"
|
||
onClick={() => handleViewPresentation(presentation.metadata.id, presentation.slides.length)}
|
||
title="View presentation"
|
||
>
|
||
👁️
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="action-icon delete"
|
||
onClick={() => handleDeletePresentation(presentation.metadata.id, presentation.metadata.name)}
|
||
disabled={deleting === presentation.metadata.id}
|
||
title="Delete presentation"
|
||
>
|
||
{deleting === presentation.metadata.id ? '⏳' : '🗑️'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="card-content">
|
||
<div className="presentation-stats">
|
||
<div className="stat-item">
|
||
<span className="stat-label">Theme</span>
|
||
<span className="stat-value theme-name">{presentation.metadata.theme}</span>
|
||
</div>
|
||
<div className="stat-item">
|
||
<span className="stat-label">Slides</span>
|
||
<span className="stat-value slides-count">
|
||
{presentation.slides.length} slide{presentation.slides.length !== 1 ? 's' : ''}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="presentation-meta">
|
||
<div className="meta-item">
|
||
<span className="meta-label">Created</span>
|
||
<span className="meta-value">{formatDate(presentation.metadata.createdAt)}</span>
|
||
</div>
|
||
{presentation.metadata.updatedAt !== presentation.metadata.createdAt && (
|
||
<div className="meta-item">
|
||
<span className="meta-label">Updated</span>
|
||
<span className="meta-value">{formatDate(presentation.metadata.updatedAt)}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="card-footer">
|
||
<button
|
||
type="button"
|
||
className="card-action primary"
|
||
onClick={() => handleEditPresentation(presentation.metadata.id, presentation.slides.length)}
|
||
>
|
||
Edit
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="card-action secondary"
|
||
onClick={() => handleViewPresentation(presentation.metadata.id, presentation.slides.length)}
|
||
>
|
||
View
|
||
</button>
|
||
{presentation.slides.length > 0 && (
|
||
<button
|
||
type="button"
|
||
className="card-action secondary"
|
||
onClick={() => {
|
||
// TODO: Implement presentation mode
|
||
alert('Presentation mode coming soon!');
|
||
}}
|
||
>
|
||
Present
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{presentations.length > 0 && (
|
||
<div className="list-footer">
|
||
<div className="summary-stats">
|
||
<p>
|
||
{presentations.length} presentation{presentations.length !== 1 ? 's' : ''} • {' '}
|
||
{presentations.reduce((total, p) => total + p.slides.length, 0)} total slides
|
||
</p>
|
||
</div>
|
||
<div className="footer-actions">
|
||
<button
|
||
onClick={loadPresentations}
|
||
className="action-button secondary"
|
||
>
|
||
Refresh
|
||
</button>
|
||
<Link
|
||
to="/presentations/new"
|
||
className="action-button primary"
|
||
>
|
||
Create New
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
);
|
||
}; |