nextjs api router transfer to electron
This commit is contained in:
parent
3e34f06093
commit
14442072e3
24 changed files with 215 additions and 913 deletions
32
app/ipc/footer_handlers.ts
Normal file
32
app/ipc/footer_handlers.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
32
app/ipc/theme_handlers.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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
68
app/settings-store.ts
Normal 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();
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -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" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,9 @@
|
|||
import React from 'react'
|
||||
import SettingPage from './SettingPage'
|
||||
export default SettingPage
|
||||
const page = () => {
|
||||
return (
|
||||
<SettingPage />
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue