nextjs api router transfer to electron

This commit is contained in:
shiva raj badu 2025-05-12 13:24:28 +05:45
parent 3e34f06093
commit 14442072e3
24 changed files with 215 additions and 913 deletions

View file

@ -0,0 +1,32 @@
import { ipcMain } from 'electron';
import { settingsStore } from '../settings-store';
const FOOTER_KEY = 'footer';
export function setupFooterHandlers() {
ipcMain.handle('get-footer', async () => {
try {
const properties = settingsStore.get(FOOTER_KEY);
console.log('Getting footer properties:', properties);
return { properties };
} catch (error) {
console.error('Error retrieving footer properties:', error);
throw error;
}
});
ipcMain.handle('set-footer', async (_, properties: any) => {
try {
if (!properties) {
throw new Error('Properties are required');
}
console.log('Setting footer properties:', properties);
settingsStore.set(FOOTER_KEY, properties);
return { success: true };
} catch (error) {
console.error('Error saving footer properties:', error);
throw error;
}
});
}

View file

@ -1,8 +1,15 @@
import { setupExportHandlers } from "./export_handlers";
import { setupUserConfigHandlers } from "./user_config_handlers";
import { setupSlideMetadataHandlers } from "./slide_metadata";
import { setupReadFile } from "./read_file";
import { setupFooterHandlers } from "./footer_handlers";
import { setupThemeHandlers } from "./theme_handlers";
export function setupIpcHandlers() {
setupExportHandlers();
setupUserConfigHandlers();
setupSlideMetadataHandlers();
setupReadFile();
setupFooterHandlers();
setupThemeHandlers();
}

View file

