fix: change 'layout' to 'layout'

This commit is contained in:
Suraj Jha 2025-08-09 19:42:21 +05:45
parent 3b49264b24
commit 36bbefa5aa
No known key found for this signature in database
GPG key ID: 5AC6C16355CE2C14
39 changed files with 224 additions and 67 deletions

View file

@ -0,0 +1,18 @@
import React from "react";
import Header from "@/components/Header";
export const OpenAIKeyWarning: React.FC = () => {
return (
<div className="min-h-screen font-roboto bg-gradient-to-br from-slate-50 to-slate-100">
<Header />
<div className="flex items-center justify-center aspect-video mx-auto px-6">
<div className="text-center space-y-2 my-6 bg-white p-10 rounded-lg shadow-lg">
<h1 className="text-xl font-bold text-gray-900">Please add your OpenAI API Key to process the layout</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
This feature requires an OpenAI model GPT-5. Configure your key in settings or via environment variables.
</p>
</div>
</div>
</div>
);
};

View file

@ -25,12 +25,12 @@ export const SaveLayoutButton: React.FC<SaveLayoutButtonProps> = ({
{isSaving ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Saving Layout...
Saving Template...
</>
) : (
<>
<FileText className="w-5 h-5 mr-2" />
Save Layout
Save Template
</>
)}
</Button>

View file

@ -53,22 +53,22 @@ export const SaveLayoutModal: React.FC<SaveLayoutModalProps> = ({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Save className="w-5 h-5 text-green-600" />
Save Layout
Save Template
</DialogTitle>
<DialogDescription>
Enter a name and description for your layout. This will help you identify it later.
Enter a name and description for your template. This will help you identify it later.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="layout-name" className="text-sm font-medium">
Layout Name *
Template Name *
</Label>
<Input
id="layout-name"
value={layoutName}
onChange={(e) => setLayoutName(e.target.value)}
placeholder="Enter layout name..."
placeholder="Enter template name..."
disabled={isSaving}
className="w-full"
/>
@ -81,7 +81,7 @@ export const SaveLayoutModal: React.FC<SaveLayoutModalProps> = ({
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter a description for your layout..."
placeholder="Enter a description for your template..."
disabled={isSaving}
className="w-full resize-none"
rows={3}
@ -109,7 +109,7 @@ export const SaveLayoutModal: React.FC<SaveLayoutModalProps> = ({
) : (
<>
<Save className="w-4 h-4 mr-2" />
Save Layout
Save Template
</>
)}
</Button>

View file

@ -0,0 +1,18 @@
import { useState, useEffect } from "react";
export const useOpenAIKeyCheck = () => {
const [hasOpenAIKey, setHasOpenAIKey] = useState(false);
const [isOpenAIKeyLoading, setIsOpenAIKeyLoading] = useState(true);
useEffect(() => {
fetch("/api/has-openai-key")
.then((res) => res.json())
.then((data) => {
setHasOpenAIKey(Boolean(data.hasKey));
setIsOpenAIKeyLoading(false);
})
.catch(() => setIsOpenAIKeyLoading(false));
}, []);
return { hasOpenAIKey, isOpenAIKeyLoading };
};

View file

@ -125,7 +125,7 @@ const CustomLayoutPage = () => {
</div>
)}
{/* Floating Save Layout Button */}
{/* Floating Save Template Button */}
{slides.length > 0 && slides.some((s) => s.processed) && (
<SaveLayoutButton
onSave={openSaveModal}
@ -134,7 +134,7 @@ const CustomLayoutPage = () => {
/>
)}
{/* Save Layout Modal */}
{/* Save Template Modal */}
<SaveLayoutModal
isOpen={isModalOpen}
onClose={closeSaveModal}

View file

@ -25,13 +25,13 @@ const Header = () => {
</div>
<div className="flex items-center gap-3">
<Link
href="/layout-preview"
href="/template-preview"
prefetch={false}
className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none"
role="menuitem"
>
<Layout className="w-5 h-5" />
<span className="text-sm font-medium font-inter">Layouts</span>
<span className="text-sm font-medium font-inter">Templates</span>
</Link>
<HeaderNav />
</div>

View file

@ -23,7 +23,7 @@ const GenerateButton: React.FC<GenerateButtonProps> = ({
const getButtonText = () => {
if (loadingState.isLoading) return loadingState.message;
if (streamState.isLoading || streamState.isStreaming) return "Loading...";
if (!selectedLayoutGroup) return "Select a Layout Style";
if (!selectedLayoutGroup) return "Select a Templae";
return "Generate Presentation";
};

View file

@ -82,10 +82,10 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
<div className="space-y-6">
<div className="text-center py-8">
<h5 className="text-lg font-medium mb-2 text-gray-700">
No Layout Styles Available
No Templates Available
</h5>
<p className="text-gray-600 text-sm">
No presentation layout styles could be loaded. Please try refreshing the page.
No presentation templates could be loaded. Please try refreshing the page.
</p>
</div>
</div>

View file

@ -51,7 +51,7 @@ const OutlinePage: React.FC = () => {
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
<TabsList className="grid w-[50%] mx-auto my-4 grid-cols-2">
<TabsTrigger value={TABS.OUTLINE}>Outline & Content</TabsTrigger>
<TabsTrigger value={TABS.LAYOUTS}>Layout Style</TabsTrigger>
<TabsTrigger value={TABS.LAYOUTS}>Select Template</TabsTrigger>
</TabsList>
<div className="flex-grow w-full overflow-y-auto custom_scrollbar">

View file

@ -44,7 +44,7 @@ const GroupLayoutPreview = () => {
method: "DELETE",
});
if (response.ok) {
router.push("/layout-preview");
router.push("/template-preview");
}
}
return (
@ -66,7 +66,7 @@ const GroupLayoutPreview = () => {
<Button
variant="outline"
size="sm"
onClick={() => router.push("/layout-preview")}
onClick={() => router.push("/template-preview")}
className="flex items-center gap-2"
>
<Home className="w-4 h-4" />

View file

@ -23,9 +23,9 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
setLoading(true)
setError(null)
const response = await fetch('/api/layouts')
const response = await fetch('/api/templates')
if (!response.ok) {
toast.error('Error loading layouts', {
toast.error('Error loading templates', {
description: response.statusText,
})
return
@ -38,7 +38,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
const groupLayouts: LayoutInfo[] = []
const groupSettings: GroupSetting = groupData.settings ? groupData.settings : {
description: `${groupData.groupName} presentation layouts`,
description: `${groupData.groupName} presentation templates`,
ordered: false,
default: false
}
@ -50,7 +50,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
if (!module.default) {
toast.error(`${layoutName} has no default export`, {
description: 'Please ensure the layout file exports a default component',
description: 'Please ensure the template file exports a default component',
})
console.warn(`${layoutName} has no default export`)
throw new Error(`${layoutName} has no default export`)
@ -58,14 +58,12 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
if (!module.Schema) {
toast.error(`${layoutName} is missing required Schema export`, {
description: 'Please ensure the layout file exports a Schema',
description: 'Please ensure the template file exports a Schema',
})
console.error(`${layoutName} is missing required Schema export`)
throw new Error(`${layoutName} is missing required Schema export`)
}
// Use empty object to let schema apply its default values
// User will need to provide actual data when using the layouts
const sampleData = module.Schema.parse({})
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
@ -85,15 +83,12 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
} catch (importError) {
console.error(`Failed to import ${fileName} from ${groupData.groupName}:`, importError)
// Try alternative import path
try {
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`)
if (module.default && module.Schema) {
// Use empty object to let schema apply its default values
const sampleData = module.Schema.parse({})
// if layoutId is not provided, use the layoutName
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
const layoutInfo: LayoutInfo = {
@ -108,7 +103,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
groupLayouts.push(layoutInfo)
allLayouts.push(layoutInfo)
} else {
console.error(`${layoutName} is missing required exports (default component or Schema)`)
console.error(`${layoutName} is missing required exports (default component or Schema)`)
}
} catch (altError) {
console.error(`Alternative import also failed for ${fileName} from ${groupData.groupName}:`, altError)
@ -126,10 +121,10 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
}
if (allLayouts.length === 0) {
toast.error('No valid layouts found', {
description: 'Make sure your layout files export both a default component and a Schema.',
toast.error('No valid templates found', {
description: 'Make sure your template files export both a default component and a Schema.',
})
setError('No valid layouts found. Make sure your layout files export both a default component and a Schema.')
setError('No valid templates found. Make sure your template files export both a default component and a Schema.')
} else {
setLayoutGroups(loadedGroups)
setLayouts(allLayouts)
@ -137,8 +132,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
}
} catch (error) {
console.error('Error loading layouts:', error)
setError(error instanceof Error ? error.message : 'Failed to load layouts')
console.error('Error loading templates:', error)
setError(error instanceof Error ? error.message : 'Failed to load templates')
} finally {
setLoading(false)
}

View file

@ -74,7 +74,7 @@ const LayoutPreview = () => {
key={group.groupName}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() =>
router.push(`/layout-preview/${group.groupName}`)
router.push(`/template-preview/${group.groupName}`)
}
>
<div className="p-6">

View file

@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import fs from "fs";
export const dynamic = "force-dynamic";
export async function GET() {
const userConfigPath = process.env.USER_CONFIG_PATH;
let keyFromFile = "";
if (userConfigPath && fs.existsSync(userConfigPath)) {
try {
const raw = fs.readFileSync(userConfigPath, "utf-8");
const cfg = JSON.parse(raw || "{}");
keyFromFile = cfg?.OPENAI_API_KEY || "";
} catch {}
}
console.log(keyFromFile);
const keyFromEnv = process.env.OPENAI_API_KEY || "";
console.log(keyFromEnv);
const hasKey = Boolean((keyFromFile || keyFromEnv).trim());
return NextResponse.json({ hasKey });
}

View file

@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'
import { promises as fs } from 'fs'
import path from 'path'
import { GroupSetting } from '@/app/(presentation-generator)/layout-preview/types'
import { GroupSetting } from '@/app/(presentation-generator)/template-preview/types'
export async function GET() {
try {

View file

@ -0,0 +1,60 @@
import { NextResponse } from "next/server";
import puppeteer from "puppeteer";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const groupName = searchParams.get("group");
if (!groupName) {
return NextResponse.json({ error: "Missing group name" }, { status: 400 });
}
const schemaPageUrl = `http://localhost/schema?group=${encodeURIComponent(groupName)}`;
let browser;
try {
browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox", "--disable-web-security"] });
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720 });
await page.goto(schemaPageUrl, { waitUntil: "networkidle0", timeout: 80000 });
await page.waitForSelector("[data-layouts]", { timeout: 30000 });
const { dataLayouts, dataGroupSettings } = await page.$eval(
"[data-layouts]",
(el) => ({
dataLayouts: el.getAttribute("data-layouts"),
dataGroupSettings: el.getAttribute("data-group-settings"),
})
);
let slides, groupSettings;
try {
slides = JSON.parse(dataLayouts || "[]");
} catch (e) {
slides = [];
}
try {
groupSettings = JSON.parse(dataGroupSettings || "null");
} catch (e) {
groupSettings = null;
}
const response = {
name: groupName,
ordered: groupSettings?.ordered ?? false,
slides: slides.map((slide: any) => ({
id: slide.id,
name: slide.name,
description: slide.description,
json_schema: slide.json_schema,
})),
};
return NextResponse.json(response);
} catch (err) {
return NextResponse.json({ error: "Failed to fetch or parse client page" }, { status: 500 });
} finally {
if (browser) await browser.close();
}
}

View file

@ -0,0 +1,57 @@
import { NextResponse } from 'next/server'
import { promises as fs } from 'fs'
import path from 'path'
import { GroupSetting } from '@/app/(presentation-generator)/template-preview/types'
export async function GET() {
try {
const layoutsDirectory = path.join(process.cwd(), 'presentation-layouts')
const items = await fs.readdir(layoutsDirectory, { withFileTypes: true })
const groupDirectories = items.filter(item => item.isDirectory()).map(dir => dir.name)
const allLayouts: { groupName: string; files: string[]; settings: GroupSetting | null }[] = []
for (const groupName of groupDirectories) {
try {
const groupPath = path.join(layoutsDirectory, groupName)
const groupFiles = await fs.readdir(groupPath)
const layoutFiles = groupFiles.filter(file =>
file.endsWith('.tsx') &&
!file.startsWith('.') &&
!file.includes('.test.') &&
!file.includes('.spec.') &&
file !== 'settings.json'
)
let settings: GroupSetting | null = null
const settingsPath = path.join(groupPath, 'settings.json')
try {
const settingsContent = await fs.readFile(settingsPath, 'utf-8')
settings = JSON.parse(settingsContent) as GroupSetting
} catch {
settings = {
description: `${groupName} presentation templates`,
ordered: false,
default: false,
}
}
if (layoutFiles.length > 0) {
allLayouts.push({ groupName, files: layoutFiles, settings })
}
} catch (error) {
console.error(`Error reading group directory ${groupName}:`, error)
}
}
return NextResponse.json(allLayouts)
} catch (error) {
console.error('Error reading presentation-layouts directory:', error)
return NextResponse.json(
{ error: 'Failed to read presentation layouts directory' },
{ status: 500 }
)
}
}

View file

@ -1,43 +1,27 @@
"use client";
import Wrapper from "@/components/Wrapper";
import React from "react";
import Link from "next/link";
import BackBtn from "@/components/BackBtn";
import { usePathname } from "next/navigation";
import HeaderNav from "@/app/(presentation-generator)/components/HeaderNab";
import { Layout } from "lucide-react";
const Header = () => {
const pathname = usePathname();
const Header: React.FC = () => {
return (
<div className="bg-[#5146E5] w-full shadow-lg sticky top-0 z-50">
<Wrapper>
<div className="flex items-center justify-between py-1">
<div className="flex items-center gap-3">
{pathname !== "/upload" && <BackBtn />}
<Link href="/dashboard">
<img
src="/logo-white.png"
alt="Presentation logo"
className="h-16"
/>
</Link>
</div>
<div className="flex items-center gap-3">
<Link
href="/layout-preview"
prefetch={false}
className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none"
role="menuitem"
>
<header className="w-full border-b bg-white/60 backdrop-blur supports-[backdrop-filter]:bg-white/60 sticky top-0 z-50">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<Link href="/" className="flex items-center gap-2">
<img src="/logo-white.png" alt="Presenton" className="h-6 w-auto" />
</Link>
<nav className="flex items-center gap-4">
<Link href="/template-preview" className="inline-flex items-center gap-2 text-gray-700 hover:text-gray-900">
<Layout className="w-5 h-5" />
<span className="text-sm font-medium font-inter">Layouts</span>
<span className="text-sm font-medium font-inter">Templates</span>
</Link>
<HeaderNav />
</div>
</nav>
</div>
</Wrapper>
</div>
</div>
</header>
);
};