feat: Enhance dashboard and settings layout with new components and improved UI elements

This commit is contained in:
shiva raj badu 2026-02-24 17:26:49 +05:45
parent 7abb446d32
commit d8a4a565d3
No known key found for this signature in database
18 changed files with 503 additions and 302 deletions

View file

@ -4,6 +4,8 @@ import React, { useState, useEffect } from "react";
import { DashboardApi } from "@/app/(presentation-generator)/services/api/dashboard";
import { PresentationGrid } from "@/app/(presentation-generator)/(dashboard)/dashboard/components/PresentationGrid";
import Link from "next/link";
import { ChevronRight } from "lucide-react";
@ -44,7 +46,49 @@ const DashboardPage: React.FC = () => {
};
return (
<div className="min-h-screen w-full">
<div className="min-h-screen w-full px-6 pb-10">
<div className="sticky top-0 right-0 z-50 py-[28px] backdrop-blur mb-4 ">
<div className="flex xl:flex-row flex-col gap-6 xl:gap-0 items-center justify-between">
<h3 className=" text-[28px] tracking-[-0.84px] font-unbounded font-normal text-[#101828] flex items-center gap-2">
Slide Presentations
</h3>
<div className="flex gap-2.5 max-sm:w-full max-md:justify-center max-sm:flex-wrap">
{<Link
href="/generate"
className="inline-flex items-center gap-2 rounded-xl px-4 py-2.5 text-black text-sm font-medium shadow-sm hover:shadow-md"
aria-label="Create new presentation"
style={{
borderRadius: "48px",
background: "linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)",
}}
>
<span className="hidden md:inline">New presentation</span>
<span className="md:hidden">New</span>
<ChevronRight className="w-4 h-4" />
</Link>}
{/* {
<Link
href="/theme?tab=new-theme"
className="inline-flex items-center font-inter font-normal gap-2 rounded-xl px-4 py-2.5 text-black text-sm shadow-sm hover:shadow-md"
aria-label="Create new themes"
style={{
borderRadius: "48px",
background: "linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)",
}}
>
<span className="hidden md:inline">New Themes</span>
<span className="md:hidden">New</span>
<ChevronRight className="w-4 h-4" />
</Link>
} */}
</div>
</div>
</div>
<PresentationGrid
presentations={presentations}
type="slide"

View file

@ -15,7 +15,7 @@ const Header = () => {
<Wrapper>
<div className="flex items-center justify-between py-1">
<div className="flex items-center gap-3">
{(pathname !== "/upload" && pathname !== "/dashboard") && <BackBtn />}
{/* {(pathname !== "/upload" && pathname !== "/dashboard") && <BackBtn />} */}
<Link href="/dashboard" onClick={() => trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" })}>
<img
src="/Logo.png"

View file

@ -69,7 +69,7 @@ export const PresentationGrid = ({
if (isLoading) {
return (
<div className="grid grid-cols-1 mt-10 md:grid-cols-2 lg:grid-cols-4 gap-5 sm:gap-6 w-full">
<div className="grid grid-cols-1 px-6 mt-10 md:grid-cols-2 lg:grid-cols-4 gap-5 sm:gap-6 w-full">
<div className="flex flex-col gap-4 min-h-[200px] cursor-pointer group ring-1 ring-inset ring-slate-200 bg-white/80 rounded-xl items-center justify-center animate-pulse">
<div className="rounded-full bg-slate-200 p-4">
<div className="w-8 h-8" />

View file

@ -1,23 +1,26 @@
import { Skeleton } from '@/components/ui/skeleton'
import React from 'react'
const loading = () => {
return (
<div className=''>
<div className='container mx-auto px-4 py-8'>
<div className=" mx-auto pb-10 grid xl:grid-cols-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 ">
{
Array.from({ length: 8 }).map((_, index) => (
<Skeleton key={index} className="h-72 w-full bg-gray-300 aspect-video mx-auto" />
))
}
<div className="grid grid-cols-1 px-6 mt-10 md:grid-cols-2 lg:grid-cols-4 gap-5 sm:gap-6 w-full">
<div className="flex flex-col gap-4 min-h-[200px] cursor-pointer group ring-1 ring-inset ring-slate-200 bg-white/80 rounded-xl items-center justify-center animate-pulse">
<div className="rounded-full bg-slate-200 p-4">
<div className="w-8 h-8" />
</div>
<div className="text-center space-y-2">
<div className="h-4 bg-slate-200 rounded w-32 mx-auto"></div>
<div className="h-3 bg-slate-200 rounded w-48 mx-auto"></div>
</div>
</div>
{[...Array(15)].map((_, i) => (
<div key={i} className="flex flex-col gap-4 min-h-[200px] bg-white/70 rounded-lg p-4 animate-pulse">
<div className="w-full h-24 bg-gray-200 rounded-lg"></div>
<div className="space-y-3">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
))}
</div>
)
}

View file

@ -1,13 +1,12 @@
import React from 'react'
import DashboardSidebar from './Components/DashboardSidebar'
import DashboardNav from './Components/DashboardNav'
const layout = ({ children }: { children: React.ReactNode }) => {
return (
<div className='flex gap-6 pr-4 bg-white'>
<div className='flex pr-4 bg-white'>
<DashboardSidebar />
<div className='w-full'>
<DashboardNav />
{children}
</div>
</div>

View file

@ -14,6 +14,7 @@ import LLMProviderSelection from "@/components/LLMSelection";
import Header from "../dashboard/components/Header";
import { LLMConfig } from "@/types/llm_config";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import SettingSideBar from "./SettingSideBar";
// Button state interface
interface ButtonState {
@ -28,6 +29,8 @@ interface ButtonState {
const SettingsPage = () => {
const router = useRouter();
const pathname = usePathname();
const [mode, setMode] = useState<'nanobanana' | 'presenton'>('presenton')
const [selectedProvider, setSelectedProvider] = useState<'text-provider' | 'image-provider'>('text-provider')
const userConfigState = useSelector((state: RootState) => state.userConfig);
const [llmConfig, setLlmConfig] = useState<LLMConfig>(
userConfigState.llm_config
@ -155,15 +158,30 @@ const SettingsPage = () => {
return (
<div className="h-screen font-instrument_sans flex flex-col overflow-hidden">
<main className="w-full mx-auto px-4 overflow-hidden flex flex-col">
<main className="w-full mx-auto gap-6 overflow-hidden flex ">
<SettingSideBar mode={mode} setMode={setMode} selectedProvider={selectedProvider} setSelectedProvider={setSelectedProvider} />
<div className="w-full">
<div className="sticky top-0 right-0 z-50 py-[28px] backdrop-blur mb-4 ">
<div className="flex xl:flex-row flex-col gap-6 xl:gap-0 items-center justify-between">
<h3 className=" text-[28px] tracking-[-0.84px] font-unbounded font-normal text-black flex items-center gap-2">
Settings
</h3>
<div className="flex gap-2.5 max-sm:w-full max-md:justify-center max-sm:flex-wrap">
</div>
</div>
</div>
<LLMProviderSelection
initialLLMConfig={llmConfig}
onConfigChange={setLlmConfig}
buttonState={buttonState}
setButtonState={setButtonState}
/>
{mode === 'nanobanana' && <div className=" w-full bg-[#F9F8F8] p-7 rounded-[20px]">
<h4>Nano Banana</h4>
</div>}
{mode === 'presenton' && <LLMProviderSelection
initialLLMConfig={llmConfig}
onConfigChange={setLlmConfig}
buttonState={buttonState as any}
setButtonState={setButtonState as any}
/>}
</div>
</main>
{/* Fixed Bottom Button */}

View file

@ -0,0 +1,59 @@
import React from 'react'
const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }: { mode: 'nanobanana' | 'presenton', setMode: (mode: 'nanobanana' | 'presenton') => void, selectedProvider: 'text-provider' | 'image-provider', setSelectedProvider: (provider: 'text-provider' | 'image-provider') => void }) => {
return (
<div className='w-full max-w-[230px] h-screen px-4 pt-[22px] bg-[#F9FAFB]'>
<p className='text-xs text-black font-medium border-b mt-[3.15rem] border-[#E1E1E5] pb-3.5'>FILTER BY:</p>
<div className='mt-6'>
<p className='text-[#3A3A3A] text-xs font-medium pb-2.5'>Select Mode</p>
<div className='p-1 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center mb-[34px] '>
<button className='px-3 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => setMode('nanobanana')}
style={{
background: mode === 'nanobanana' ? '#F4F3FF' : 'transparent',
color: mode === 'nanobanana' ? '#5146E5' : '#3A3A3A'
}}
>Nanobanana</button>
<svg xmlns="http://www.w3.org/2000/svg" className='mx-1' width="2" height="17" viewBox="0 0 2 17" fill="none">
<path d="M1 0V16.5" stroke="#EDECEC" strokeWidth="2" />
</svg>
<button className='px-3 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => setMode('presenton')}
style={{
background: mode === 'presenton' ? '#F4F3FF' : 'transparent',
color: mode === 'presenton' ? '#5146E5' : '#3A3A3A'
}}
>Presenton</button>
</div>
<p className='text-[#3A3A3A] text-xs font-medium pb-2.5'>Select Provider</p>
{mode === 'presenton' && <div className='space-y-2.5'>
<button className={`bg-white w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border border-[#E1E1E5] ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : ''}`} onClick={() => setSelectedProvider('text-provider')}>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src='/providers/openai.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
</div>
<p className='text-[#191919] text-xs font-medium' >Text Provider</p>
</button>
<button className={`bg-white w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border border-[#E1E1E5] ${selectedProvider === 'image-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : ''}`} onClick={() => setSelectedProvider('image-provider')}>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src='/providers/image-provider.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
</div>
<p className='text-[#191919] text-xs font-medium' >Image Provider</p>
</button>
</div>}
{
mode === 'nanobanana' && <div>
<button className={`bg-white w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border border-[#E1E1E5] ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : ''}`}>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src='/providers/openai.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
</div>
<p className='text-[#191919] text-xs font-medium' >Nanobanana</p>
</button>
</div>
}
</div>
</div>
)
}
export default SettingSideBar

View file

@ -2,7 +2,7 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { ExternalLink, Loader2, Plus } from "lucide-react";
import { ChevronRight, ExternalLink, Loader2, Plus } from "lucide-react";
import { templates } from "@/app/presentation-templates";
import { TemplateWithData, TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import {
@ -12,11 +12,12 @@ import {
} from "@/app/hooks/useCustomTemplates";
import { CompiledLayout } from "@/app/hooks/compileLayout";
import CreateCustomTemplate from "./CreateCustomTemplate";
import Link from "next/link";
// Component for rendering custom template card with lazy-loaded previews
export const CustomTemplateCard = React.memo(function CustomTemplateCard({ template }: { template: CustomTemplates }) {
const router = useRouter();
const { previewLayouts, loading } = useCustomTemplatePreview(`${template.id}`);
const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(`${template.id}`);
const handleOpen = useCallback(() => {
if (template.id.startsWith('custom-')) {
router.push(`/template-preview/${template.id}`)
@ -34,7 +35,7 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {template.layoutCount}
Layouts- {totalLayouts}
</span>
<div className="p-5">
@ -157,7 +158,7 @@ const InbuiltTemplateCard = React.memo(function InbuiltTemplateCard({
});
const LayoutPreview = () => {
const [tab, setTab] = useState<'custom' | 'default'>('custom');
const [tab, setTab] = useState<'custom' | 'default'>('default');
const router = useRouter();
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
@ -191,6 +192,33 @@ const LayoutPreview = () => {
return (
<div className="min-h-screen ">
<div className="sticky top-0 right-0 z-50 py-[28px] px-6 backdrop-blur ">
<div className="flex xl:flex-row flex-col gap-6 xl:gap-0 items-center justify-between">
<h3 className=" text-[28px] tracking-[-0.84px] font-unbounded font-normal text-[#101828] flex items-center gap-2">
Templates
</h3>
<div className="flex gap-2.5 max-sm:w-full max-md:justify-center max-sm:flex-wrap">
<Link
href="/custom-template"
className="inline-flex items-center font-inter font-normal gap-2 rounded-xl px-4 py-2.5 text-black text-sm shadow-sm hover:shadow-md"
aria-label="Create new themes"
style={{
borderRadius: "48px",
background: "linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)",
}}
>
<span className="hidden md:inline">New Template</span>
<span className="md:hidden">New</span>
<ChevronRight className="w-4 h-4" />
</Link>
</div>
</div>
</div>
<div className="l mx-auto px-6 py-8">
<div className='p-1 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center '>

View file

@ -28,70 +28,11 @@ LayoutPreview.displayName = 'LayoutPreview';
export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTemplate }: { template: CustomTemplates, onSelectTemplate: (template: string) => void, selectedTemplate: string | null }) => {
const { previewLayouts, loading: customLoading } = useCustomTemplatePreview(template.id);
const { previewLayouts, loading: customLoading, totalLayouts } = useCustomTemplatePreview(template.id);
const isSelected = selectedTemplate === template.id;
return (
// <Card
// className={`${isSelected ? 'border-2 border-blue-500' : ''} cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden relative`}
// style={{ contain: 'layout style paint' }}
// onClick={() => {
// onSelectTemplate(template.id);
// }}
// >
// <div className="p-5">
// <div className="flex items-center justify-between mb-2">
// <h3 className="text-xl font-bold text-gray-900">
// {template.name}
// </h3>
// </div>
// {/* Layout previews */}
// <div className="grid grid-cols-2 gap-2">
// {customLoading ? (
// // Loading placeholders
// [...Array(Math.min(4, template.layoutCount))].map((_, index) => (
// <div
// key={`${template.id}-loading-${index}`}
// className="relative bg-gradient-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
// >
// <Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
// </div>
// ))
// ) : previewLayouts && previewLayouts?.length > 0 ? (
// // Actual layout previews - using memoized component
// previewLayouts?.slice(0, 4).map((layout: CompiledLayout, index: number) => (
// <LayoutPreview
// key={`${template.id}-preview-${index}`}
// layout={layout}
// templateId={template.id}
// index={index}
// />
// ))
// ) : (
// // Empty state placeholders
// [...Array(Math.min(4, template.layoutCount))].map((_, index) => (
// <div
// key={`${template.id}-empty-${index}`}
// className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
// >
// <span className="text-xs text-gray-400">No preview</span>
// </div>
// ))
// )}
// </div>
// </div>
// {isSelected && (
// <div className="absolute top-0 right-0 bg-blue-500 text-white px-2 py-1 rounded-bl-lg">
// Selected
// </div>
// )}
// </Card>
<Card
className={`${isSelected ? 'border-2 border-blue-500' : ''} cursor-pointer flex flex-col justify-between relative hover:shadow-lg transition-all duration-200 group overflow-hidden`}
onClick={() => onSelectTemplate(template.id)}
@ -99,7 +40,7 @@ export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTe
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {template.layoutCount}
Layouts- {totalLayouts}
</span>
<div className="p-5">

View file

@ -48,10 +48,10 @@ const OutlinePage: React.FC = () => {
duration={loadingState.duration}
/>
<Wrapper className="h-full flex flex-col w-full">
<div className="flex-grow overflow-y-hidden w-[1200px] mx-auto mt-6">
<Wrapper className="h-full flex flex-col w-full">
<div className="flex-grow w-full overflow-y-hidden mx-auto mt-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
<TabsList className="my-4grid h-auto w-fit grid-cols-2 rounded-full border border-[#DFDFE1] bg-[#F8F8F9] p-1.5">
<TabsList className="my-4 h-auto w-fit rounded-full border border-[#DFDFE1] bg-[#F8F8F9] p-1.5">
<TabsTrigger
value={TABS.OUTLINE}
className="rounded-full px-5 py-2 text-xs font-medium text-[#2D2D2D] shadow-none data-[state=active]:bg-[#E9E2F8] data-[state=active]:text-[#7E3AF2] data-[state=active]:shadow-none"

View file

@ -1,5 +1,5 @@
"use client";
import React, { useEffect } from "react";
import React, { useEffect, useMemo, useCallback, memo } from "react";
import { templates } from "@/app/presentation-templates";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
@ -8,19 +8,86 @@ import { TemplateWithData } from "@/app/presentation-templates/utils";
import { CustomTemplates, useCustomTemplateSummaries } from "@/app/hooks/useCustomTemplates";
import { Loader2 } from "lucide-react";
import { CustomTemplateCard } from "./CustomTemplateCard";
// Memoized layout preview for built-in templates
const BuiltInLayoutPreview = memo(({ layout, templateId, index }: {
layout: TemplateWithData;
templateId: string;
index: number;
}) => {
const LayoutComponent = layout.component;
return (
<div
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
style={{ contain: 'layout style paint' }}
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
});
BuiltInLayoutPreview.displayName = 'BuiltInLayoutPreview';
// Memoized built-in template card
const BuiltInTemplateCard = memo(({ template, isSelected, onSelect }: {
template: TemplateLayoutsWithSettings;
isSelected: boolean;
onSelect: (template: TemplateLayoutsWithSettings) => void;
}) => {
const previewLayouts = useMemo(() => template.layouts.slice(0, 4), [template.layouts]);
const handleClick = useCallback(() => onSelect(template), [onSelect, template]);
return (
<Card
className={`${isSelected ? 'border-2 border-blue-500' : ''} cursor-pointer relative hover:shadow-lg transition-all duration-200 group overflow-hidden`}
onClick={handleClick}
>
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {template.layouts.length}
</span>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<div className="p-5">
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => (
<BuiltInLayoutPreview
key={`${template.id}-preview-${index}`}
layout={layout}
templateId={template.id}
index={index}
/>
))}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40">
<div>
<h3 className="text-sm font-bold text-gray-900 capitalize">
{template.name}
</h3>
<p className="text-xs text-gray-600 mb-4 line-clamp-2">
{template.description}
</p>
</div>
</div>
</Card>
);
});
BuiltInTemplateCard.displayName = 'BuiltInTemplateCard';
interface TemplateSelectionProps {
selectedTemplate: (TemplateLayoutsWithSettings | string) | null;
onSelectTemplate: (template: TemplateLayoutsWithSettings | string) => void;
}
const TemplateSelection: React.FC<TemplateSelectionProps> = ({
const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
selectedTemplate,
onSelectTemplate
}) => {
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
@ -30,146 +97,101 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = ({
script.async = true;
document.head.appendChild(script);
}
}, []);
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
// Stable callback for custom template selection
const handleCustomSelect = useCallback(
(template: TemplateLayoutsWithSettings | string) => onSelectTemplate(template),
[onSelectTemplate]
);
// Stable callback for built-in template selection
const handleBuiltInSelect = useCallback(
(template: TemplateLayoutsWithSettings) => onSelectTemplate(template),
[onSelectTemplate]
);
// Derive the selected custom template id only when selectedTemplate changes
const selectedCustomId = useMemo(
() => (typeof selectedTemplate === 'string' ? selectedTemplate : null),
[selectedTemplate]
);
// Derive the selected built-in template id only when selectedTemplate changes
const selectedBuiltInId = useMemo(
() => (typeof selectedTemplate !== 'string' ? selectedTemplate?.id ?? null : null),
[selectedTemplate]
);
// Memoize the custom templates section
const customTemplateCards = useMemo(() => {
if (customLoading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Loading custom templates...</span>
</div>
);
}
if (customTemplates.length === 0) {
return (
<Card className="p-8 text-center">
<p className="text-gray-500">No custom templates yet.</p>
<p className="text-sm text-gray-400 mt-2">
Custom templates you create will appear here.
</p>
</Card>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{customTemplates.map((template: CustomTemplates) => (
<CustomTemplateCard
key={template.id}
template={template}
onSelectTemplate={handleCustomSelect}
selectedTemplate={selectedCustomId}
/>
))}
</div>
);
}, [customLoading, customTemplates, handleCustomSelect, selectedCustomId]);
// Memoize the built-in templates list
const builtInTemplateCards = useMemo(
() =>
templates.map((template: TemplateLayoutsWithSettings) => (
<BuiltInTemplateCard
key={template.id}
template={template}
isSelected={selectedBuiltInId === template.id}
onSelect={handleBuiltInSelect}
/>
)),
[selectedBuiltInId, handleBuiltInSelect]
);
return (
<div className="space-y-8 mb-4">
{/* In Built Templates */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">In Built Templates</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{templates.map((template: TemplateLayoutsWithSettings) => {
const previewLayouts = template.layouts.slice(0, 4);
return (
<Card
key={template.id}
className={`${typeof selectedTemplate !== 'string' && selectedTemplate?.id === template.id ? 'border-2 border-blue-500' : ''} cursor-pointer relative hover:shadow-lg transition-all duration-200 group overflow-hidden`}
onClick={() => onSelectTemplate(template)}
>
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {template.layouts.length}
</span>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<div className="p-5">
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40 ">
<div>
<h3 className="text-sm font-bold text-gray-900 capitalize">
{template.name}
</h3>
<p className="text-xs text-gray-600 mb-4 line-clamp-2">
{template.description}
</p>
</div>
</div>
</Card>
// <Card
// key={template.id}
// className={`${typeof selectedTemplate !== 'string' && selectedTemplate?.id === template.id ? 'border-2 border-blue-500' : ''} cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden relative`}
// onClick={() => onSelectTemplate(template)}
// >
// <div className="p-5">
// <div className="flex items-center justify-between mb-2">
// <h3 className="text-xl font-bold text-gray-900 capitalize">
// {template.name}
// </h3>
// </div>
// <p className="text-sm text-gray-600 mb-4 line-clamp-2">
// {template.description}
// </p>
// <div className="grid grid-cols-2 gap-2">
// {previewLayouts.map((layout: TemplateWithData, index: number) => {
// const LayoutComponent = layout.component;
// return (
// <div
// key={`${template.id}-preview-${index}`}
// className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
// style={{ contain: 'layout style paint' }}
// >
// <div className="absolute inset-0 bg-transparent z-10" />
// <div
// className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]"
// style={{ transform: 'scale(0.2) translateZ(0)', backfaceVisibility: 'hidden' }}
// >
// <LayoutComponent data={layout.sampleData} />
// </div>
// </div>
// );
// })}
// </div>
// </div>
// {typeof selectedTemplate !== 'string' && selectedTemplate?.id === template.id && (
// <div className="absolute top-0 right-0 bg-blue-500 text-white px-2 py-1 rounded-bl-lg">
// Selected
// </div>
// )}
// </Card>
);
})}
</div>
</div>
{/* Custom AI Templates */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900">Custom AI Templates</h3>
<h3 className="text-lg font-semibold text-gray-900">Custom</h3>
</div>
{customTemplateCards}
</div>
{/* In Built Templates */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">In Built</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{builtInTemplateCards}
</div>
{customLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Loading custom templates...</span>
</div>
) : customTemplates.length === 0 ? (
<Card className="p-8 text-center">
<p className="text-gray-500">No custom templates yet.</p>
<p className="text-sm text-gray-400 mt-2">
Custom templates you create will appear here.
</p>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{customTemplates.map((template: CustomTemplates) => (
<CustomTemplateCard
key={template.id}
template={template}
onSelectTemplate={onSelectTemplate}
selectedTemplate={typeof selectedTemplate === 'string' ? selectedTemplate : null}
/>
))}
</div>
)}
</div>
</div>
);
};
});
TemplateSelection.displayName = 'TemplateSelection';
export default TemplateSelection;

View file

@ -1,17 +1,18 @@
"use client";
import { Button } from "@/components/ui/button";
import {
SquareArrowOutUpRight,
Play,
Loader2,
Redo2,
Undo2,
RotateCcw,
ArrowRightFromLine,
ExternalLink,
MoveUpRight,
ArrowUpRight,
} from "lucide-react";
import React, { useState } from "react";
import Wrapper from "@/components/Wrapper";
import { useRouter, usePathname } from "next/navigation";
import {
Popover,
@ -19,27 +20,21 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { OverlayLoader } from "@/components/ui/overlay-loader";
import { useDispatch, useSelector } from "react-redux";
import Link from "next/link";
import { RootState } from "@/store/store";
import { toast } from "sonner";
import Announcement from "@/components/Announcement";
import { PptxPresentationModel } from "@/types/pptx_models";
import HeaderNav from "../../components/HeaderNab";
import PDFIMAGE from "@/public/pdf.svg";
import PPTXIMAGE from "@/public/pptx.svg";
import Image from "next/image";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { usePresentationUndoRedo } from "../hooks/PresentationUndoRedo";
import ToolTip from "@/components/ToolTip";
import { clearPresentationData } from "@/store/slices/presentationGeneration";
import { clearHistory } from "@/store/slices/undoRedoSlice";
import { Separator } from "@/components/ui/separator";
import ThemeSelector from "./ThemeSelector";
const PresentationHeader = ({
presentation_id,
@ -157,28 +152,35 @@ const PresentationHeader = ({
};
const ExportOptions = ({ mobile }: { mobile: boolean }) => (
<div className={`space-y-2 max-md:mt-4 ${mobile ? "" : "bg-white"} rounded-lg`}>
<Button
onClick={() => {
trackEvent(MixpanelEvent.Header_Export_PDF_Button_Clicked, { pathname });
handleExportPdf();
}}
variant="ghost"
className={`pb-4 border-b rounded-none border-gray-300 w-full flex justify-start text-[#5146E5] ${mobile ? "bg-white py-6 border-none rounded-lg" : ""}`} >
<Image src={PDFIMAGE} alt="pdf export" width={30} height={30} />
Export as PDF
</Button>
<Button
onClick={() => {
trackEvent(MixpanelEvent.Header_Export_PPTX_Button_Clicked, { pathname });
handleExportPptx();
}}
variant="ghost"
className={`w-full flex justify-start text-[#5146E5] ${mobile ? "bg-white py-6" : ""}`}
>
<Image src={PPTXIMAGE} alt="pptx export" width={30} height={30} />
Export as PPTX
</Button>
<div className={` rounded-[18px] max-md:mt-4 ${mobile ? "" : "bg-white"} p-5`}>
<p className="text-sm font-medium text-[#19001F]">Export as</p>
<div className="my-[18px] h-[1px] bg-[#E8E8E8]" />
<div className="space-y-3">
<Button
onClick={() => {
trackEvent(MixpanelEvent.Header_Export_PDF_Button_Clicked, { pathname });
handleExportPdf();
}}
variant="ghost"
className={` rounded-none px-0 w-full text-xs flex justify-start text-black hover:bg-transparent ${mobile ? "bg-white py-6 border-none rounded-lg" : ""}`} >
PDF
<ArrowUpRight className="w-3.5 h-3.5" />
</Button>
<Button
onClick={() => {
trackEvent(MixpanelEvent.Header_Export_PPTX_Button_Clicked, { pathname });
handleExportPptx();
}}
variant="ghost"
className={`w-full flex px-0 justify-start text-xs text-black hover:bg-transparent ${mobile ? "bg-white py-6" : ""}`}
>
PPTX
<ArrowUpRight className="w-3.5 h-3.5" />
</Button>
</div>
</div>
@ -189,12 +191,13 @@ const PresentationHeader = ({
return (
<>
<div className="py-7 sticky top-0 bg-white z-50 mb-[17px] pr-[25px] flex justify-between items-center">
<h2 className="text-[28px] text-[#101323] w-[600px] truncate">{presentationData?.title || "Presentation"}</h2>
<h2 className="text-lg text-[#101323] w-[600px] truncate">{presentationData?.title || "Presentation"}</h2>
<div className="flex items-center gap-2.5">
{isPresentationSaving && <div className="flex items-center gap-2">
<Loader2 className="w-3.5 h-3.5 animate-spin" />
</div>}
<ThemeSelector presentation_id={presentation_id} current_theme={{}} themes={[]} />
<div className="flex items-center gap-2 bg-[#F6F6F9] px-3.5 h-[38px] border border-[#EDECEC] rounded-[80px]">
@ -246,10 +249,10 @@ const PresentationHeader = ({
}}
disabled={isExporting}
>
{isExporting ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : "Export"} <ArrowRightFromLine />
{isExporting ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : "Export"} <ArrowRightFromLine className="w-3.5 h-3.5" />
</button>
</PopoverTrigger>
<PopoverContent align="end" className="w-[250px] space-y-2 py-3 px-2 ">
<PopoverContent align="end" className="w-[200px] rounded-[18px] space-y-2 p-0 ">
<ExportOptions mobile={false} />
</PopoverContent>
</Popover>

View file

@ -0,0 +1,120 @@
"use client";
import React, { useState } from 'react'
// import { Theme } from '@/app/(presentation-generator)/services/api/types'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Palette } from 'lucide-react';
import { useDispatch } from 'react-redux';
// import { updateTheme } from '@/store/slices/presentationGeneration';
import { useRouter } from 'next/navigation';
import { useFontLoader } from '../../hooks/useFontLoader';
const ThemeSelector = ({ presentation_id, current_theme, themes: allThemes }: { presentation_id: string, current_theme: any, themes: any[] }) => {
const [currentTheme, setCurrentTheme] = useState<any>(current_theme)
const dispatch = useDispatch()
const router = useRouter()
const applyTheme = async (theme: any) => {
const element = document.getElementById('presentation-slides-wrapper')
if (!element) return;
if (allThemes.length === 0) return;
setCurrentTheme(theme)
clearTheme()
if (!theme.data.colors['graph_0']) { return; }
const cssVariables = {
'--primary-color': theme.data.colors['primary'],
'--background-color': theme.data.colors['background'],
'--card-color': theme.data.colors['card'],
'--stroke': theme.data.colors['stroke'],
'--primary-text': theme.data.colors['primary_text'],
'--background-text': theme.data.colors['background_text'],
'--graph-0': theme.data.colors['graph_0'],
'--graph-1': theme.data.colors['graph_1'],
'--graph-2': theme.data.colors['graph_2'],
'--graph-3': theme.data.colors['graph_3'],
'--graph-4': theme.data.colors['graph_4'],
'--graph-5': theme.data.colors['graph_5'],
'--graph-6': theme.data.colors['graph_6'],
'--graph-7': theme.data.colors['graph_7'],
'--graph-8': theme.data.colors['graph_8'],
'--graph-9': theme.data.colors['graph_9'],
}
Object.entries(cssVariables).forEach(([key, value]) => {
element.style.setProperty(key, value)
})
// useFontLoader({ [theme.data.fonts.textFont.name]: theme.data.fonts.textFont.url })
// Apply fonts to preview container
element.style.setProperty('font-family', `"${theme.data.fonts.textFont.name}"`)
element.style.setProperty('--heading-font-family', `"${theme.data.fonts.textFont.name}"`)
// dispatch(updateTheme(theme))
}
const clearTheme = () => {
const element = document.getElementById('presentation-slides-wrapper')
if (!element) return;
element.style.removeProperty('--primary-color');
element.style.removeProperty('--background-color');
element.style.removeProperty('--card-color');
element.style.removeProperty('--stroke');
element.style.removeProperty('--primary-text');
element.style.removeProperty('--background-text');
element.style.removeProperty('--graph-0');
element.style.removeProperty('--graph-1');
element.style.removeProperty('--graph-2');
element.style.removeProperty('--graph-3');
element.style.removeProperty('--graph-4');
element.style.removeProperty('--graph-5');
element.style.removeProperty('--graph-6');
element.style.removeProperty('--graph-7');
element.style.removeProperty('--graph-8');
element.style.removeProperty('--graph-9');
}
const resetTheme = async () => {
clearTheme();
// dispatch(updateTheme({} as any))
}
return (
<Popover>
<PopoverTrigger>
<button className="text-sm px-[18px] py-2.5 gap-1.5 flex items-center font-inter border border-[#EDEEEF] bg-[#F6F6F9] text-black hover:text-blue-500 duration-300 rounded-[88px] font-medium">
<Palette className="h-4 w-4" /> Theme
</button>
</PopoverTrigger>
<PopoverContent className="w-fit max-h-80 overflow-y-auto custom_scrollbar">
<div className='pb-2 flex gap-2 justify-end'>
<button className='text-xs text-gray-500 pb-2 text-right underline' onClick={() => router.push(`/theme?tab=new-theme`)}>+Customize Theme</button>
<button className='text-xs text-gray-500 pb-2 text-right underline' onClick={resetTheme}>Reset Theme</button>
</div>
<div className="grid grid-cols-3 gap-4">
{allThemes && allThemes.length > 0 && allThemes.map((t) => (
<div
key={t.id}
onClick={() => applyTheme(t)}
className={`text-left group relative`}
>
<div className={`rounded-xl cursor-pointer p-1 border shadow-sm bg-white transition-all group-hover:shadow-md ${currentTheme.id === t.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'}`}>
<div className="rounded-lg p-2" style={{ backgroundColor: t.data.colors['background'] }}>
<div className="rounded-md shadow-sm p-3" style={{ backgroundColor: t.data.colors['card'] }}>
<div className="w-16 h-2 rounded-full mb-2" style={{ backgroundColor: t.data.colors['background_text'] }} />
<div className="w-12 h-2 rounded-full mb-1" style={{ backgroundColor: t.data.colors['background_text'] }} />
<div className="w-8 h-2 rounded-full mb-3" style={{ backgroundColor: t.data.colors['background_text'] }} />
<div className="w-8 h-3 rounded-full" style={{ backgroundColor: t.data.colors['primary'] }} />
</div>
</div>
</div>
<p className="mt-2 text-xs text-center font-medium text-gray-700 truncate w-full">
{t.name}
</p>
</div>
))}
</div>
</PopoverContent>
</Popover>
)
}
export default ThemeSelector

View file

@ -5,8 +5,7 @@ import { Card } from "@/components/ui/card";
import { ExternalLink, Loader2, Plus } from "lucide-react";
import { templates } from "@/app/presentation-templates";
import type { TemplateLayoutsWithSettings } from "@/app/presentation-templates";
import { TemplateWithData } from "@/app/presentation-templates/utils";
import { TemplateLayoutsWithSettings, TemplateWithData } from "@/app/presentation-templates/utils";
import {
useCustomTemplateSummaries,
useCustomTemplatePreview,

View file

@ -46,7 +46,7 @@ const UploadPage = () => {
// State management
const [files, setFiles] = useState<File[]>([]);
const [config, setConfig] = useState<PresentationConfig>({
slides: "8",
slides: "5",
language: LanguageType.English,
prompt: "",
tone: ToneType.Default,

View file

@ -34,28 +34,7 @@ interface LLMProviderSelectionProps {
) => void;
}
const LLM_TABS = [
{
label: 'OpenAI',
value: 'openai',
},
{
label: 'Google',
value: 'google',
},
{
label: 'Anthropic',
value: 'anthropic',
},
{
label: 'Ollama',
value: 'ollama',
},
{
label: 'Custom',
value: 'custom',
},
];
export default function LLMProviderSelection({
initialLLMConfig,
onConfigChange,
@ -236,23 +215,9 @@ export default function LLMProviderSelection({
return (
<div className="h-full flex flex-col">
{/* Provider Selection - Fixed Header */}
<div className="p-1.5 rounded-[41px] bg-white ">
<div className='p-1 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center '>
{LLM_TABS.map((tab) => (
<button key={tab.value} className='px-5 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => { handleProviderChange(tab.value) }}
style={{
background: tab.value === llmConfig.LLM ? '#F4F3FF' : 'transparent',
color: tab.value === llmConfig.LLM ? '#5146E5' : '#3A3A3A'
}}
>{tab.label}</button>
))}
<div className="h-full flex flex-col w-full">
</div>
</div>
{/* Scrollable Content */}
<div className="flex-1 bg-[#F9F8F8] p-7 rounded-[20px] overflow-y-auto pt-0 custom_scrollbar">

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB