diff --git a/public/themes-manifest.json b/public/themes-manifest.json index e5faf01..c3fed76 100644 --- a/public/themes-manifest.json +++ b/public/themes-manifest.json @@ -12,5 +12,5 @@ "hasMasterSlide": true } }, - "generated": "2025-08-20T22:36:56.857Z" + "generated": "2025-08-21T11:07:28.288Z" } \ No newline at end of file diff --git a/src/components/presentations/AspectRatioSelector.css b/src/components/presentations/AspectRatioSelector.css new file mode 100644 index 0000000..29c029d --- /dev/null +++ b/src/components/presentations/AspectRatioSelector.css @@ -0,0 +1,88 @@ +.aspect-ratio-selection h2 { + font-size: 1.25rem; + font-weight: 600; + color: #1f2937; + margin-bottom: 0.5rem; +} + +.section-description { + color: #6b7280; + margin-bottom: 1.5rem; + font-size: 0.875rem; +} + +.aspect-ratio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.aspect-ratio-card { + border: 2px solid #e5e7eb; + border-radius: 0.75rem; + padding: 1rem; + cursor: pointer; + transition: all 0.2s ease; + background: #ffffff; +} + +.aspect-ratio-card:hover { + border-color: #3b82f6; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15); +} + +.aspect-ratio-card.selected { + border-color: #3b82f6; + background: #eff6ff; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15); +} + +.aspect-ratio-preview { + display: flex; + justify-content: center; + align-items: center; + height: 60px; + margin-bottom: 1rem; + background: #f9fafb; + border-radius: 0.5rem; +} + +.preview-box { + background: #3b82f6; + border-radius: 2px; +} + +.preview-box.aspect-16-9 { + width: 48px; + height: 27px; +} + +.preview-box.aspect-4-3 { + width: 40px; + height: 30px; +} + +.preview-box.aspect-1-1 { + width: 32px; + height: 32px; +} + +.aspect-ratio-info h3 { + font-size: 1rem; + font-weight: 600; + color: #1f2937; + margin-bottom: 0.25rem; +} + +.ratio-description { + color: #6b7280; + font-size: 0.75rem; + margin-bottom: 0.5rem; +} + +.ratio-dimensions { + font-size: 0.75rem; + color: #9ca3af; + font-weight: 500; +} \ No newline at end of file diff --git a/src/components/presentations/AspectRatioSelector.tsx b/src/components/presentations/AspectRatioSelector.tsx new file mode 100644 index 0000000..62099a8 --- /dev/null +++ b/src/components/presentations/AspectRatioSelector.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import type { AspectRatio } from '../../types/presentation'; +import { ASPECT_RATIOS } from '../../types/presentation'; +import './AspectRatioSelector.css'; + +interface AspectRatioSelectorProps { + selectedAspectRatio: AspectRatio; + onAspectRatioChange: (ratio: AspectRatio) => void; +} + +export const AspectRatioSelector: React.FC = ({ + selectedAspectRatio, + onAspectRatioChange +}) => { + return ( +
+

Choose Aspect Ratio

+

+ Select the aspect ratio that best fits your display setup +

+ +
+ {ASPECT_RATIOS.map((ratio) => ( +
onAspectRatioChange(ratio.id)} + > +
+
+
+
+

{ratio.name}

+

{ratio.description}

+
+ {ratio.width} × {ratio.height} +
+
+
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/presentations/CreationActions.css b/src/components/presentations/CreationActions.css new file mode 100644 index 0000000..0db2ed2 --- /dev/null +++ b/src/components/presentations/CreationActions.css @@ -0,0 +1,96 @@ +.creation-actions { + margin-top: 2rem; +} + +.selected-theme-info { + margin-bottom: 2rem; +} + +.theme-preview-info h3 { + font-size: 1.125rem; + font-weight: 600; + color: #1f2937; + margin-bottom: 0.5rem; +} + +.theme-preview-info p { + color: #6b7280; + margin-bottom: 1rem; + line-height: 1.5; +} + +.theme-stats { + display: flex; + gap: 1rem; + font-size: 0.875rem; + color: #9ca3af; +} + +.theme-stats span { + background: #f3f4f6; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +.creation-error { + background: #fee2e2; + border: 1px solid #fca5a5; + border-radius: 0.5rem; + padding: 1rem; + margin-top: 1rem; +} + +.creation-error p { + color: #dc2626; + margin: 0; + font-size: 0.875rem; +} + +.action-buttons { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + padding-top: 2rem; + border-top: 1px solid #e5e7eb; +} + +.button { + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 500; + font-size: 0.875rem; + border: none; + cursor: pointer; + transition: all 0.2s ease; + min-width: 120px; +} + +.button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.button.secondary { + background: #f9fafb; + color: #374151; + border: 1px solid #d1d5db; +} + +.button.secondary:hover:not(:disabled) { + background: #f3f4f6; + border-color: #9ca3af; +} + +.button.primary { + background: #3b82f6; + color: white; +} + +.button.primary:hover:not(:disabled) { + background: #2563eb; +} + +.button.primary:focus { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} \ No newline at end of file diff --git a/src/components/presentations/CreationActions.tsx b/src/components/presentations/CreationActions.tsx new file mode 100644 index 0000000..f9807dc --- /dev/null +++ b/src/components/presentations/CreationActions.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import type { Theme } from '../../types/theme'; +import './CreationActions.css'; + +interface CreationActionsProps { + selectedTheme: Theme | null; + error: string | null; + creating: boolean; + presentationTitle: string; + onCancel: () => void; + onCreate: () => void; +} + +export const CreationActions: React.FC = ({ + selectedTheme, + error, + creating, + presentationTitle, + onCancel, + onCreate +}) => { + return ( +
+
+ {selectedTheme && ( +
+

Selected Theme: {selectedTheme.name}

+

{selectedTheme.description}

+
+ {selectedTheme.layouts.length} layouts available + {selectedTheme.author && by {selectedTheme.author}} +
+
+ )} + + {error && ( +
+

Failed to create presentation: {error}

+
+ )} +
+ +
+ + +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/presentations/EmptyPresentationState.css b/src/components/presentations/EmptyPresentationState.css new file mode 100644 index 0000000..ebfc844 --- /dev/null +++ b/src/components/presentations/EmptyPresentationState.css @@ -0,0 +1,129 @@ +.empty-presentation { + display: flex; + justify-content: center; + align-items: center; + min-height: 400px; + padding: 2rem; +} + +.empty-content { + text-align: center; + max-width: 600px; +} + +.empty-content h2 { + font-size: 1.875rem; + font-weight: 700; + color: #1f2937; + margin-bottom: 1rem; +} + +.empty-content > p { + font-size: 1.125rem; + color: #6b7280; + margin-bottom: 2rem; +} + +.theme-preview { + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 0.75rem; + padding: 1.5rem; + margin: 2rem 0; + text-align: left; +} + +.theme-preview h3 { + font-size: 1.25rem; + font-weight: 600; + color: #1f2937; + margin-bottom: 0.5rem; +} + +.theme-description { + color: #6b7280; + margin-bottom: 1.5rem; + line-height: 1.5; +} + +.available-layouts h4 { + font-size: 1rem; + font-weight: 600; + color: #374151; + margin-bottom: 1rem; +} + +.layouts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; +} + +.layout-preview-card { + background: #ffffff; + border: 1px solid #d1d5db; + border-radius: 0.5rem; + padding: 1rem; + text-align: center; +} + +.layout-preview-card .layout-name { + font-weight: 600; + color: #1f2937; + font-size: 0.875rem; + margin-bottom: 0.5rem; + display: block; +} + +.layout-preview-card .layout-description { + color: #6b7280; + font-size: 0.75rem; + margin-bottom: 0.5rem; + display: block; +} + +.layout-preview-card .slot-count { + color: #9ca3af; + font-size: 0.75rem; + font-weight: 500; +} + +.more-layouts { + display: flex; + align-items: center; + justify-content: center; + background: #f3f4f6; + border: 1px dashed #9ca3af; + border-radius: 0.5rem; + color: #6b7280; + font-size: 0.875rem; + font-weight: 500; +} + +.action-button { + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 500; + font-size: 0.875rem; + border: none; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.action-button.primary { + background: #3b82f6; + color: white; +} + +.action-button.primary:hover { + background: #2563eb; +} + +.action-button.large { + padding: 1rem 2rem; + font-size: 1rem; +} \ No newline at end of file diff --git a/src/components/presentations/EmptyPresentationState.tsx b/src/components/presentations/EmptyPresentationState.tsx new file mode 100644 index 0000000..b04f8f1 --- /dev/null +++ b/src/components/presentations/EmptyPresentationState.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import type { Theme } from '../../types/theme'; +import './EmptyPresentationState.css'; + +interface EmptyPresentationStateProps { + theme: Theme | null; + onAddFirstSlide: () => void; +} + +export const EmptyPresentationState: React.FC = ({ + theme, + onAddFirstSlide +}) => { + return ( +
+
+

Start creating your presentation

+

Add your first slide to begin editing your presentation

+ + {theme && ( +
+

Using Theme: {theme.name}

+

{theme.description}

+ +
+

Available Layouts ({theme.layouts.length})

+
+ {theme.layouts.slice(0, 6).map((layout) => ( +
+
{layout.name}
+
{layout.description}
+
{layout.slots.length} slots
+
+ ))} + {theme.layouts.length > 6 && ( +
+ +{theme.layouts.length - 6} more layouts +
+ )} +
+
+
+ )} + + +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/presentations/NewPresentationPage.tsx b/src/components/presentations/NewPresentationPage.tsx index 7879275..79f8375 100644 --- a/src/components/presentations/NewPresentationPage.tsx +++ b/src/components/presentations/NewPresentationPage.tsx @@ -2,10 +2,14 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import type { Theme } from '../../types/theme'; import type { AspectRatio } from '../../types/presentation'; -import { ASPECT_RATIOS } from '../../types/presentation'; import { getThemes } from '../../themes'; import { createPresentation } from '../../utils/presentationStorage'; -import { ThemeSelector } from './ThemeSelector'; +import { loggers } from '../../utils/logger'; +import { AlertDialog } from '../ui/AlertDialog.tsx'; +import { PresentationDetailsForm } from './PresentationDetailsForm.tsx'; +import { AspectRatioSelector } from './AspectRatioSelector.tsx'; +import { ThemeSelectionSection } from './ThemeSelectionSection.tsx'; +import { CreationActions } from './CreationActions.tsx'; import './NewPresentationPage.css'; export const NewPresentationPage: React.FC = () => { @@ -18,6 +22,10 @@ export const NewPresentationPage: React.FC = () => { const [presentationTitle, setPresentationTitle] = useState(''); const [presentationDescription, setPresentationDescription] = useState(''); const [creating, setCreating] = useState(false); + const [alertDialog, setAlertDialog] = useState<{ isOpen: boolean; message: string; type?: 'info' | 'warning' | 'error' | 'success' }>({ + isOpen: false, + message: '' + }); useEffect(() => { const loadThemes = async () => { @@ -42,12 +50,20 @@ export const NewPresentationPage: React.FC = () => { const handleCreatePresentation = async () => { if (!selectedTheme) { - alert('Please select a theme for your presentation'); + setAlertDialog({ + isOpen: true, + message: 'Please select a theme for your presentation', + type: 'warning' + }); return; } if (!presentationTitle.trim()) { - alert('Please enter a title for your presentation'); + setAlertDialog({ + isOpen: true, + message: 'Please enter a title for your presentation', + type: 'warning' + }); return; } @@ -62,13 +78,13 @@ export const NewPresentationPage: React.FC = () => { aspectRatio: selectedAspectRatio }); - console.log('Presentation created successfully:', presentation); + loggers.presentation.info('Presentation created successfully', { presentationId: presentation.metadata.id, name: presentation.metadata.name }); // Navigate to the new presentation editor (slide 1) navigate(`/presentations/${presentation.metadata.id}/edit/slides/1`); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create presentation'); - console.error('Error creating presentation:', err); + loggers.presentation.error('Failed to create presentation', err instanceof Error ? err : new Error(String(err))); } finally { setCreating(false); } @@ -116,122 +132,41 @@ export const NewPresentationPage: React.FC = () => {
-
-

Presentation Details

-
- - setPresentationTitle(e.target.value)} - placeholder="Enter presentation title" - className="form-input" - required - /> -
- -
- -