From c9f862bf5111ea22d5f47a4f058a74cce6c35776 Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Thu, 17 Jul 2025 02:32:54 +0545 Subject: [PATCH] feat(Nextjs): Layouts groups and display groups --- .../outline/components/LayoutSelection.tsx | 105 ++++++++ .../outline/components/OutlineContent.tsx | 132 +++++++++ .../outline/components/OutlineItem.tsx | 4 +- .../outline/components/OutlinePage.tsx | 252 +++++++----------- .../nextjs/components/layouts/layoutGroup.ts | 96 ++++++- 5 files changed, 423 insertions(+), 166 deletions(-) create mode 100644 servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx create mode 100644 servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx new file mode 100644 index 00000000..b676e1f5 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx @@ -0,0 +1,105 @@ +"use client"; +import React from "react"; +import { LayoutGroups, LayoutGroup } from "@/components/layouts/layoutGroup"; +import { useLayout } from "../../context/LayoutContext"; +import { CheckCircle } from "lucide-react"; + +interface LayoutSelectionProps { + selectedLayoutGroup: LayoutGroup | null; + onSelectLayoutGroup: (group: LayoutGroup) => void; +} + +const LayoutSelection: React.FC = ({ + selectedLayoutGroup, + onSelectLayoutGroup +}) => { + const { getLayout } = useLayout(); + + const renderLayoutPreview = (layoutId: string) => { + const Layout = getLayout(layoutId); + if (!Layout) { + return ( +
+ Preview unavailable +
+ ); + } + + // Sample data for preview + const sampleData = { + title: "Sample Title", + description: "This is a preview of the layout", + subtitle: "Sample subtitle", + }; + + return ( +
+
+ +
+
+ ); + }; + + return ( +
+
+
+ Select Your Presentation Style +
+

+ Choose a layout group that best fits your presentation style and content. +

+
+ +
+ {LayoutGroups.map((group) => ( +
onSelectLayoutGroup(group)} + className={`relative p-4 rounded-lg border cursor-pointer ${selectedLayoutGroup?.id === group.id + ? 'border-blue-500 bg-blue-50' + : 'border-gray-200 bg-white' + }`} + > + {selectedLayoutGroup?.id === group.id && ( +
+ +
+ )} + +
+
+ {group.name} +
+

+ {group.description} +

+
+ + {/* Layout previews */} +
+ {group.slides.slice(0, 6).map((layoutId, index) => ( +
+ {renderLayoutPreview(layoutId)} +
+ ))} +
+ +
+ {group.slides.length} layouts + + {group.ordered ? 'Structured' : 'Flexible'} + +
+
+ ))} +
+
+ ); +}; + +export default LayoutSelection; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx new file mode 100644 index 00000000..12250105 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx @@ -0,0 +1,132 @@ +"use client"; +import React from "react"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { OutlineItem } from "./OutlineItem"; +import { Button } from "@/components/ui/button"; +import { SlideOutline } from "@/store/slices/presentationGeneration"; +import { FileText } from "lucide-react"; + +interface OutlineContentProps { + outlines: SlideOutline[] | null; + isLoading: boolean; + isStreaming: boolean; + onDragEnd: (event: any) => void; + onAddSlide: () => void; +} + +const OutlineContent: React.FC = ({ + outlines, + isLoading, + isStreaming, + onDragEnd, + onAddSlide +}) => { + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + return ( +
+
+
+ Presentation Outline +
+ {isStreaming && ( +
+
+ Generating outlines... +
+ )} +
+ + {/* Skeleton loading state */} + {isLoading && ( +
+ {[...Array(5)].map((_, index) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ )} + + {/* Outlines content */} + {outlines && outlines.length > 0 && ( +
+ + ({ id: item.title || `slide-${index}` })) || []} + strategy={verticalListSortingStrategy} + > + {outlines?.map((item, index) => ( + + ))} + + + + +
+ )} + + {/* Empty state */} + {!isLoading && outlines && outlines.length === 0 && ( +
+ +

No outlines available

+ +
+ )} +
+ ); +}; + +export default OutlineContent; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx index f137ec7a..936835f0 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx @@ -103,8 +103,8 @@ export function OutlineItem({
handleSlideChange({ ...slideOutline, title: e.target.value })} + defaultValue={slideOutline.title || ''} + onBlur={(e) => handleSlideChange({ ...slideOutline, title: e.target.value })} className="text-md sm:text-lg flex-1 font-semibold bg-transparent outline-none" placeholder="Title goes here" /> diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index 08a55dfe..e2eaf9fd 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -1,21 +1,8 @@ "use client"; import React, { useEffect, useState } from "react"; -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; -import { OutlineItem } from "./OutlineItem"; +import { arrayMove } from "@dnd-kit/sortable"; import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { RootState } from "@/store/store"; import { useSelector, useDispatch } from "react-redux"; import { useRouter } from "next/navigation"; @@ -29,26 +16,20 @@ import { import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; import { jsonrepair } from "jsonrepair"; +import { LayoutGroup, getDefaultLayoutGroup } from "@/components/layouts/layoutGroup"; +import OutlineContent from "./OutlineContent"; +import LayoutSelection from "./LayoutSelection"; const OutlinePage = () => { const dispatch = useDispatch(); const router = useRouter(); - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ); - const { presentation_id, outlines } = useSelector( (state: RootState) => state.presentationGeneration ); - const { currentTheme, currentColors } = useSelector( - (state: RootState) => state.theme - ); - + const [activeTab, setActiveTab] = useState('outline'); + const [selectedLayoutGroup, setSelectedLayoutGroup] = useState(getDefaultLayoutGroup()); const [loadingState, setLoadingState] = useState({ message: "", isLoading: false, @@ -182,6 +163,16 @@ const OutlinePage = () => { }); return; } + + if (!selectedLayoutGroup) { + toast({ + title: "Select Layout Group", + description: "Please select a layout group before generating presentation", + variant: "destructive", + }); + return; + } + // Generate data setLoadingState({ message: "Generating presentation data...", @@ -194,17 +185,11 @@ const OutlinePage = () => { const response = await PresentationGenerationApi.presentationPrepare({ presentation_id: presentation_id, outlines: outlines, + layoutGroup: selectedLayoutGroup, }); if (response) { dispatch(setPresentationData(response)); - - toast({ - title: "Success", - description: "Presentation generated successfully!", - variant: "default", - }); - router.push(`/presentation?id=${presentation_id}&stream=true`); } } catch (error) { @@ -238,7 +223,6 @@ const OutlinePage = () => { dispatch(setOutlines(updatedOutlines)); }; - if (!presentation_id) { return ( @@ -248,7 +232,7 @@ const OutlinePage = () => { No Presentation ID Found

Please start a new presentation.

-
@@ -266,129 +250,93 @@ const OutlinePage = () => { duration={loadingState.duration} /> -
-
-
-

- Outline +
+
+ + {/* Header */} +
+

+ Customize Your Presentation

- {isStreaming && ( -
-
- Generating outlines... -
- )} +

+ Review your outline and select a layout style for your presentation. +

- {/* Skeleton loading state */} - {isLoading && ( -
- {[...Array(5)].map((_, index) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} -
-
- )} + {/* Tabs */} + + + Outline & Content + Layout Style + - {/* Outlines content */} - {outlines && outlines.length > 0 && ( -
- + - ({ id: item.title || `slide-${index}` })) || []} - strategy={verticalListSortingStrategy} - > - {outlines?.map((item, index) => ( - - ))} - - - - -
- )} - - {/* Empty state */} - {!isLoading && outlines && outlines.length === 0 && ( -
-

No outlines available

- -
- )} -
- - {/* Generate button */} - {!isStreaming && } + + + + + + + + {/* Generate button */} +
+ +
+

); diff --git a/servers/nextjs/components/layouts/layoutGroup.ts b/servers/nextjs/components/layouts/layoutGroup.ts index feff303a..67044221 100644 --- a/servers/nextjs/components/layouts/layoutGroup.ts +++ b/servers/nextjs/components/layouts/layoutGroup.ts @@ -1,25 +1,97 @@ -export const ProfessionalLayoutGroup = { +export interface LayoutGroup { + id: string; + name: string; + description: string; + ordered: boolean; + isDefault?: boolean; + slides: string[]; +} + +export const ProfessionalLayoutGroup: LayoutGroup = { id: 'professional', + name: 'Professional', + description: 'Clean, corporate designs perfect for business presentations', ordered: true, - slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide'] + isDefault: true, + slides: [ + 'first-slide', + 'content-slide', + 'bullet-point-slide', + 'comparison-slide', + 'type4-slide', + 'statistics-slide', + 'team-slide', + 'quote-slide' + ] } -export const CasualLayoutGroup = { - id: 'casual', - ordered: false, - slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide'] -} - -export const CreativeLayoutGroup = { +export const CreativeLayoutGroup: LayoutGroup = { id: 'creative', + name: 'Creative', + description: 'Vibrant, artistic layouts for innovative and creative presentations', ordered: false, - slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide'] + slides: [ + 'image-slide', + 'icon-slide', + 'card-slide', + 'type1-slide', + 'type2-slide', + 'type3-slide', + 'process-slide' + ] } -export const ModernLayoutGroup = { +export const ModernLayoutGroup: LayoutGroup = { id: 'modern', + name: 'Modern', + description: 'Contemporary designs with clean lines and sophisticated layouts', ordered: true, - slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide'] + slides: [ + 'type5-slide', + 'type6-slide', + 'type7-slide', + 'type8-slide', + 'timeline-slide', + 'type2-timeline-slide', + 'number-box-slide' + ] } +export const MinimalLayoutGroup: LayoutGroup = { + id: 'minimal', + name: 'Minimal', + description: 'Simple, focused layouts that emphasize content over decoration', + ordered: false, + slides: [ + 'content-slide', + 'bullet-point-slide', + 'type2-numbered-slide', + 'quote-slide', + 'statistics-slide' + ] +} + +export const LayoutGroups = [ + ProfessionalLayoutGroup, + CreativeLayoutGroup, + ModernLayoutGroup, + MinimalLayoutGroup +]; + +export const getDefaultLayoutGroup = (): LayoutGroup => { + return LayoutGroups.find(group => group.isDefault) || ProfessionalLayoutGroup; +}; + +export const getAllLayouts = (): string[] => { + const allLayouts = new Set(); + LayoutGroups.forEach(group => { + group.slides.forEach(slide => allLayouts.add(slide)); + }); + return Array.from(allLayouts); +}; + +export const getGroupByLayoutId = (layoutId: string): LayoutGroup | undefined => { + return LayoutGroups.find(group => group.slides.includes(layoutId)); +}; +