feat: Enhance dashboard and settings layout with new components and improved UI elements
This commit is contained in:
parent
7abb446d32
commit
d8a4a565d3
18 changed files with 503 additions and 302 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 '>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
BIN
servers/nextjs/public/providers/image-provider.png
Normal file
BIN
servers/nextjs/public/providers/image-provider.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
servers/nextjs/public/providers/openai.png
Normal file
BIN
servers/nextjs/public/providers/openai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Loading…
Add table
Reference in a new issue