refactor: update Ui components in settings & templates
This commit is contained in:
parent
fa4e9f2873
commit
a95a43a5a2
13 changed files with 426 additions and 425 deletions
|
|
@ -175,7 +175,7 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
|
|||
<PopoverContent
|
||||
className="p-0"
|
||||
align="start"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
style={{ width: "300px" }}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search provider..." />
|
||||
|
|
|
|||
|
|
@ -13,19 +13,20 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
|
|||
<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 flex-1'>
|
||||
<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]'
|
||||
<div className='p-0.5 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center mb-[34px] '>
|
||||
<button className='px-3 font-syne h-[26px] text-xs font-medium text-[#3A3A3A] rounded-[70px]'
|
||||
onClick={() => setMode('presenton')}
|
||||
style={{
|
||||
background: mode === 'presenton' ? '#F4F3FF' : 'transparent',
|
||||
color: mode === 'presenton' ? '#5146E5' : '#3A3A3A'
|
||||
}}
|
||||
>Presenton</button>
|
||||
>Presenton
|
||||
</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>
|
||||
<div className='relative'>
|
||||
<button className='px-3 py-2 text-xs font-medium rounded-[70px] cursor-not-allowed opacity-60'
|
||||
<button className='px-3 font-syne h-[26px] text-xs font-medium rounded-[70px] cursor-not-allowed opacity-60'
|
||||
disabled
|
||||
style={{
|
||||
background: 'transparent',
|
||||
|
|
@ -43,15 +44,15 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
|
|||
</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={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('text-provider')}>
|
||||
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
|
||||
<button className={` w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('text-provider')}>
|
||||
<div className='relative w-[18px] h-[18px] rounded-full overflow-hidden border border-[#EDEEEF]'>
|
||||
|
||||
<img src={textProviderIcon} 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={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'image-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('image-provider')}>
|
||||
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
|
||||
<button className={` w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'image-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('image-provider')}>
|
||||
<div className='relative w-[18px] h-[18px] rounded-full overflow-hidden border border-[#EDEEEF]'>
|
||||
<img src={imageProviderIcon} className=' object-cover w-full h-full overflow-hidden' alt='google' />
|
||||
</div>
|
||||
<p className='text-[#191919] text-xs font-medium' >Image Provider</p>
|
||||
|
|
@ -59,8 +60,8 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
|
|||
</div>}
|
||||
{
|
||||
mode === 'nanobanana' && <div>
|
||||
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border bg-[#F4F3FF] border-[#D9D6FE]`}>
|
||||
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
|
||||
<button className={` w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border bg-[#F4F3FF] border-[#D9D6FE]`}>
|
||||
<div className='relative w-[18px] h-[18px] rounded-full overflow-hidden border border-[#EDEEEF]'>
|
||||
|
||||
<img src='/providers/openai.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -216,8 +216,8 @@ const TextProvider = ({
|
|||
return (
|
||||
<div className="space-y-6 bg-[#F9F8F8] p-7 rounded-[12px] ">
|
||||
{/* API Key Input */}
|
||||
<div className="mb-4 flex items-center justify-between rounded-[12px] bg-white pt-5 pb-10 px-10">
|
||||
<div className=" max-w-[290px] pb-[50px]">
|
||||
<div className="mb-4 flex items-end justify-between rounded-[12px] bg-white pt-5 pb-10 px-10">
|
||||
<div className=" max-w-[290px] ">
|
||||
<div className='w-[60px] h-[60px] rounded-[4px] flex items-center justify-center'
|
||||
style={{ backgroundColor: '#4C55541A' }}
|
||||
>
|
||||
|
|
@ -232,11 +232,10 @@ const TextProvider = ({
|
|||
Choosing where text contets come from
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className='flex flex-col justify-end items-end gap-4'>
|
||||
<div className={`flex gap-4 justify-end ${selectedProvider === 'codex' ? 'items-end' : 'items-start'}`}>
|
||||
<div className={`relative ${selectedProvider === 'codex' ? 'w-[240px]' : 'w-[222px]'}`}>
|
||||
<div className="flex flex-col justify-start ">
|
||||
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Text Provider
|
||||
</label>
|
||||
|
|
@ -265,7 +264,7 @@ const TextProvider = ({
|
|||
<PopoverContent
|
||||
className="p-0"
|
||||
align="start"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
style={{ width: "300px" }}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search provider..." />
|
||||
|
|
@ -311,8 +310,6 @@ const TextProvider = ({
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div className={`relative flex flex-col justify-end ${selectedProvider === 'codex' ? 'items-end w-[262px] max-w-full' : 'items-end w-[222px]'}`}>
|
||||
<div className="flex flex-col justify-start w-full ">
|
||||
|
|
@ -431,89 +428,86 @@ const TextProvider = ({
|
|||
"Check models"
|
||||
)}
|
||||
</button>
|
||||
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Model Selection - only show if models are available */}
|
||||
{selectedProvider !== 'codex' && modelsChecked && availableModels.length > 0 ? (
|
||||
<div className="w-[222px]">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
{selectedProvider === 'ollama' ? 'Choose a supported model' : `Select ${modelLabel} Model`}
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<Popover
|
||||
open={openModelSelect}
|
||||
onOpenChange={setOpenModelSelect}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openModelSelect}
|
||||
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
|
||||
>
|
||||
<span className="text-sm truncate font-medium text-gray-900">
|
||||
{currentModel
|
||||
? availableModels.find(model => model === currentModel) || currentModel
|
||||
: "Select a model"}
|
||||
</span>
|
||||
|
||||
<ChevronUp className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
align="start"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
</div>
|
||||
{/* Model Selection - only show if models are available */}
|
||||
{selectedProvider !== 'codex' && modelsChecked && availableModels.length > 0 ? (
|
||||
<div className="w-[222px]">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
{selectedProvider === 'ollama' ? 'Choose a supported model' : `Select ${modelLabel} Model`}
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<Popover
|
||||
open={openModelSelect}
|
||||
onOpenChange={setOpenModelSelect}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openModelSelect}
|
||||
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search models..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableModels.map((model, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={model}
|
||||
onSelect={(value) => {
|
||||
if (currentModelField) {
|
||||
onInputChange(value, currentModelField);
|
||||
}
|
||||
setOpenModelSelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
currentModel === model
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{model}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm truncate font-medium text-gray-900">
|
||||
{currentModel
|
||||
? availableModels.find(model => model === currentModel) || currentModel
|
||||
: "Select a model"}
|
||||
</span>
|
||||
|
||||
<ChevronUp className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
align="start"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search models..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableModels.map((model, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={model}
|
||||
onSelect={(value) => {
|
||||
if (currentModelField) {
|
||||
onInputChange(value, currentModelField);
|
||||
}
|
||||
setOpenModelSelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
currentModel === model
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{model}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* Show message if no models found */}
|
||||
|
|
@ -526,7 +520,7 @@ const TextProvider = ({
|
|||
)}
|
||||
|
||||
|
||||
{/* Web Grounding Toggle - show at the end, below models dropdown */}
|
||||
|
||||
<div className="bg-white flex justify-between items-center p-10 rounded-[12px]">
|
||||
<div className=' max-w-[290px]'>
|
||||
|
||||
|
|
@ -536,7 +530,6 @@ const TextProvider = ({
|
|||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
<div className="w-[222px]">
|
||||
<div className="flex items-center mb-4 gap-2.5 ">
|
||||
<Switch
|
||||
|
|
@ -547,16 +540,9 @@ const TextProvider = ({
|
|||
Enable Web Grounding
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{/* <div className="w-[295px]"></div> */}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,76 +1,113 @@
|
|||
import { Card } from "@/components/ui/card";
|
||||
|
||||
export default function LoadingProfile() {
|
||||
function Shimmer({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className="h-screen bg-gradient-to-b font-instrument_sans from-gray-50 to-white flex flex-col overflow-hidden">
|
||||
{/* Header Skeleton */}
|
||||
<div className="flex-shrink-0 bg-white border-b border-gray-200 p-4">
|
||||
<div className="container mx-auto max-w-3xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-8 w-32 bg-gray-200 animate-pulse rounded-md" />
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-8 w-8 bg-gray-200 animate-pulse rounded-full" />
|
||||
<div className="h-8 w-24 bg-gray-200 animate-pulse rounded-md" />
|
||||
<div
|
||||
className={`bg-[#E1E1E5] animate-pulse rounded-md ${className ?? ""}`}
|
||||
aria-hidden
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoadingSettings() {
|
||||
return (
|
||||
<div className="h-screen font-syne flex flex-col overflow-hidden relative">
|
||||
<div
|
||||
className="fixed z-0 bottom-[-14.5rem] left-0 w-full h-full pointer-events-none"
|
||||
style={{
|
||||
height: "341px",
|
||||
borderRadius: "1440px",
|
||||
background:
|
||||
"radial-gradient(5.92% 104.69% at 50% 100%, rgba(122, 90, 248, 0.00) 0%, rgba(255, 255, 255, 0.00) 100%), radial-gradient(50% 50% at 50% 50%, rgba(122, 90, 248, 0.80) 0%, rgba(122, 90, 248, 0.00) 100%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<main className="w-full mx-auto gap-6 overflow-hidden flex">
|
||||
{/* SettingSideBar structure */}
|
||||
<div className="w-full max-w-[230px] h-screen px-4 pt-[22px] bg-[#F9FAFB] flex flex-col shrink-0">
|
||||
<div className="mt-[3.15rem] border-b border-[#E1E1E5] pb-3.5">
|
||||
<Shimmer className="h-3 w-16" />
|
||||
</div>
|
||||
<div className="mt-6 flex-1 min-h-0">
|
||||
<Shimmer className="h-3 w-24 mb-2.5" />
|
||||
<div className="p-0.5 rounded-[40px] bg-white w-full max-w-[210px] border border-[#EDEEEF] flex items-center mb-[34px] h-[30px]">
|
||||
<Shimmer className="h-[26px] flex-1 rounded-[70px] mx-0.5" />
|
||||
<Shimmer className="h-[26px] flex-1 rounded-[70px] mx-0.5 opacity-70" />
|
||||
</div>
|
||||
<Shimmer className="h-3 w-28 mb-2.5" />
|
||||
<div className="space-y-2.5">
|
||||
{[0, 1].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border border-[#EDEEEF] bg-white"
|
||||
>
|
||||
<Shimmer className="h-[18px] w-[18px] rounded-full shrink-0" />
|
||||
<Shimmer className="h-3 flex-1 max-w-[100px]" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-[#E1E1E5] py-5">
|
||||
<Shimmer className="h-3 w-12 mb-2.5" />
|
||||
<div className="w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border border-[#EDEEEF] bg-white">
|
||||
<Shimmer className="h-6 w-6 rounded-full shrink-0" />
|
||||
<Shimmer className="h-3 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Skeleton */}
|
||||
<main className="flex-1 container mx-auto px-4 max-w-3xl overflow-hidden flex flex-col">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* LLM Selection Content Skeleton */}
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Page Title */}
|
||||
<div className="space-y-2">
|
||||
<div className="h-8 w-48 bg-gray-200 animate-pulse rounded-md" />
|
||||
<div className="h-5 w-72 bg-gray-200 animate-pulse rounded-md" />
|
||||
{/* Main column — matches SettingPage + TextProvider default */}
|
||||
<div className="w-full min-w-0 flex flex-col">
|
||||
<div className="sticky top-0 right-0 z-50 py-[28px] backdrop-blur mb-4">
|
||||
<div className="flex gap-3 items-center flex-wrap">
|
||||
<Shimmer className="h-8 w-[132px] rounded-md" />
|
||||
<Shimmer className="h-[22px] w-[min(320px,55%)] rounded-[50px]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LLM Provider Cards */}
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<Card key={index} className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 bg-gray-200 animate-pulse rounded-md" />
|
||||
<div className="space-y-1">
|
||||
<div className="h-5 w-32 bg-gray-200 animate-pulse rounded-md" />
|
||||
<div className="h-4 w-48 bg-gray-200 animate-pulse rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-6 w-6 bg-gray-200 animate-pulse rounded-full" />
|
||||
</div>
|
||||
|
||||
{/* Configuration Fields */}
|
||||
<div className="space-y-4">
|
||||
{[...Array(2)].map((_, fieldIndex) => (
|
||||
<div key={fieldIndex} className="space-y-2">
|
||||
<div className="h-4 w-24 bg-gray-200 animate-pulse rounded-md" />
|
||||
<div className="h-10 w-full bg-gray-200 animate-pulse rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Model Selection */}
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="h-5 w-32 bg-gray-200 animate-pulse rounded-md" />
|
||||
<div className="h-10 w-full bg-gray-200 animate-pulse rounded-md" />
|
||||
<div className="space-y-6 bg-[#F9F8F8] p-7 rounded-[12px] pr-4 sm:pr-7">
|
||||
{/* TextProvider top card: white panel, icon + copy left, controls right */}
|
||||
<div className="mb-4 flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 rounded-[12px] bg-white pt-5 pb-10 px-6 sm:px-10">
|
||||
<div className="max-w-[290px] shrink-0">
|
||||
<Shimmer className="w-[60px] h-[60px] rounded-[4px]" />
|
||||
<Shimmer className="h-6 w-48 mt-2.5 mb-2" />
|
||||
<Shimmer className="h-4 w-full max-w-[260px]" />
|
||||
<Shimmer className="h-4 w-40 mt-1.5" />
|
||||
</div>
|
||||
</Card>
|
||||
<div className="flex flex-col items-stretch lg:items-end gap-4 flex-1 min-w-0">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:justify-end w-full">
|
||||
<div className="w-full sm:w-[222px]">
|
||||
<Shimmer className="h-4 w-36 mb-2" />
|
||||
<Shimmer className="h-12 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="w-full sm:w-[222px]">
|
||||
<Shimmer className="h-4 w-28 mb-2" />
|
||||
<Shimmer className="h-12 w-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full sm:w-[222px] sm:ml-auto">
|
||||
<Shimmer className="h-4 w-40 mb-2" />
|
||||
<Shimmer className="h-12 w-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TextProvider “Advanced” card */}
|
||||
<div className="bg-white flex flex-col sm:flex-row sm:justify-between sm:items-center gap-6 p-6 sm:p-10 rounded-[12px]">
|
||||
<div className="max-w-[290px] shrink-0">
|
||||
<Shimmer className="h-6 w-28 mb-2" />
|
||||
<Shimmer className="h-4 w-52" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 w-full sm:w-[222px] sm:justify-start">
|
||||
<Shimmer className="h-6 w-11 rounded-full shrink-0" />
|
||||
<Shimmer className="h-4 flex-1 max-w-[160px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Fixed Bottom Button Skeleton */}
|
||||
<div className="flex-shrink-0 bg-white border-t border-gray-200 p-4">
|
||||
<div className="container mx-auto max-w-3xl">
|
||||
<div className="h-12 w-full bg-gray-200 animate-pulse rounded-lg" />
|
||||
</div>
|
||||
{/* Fixed save button — matches SettingPage placement */}
|
||||
<div className="mx-auto fixed bottom-20 right-5 z-40">
|
||||
<Shimmer className="h-12 w-[200px] sm:w-[240px] rounded-[58px]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const CreateCustomTemplate = () => {
|
|||
trackEvent(MixpanelEvent.Templates_Build_Template_Clicked);
|
||||
router.push('/custom-template')
|
||||
}}
|
||||
className='w-full rounded-xl border border-[#EDEEEF] cursor-pointer font-syne'>
|
||||
className='w-full rounded-[22px] border border-[#EDEEEF] cursor-pointer font-syne'>
|
||||
<div className='relative h-[215px] flex justify-center items-center '>
|
||||
<img src="/card_bg.svg" alt="" className="absolute top-0 z-[1] left-0 w-full h-full object-cover" />
|
||||
<div className='w-[36px] h-[36px] relative z-[4] rounded-full bg-[#7A5AF8] flex items-center justify-center'
|
||||
|
|
@ -24,7 +24,7 @@ const CreateCustomTemplate = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-5 py-4 bg-white flex items-center gap-4 border-t border-[#EDEEEF]'>
|
||||
<div className='px-5 py-4 bg-white flex items-center gap-4 overflow-hidden border-t border-[#EDEEEF]'>
|
||||
<div className='bg-[#7A5AF8] w-[45px] h-[45px] rounded-lg p-2 flex items-center justify-center'>
|
||||
|
||||
<Sparkles className='w-6 h-6 text-white' />
|
||||
|
|
|
|||
|
|
@ -2,20 +2,24 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ArrowUpRight, ChevronRight, ExternalLink, Loader2, Plus } from "lucide-react";
|
||||
import { ArrowUpRight, ChevronRight, Loader2 } from "lucide-react";
|
||||
import { templates } from "@/app/presentation-templates";
|
||||
import { TemplateWithData, TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
|
||||
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
|
||||
import {
|
||||
useCustomTemplateSummaries,
|
||||
useCustomTemplatePreview,
|
||||
CustomTemplates,
|
||||
} from "@/app/hooks/useCustomTemplates";
|
||||
import { CompiledLayout } from "@/app/hooks/compileLayout";
|
||||
import CreateCustomTemplate from "./CreateCustomTemplate";
|
||||
import Link from "next/link";
|
||||
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
|
||||
import {
|
||||
TemplatePreviewStage,
|
||||
LayoutsBadge,
|
||||
InbuiltTemplatePreview,
|
||||
CustomTemplatePreview,
|
||||
} from "../../../components/TemplatePreviewComponents";
|
||||
|
||||
// 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, totalLayouts } = useCustomTemplatePreview(`${template.id}`);
|
||||
|
|
@ -26,73 +30,29 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ
|
|||
} else {
|
||||
router.push(`/template-preview?slug=custom-${template.id}`)
|
||||
}
|
||||
}
|
||||
, [router, template.id, template.name]);
|
||||
}, [router, template.id, template.name]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="cursor-pointer flex flex-col justify-between shadow-none sm:shadow-none relative hover:shadow-lg transition-all duration-200 group overflow-hidden"
|
||||
className="cursor-pointer flex flex-col shadow-none sm:shadow-none relative hover:shadow-sm transition-all duration-200 group overflow-hidden rounded-[22px] border border-[#E8E9EC] bg-white"
|
||||
onClick={handleOpen}
|
||||
>
|
||||
|
||||
<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">
|
||||
{totalLayouts} {totalLayouts === 1 ? 'Layout' : 'Layouts'}
|
||||
</span>
|
||||
<div className="p-5">
|
||||
|
||||
{/* Layout previews */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{loading ? (
|
||||
// Loading placeholders
|
||||
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
|
||||
<div
|
||||
key={`${template.id}-loading-${index}`}
|
||||
className="relative bg-linear-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.length > 0 && (
|
||||
// Actual layout previews
|
||||
previewLayouts.slice(0, 4).map((layout: CompiledLayout, 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 ">
|
||||
<h3 className="text-sm font-bold w-[191px] text-gray-900">
|
||||
{template.name}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
|
||||
</div>
|
||||
<TemplatePreviewStage>
|
||||
<LayoutsBadge count={totalLayouts} />
|
||||
<CustomTemplatePreview
|
||||
previewLayouts={previewLayouts}
|
||||
loading={loading}
|
||||
templateId={template.id}
|
||||
/>
|
||||
</TemplatePreviewStage>
|
||||
<div className="relative z-40 flex items-center justify-between border-t border-[#EDEEEF] bg-white px-6 py-5">
|
||||
<h3 className="max-w-[min(191px,65%)] text-base font-bold text-gray-900">{template.name}</h3>
|
||||
<ArrowUpRight className="h-4 w-4 shrink-0 text-gray-400 transition-colors group-hover:text-purple-600" />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}, (prev, next) => {
|
||||
// Custom templates may be refetched, producing new object references; compare on fields we render/use.
|
||||
return (
|
||||
prev.template.id === next.template.id &&
|
||||
prev.template.id === next.template.id &&
|
||||
prev.template.name === next.template.name &&
|
||||
prev.template.layoutCount === next.template.layoutCount
|
||||
|
|
@ -106,54 +66,24 @@ const InbuiltTemplateCard = React.memo(function InbuiltTemplateCard({
|
|||
template: TemplateLayoutsWithSettings;
|
||||
onOpen: (id: string) => void;
|
||||
}) {
|
||||
const previewLayouts = useMemo(() => template.layouts.slice(0, 4), [template.layouts]);
|
||||
const handleOpen = useCallback(() => onOpen(template.id), [onOpen, template.id]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={template.id}
|
||||
className="cursor-pointer relative sm:shadow-none shadow-none hover:shadow-lg transition-all duration-200 group overflow-hidden"
|
||||
className="group relative cursor-pointer overflow-hidden rounded-[22px] border border-[#E8E9EC] bg-white shadow-none sm:shadow-none transition-all duration-200 hover:shadow-sm"
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<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">
|
||||
{template.layouts.length} {template.layouts.length === 1 ? 'Layout' : 'Layouts'}
|
||||
</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 className="w-[191px]">
|
||||
|
||||
<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 className="flex items-center gap-2">
|
||||
|
||||
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
|
||||
<TemplatePreviewStage>
|
||||
<LayoutsBadge count={template.layouts.length} />
|
||||
<InbuiltTemplatePreview layouts={template.layouts} templateId={template.id} />
|
||||
</TemplatePreviewStage>
|
||||
<div className="relative z-40 flex items-center justify-between gap-4 border-t border-[#EDEEEF] bg-white px-6 py-5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-base font-bold capitalize text-gray-900">{template.name}</h3>
|
||||
<p className="mt-1 line-clamp-2 text-sm text-gray-500">{template.description}</p>
|
||||
</div>
|
||||
<ArrowUpRight className="h-4 w-4 shrink-0 text-gray-400 transition-colors group-hover:text-blue-600" />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
|
@ -254,7 +184,7 @@ const LayoutPreview = () => {
|
|||
{/* Inbuilt Templates Section: non-neo first, then Report (neo) */}
|
||||
{tab === 'default' && (
|
||||
<section className="my-12 space-y-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{nonNeoInbuilt.map((template) => (
|
||||
<InbuiltTemplateCard
|
||||
key={template.id}
|
||||
|
|
@ -268,7 +198,7 @@ const LayoutPreview = () => {
|
|||
<h4 className="text-base font-semibold text-[#101828] mb-6 font-syne tracking-tight">
|
||||
Report
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{neoInbuilt.map((template) => (
|
||||
<InbuiltTemplateCard
|
||||
key={template.id}
|
||||
|
|
@ -290,7 +220,7 @@ const LayoutPreview = () => {
|
|||
<span className="ml-3 text-gray-600">Loading custom templates...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 items-center lg:grid-cols-4 gap-6">
|
||||
<CreateCustomTemplate />
|
||||
{customTemplateCards}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
"use client";
|
||||
import React, { memo, useMemo } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { TemplateWithData } from "@/app/presentation-templates/utils";
|
||||
import { CompiledLayout } from "@/app/hooks/compileLayout";
|
||||
|
||||
|
||||
|
||||
|
||||
export function TemplatePreviewStage({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="relative overflow-hidden px-5 pb-5 pt-5 h-[230px]">
|
||||
<img
|
||||
src="/card_bg.svg"
|
||||
alt=""
|
||||
className="absolute top-0 left-0 w-full h-full object-cover"
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const LayoutsBadge = memo(function LayoutsBadge({ count }: { count: number }) {
|
||||
return (
|
||||
<span className="text-xs font-syne absolute top-3.5 left-4 z-40 inline-flex items-center rounded-full bg-[#333333] px-3 py-1 font-semibold text-white">
|
||||
Layouts-{count}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
export const ScaledSlidePreview = memo(function ScaledSlidePreview({
|
||||
children,
|
||||
id,
|
||||
index,
|
||||
isOutline = false,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
id: string;
|
||||
index: number;
|
||||
isOutline?: boolean;
|
||||
}) {
|
||||
const PREVIEW_SCALE = isOutline ? 0.2 : 0.24;
|
||||
const SLIDE_HEIGHT = 720 * PREVIEW_SCALE;
|
||||
const SLIDE_WIDTH = 1280;
|
||||
const SLIDE_NATIVE_HEIGHT = 720;
|
||||
return (
|
||||
<div
|
||||
key={`${id}-preview-${index}`}
|
||||
className="relative"
|
||||
style={{ height: `${SLIDE_HEIGHT}px`, overflow: "hidden" }}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-0 ${isOutline ? "left-0" : "left-8"} pointer-events-none`}
|
||||
style={{
|
||||
width: SLIDE_WIDTH,
|
||||
height: SLIDE_NATIVE_HEIGHT,
|
||||
transformOrigin: "top left",
|
||||
transform: `scale(${PREVIEW_SCALE})`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const InbuiltTemplatePreview = memo(function InbuiltTemplatePreview({
|
||||
layouts,
|
||||
templateId,
|
||||
isOutline = false,
|
||||
}: {
|
||||
layouts: TemplateWithData[];
|
||||
templateId: string;
|
||||
isOutline?: boolean;
|
||||
}) {
|
||||
const previewLayouts = useMemo(() => layouts.slice(0, 2), [layouts]);
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col gap-3 overflow-hidden">
|
||||
{previewLayouts.map((layout, index) => {
|
||||
const LayoutComponent = layout.component;
|
||||
return (
|
||||
<ScaledSlidePreview key={`${templateId}-preview-${index}`} id={templateId} index={index} isOutline={isOutline}>
|
||||
<LayoutComponent data={layout.sampleData} />
|
||||
</ScaledSlidePreview>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const CustomTemplatePreview = memo(function CustomTemplatePreview({
|
||||
previewLayouts,
|
||||
loading,
|
||||
templateId,
|
||||
isOutline = false,
|
||||
}: {
|
||||
previewLayouts: CompiledLayout[];
|
||||
loading: boolean;
|
||||
templateId: string;
|
||||
isOutline?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col gap-3">
|
||||
{loading ? (
|
||||
[...Array(2)].map((_, index) => (
|
||||
<div
|
||||
key={`${templateId}-loading-${index}`}
|
||||
className="relative w-full aspect-video flex items-center justify-center"
|
||||
>
|
||||
<Loader2 className="h-4 w-4 animate-spin text-slate-300" />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
previewLayouts.slice(0, 2).map((layout, index) => {
|
||||
const LayoutComponent = layout.component;
|
||||
return (
|
||||
<ScaledSlidePreview key={`${templateId}-preview-${index}`} id={templateId} index={index} isOutline={isOutline}>
|
||||
<LayoutComponent data={layout.sampleData} />
|
||||
</ScaledSlidePreview>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,94 +1,50 @@
|
|||
"use client";
|
||||
import React, { memo } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CustomTemplates, useCustomTemplatePreview } from "@/app/hooks/useCustomTemplates";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { CompiledLayout } from "@/app/hooks/compileLayout";
|
||||
import {
|
||||
TemplatePreviewStage,
|
||||
LayoutsBadge,
|
||||
CustomTemplatePreview,
|
||||
} from "../../components/TemplatePreviewComponents";
|
||||
|
||||
// Memoized preview component to prevent re-renders during scroll
|
||||
export const LayoutPreview = memo(({ layout, templateId, index }: { layout: CompiledLayout, templateId: string, index: number }) => {
|
||||
const LayoutComponent = layout.component;
|
||||
return (
|
||||
<div
|
||||
key={`${templateId}-preview-${index}`}
|
||||
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
|
||||
style={{ contain: 'layout style paint', willChange: 'auto' }}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
});
|
||||
LayoutPreview.displayName = 'LayoutPreview';
|
||||
|
||||
export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTemplate }: { template: CustomTemplates, onSelectTemplate: (template: string) => void, selectedTemplate: string | null }) => {
|
||||
|
||||
const { previewLayouts, loading: customLoading, totalLayouts } = useCustomTemplatePreview(template.id);
|
||||
export const CustomTemplateCard = memo(function CustomTemplateCard({
|
||||
template,
|
||||
onSelectTemplate,
|
||||
selectedTemplate,
|
||||
}: {
|
||||
template: CustomTemplates;
|
||||
onSelectTemplate: (template: string) => void;
|
||||
selectedTemplate: string | null;
|
||||
}) {
|
||||
const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(template.id);
|
||||
const isSelected = selectedTemplate === template.id;
|
||||
|
||||
return (
|
||||
|
||||
<Card
|
||||
className={`${isSelected ? 'border-2 border-blue-500' : ''} font-syne cursor-pointer flex flex-col justify-between relative hover:shadow-lg transition-all duration-200 group overflow-hidden`}
|
||||
className={cn(
|
||||
"font-syne cursor-pointer flex flex-col justify-between relative hover:shadow-sm transition-all duration-200 group overflow-hidden rounded-[22px] bg-white border",
|
||||
isSelected
|
||||
? " border-blue-500 ring-2 ring-blue-500/25 shadow-sm"
|
||||
: " border-[#E8E9EC]"
|
||||
)}
|
||||
onClick={() => onSelectTemplate(template.id)}
|
||||
>
|
||||
|
||||
<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- {totalLayouts}
|
||||
</span>
|
||||
<div className="p-5">
|
||||
|
||||
{/* 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.length > 0 && (
|
||||
// Actual layout previews
|
||||
previewLayouts.slice(0, 4).map((layout: CompiledLayout, 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 ">
|
||||
<h3 className="text-sm font-bold text-gray-900">
|
||||
<TemplatePreviewStage>
|
||||
<LayoutsBadge count={totalLayouts} />
|
||||
<CustomTemplatePreview
|
||||
previewLayouts={previewLayouts}
|
||||
loading={loading}
|
||||
templateId={template.id}
|
||||
isOutline={true}
|
||||
/>
|
||||
</TemplatePreviewStage>
|
||||
<div className="flex items-center justify-between px-6 py-5 bg-white border-t border-[#EDEEEF] relative z-40">
|
||||
<h3 className="text-sm font-bold text-gray-900 font-syne">
|
||||
{template.name}
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
CustomTemplateCard.displayName = 'CustomTemplateCard';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,72 +4,49 @@ import React, { useEffect, useMemo, useCallback, memo } from "react";
|
|||
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
|
||||
import { templates } from "@/app/presentation-templates";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { TemplateWithData } from "@/app/presentation-templates/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CustomTemplates, useCustomTemplateSummaries } from "@/app/hooks/useCustomTemplates";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { CustomTemplateCard } from "./CustomTemplateCard";
|
||||
|
||||
import CreateCustomTemplate from "../../(dashboard)/templates/components/CreateCustomTemplate";
|
||||
import { CustomTemplateCard } from "./CustomTemplateCard";
|
||||
import {
|
||||
TemplatePreviewStage,
|
||||
LayoutsBadge,
|
||||
InbuiltTemplatePreview,
|
||||
} from "../../components/TemplatePreviewComponents";
|
||||
|
||||
// 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 font-syne 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 }: {
|
||||
const BuiltInTemplateCard = memo(function BuiltInTemplateCard({
|
||||
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`}
|
||||
className={cn(
|
||||
"cursor-pointer relative hover:shadow-sm transition-all duration-200 group overflow-hidden rounded-[22px] bg-white border",
|
||||
isSelected
|
||||
? " border-blue-500 ring-2 ring-blue-500/25 shadow-sm"
|
||||
: " border-[#E8E9EC]"
|
||||
)}
|
||||
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">
|
||||
{template.layouts.length} {template.layouts.length === 1 ? 'Layout' : 'Layouts'}
|
||||
</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>
|
||||
<TemplatePreviewStage>
|
||||
<LayoutsBadge count={template.layouts.length} />
|
||||
<InbuiltTemplatePreview layouts={template.layouts} templateId={template.id} isOutline={true} />
|
||||
</TemplatePreviewStage>
|
||||
<div className="flex items-center justify-between px-6 py-5 bg-white border-t border-[#EDEEEF] relative z-40">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-sm font-bold text-gray-900 capitalize font-syne">
|
||||
{template.name}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-600 line-clamp-2 font-syne">
|
||||
<p className="text-xs text-gray-600 line-clamp-2 font-syne">
|
||||
{template.description}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -77,17 +54,16 @@ const BuiltInTemplateCard = memo(({ template, isSelected, onSelect }: {
|
|||
</Card>
|
||||
);
|
||||
});
|
||||
BuiltInTemplateCard.displayName = 'BuiltInTemplateCard';
|
||||
|
||||
interface TemplateSelectionProps {
|
||||
selectedTemplate: (TemplateLayoutsWithSettings | string) | null;
|
||||
onSelectTemplate: (template: TemplateLayoutsWithSettings | string) => void;
|
||||
}
|
||||
|
||||
const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
|
||||
const TemplateSelection: React.FC<TemplateSelectionProps> = memo(function TemplateSelection({
|
||||
selectedTemplate,
|
||||
onSelectTemplate
|
||||
}) => {
|
||||
onSelectTemplate,
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const existingScript = document.querySelector(
|
||||
'script[src*="tailwindcss.com"]'
|
||||
|
|
@ -102,50 +78,44 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
|
|||
|
||||
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),
|
||||
() => (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),
|
||||
() => (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 font-syne">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600 font-syne" />
|
||||
<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 (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<CreateCustomTemplate />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{customTemplates.map((template: CustomTemplates) => (
|
||||
<CustomTemplateCard
|
||||
key={template.id}
|
||||
|
|
@ -158,7 +128,6 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
|
|||
);
|
||||
}, [customLoading, customTemplates, handleCustomSelect, selectedCustomId]);
|
||||
|
||||
// Memoize the built-in templates list
|
||||
const builtInTemplateCards = useMemo(
|
||||
() =>
|
||||
templates.map((template: TemplateLayoutsWithSettings) => (
|
||||
|
|
@ -174,23 +143,20 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
|
|||
|
||||
return (
|
||||
<div className="space-y-[30px] mb-4">
|
||||
{/* Custom AI Templates */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-base font-semibold text-gray-900 font-syne">Custom</h3>
|
||||
</div>
|
||||
{customTemplateCards}
|
||||
</div>
|
||||
{/* In Built Templates */}
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-3 font-syne">In Built</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{builtInTemplateCards}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
TemplateSelection.displayName = 'TemplateSelection';
|
||||
|
||||
export default TemplateSelection;
|
||||
|
|
|
|||
|
|
@ -73,14 +73,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: Sor
|
|||
<V1ContentRender slide={slide} isEditMode={true} />
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className=" slide-box relative z-50 overflow-hidden aspect-video">
|
||||
<div className="absolute bg-transparent z-50 top-0 left-0 w-full h-full" />
|
||||
<div className="transform scale-[0.2] flex pointer-events-none justify-center items-center origin-top-left w-[500%] h-[500%]"
|
||||
|
||||
>
|
||||
<ContentRender slide={slide} isEditMode={true} />
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -403,7 +403,7 @@ export function useCustomTemplatePreview(presentationId: string) {
|
|||
setTotalLayouts(data.layouts.length);
|
||||
// Compile first 4 layouts for preview
|
||||
const compiled: CompiledLayout[] = [];
|
||||
const layoutsToPreview = data.layouts.slice(0, 4);
|
||||
const layoutsToPreview = data.layouts.slice(0, 2);
|
||||
|
||||
for (const layout of layoutsToPreview) {
|
||||
try {
|
||||
|
|
|
|||
BIN
electron/servers/nextjs/public/placeholder.jpg
Normal file
BIN
electron/servers/nextjs/public/placeholder.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
7
electron/servers/nextjs/public/placeholder.svg
Normal file
7
electron/servers/nextjs/public/placeholder.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256px" height="256px" viewBox="0 0 256 256" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 76.753906 52.871094 C 77.4375 52.867188 77.4375 52.867188 78.136719 52.863281 C 79.65625 52.859375 81.175781 52.859375 82.695312 52.863281 C 83.792969 52.859375 84.886719 52.859375 85.980469 52.855469 C 88.945312 52.847656 91.910156 52.847656 94.875 52.847656 C 97.355469 52.851562 99.832031 52.847656 102.3125 52.84375 C 108.496094 52.839844 114.679688 52.839844 120.863281 52.84375 C 125.890625 52.84375 130.917969 52.839844 135.941406 52.832031 C 141.789062 52.820312 147.640625 52.816406 153.488281 52.816406 C 156.578125 52.820312 159.667969 52.816406 162.761719 52.8125 C 165.667969 52.804688 168.578125 52.804688 171.488281 52.8125 C 172.550781 52.8125 173.613281 52.8125 174.679688 52.808594 C 183.050781 52.777344 190.167969 53.09375 196.644531 59.050781 C 201.476562 64.164062 203.160156 69.855469 203.128906 76.753906 C 203.132812 77.210938 203.132812 77.667969 203.136719 78.136719 C 203.140625 79.65625 203.140625 81.175781 203.136719 82.695312 C 203.140625 83.792969 203.140625 84.886719 203.144531 85.980469 C 203.152344 88.945312 203.152344 91.910156 203.152344 94.875 C 203.148438 97.355469 203.152344 99.832031 203.15625 102.3125 C 203.160156 108.496094 203.160156 114.679688 203.15625 120.863281 C 203.15625 125.890625 203.160156 130.917969 203.167969 135.941406 C 203.179688 141.789062 203.183594 147.640625 203.183594 153.488281 C 203.179688 156.578125 203.183594 159.667969 203.1875 162.761719 C 203.195312 165.667969 203.195312 168.578125 203.1875 171.488281 C 203.1875 172.550781 203.1875 173.613281 203.191406 174.679688 C 203.222656 183.050781 202.90625 190.167969 196.945312 196.644531 C 191.835938 201.476562 186.144531 203.160156 179.242188 203.128906 C 178.558594 203.132812 178.558594 203.132812 177.859375 203.136719 C 176.335938 203.140625 174.816406 203.140625 173.296875 203.136719 C 172.199219 203.140625 171.105469 203.140625 170.007812 203.144531 C 167.042969 203.152344 164.074219 203.152344 161.109375 203.152344 C 158.628906 203.148438 156.148438 203.152344 153.667969 203.15625 C 147.480469 203.160156 141.296875 203.160156 135.109375 203.15625 C 130.078125 203.15625 125.050781 203.160156 120.019531 203.167969 C 114.167969 203.179688 108.316406 203.183594 102.464844 203.183594 C 99.371094 203.179688 96.28125 203.183594 93.1875 203.1875 C 90.277344 203.195312 87.367188 203.195312 84.457031 203.1875 C 83.390625 203.1875 82.328125 203.1875 81.261719 203.191406 C 70.902344 203.230469 70.902344 203.230469 66.5 201.5 C 66.140625 201.359375 65.777344 201.21875 65.40625 201.078125 C 60.472656 199.027344 56.71875 194.75 54.5 190 C 52.453125 184.570312 52.789062 178.835938 52.804688 173.132812 C 52.804688 172.039062 52.800781 170.945312 52.796875 169.847656 C 52.792969 166.890625 52.792969 163.933594 52.796875 160.972656 C 52.800781 157.867188 52.796875 154.765625 52.796875 151.660156 C 52.792969 146.117188 52.796875 140.570312 52.804688 135.027344 C 52.8125 130.007812 52.8125 124.992188 52.804688 119.972656 C 52.796875 114.132812 52.792969 108.292969 52.796875 102.453125 C 52.800781 99.367188 52.800781 96.28125 52.796875 93.191406 C 52.792969 90.289062 52.792969 87.386719 52.804688 84.484375 C 52.804688 83.425781 52.804688 82.363281 52.800781 81.300781 C 52.777344 72.941406 53.101562 65.824219 59.050781 59.355469 C 64.164062 54.523438 69.855469 52.839844 76.753906 52.871094 Z M 75 75 C 75 109.980469 75 144.960938 75 181 C 109.980469 181 144.960938 181 181 181 C 181 146.019531 181 111.039062 181 75 C 146.019531 75 111.039062 75 75 75 Z M 75 75 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 96 96 C 117.121094 96 138.238281 96 160 96 C 160 117.121094 160 138.238281 160 160 C 138.878906 160 117.761719 160 96 160 C 96 138.878906 96 117.761719 96 96 Z M 96 96 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
Loading…
Add table
Reference in a new issue