@ -86,19 +86,10 @@ interface SlideMetadata {
elements: SlideElement[];
}
interface ThemeParams {
theme: string;
customColors?: {
slideBg: string;
slideTitle: string;
slideHeading: string;
slideDescription: string;
slideBox: string;
};
}
export function setupSlideMetadataHandlers() {
ipcMain.handle("get-slide-metadata", async (_, url: string, theme: string, customColors?: ThemeParams["customColors"]) => {
ipcMain.handle("get-slide-metadata", async (_, url: string, theme: string, customColors?: any) => {
let browser;
try {
browser = await puppeteer.launch({
@ -108,12 +99,18 @@ export function setupSlideMetadataHandlers() {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
// Inject the environment variables before loading the page
await page.evaluateOnNewDocument(`
window.env = {
NEXT_PUBLIC_FAST_API: "${process.env.NEXT_PUBLIC_FAST_API}",
};
`);
await page.goto(url, { waitUntil: "networkidle0", timeout: 60000 });
await page.waitForSelector('[data-element-type="slide-container"]', { timeout: 80000 });
// Apply theme
await page.evaluate((params: ThemeParams) => {
await page.evaluate((params: any) => {
const { theme, customColors } = params;
document.querySelectorAll(".slide-theme").forEach((container) => {
container.setAttribute("data-theme", theme);
@ -245,7 +242,6 @@ export function setupSlideMetadataHandlers() {
const filename = `chart-${graphId}-${Date.now()}.jpg`;
const filePath = path.join(tempDir, filename);
fs.writeFileSync(filePath, Buffer.from(screenshot, 'base64'));
metadata.forEach(slide => {

32
app/ipc/theme_handlers.ts Normal file
View file

@ -0,0 +1,32 @@
import { ipcMain } from 'electron';
import { settingsStore } from '../settings-store';
const THEME_KEY = 'theme';
export function setupThemeHandlers() {
ipcMain.handle('get-theme', async () => {
try {
const theme = settingsStore.get(THEME_KEY);
console.log('Getting theme:', theme);
return { theme };
} catch (error) {
console.error('Error retrieving theme:', error);
throw error;
}
});
ipcMain.handle('set-theme', async (_, themeData: any) => {
try {
if (!themeData) {
throw new Error('Theme data is required');
}
console.log('Setting theme:', themeData);
settingsStore.set(THEME_KEY, themeData);
return { success: true };
} catch (error) {
console.error('Error saving theme:', error);
throw error;
}
});
}

View file

@ -13,5 +13,10 @@ contextBridge.exposeInMainWorld('electron', {
getUserConfig: () => ipcRenderer.invoke("get-user-config"),
setUserConfig: (userConfig: UserConfig) => ipcRenderer.invoke("set-user-config", userConfig),
readFile: (filePath: string) => ipcRenderer.invoke("read-file", filePath),
getSlideMetadata: (url: string, theme: string, customColors?: any) => ipcRenderer.invoke("get-slide-metadata", url, theme, customColors),
getSlideMetadata: (url: string, theme: string, customColors?: any, tempDirectory?: string) =>
ipcRenderer.invoke("get-slide-metadata", url, theme, customColors, tempDirectory),
getFooter: (userId: string) => ipcRenderer.invoke("get-footer", userId),
setFooter: (userId: string, properties: any) => ipcRenderer.invoke("set-footer", userId, properties),
getTheme: (userId: string) => ipcRenderer.invoke("get-theme", userId),
setTheme: (userId: string, themeData: any) => ipcRenderer.invoke("set-theme", userId, themeData),
});

68
app/settings-store.ts Normal file
View file

@ -0,0 +1,68 @@
import { app } from 'electron';
import path from 'path';
import fs from 'fs';
class SettingsStore {
private settingsPath: string;
private settings: { [key: string]: any };
constructor() {
this.settingsPath = path.join(app.getPath('userData'), 'settings.json');
this.settings = {};
this.loadSettings();
}
private loadSettings() {
try {
if (fs.existsSync(this.settingsPath)) {
const data = fs.readFileSync(this.settingsPath, 'utf-8');
this.settings = JSON.parse(data);
} else {
this.settings = {};
this.saveSettings();
}
} catch (error) {
console.error('Error loading settings:', error);
this.settings = {};
}
}
private saveSettings() {
try {
fs.writeFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2));
} catch (error) {
console.error('Error saving settings:', error);
throw error;
}
}
get(key: string, defaultValue: any = null): any {
const value = this.settings[key];
return value || defaultValue;
}
set(key: string, value: any): void {
this.settings[key] = value;
this.saveSettings();
}
// Helper method to check if settings exist
has(key: string): boolean {
return key in this.settings;
}
// Helper method to delete a setting
delete(key: string): void {
delete this.settings[key];
this.saveSettings();
}
}
// Export a singleton instance
export const settingsStore = new SettingsStore();

View file

@ -11,7 +11,7 @@ import {
} from "../store/themeSlice";
import { ThemeType } from "../upload/type";
import { useThemeService, ThemeColors } from "../services/themeSqliteService";
import { useThemeService, ThemeColors } from "../services/themeService";
interface CustomThemeSettingsProps {
onClose?: () => void;
@ -23,12 +23,16 @@ const CustomThemeSettings = ({
presentationId,
}: CustomThemeSettingsProps) => {
const dispatch = useDispatch();
const { currentColors } = useSelector((state: RootState) => state.theme);
const [draftColors, setDraftColors] = useState<ThemeColors>({
...currentColors,
iconBg: currentColors.iconBg || "#1F1F2D",
chartColors: currentColors.chartColors || ["#1F1F2D"],
fontFamily: currentColors.fontFamily || "var(--font-inter)",
background: "#63ceff",
slideBg: "#F4F4F4",
slideTitle: "#1A1A1A",
slideHeading: "#2D2D2D",
slideDescription: "#4A4A4A",
slideBox: "#d8c6c6",
iconBg: "#281810",
chartColors: ["#281810", "#4A3728", "#665E57", "#665E57", "#665E57"],
fontFamily: "var(--font-inter)",
});
const themeService = useThemeService();
@ -132,7 +136,6 @@ const CustomThemeSettings = ({
dispatch(setLoadingState(true));
const savedTheme = await themeService.getTheme();
if (savedTheme) {
// dispatch(loadSavedTheme(savedTheme));
setDraftColors(savedTheme.colors);
}
} catch (error) {

View file

@ -4,7 +4,7 @@ import React, { createContext, useContext, useState, useEffect } from "react";
import {
FooterProperties,
useFooterService,
} from "../services/footerSqliteService";
} from "../services/footerService";
// Default footer properties
export const defaultFooterProperties: FooterProperties = {
@ -52,13 +52,12 @@ export const FooterProvider: React.FC<{ children: React.ReactNode }> = ({
}) => {
const [footerProperties, setFooterProperties] = useState<FooterProperties>(defaultFooterProperties);
const footerService = useFooterService();
const userId = "local-user"; // Since this is a desktop app, we can use a fixed ID
// Load footer properties only once when the provider mounts
useEffect(() => {
const loadFooterProperties = async () => {
try {
const properties = await footerService.getFooterProperties(userId);
const properties = await footerService.getFooterProperties();
if (properties) {
setFooterProperties(properties);
}
@ -70,20 +69,9 @@ export const FooterProvider: React.FC<{ children: React.ReactNode }> = ({
loadFooterProperties();
}, []); // Empty dependency array ensures this runs only once
// const updateFooterProperties = async (newProperties: FooterProperties) => {
// try {
// const success = await footerService.saveFooterProperties(userId, newProperties);
// if (success) {
// setFooterProperties(newProperties);
// }
// } catch (error) {
// console.error("Failed to update footer properties:", error);
// }
// };
const resetFooterProperties = async () => {
try {
const success = await footerService.resetFooterProperties(userId, defaultFooterProperties);
const success = await footerService.resetFooterProperties(defaultFooterProperties);
if (success) {
setFooterProperties(defaultFooterProperties);
}
@ -91,9 +79,10 @@ export const FooterProvider: React.FC<{ children: React.ReactNode }> = ({
console.error("Failed to reset footer properties:", error);
}
};
const saveFooterProperties = async (newProperties: FooterProperties) => {
try {
const success = await footerService.saveFooterProperties(userId, newProperties);
const success = await footerService.saveFooterProperties(newProperties);
if (success) {
setFooterProperties(newProperties);
}
@ -106,7 +95,6 @@ export const FooterProvider: React.FC<{ children: React.ReactNode }> = ({
<FooterContext.Provider
value={{
footerProperties,
// updateFooterProperties,
setFooterProperties,
resetFooterProperties,
saveFooterProperties,

View file

@ -25,11 +25,11 @@ import { useRouter } from "next/navigation";
import { RootState } from "@/store/store";
import { Button } from "@/components/ui/button";
import { toast } from "@/hooks/use-toast";
import Header from "@/app/dashboard/components/Header";
import MarkdownRenderer from "./MarkdownRenderer";
import { getIconFromFile, removeUUID } from "../../utils/others";
import { ChevronRight, PanelRightOpen, X } from "lucide-react";
import ToolTip from "@/components/ToolTip";
import Header from "@/app/dashboard/components/Header";
// Types
interface LoadingState {
@ -350,7 +350,6 @@ const DocumentsPreviewPage: React.FC = () => {
showProgress={showLoading.progress}
duration={showLoading.duration}
/>
<Header />
<div className="flex mt-6 gap-4">
{!isOpen && (

View file

@ -1,38 +0,0 @@
'use client'
import Wrapper from '@/components/Wrapper'
import React from 'react'
import Image from 'next/image'
import Link from 'next/link'
import UserAccount from '@/app/(presentation-generator)/components/UserAccount'
import { usePathname } from 'next/navigation'
import BackBtn from '@/components/BackBtn'
const Header = ({ children }: { children: any }) => {
const pathname = usePathname();
return (
<div className='bg-[#5146E5] w-full shadow-lg sticky top-0 z-50'>
<Wrapper>
<div className='flex items-center justify-between py-2'>
<div className='flex items-center gap-3'>
<BackBtn />
<Link href="/">
<Image
src="/logo-white.png"
alt="Presentation logo"
width={162}
height={32}
priority
/>
</Link>
</div>
<div className='flex items-center gap-3 sm:gap-5 md:gap-10'>
{children}
<UserAccount />
</div>
</div>
</Wrapper>
</div>
)
}
export default Header

View file

@ -131,28 +131,15 @@ const Header = ({
try {
// Get the current URL without any query parameters
const baseUrl = window.location.origin + window.location.pathname;
const urls = getEnv();
const response = await fetch(
`${urls.NEXT_PUBLIC_URL}/api/slide-metadata`,
{
method: "POST",
headers: getHeader(),
body: JSON.stringify({
url: baseUrl,
theme: currentTheme,
customColors: currentColors,
tempDirectory: urls.TEMP_DIRECTORY,
baseUrl: urls.BASE_URL,
}),
}
// @ts-ignore
const metadata = await window.electron.getSlideMetadata(
baseUrl,
currentTheme,
currentColors,
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to fetch metadata");
}
const metadata = await response.json();
console.log("metadata", metadata);
return metadata;
} catch (error) {
@ -269,7 +256,7 @@ const Header = ({
variant="ghost"
className="pb-4 border-b rounded-none border-gray-300 w-full flex justify-start text-[#5146E5]"
>
<Image src="/pdf.svg" alt="pdf export" width={30} height={30} />
<img src="/pdf.svg" alt="pdf export" width={30} height={30} />
Export as PDF
</Button>
<Button
@ -277,7 +264,7 @@ const Header = ({
variant="ghost"
className="w-full flex justify-start text-[#5146E5]"
>
<Image src="/pptx.svg" alt="pptx export" width={30} height={30} />
<img src="/pptx.svg" alt="pptx export" width={30} height={30} />
Export as PPTX
</Button>
</div>
@ -329,12 +316,11 @@ const Header = ({
<Announcement />
<Wrapper className="flex items-center justify-between py-2">
<Link href="/dashboard" className="min-w-[162px]">
<Image
<img
src="/logo-white.png"
alt="Presentation logo"
width={162}
height={32}
priority
/>
</Link>

View file

@ -1,4 +1,3 @@
// app/(presentation-generator)/services/footerService.ts
import { useCallback } from "react";
export interface FooterProperties {
@ -28,20 +27,11 @@ export interface FooterProperties {
export const useFooterService = () => {
// Get footer properties
const getFooterProperties = useCallback(
async (userId: string): Promise<FooterProperties | null> => {
async (): Promise<FooterProperties | null> => {
try {
const response = await fetch(
`/api/footer?userId=${encodeURIComponent(userId)}`
);
if (!response.ok) {
throw new Error(
`Failed to fetch footer properties: ${response.status}`
);
}
const data = await response.json();
return data.properties;
// @ts-ignore
const result = await window.electron.getFooter();
return result.properties;
} catch (error) {
console.error("Error retrieving footer properties:", error);
return null;
@ -52,24 +42,11 @@ export const useFooterService = () => {
// Save footer properties
const saveFooterProperties = useCallback(
async (userId: string, properties: FooterProperties): Promise<boolean> => {
async (properties: FooterProperties): Promise<boolean> => {
try {
const response = await fetch("/api/footer", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId, properties }),
});
if (!response.ok) {
throw new Error(
`Failed to save footer properties: ${response.status}`
);
}
const data = await response.json();
return data.success;
// @ts-ignore
const result = await window.electron.setFooter(properties);
return result.success;
} catch (error) {
console.error("Error saving footer properties:", error);
return false;
@ -80,11 +57,8 @@ export const useFooterService = () => {
// Reset footer properties
const resetFooterProperties = useCallback(
async (
userId: string,
defaultProperties: FooterProperties
): Promise<boolean> => {
return saveFooterProperties(userId, defaultProperties);
async (defaultProperties: FooterProperties): Promise<boolean> => {
return saveFooterProperties(defaultProperties);
},
[saveFooterProperties]
);
@ -94,4 +68,4 @@ export const useFooterService = () => {
saveFooterProperties,
resetFooterProperties,
};
};
};

View file

@ -20,16 +20,9 @@ export const useThemeService = () => {
colors: ThemeColors;
} | null> => {
try {
// Since this is a desktop app, we can use a fixed user ID
const userId = "local-user";
const response = await fetch(`/api/theme?userId=${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch theme: ${response.status}`);
}
const data = await response.json();
return data.theme;
// @ts-ignore
const result = await window.electron.getTheme();
return result.theme;
} catch (error) {
console.error("Error retrieving theme:", error);
return null;
@ -42,21 +35,9 @@ export const useThemeService = () => {
colors: ThemeColors;
}): Promise<boolean> => {
try {
const userId = "local-user";
const response = await fetch("/api/theme", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId, themeData }),
});
if (!response.ok) {
throw new Error(`Failed to save theme: ${response.status}`);
}
const data = await response.json();
return data.success;
// @ts-ignore
const result = await window.electron.setTheme(themeData);
return result.success;
} catch (error) {
console.error("Error saving theme:", error);
return false;
@ -69,4 +50,4 @@ export const useThemeService = () => {
getTheme,
saveTheme,
};
};
};

View file

@ -185,7 +185,7 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
transition-colors flex items-center justify-center relative"
>
{isImageFile(file) ? (
<Image className="w-8 h-8 text-purple-600" />
<img src={URL.createObjectURL(file)} className="w-10 h-10 text-purple-600" />
) : (
<File className="w-8 h-8 text-purple-600" />
)}

View file

@ -1,71 +0,0 @@
// app/api/footer/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getDb } from "../../(presentation-generator)/services/db";
// GET handler to retrieve properties
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId");
if (!userId) {
return NextResponse.json(
{ error: "User ID is required" },
{ status: 400 }
);
}
const db = await getDb();
const result = await db.get(
"SELECT data FROM settings WHERE key = 'footer' AND userId = ?",
userId
);
await db.close();
if (result) {
return NextResponse.json({ properties: JSON.parse(result.data) });
}
return NextResponse.json({ properties: null });
} catch (error) {
console.error("Error retrieving footer properties:", error);
return NextResponse.json(
{ error: "Failed to retrieve footer properties" },
{ status: 500 }
);
}
}
// POST handler to save properties
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { userId, properties } = body;
if (!userId || !properties) {
return NextResponse.json(
{ error: "User ID and properties are required" },
{ status: 400 }
);
}
const db = await getDb();
const propertiesJson = JSON.stringify(properties);
await db.run(
`INSERT OR REPLACE INTO settings (key, userId, data, updated_at)
VALUES ('footer', ?, ?, CURRENT_TIMESTAMP)`,
[userId, propertiesJson]
);
await db.close();
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error saving footer properties:", error);
return NextResponse.json(
{ error: "Failed to save footer properties" },
{ status: 500 }
);
}
}

View file

@ -1,411 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import puppeteer from "puppeteer";
import fs from 'fs';
import path from 'path';
import os from 'os';
interface Position {
left: number;
top: number;
width: number;
height: number;
}
interface FontStyles {
name: string;
size: number;
bold: boolean;
weight: number;
color: string;
}
interface TextElement {
position: Position;
paragraphs: {
alignment: number;
text: string;
font: FontStyles;
}[];
}
interface PictureElement {
position: Position;
picture: {
is_network: boolean;
path: string;
};
shape: string | null;
object_fit: {
fit: string | null;
focus: number[];
};
overlay: string | null;
border_radius: number[];
}
interface BoxElement {
position: Position;
type: number;
fill: {
color: string;
};
border_radius: number;
stroke: {
color: string;
thickness: number;
};
shadow: {
radius: number;
color: string;
offset: number;
opacity: number;
angle: number;
};
}
interface LineElement {
position: Position;
lineType: number;
thickness: string;
color: string;
}
interface GraphElement {
position: Position;
picture: {
is_network: boolean;
path: string;
};
border_radius: number[];
}
type SlideElement = TextElement | PictureElement | BoxElement | LineElement | GraphElement;
interface SlideMetadata {
slideIndex: number;
backgroundColor: string;
elements: SlideElement[];
}
interface ThemeParams {
theme: string;
customColors?: {
slideBg: string;
slideTitle: string;
slideHeading: string;
slideDescription: string;
slideBox: string;
};
}
export async function POST(request: NextRequest) {
let browser;
try {
const body = await request.json();
const { url, theme, customColors, tempDirectory, baseUrl } = body;
if (!url) {
return NextResponse.json({ error: "Missing URL" }, { status: 400 });
}
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
// Inject the environment variables before loading the page
await page.evaluateOnNewDocument(`
window.env = {
NEXT_PUBLIC_FAST_API: "${baseUrl || 'http://localhost:8000'}",
NEXT_PUBLIC_URL: "${url}",
};
`);
try {
await page.goto(url, {
waitUntil: "networkidle0",
timeout: 60000,
});
} catch (error) {
console.error("Navigation error:", error);
await browser.close();
return NextResponse.json({ error: "Failed to Navigate to provided URL" }, { status: 500 });
}
try {
await page.waitForSelector('[data-element-type="slide-container"]', {
timeout: 80000,
});
await page.evaluate(
async (params: ThemeParams) => {
const { theme, customColors } = params;
const containers = document.querySelectorAll(".slide-theme");
containers.forEach((container) => {
container.removeAttribute("data-theme");
container.setAttribute("data-theme", theme);
});
if (theme === "custom" && customColors) {
const root = document.documentElement;
root.style.setProperty("--custom-slide-bg", customColors.slideBg);
root.style.setProperty("--custom-slide-title", customColors.slideTitle);
root.style.setProperty("--custom-slide-heading", customColors.slideHeading);
root.style.setProperty("--custom-slide-description", customColors.slideDescription);
root.style.setProperty("--custom-slide-box", customColors.slideBox);
}
},
{ theme, customColors }
);
} catch (error) {
await browser.close();
return NextResponse.json({ error: "Slide container not found" }, { status: 500 });
}
const metadata = await page.evaluate(async () => {
function rgbToHex(color: string) {
if (!color || color === "transparent" || color === "none") return "000000";
if (color.startsWith("#")) return color.replace("#", "");
const matches = color.match(/\d+/g);
if (!matches) return "000000";
const r = parseInt(matches[0]);
const g = parseInt(matches[1]);
const b = parseInt(matches[2]);
return [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("");
}
async function collectSlideMetadata(): Promise<SlideMetadata[]> {
const slidesMetadata: SlideMetadata[] = [];
const slideContainers = Array.from(
document.querySelectorAll('[data-element-type="slide-container"]')
);
for (const container of slideContainers) {
const containerEl = container as HTMLElement;
containerEl.style.width = "1280px";
containerEl.style.height = "720px";
containerEl.style.transform = "none";
const containerRect = containerEl.getBoundingClientRect();
const slideIndex = parseInt(
containerEl.getAttribute("data-slide-index") || "0"
);
const containerComputedStyle = window.getComputedStyle(containerEl);
const slideMetadata: SlideMetadata = {
slideIndex,
backgroundColor: rgbToHex(containerComputedStyle.backgroundColor),
elements: [],
};
const slideType = containerEl.getAttribute("data-slide-type");
const elements = Array.from(
containerEl.querySelectorAll(
'[data-slide-element]:not([data-element-type="slide-container"])'
)
);
for (const element of elements) {
const el = element as HTMLElement;
const isIcon = el.getAttribute("data-is-icon");
const isAlign = el.getAttribute("data-is-align");
const elementRect = el.getBoundingClientRect();
const computedStyle = window.getComputedStyle(el);
const position: Position = {
left: Math.round(elementRect.left - containerRect.left),
top: Math.round(elementRect.top - containerRect.top),
width: Math.round(elementRect.width),
height: Math.round(elementRect.height),
};
const elementType = el.getAttribute("data-element-type");
if (!elementType) continue;
const fontStyles: FontStyles = {
name: computedStyle.fontFamily.split('_')[2] || 'Inter',
size: parseInt(computedStyle.fontSize),
bold: parseInt(computedStyle.fontWeight) >= 500 ? true : false,
weight: parseInt(computedStyle.fontWeight),
color: rgbToHex(computedStyle.color),
};
switch (elementType) {
case "text":
const textContent = el.getAttribute("data-text-content");
const textElement: TextElement = {
position,
paragraphs: [
{
alignment: isAlign === 'true' ? 2 : 1,
text: textContent || el.textContent || "",
font: fontStyles,
},
],
};
slideMetadata.elements.push(textElement);
break;
case "picture":
const imgEl = el.tagName.toLowerCase() === "img" ? el as HTMLImageElement : el.querySelector("img") as HTMLImageElement;
if (imgEl) {
const focialPointx = parseFloat(imgEl.getAttribute('data-focial-point-x') || '0');
const focialPointy = parseFloat(imgEl.getAttribute('data-focial-point-y') || '0');
const image_type = imgEl.getAttribute('data-image-type');
const objectFit = imgEl.getAttribute('data-object-fit');
const pictureElement: PictureElement = {
position,
picture: {
is_network: imgEl.src.startsWith("http"),
path: imgEl.src || imgEl.getAttribute("data-image-path") || "",
},
shape: image_type,
object_fit: {
fit: objectFit,
focus: [focialPointx, focialPointy],
},
overlay: isIcon ? "ffffff" : null,
border_radius: slideType === "4"
? [parseInt(computedStyle.borderRadius), parseInt(computedStyle.borderRadius), 0, 0]
: [parseInt(computedStyle.borderRadius), parseInt(computedStyle.borderRadius), parseInt(computedStyle.borderRadius), parseInt(computedStyle.borderRadius)],
};
slideMetadata.elements.push(pictureElement);
}
break;
case "slide-box":
case "filledbox":
const boxShadow = computedStyle.boxShadow;
let shadowRadius = 0;
let shadowColor = "000000";
let shadowOffsetX = 0;
let shadowOffsetY = 0;
let shadowOpacity = 0;
if (boxShadow && boxShadow !== "none") {
const boxShadowRegex = /rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([\d.]+)?\)?\s+(-?\d+)px\s+(-?\d+)px\s+(-?\d+)px/;
const match = boxShadow.match(boxShadowRegex);
if (match) {
const r = match[1];
const g = match[2];
const b = match[3];
const rgbStr = `rgb(${r}, ${g}, ${b})`;
shadowColor = rgbToHex(rgbStr);
shadowOpacity = match[4] ? parseFloat(match[4]) : 1;
shadowOffsetX = parseInt(match[5]);
shadowOffsetY = parseInt(match[6]);
shadowRadius = parseInt(match[7]);
}
}
const boxElement: BoxElement = {
position,
type: computedStyle.borderRadius === "9999px" || computedStyle.borderRadius === "50%" ? 9 : 5,
fill: {
color: rgbToHex(computedStyle.backgroundColor),
},
border_radius: parseInt(computedStyle.borderRadius) || 0,
stroke: {
color: rgbToHex(computedStyle.borderColor),
thickness: parseInt(computedStyle.borderWidth) || 0,
},
shadow: {
radius: shadowRadius,
color: shadowColor,
offset: Math.sqrt(shadowOffsetX * shadowOffsetX + shadowOffsetY * shadowOffsetY),
opacity: shadowOpacity,
angle: Math.round((Math.atan2(shadowOffsetY, shadowOffsetX) * 180) / Math.PI),
},
};
slideMetadata.elements.push(boxElement);
break;
case "line":
const lineElement: LineElement = {
position,
lineType: 1,
thickness: computedStyle.borderWidth || computedStyle.height,
color: rgbToHex(computedStyle.borderColor || computedStyle.backgroundColor),
};
slideMetadata.elements.push(lineElement);
break;
case "graph":
const graphId = el.getAttribute("data-element-id");
const graphElement: GraphElement = {
position,
picture: {
is_network: true,
path: `__GRAPH_PLACEHOLDER__${graphId}`,
},
border_radius: [0, 0, 0, 0],
};
slideMetadata.elements.push(graphElement);
break;
}
}
slidesMetadata.push(slideMetadata);
}
return slidesMetadata;
}
return await collectSlideMetadata();
});
const graphElements = await page.$$('[data-element-type="graph"]');
for (const graphElement of graphElements) {
const graphId = await graphElement.evaluate((el: Element) =>
el.getAttribute("data-element-id")
);
const screenshot = await graphElement.screenshot({
type: "jpeg",
encoding: "base64",
quality: 100,
omitBackground: true,
});
try {
const tempDir = tempDirectory || os.tmpdir();
// Generate a unique filename
const filename = `chart-${graphId}-${Date.now()}.jpg`;
const filePath = path.join(tempDir, filename);
// Save the file
fs.writeFileSync(filePath, Buffer.from(screenshot, 'base64'));
metadata.forEach((slide) => {
slide.elements.forEach((element) => {
if ('picture' in element && element.picture.path === `__GRAPH_PLACEHOLDER__${graphId}`) {
element.picture.path = filePath;
}
});
});
} catch (error) {
console.error('Error saving screenshot:', error);
continue;
}
}
await browser.close();
return NextResponse.json(metadata);
} catch (error) {
console.error("Error during page preparation:", error);
if (browser) await browser.close();
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
} finally {
if (browser) await browser.close();
}
}

View file

@ -1,68 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { getDb } from "../../(presentation-generator)/services/db";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId");
if (!userId) {
return NextResponse.json(
{ error: "User ID is required" },
{ status: 400 }
);
}
const db = await getDb();
const result = await db.get(
"SELECT data FROM settings WHERE key = 'theme' AND userId = ?",
userId
);
await db.close();
if (result) {
return NextResponse.json({ theme: JSON.parse(result.data) });
}
return NextResponse.json({ theme: null });
} catch (error) {
console.error("Error retrieving theme:", error);
return NextResponse.json(
{ error: "Failed to retrieve theme" },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { userId, themeData } = body;
if (!userId || !themeData) {
return NextResponse.json(
{ error: "User ID and theme data are required" },
{ status: 400 }
);
}
const db = await getDb();
const themeDataJson = JSON.stringify(themeData);
await db.run(
`INSERT OR REPLACE INTO settings (key, userId, data, updated_at)
VALUES ('theme', ?, ?, CURRENT_TIMESTAMP)`,
[userId, themeDataJson]
);
await db.close();
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error saving theme:", error);
return NextResponse.json(
{ error: "Failed to save theme" },
{ status: 500 }
);
}
}

View file

@ -16,12 +16,12 @@ const Header = () => {
<div className="flex items-center gap-3">
{pathname !== '/upload' && <BackBtn />}
<Link href="/dashboard">
<Image
<img
src="/logo-white.png"
alt="Presentation logo"
width={162}
height={32}
priority
/>
</Link>
</div>

View file

@ -14,10 +14,9 @@ export const PresentationListItem: React.FC<Presentation> = ({
<Card>
<CardContent className="flex items-center gap-4 p-4">
<div className="relative w-[120px] aspect-video rounded-md overflow-hidden">
<Image
<img
src={thumbnail}
alt={title}
fill
className="object-cover"
/>
</div>

View file

@ -1,2 +1,9 @@
import React from 'react'
import SettingPage from './SettingPage'
export default SettingPage
const page = () => {
return (
<SettingPage />
)
}
export default page

View file

@ -1,115 +0,0 @@
import React from "react";
import Link from "next/link";
import Wrapper from "@/components/Wrapper";
interface FooterLink {
label: string;
href: string;
}
interface FooterSection {
title: string;
links: FooterLink[];
}
const footerSections: FooterSection[] = [
{
title: "Main Menu",
links: [
// { label: "About Us", href: "/about" },
// { label: "Templates", href: "/templates" },
{ label: "Blogs", href: "/blogs" },
{ label: "Pricing", href: "#pricing" },
// { label: "Enterprise", href: "/enterprise" },
{ label: "Contact Us", href: "/contact" },
],
},
{
title: "Our Products",
links: [
{ label: "Presentation Generator", href: "/upload" },
{ label: "Presentation-to-video", href: "/editor" },
// { label: "Presentation-to-ppt", href: "/product-3" },
],
},
// {
// title: "Solutions",
// links: [
// { label: "Solution 1", href: "/solution-1" },
// { label: "Solution 2", href: "/solution-2" },
// { label: "Solution 3", href: "/solution-3" },
// ]
// },
{
title: "Other Links",
links: [
{ label: "FAQ", href: "#faq" },
{ label: "Terms & Conditions", href: "/terms-and-conditions" },
{ label: "Privacy Policy", href: "/privacy-policy" },
// { label: "Cookies Policy", href: "/cookies-policy" },
],
},
];
const Footer = () => {
return (
<footer className="bg-black text-white pt-20 pb-10">
<Wrapper>
<div className="flex flex-col lg:flex-row items-start justify-between gap-10">
{/* Logo and Description Section */}
<div className="lg:w-[30%] space-y-6">
<Link href="/" className="inline-block">
<img
src="/logo-white.png"
alt="Presenton.ai Logo"
className="w-[204px] h-[48px] object-cover"
/>
</Link>
<p className="text-white font-normal font-satoshi">
No more struggling with slides. Just drop your content and let
your AI buddy craft beautiful, ready-to-share presentations for
work, study, or business in minutes.
</p>
</div>
{/* Links Sections */}
<div
style={{
gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))",
}}
className="w-full lg:w-[60%] grid gap-10"
>
{footerSections.map((section) => (
<div key={section.title}>
<h3 className="text-lg font-switzer font-bold mb-6">
{section.title}{" "}
</h3>
<ul className="space-y-4">
{section.links.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="text-gray-400 hover:text-white transition-colors font-satoshi"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
{/* Copyright Section */}
<div className="mt-16 pt-8 border-t border-gray-800 flex flex-col md:flex-row justify-center items-center gap-4">
<p className="text-gray-400 text-sm font-satoshi">
Copyright {new Date().getFullYear()} Presenton. All Right Reserved.
</p>
</div>
</Wrapper>
</footer>
);
};
export default Footer;

View file

@ -1,37 +0,0 @@
import Link from "next/link";
import Image from "next/image";
import Wrapper from "../Wrapper";
export default function Header() {
return (
<Wrapper className="py-4">
<div className="flex justify-between items-center ">
<div className="flex items-center w-[102px] h-[24px] md:w-[162px] md:h-[32px]">
<a href="/">
{" "}
<Image
src="/Logo.png"
alt="Presentation logo"
width={162}
height={32}
priority
/>
</a>
</div>
<div className="flex items-center gap-4">
{/* <div className="hidden md:flex items-center gap-4">
<Link href="/" className="text-[#000] text-center font-neue-montreal text-[16px] font-[400] leading-6 tracking-[0.64px]">Home</Link>
<Link href="/" className="text-[#000] text-center font-neue-montreal text-[16px] font-[400] leading-6 tracking-[0.64px]">About</Link>
<Link href="/" className="text-[#000] text-center font-neue-montreal text-[16px] font-[400] leading-6 tracking-[0.64px]">Contact</Link>
</div> */}
<Link
href="/auth/login"
className=" bg-gradient-to-r text-xs md:text-base from-[#9034EA] to-[#5146E5] text-white font-semibold md:py-3 md:px-8 py-2 px-4 rounded-full"
>
Get Started
</Link>
</div>
</div>
</Wrapper>
);
}

View file

@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// output:"export",
reactStrictMode: false,
images: {
@ -42,43 +43,8 @@ const nextConfig = {
},
],
},
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "*",
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "X-Requested-With, Content-Type, Authorization",
},
],
},
];
},
transpilePackages: ["remotion"],
webpack: (config) => {
config.externals = [
...config.externals,
{
canvas: "canvas",
},
];
return config;
},
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
};
export default nextConfig;

View file

@ -46,18 +46,17 @@
"lucide-react": "^0.447.0",
"marked": "^15.0.11",
"next": "^14.2.14",
"puppeteer": "^24.8.2",
"react": "^18",
"react-dom": "^18",
"react-redux": "^9.1.2",
"recharts": "^2.15.0",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"tailwind-merge": "^2.5.3",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"tiptap-markdown": "^0.8.10",
"@tailwindcss/typography": "^0.5.16"
"@tailwindcss/typography": "^0.5.16"
},
"devDependencies": {