Merge branch 'feat/custom_schema_and_layout' of https://github.com/presenton/presenton into feat/custom_schema_and_layout

merge
This commit is contained in:
Suraj Jha 2025-07-20 15:25:28 +05:45
commit bc5d772733
109 changed files with 1260 additions and 7987 deletions

24
package-lock.json generated Normal file
View file

@ -0,0 +1,24 @@
{
"name": "presenton",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"uuid": "^11.1.0"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/esm/bin/uuid"
}
}
}
}

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"dependencies": {
"uuid": "^11.1.0"
}
}

View file

@ -4,7 +4,7 @@ from fastapi.staticfiles import StaticFiles
from api.lifespan import app_lifespan
from api.middlewares import UserConfigEnvUpdateMiddleware
from api.v1.ppt.router import API_V1_PPT_ROUTER
from utils.asset_directory_utils import get_exports_directory, get_images_directory
from utils.asset_directory_utils import get_exports_directory, get_images_directory, get_uploads_directory
app = FastAPI(lifespan=app_lifespan)
@ -25,6 +25,11 @@ app.mount(
StaticFiles(directory=get_exports_directory()),
name="app_data/exports",
)
app.mount(
"/app_data/uploads",
StaticFiles(directory=get_uploads_directory()),
name="app_data/uploads",
)
# Middlewares

View file

@ -61,11 +61,13 @@ class PptxFontModel(BaseModel):
class PptxFillModel(BaseModel):
color: str
opacity: float = 1.0
class PptxStrokeModel(BaseModel):
color: str
thickness: float
opacity: float = 1.0
class PptxShadowModel(BaseModel):
@ -85,6 +87,7 @@ class PptxParagraphModel(BaseModel):
spacing: Optional[PptxSpacingModel] = None
alignment: Optional[PP_ALIGN] = None
font: Optional[PptxFontModel] = None
line_height: Optional[float] = None
text: Optional[str] = None
text_runs: Optional[List[PptxTextRunModel]] = None
@ -141,6 +144,7 @@ class PptxConnectorModel(PptxShapeModel):
position: PptxPositionModel
thickness: float = 0.5
color: str = "000000"
opacity: float = 1.0
class PptxSlideModel(BaseModel):

View file

@ -10,6 +10,7 @@ from pptx.text.text import _Paragraph, TextFrame, Font, _Run
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
from lxml.etree import fromstring, tostring
from PIL import Image
from pptx.oxml.xmlchemy import OxmlElement
from pptx.util import Pt
from pptx.dml.color import RGBColor
@ -55,6 +56,13 @@ class PptxPresentationCreator:
self._ppt.slide_width = Pt(1280)
self._ppt.slide_height = Pt(720)
def get_sub_element(self, parent, tagname, **kwargs):
"""Helper method to create XML elements"""
element = OxmlElement(tagname)
element.attrib.update(kwargs)
parent.append(element)
return element
async def fetch_network_assets(self):
image_urls = []
models_with_network_asset: List[PptxPictureBoxModel] = []
@ -158,6 +166,8 @@ class PptxPresentationCreator:
)
connector_shape.line.width = Pt(connector_model.thickness)
connector_shape.line.color.rgb = RGBColor.from_string(connector_model.color)
# Set line opacity using XML manipulation for better reliability
self.set_line_opacity(connector_shape, connector_model.opacity)
def add_picture(self, slide: Slide, picture_model: PptxPictureBoxModel):
image_path = picture_model.picture.path
@ -252,6 +262,9 @@ class PptxPresentationCreator:
if paragraph_model.spacing:
self.apply_spacing_to_paragraph(paragraph, paragraph_model.spacing)
if paragraph_model.line_height:
paragraph.line_spacing = paragraph_model.line_height
if paragraph_model.alignment:
paragraph.alignment = paragraph_model.alignment
@ -365,6 +378,7 @@ class PptxPresentationCreator:
else:
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor.from_string(fill.color)
self.set_fill_opacity(shape.fill, fill.opacity)
def apply_stroke_to_shape(
self, shape: Shape, stroke: Optional[PptxStrokeModel] = None
@ -375,6 +389,7 @@ class PptxPresentationCreator:
shape.line.fill.solid()
shape.line.fill.fore_color.rgb = RGBColor.from_string(stroke.color)
shape.line.width = Pt(stroke.thickness)
self.set_fill_opacity(shape.line.fill, stroke.opacity)
def apply_shadow_to_shape(
self, shape: Shape, shadow: Optional[PptxShadowModel] = None
@ -427,6 +442,19 @@ class PptxPresentationCreator:
nsmap=nsmap,
)
def set_fill_opacity(self, fill, opacity):
if opacity is None or opacity >= 1.0:
return
alpha = int((opacity) * 100000)
try:
ts = fill._xPr.solidFill
sF = ts.get_or_change_to_srgbClr()
self.get_sub_element(sF, "a:alpha", val=str(alpha))
except Exception as e:
print(f"Could not set fill opacity: {e}")
def get_margined_position(
self, position: PptxPositionModel, margin: Optional[PptxSpacingModel]
) -> PptxPositionModel:

View file

@ -12,3 +12,8 @@ def get_exports_directory():
export_directory = os.path.join(get_app_data_directory_env(), "exports")
os.makedirs(export_directory, exist_ok=True)
return export_directory
def get_uploads_directory():
uploads_directory = os.path.join(get_app_data_directory_env(), "uploads")
os.makedirs(uploads_directory, exist_ok=True)
return uploads_directory

View file

@ -1,291 +0,0 @@
import React, { useState, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
setThemeColors,
setTheme,
setLoadingState,
} from "../store/themeSlice";
import { ThemeType } from "../upload/type";
import { useThemeService, ThemeColors } from "../services/themeService";
import { PresentationGenerationApi } from "../services/api/presentation-generation";
interface CustomThemeSettingsProps {
onClose?: () => void;
presentationId: string;
}
const CustomThemeSettings = ({
onClose,
presentationId,
}: CustomThemeSettingsProps) => {
const dispatch = useDispatch();
const [draftColors, setDraftColors] = useState<ThemeColors>({
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();
// Refs for tracking drag state and RAF
const isDragging = useRef(false);
const rafId = useRef<number>();
const currentKey = useRef<string>();
const currentValue = useRef<string>();
const updateDraftColor = (key: string, value: string) => {
if (rafId.current) {
cancelAnimationFrame(rafId.current);
}
rafId.current = requestAnimationFrame(() => {
setDraftColors((prev) => ({
...prev,
[key]: value,
}));
});
};
const handleColorPickerChange = (key: string, value: string) => {
if (isDragging.current) {
// Update refs for current values
currentKey.current = key;
currentValue.current = value;
// Update preview immediately
const previewElement = document.getElementById(`preview-${key}`);
if (previewElement) {
previewElement.style.backgroundColor = value;
}
} else {
// For non-drag changes (like text input), update immediately
updateDraftColor(key, value);
}
};
const handleColorPickerMouseDown = () => {
isDragging.current = true;
};
const handleColorPickerMouseUp = () => {
isDragging.current = false;
// Apply the final color value
if (currentKey.current && currentValue.current) {
updateDraftColor(currentKey.current, currentValue.current);
}
};
const handleTextInputChange = (key: string, value: string) => {
updateDraftColor(key, value);
};
const handleSave = async () => {
try {
// Update UI immediately
const themeType = "custom" as ThemeType;
dispatch(setTheme(themeType));
dispatch(
setThemeColors({
...draftColors,
theme: themeType,
})
);
// Set CSS variables
const root = document.documentElement;
root.style.setProperty("--custom-slide-bg", draftColors.slideBg);
root.style.setProperty("--custom-slide-title", draftColors.slideTitle);
root.style.setProperty(
"--custom-slide-heading",
draftColors.slideHeading
);
root.style.setProperty(
"--custom-slide-description",
draftColors.slideDescription
);
root.style.setProperty("--custom-slide-box", draftColors.slideBox);
root.style.setProperty("--custom-icon-bg", draftColors.iconBg);
// Save to file and API
await Promise.all([
PresentationGenerationApi.setThemeColors(presentationId, {
name: themeType,
colors: {
...draftColors,
},
}),
themeService.saveTheme({
name: "custom",
colors: {
...draftColors,
theme: themeType,
},
}),
]);
onClose?.();
} catch (error) {
console.error("Failed to save custom theme:", error);
}
};
// Load saved theme
React.useEffect(() => {
const loadSavedCustomTheme = async () => {
try {
dispatch(setLoadingState(true));
const savedTheme = await themeService.getTheme();
if (savedTheme) {
setDraftColors(savedTheme.colors);
}
} catch (error) {
console.error("Failed to load theme preferences:", error);
} finally {
dispatch(setLoadingState(false));
}
};
loadSavedCustomTheme();
}, []);
// Cleanup RAF on unmount
React.useEffect(() => {
return () => {
if (rafId.current) {
cancelAnimationFrame(rafId.current);
}
};
}, []);
const colorInputs = [
{ key: "background", label: "Background Color", icon: "🎨" },
{ key: "slideBg", label: "Slide Background Color", icon: "🎨" },
{ key: "slideTitle", label: "Title Color", icon: "📝" },
{ key: "slideHeading", label: "Heading Color", icon: "🔤" },
{ key: "slideDescription", label: "Description Color", icon: "📄" },
{ key: "slideBox", label: "Box Color", icon: "📦" },
{ key: "iconBg", label: "Icon Background Color", icon: "📦" },
];
return (
<div className="">
<div className="h-[60vh] font-inter overflow-y-auto custom_scrollbar pr-2 pb-2">
{/* Live Preview */}
<div className=" w-full space-y-2">
<h3 className="text-xs font-medium text-gray-500">Live Preview</h3>
<div
style={{
backgroundColor: draftColors.background,
}}
className="p-3 rounded-lg"
>
<div
className="w-full h-28 rounded-lg shadow-sm transition-all overflow-hidden"
style={{
backgroundColor: draftColors.slideBg,
padding: "0.75rem",
}}
>
<div
style={{ color: draftColors.slideTitle }}
className="text-base font-bold mb-2"
>
Sample Title
</div>
<div
style={{ color: draftColors.slideHeading }}
className="text-sm mb-1"
>
Heading
</div>
<div
style={{ color: draftColors.slideDescription }}
className="text-xs"
>
Description text
</div>
<div
style={{ backgroundColor: draftColors.slideBox }}
className="mt-2 p-1.5 rounded w-20"
></div>
</div>
</div>
</div>
<div className="w-full grid grid-cols-1 gap-4">
{colorInputs.map(({ key, label, icon }) => (
<div
key={key}
className="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="relative group">
<Input
type="color"
id={key}
value={draftColors[key as keyof typeof draftColors]}
onChange={(e) => handleColorPickerChange(key, e.target.value)}
onMouseDown={() => handleColorPickerMouseDown()}
onMouseUp={() => handleColorPickerMouseUp()}
onTouchStart={() => handleColorPickerMouseDown()}
onTouchEnd={() => handleColorPickerMouseUp()}
className="w-12 h-12 p-1 cursor-pointer border rounded-lg transition-all hover:border-[#5146E5] focus:border-[#5146E5]"
/>
<div
id={`preview-${key}`}
className="absolute top-0 left-0 w-full h-full rounded-lg pointer-events-none"
style={{
backgroundColor: draftColors[
key as keyof typeof draftColors
] as string,
}}
/>
</div>
<div className="flex-1 space-y-1">
<Label
htmlFor={key}
className="text-sm font-medium text-gray-700 flex items-center gap-2"
>
<span>{icon}</span>
{label}
</Label>
<Input
type="text"
value={draftColors[key as keyof typeof draftColors]}
onChange={(e) => handleTextInputChange(key, e.target.value)}
className="h-8 font-mono text-sm"
placeholder="#000000"
/>
</div>
</div>
))}
</div>
</div>
<div className="mt-6 pt-4 font-roboto border-t flex justify-end gap-2">
<Button
variant="outline"
onClick={onClose}
className="px-4 h-9 text-sm"
>
Cancel
</Button>
<Button
onClick={handleSave}
className="bg-[#5146E5] hover:bg-[#4338ca] text-white px-4 h-9 text-sm"
>
Save Theme
</Button>
</div>
</div>
);
};
export default CustomThemeSettings;

View file

@ -1,111 +0,0 @@
import React, { useEffect, useRef } from "react";
import { useSelector } from "react-redux";
import TipTapEditor from "./Tiptap";
import { RootState } from "@/store/store";
import Typewriter from "./TypeWriter";
interface EditableTextProps {
slideIndex: number;
bodyIdx?: number;
elementId: string;
type:
| "title"
| "heading"
| "description-body"
| "description"
| "heading-description"
| "info-heading"
| "info-description";
content: string;
isAlingCenter?: boolean;
}
const EditableText = ({
slideIndex,
elementId,
type,
content,
bodyIdx = 0,
isAlingCenter = false,
}: EditableTextProps) => {
const { isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
const elementRef = useRef<HTMLDivElement>(null);
// Add useEffect to initialize content
useEffect(() => {
if (elementRef.current) {
const displayContent = content || getPlaceholder();
elementRef.current.textContent = displayContent;
// Add placeholder styling if needed
if (!content) {
elementRef.current.classList.add("text-gray-400");
}
}
}, [content]);
const getPlaceholder = () => {
switch (type) {
case "title":
return "Enter title";
case "heading":
return "Enter heading";
case "description-body":
return "Enter description";
case "description":
return "Enter description";
case "heading-description":
return "Enter description";
case "info-heading":
return "Enter heading";
case "info-description":
return "Enter description";
default:
return "Enter text";
}
};
const getTextStyle = () => {
const baseStyle = "outline-none transition-all duration-200";
switch (type) {
case "title":
return `${baseStyle} text-[40px] slide-title leading-[48px] font-bold`;
case "heading":
return `${baseStyle} text-[24px] slide-heading leading-[32px] font-bold`;
case "description":
case "description-body":
case "heading-description":
return `${baseStyle} text-[20px] slide-description leading-[24px] font-normal`;
default:
return `${baseStyle} text-[20px] slide-description leading-[24px] font-normal`;
}
};
return (
<>
{isStreaming ? (
<div
className={`w-full min-w-[60px] font-inter ${getTextStyle()} ${isAlingCenter ? "text-center " : ""
}`}
>
<Typewriter text={content ? content.replace(/\*\*/g, "") : ""} speed={20} />
</div>
) : (
<TipTapEditor
key={content}
bodyIdx={bodyIdx}
isAlingCenter={isAlingCenter}
slideIndex={slideIndex}
elementId={elementId}
type={type}
content={content}
/>
)}
</>
);
};
export default React.memo(EditableText);

View file

@ -1,43 +0,0 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreHorizontal } from "lucide-react";
import React from "react";
interface ElementMenuProps {
index: number;
handleDeleteItem: (index: number) => void;
}
const ElementMenu = ({ index, handleDeleteItem }: ElementMenuProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="absolute hidden md:block top-0 left-1/2 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
data-index={index}
>
<MoreHorizontal className="w-4 h-4 text-black" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[180px] p-2">
<DropdownMenuItem
onClick={() => handleDeleteItem(index)}
className="px-3 py-2 cursor-pointer"
>
Delete Item {index + 1}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
// Prevent unnecessary re-renders
export default React.memo(ElementMenu, (prevProps, nextProps) => {
return (
prevProps.index === nextProps.index && prevProps.index === nextProps.index
);
});

View file

@ -5,7 +5,7 @@ import Link from "next/link";
import { RootState } from "@/store/store";
import { useSelector } from "react-redux";
const UserAccount = () => {
const HeaderNav = () => {
const canChangeKeys = useSelector((state: RootState) => state.userConfig.can_change_keys);
@ -39,4 +39,4 @@ const UserAccount = () => {
);
};
export default UserAccount;
export default HeaderNav;

View file

@ -0,0 +1,68 @@
import { Trash2 } from 'lucide-react';
import React from 'react'
import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader';
import { useDispatch } from 'react-redux';
import { addNewSlide } from '@/store/slices/presentationGeneration';
import { Loader2 } from 'lucide-react';
interface NewSlideProps {
setShowNewSlideSelection: (show: boolean) => void;
group: string;
index: number;
presentationId: string;
}
const NewSlide = ({ setShowNewSlideSelection, group, index, presentationId }: NewSlideProps) => {
const dispatch = useDispatch();
const handleNewSlide = (sampleData: any, id: string) => {
const newSlide = {
id: crypto.randomUUID(),
index: index,
content: sampleData,
layout_group: group,
layout: `${group}:${id}`,
presentation: presentationId
}
dispatch(addNewSlide({ slideData: newSlide, index }));
setShowNewSlideSelection(false);
}
const { layoutGroup, loading } = useGroupLayoutLoader(group)
if (loading) {
return (
<div className='my-6 w-full bg-gray-50 p-8 max-w-[1280px]'>
<div className='flex justify-between items-center mb-8'>
<h2 className="text-2xl font-semibold">Select a Slide Layout</h2>
<Trash2 onClick={() => setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' />
</div>
<div className='flex items-center justify-center h-32'>
<Loader2 className="w-8 h-8 animate-spin text-gray-500" />
</div>
</div>
)
}
return (
<div className='my-6 w-full bg-gray-50 p-8 max-w-[1280px]'>
<div className='flex justify-between items-center mb-8'>
<h2 className="text-2xl font-semibold">Select a Slide Layout</h2>
<Trash2 onClick={() => setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' />
</div>
<div className='grid grid-cols-4 gap-4'>
{layoutGroup && layoutGroup?.layouts.map((layout: any, index: number) => {
const { component: LayoutComponent, sampleData, layoutId } = layout
return (
<div onClick={() => handleNewSlide(sampleData, layoutId)} key={`${layoutGroup?.groupName}-${index}`} className=" relative cursor-pointer overflow-hidden aspect-video">
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
)
})}
</div>
</div>
)
}
export default NewSlide

View file

@ -15,7 +15,7 @@ import { useGroupLayouts } from "../hooks/useGroupLayouts";
interface PresentationModeProps {
slides: Slide[];
currentSlide: number;
currentTheme: string;
isFullscreen: boolean;
onFullscreenToggle: () => void;
onExit: () => void;
@ -26,7 +26,7 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
slides,
currentSlide,
currentTheme,
isFullscreen,
onFullscreenToggle,
onExit,
@ -190,10 +190,9 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
<div className="flex-1 flex items-center justify-center p-8">
<div
className={`w-full max-w-[1280px] scale-110 aspect-video slide-theme slide-container border rounded-sm font-inter shadow-lg bg-white`}
data-theme={currentTheme}
>
{slides[currentSlide] &&
renderSlideContent(slides[currentSlide])}
renderSlideContent(slides[currentSlide], false)}
</div>
</div>
</div>

View file

@ -18,12 +18,12 @@ import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import React, { useRef, useState, useEffect } from "react";
import { Camera, Loader2, Plus } from "lucide-react";
import { toast } from "@/hooks/use-toast";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { getStaticFileUrl, isDarkColor } from "../../utils/others";
import { defaultFooterProperties, useFooterContext } from "../../context/footerContext";
import { FooterProperties } from "../../services/footerService";
import { toast } from "sonner";
const SlideFooter: React.FC = () => {
const [showEditor, setShowEditor] = useState<boolean>(false);
@ -43,17 +43,13 @@ const SlideFooter: React.FC = () => {
const handleSave = async () => {
await saveFooterProperties(footerProperties);
setIsPropertyChanged(false);
toast({
title: "Footer properties saved successfully",
});
toast.success("Footer properties saved successfully");
};
const handleReset = async () => {
await resetFooterProperties();
setFooterProperties(defaultFooterProperties);
toast({
title: "Footer properties reset to default",
});
toast.success("Footer properties reset to default");
};
const updateProperty = (path: string, value: any): void => {
@ -186,9 +182,7 @@ const SlideFooter: React.FC = () => {
if (!file) return;
if (!file.type.startsWith("image/")) {
toast({
title: "Please Upload An Image File",
});
toast.error("Please Upload An Image File");
return;
}
@ -208,10 +202,7 @@ const SlideFooter: React.FC = () => {
}));
} catch (error) {
console.error("Error converting image:", error);
toast({
title: "Error uploading image",
variant: "destructive",
});
toast.error("Error uploading image");
} finally {
setIsUploading({ ...isUploading, white: false });
}
@ -225,9 +216,7 @@ const SlideFooter: React.FC = () => {
if (!file) return;
if (!file.type.startsWith("image/")) {
toast({
title: "Please Upload An Image File",
});
toast.error("Please Upload An Image File");
return;
}
@ -247,10 +236,7 @@ const SlideFooter: React.FC = () => {
}));
} catch (error) {
console.error("Error converting image:", error);
toast({
title: "Error uploading image",
variant: "destructive",
});
toast.error("Error uploading image");
} finally {
setIsUploading({ ...isUploading, dark: false });
}
@ -277,10 +263,8 @@ const SlideFooter: React.FC = () => {
const handleSheetClose = () => {
if (isPropertyChanged) {
toast({
title: "Unsaved Changes",
toast.error("Unsaved Changes", {
description: "Please save changes before closing the editor",
variant: "destructive",
});
return;
}

View file

@ -1,91 +0,0 @@
"use client";
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
import Underline from "@tiptap/extension-underline";
import {
Bold,
Italic,
Underline as UnderlinedIcon,
Strikethrough,
Code,
} from "lucide-react";
const TipTapEditor = ({
content,
}: {
content: string;
}) => {
const editor = useEditor({
extensions: [StarterKit, Markdown, Underline],
content: content,
editorProps: {
attributes: {
class: "outline-none transition-all duration-200",
},
},
immediatelyRender: false,
});
return (
<div className="relative">
<BubbleMenu editor={editor} tippyOptions={{ duration: 100 }}>
<div className="flex bg-white rounded-lg shadow-lg p-2 gap-1 border-r pr-2">
<button
onClick={() => editor?.chain().focus().toggleBold().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("bold") ? "bg-gray-200" : ""
}`}
>
<Bold className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleItalic().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("italic") ? "bg-gray-200" : ""
}`}
>
<Italic className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleUnderline().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("underline") ? "bg-gray-200" : ""
}`}
>
<UnderlinedIcon className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleStrike().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("strike") ? "bg-gray-200" : ""
}`}
>
<Strikethrough className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleCode().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("codeBlock") ? "bg-gray-200" : ""
}`}
>
<Code className="h-4 w-4" />
</button>
</div>
</BubbleMenu>
<EditorContent
className={`min-w-[100px] w-full max-md:pointer-events-none ${editor?.getText() ? "" : `hover:outline hover:outline-gray-400`
} `}
onBlur={() => {
const markdown = editor?.storage.markdown.getMarkdown();
console.log("🔍 markdown", markdown);
}}
editor={editor}
/>
</div>
);
};
export default TipTapEditor;

View file

@ -107,6 +107,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
const root = ReactDOM.createRoot(tiptapContainer);
root.render(
<TiptapText
key={trimmedText}
content={trimmedText}
onContentChange={(content: string) => {
if (dataPath && onContentChange) {

View file

@ -1,14 +0,0 @@
import { useTypewriter } from "@/hooks/useTypeWriter";
const Typewriter = ({ text, speed }: { text: string; speed: number }) => {
const { displayText, isCursorVisible } = useTypewriter(text, speed, true);
return (
<p>
{displayText}
{isCursorVisible && <span className="slide-title">|</span>}
</p>
);
};
export default Typewriter;

View file

@ -1,104 +0,0 @@
import React, { useState, useEffect } from "react";
import ChartEditor from "../ChartEditor";
import { StoreChartData } from "../../utils/chartDataTransforms";
import { useDispatch, useSelector } from "react-redux";
import {
ChartSettings,
updateSlideChart,
updateSlideChartSettings,
} from "@/store/slices/presentationGeneration";
import { renderChart } from "../slide_config";
import { RootState } from "@/store/store";
interface AllChartProps {
chartData: StoreChartData;
slideIndex: number;
}
const AllChart = ({
chartData: initialChartData,
slideIndex,
}: AllChartProps) => {
const dispatch = useDispatch();
const [localChartData, setLocalChartData] =
useState<StoreChartData>(initialChartData);
const [isEditorOpen, setIsEditorOpen] = useState(false);
const { currentColors } = useSelector((state: RootState) => state.theme);
// Use chart settings from the Redux store
const chartSettings = useSelector((state: RootState) => {
const slide =
state.presentationGeneration?.presentationData?.slides[slideIndex];
const style = slide?.content.graph.style || {};
return Object.keys(
style === null || style === undefined ? {} : (style as ChartSettings)
).length > 0
? (style as ChartSettings)
: {
showLegend: false,
showGrid: false,
showAxisLabel: true,
showDataLabel: true,
dataLabel: {
dataLabelPosition:
slide?.content.graph.type === "pie"
? ("Outside" as const)
: ("Inside" as const),
dataLabelAlignment: "Center" as const,
},
};
});
useEffect(() => {
setLocalChartData(initialChartData);
}, [initialChartData]);
const handleChartClick = () => {
setIsEditorOpen(true);
};
const onChartDataChange = (newData: StoreChartData) => {
dispatch(updateSlideChart({ index: slideIndex, chart: newData }));
setLocalChartData(newData);
};
const onChartSettingsChange = (newSettings: ChartSettings) => {
dispatch(
updateSlideChartSettings({
index: slideIndex,
chartSettings: newSettings,
})
);
};
return (
<>
<div
onClick={handleChartClick}
data-slide-element
data-element-type="graph"
data-graph-type={localChartData && localChartData.type}
data-element-id={`slide-group-${slideIndex}-graph`}
className="w-full h-full min-h-[200px] lg:min-h-[300px] max-md:pointer-events-none cursor-pointer hover:opacity-90 transition-opacity relative"
>
{renderChart(localChartData, false, currentColors ?? [], chartSettings)}
{/* <img src={`/Banner.png`} alt={localChartData.type} className="w-full h-full object-cover" /> */}
</div>
{localChartData && (
<ChartEditor
chartSettings={chartSettings}
setChartSettings={onChartSettingsChange}
isOpen={isEditorOpen}
onClose={() => setIsEditorOpen(false)}
chartData={localChartData}
onChartDataChange={onChartDataChange}
/>
)}
</>
);
};
export default AllChart;

View file

@ -1,179 +0,0 @@
import { Trash2 } from 'lucide-react';
import React from 'react'
interface NewSlideProps {
onSelectLayout: (type: number) => void;
setShowNewSlideSelection: (show: boolean) => void;
}
const LayoutPreview = ({ type }: { type: string }) => {
switch (type) {
case 'type1':
return (
<div className="w-full h-[120px] bg-white p-3 flex items-center gap-2">
<div className="w-1/2 space-y-1.5">
<div className="h-3 bg-gray-200 w-3/4"></div>
<div className="h-2 bg-gray-100 w-3/4"></div>
</div>
<div className="w-1/2 h-full bg-gray-100 flex items-center justify-center">
<p className='text-gray-500 text-sm'>image</p>
</div>
</div>
)
case 'type2':
return (
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
<div className="flex gap-2 flex-1">
{[1, 2, 3].map((i) => (
<div key={i} className="flex-1 bg-gray-100 p-1.5">
<div className="h-2 bg-gray-200 w-3/4 mb-1"></div>
<div className="h-1.5 bg-gray-50 w-full"></div>
</div>
))}
</div>
</div>
)
case 'type4':
return (
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
<div className="grid grid-cols-3 gap-2 flex-1">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-gray-100 p-1.5">
<div className="h-8 bg-gray-200 mb-1 flex items-center justify-center">
<p className='text-gray-500 text-xs'>image</p>
</div>
<div className="h-1.5 bg-gray-50 w-3/4"></div>
</div>
))}
</div>
</div>
)
case 'type5':
return (
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
<div className="h-2 bg-gray-200 w-1/2 "></div>
<div className="w-full grid grid-cols-2 h-full items-center gap-2">
<div className=" bg-gray-100 h-full w-full flex items-center justify-center">
<p className='text-gray-500 text-xs'>chart</p>
</div>
<div className="h-4 bg-gray-100 w-full"></div>
</div>
</div>
)
case 'type6':
return (
<div className="w-full h-[120px] bg-white p-3 flex gap-2">
<div className="w-1/2 space-y-1.5">
<div className="h-3 bg-gray-200 w-3/4"></div>
<div className="h-2 bg-gray-100 w-full"></div>
</div>
<div className="w-1/2 space-y-1.5">
{[1, 2].map((i) => (
<div key={i} className="flex gap-1.5 bg-gray-50 p-1.5">
<div className="w-4 h-4 bg-gray-200 rounded-full flex-shrink-0"></div>
<div className="flex-1">
<div className="h-1.5 bg-gray-200 w-3/4"></div>
</div>
</div>
))}
</div>
</div>
)
case 'type7':
return (
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
<div className="flex justify-between px-6">
{[1, 2, 3].map((i) => (
<div key={i} className="text-center bg-gray-100 h-full flex flex-col items-center justify-center">
<div className="w-4 h-4 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-1">
<p className='text-gray-500 text-xs'>Icon</p>
</div>
<div className="h-1.5 bg-gray-200 w-12 mb-1"></div>
<div className="h-5 bg-gray-200 w-12"></div>
</div>
))}
</div>
</div>
)
case 'type8':
return (
<div className="w-full h-[120px] bg-white p-3 flex gap-2">
<div className="w-1/2 space-y-1.5">
<div className="h-3 bg-gray-200 w-3/4"></div>
<div className="h-2 bg-gray-100 w-full"></div>
</div>
<div className="w-1/2 space-y-2">
{[1, 2].map((i) => (
<div key={i} className="flex gap-1.5 bg-gray-50 p-1.5">
<div className="w-6 h-6 bg-gray-200 flex-shrink-0 flex items-center justify-center">
<p className='text-gray-500 text-[10px]'>Icon</p>
</div>
<div className="flex-1">
<div className="h-1.5 bg-gray-200 w-3/4"></div>
</div>
</div>
))}
</div>
</div>
)
case 'type9':
return (
<div className="w-full h-[120px] bg-white p-3 flex gap-2">
<div className="w-1/2 space-y-1.5">
<div className="h-2 bg-gray-200 w-3/4"></div>
<div className="flex-1 bg-gray-100 h-full flex items-center justify-center">
<p className='text-gray-500 text-sm'>chart</p>
</div>
</div>
<div className="w-1/2 space-y-1.5">
{[1, 2, 3].map((i) => (
<div key={i} className="flex gap-1.5 bg-gray-50 p-1.5">
<div className="w-4 h-4 bg-gray-200 rounded-full flex-shrink-0"></div>
<div className="flex-1">
<div className="h-1.5 bg-gray-200 w-3/4"></div>
</div>
</div>
))}
</div>
</div>
)
default:
return null
}
}
const NewSlide = ({ onSelectLayout, setShowNewSlideSelection }: NewSlideProps) => {
return (
<div className='my-6 w-full bg-gray-50 p-8 max-w-[1280px]'>
<div className='flex justify-between items-center mb-8'>
<h2 className="text-2xl font-semibold">Select a Slide Layout</h2>
<Trash2 onClick={() => setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' />
</div>
<div className='grid grid-cols-4 gap-4'>
{['type1', 'type2', 'type4', 'type5', 'type6', 'type7', 'type8', 'type9'].map((type) => (
<div
key={type}
className="transform hover:scale-105 transition-transform cursor-pointer"
onClick={() => onSelectLayout(parseInt(type.replace('type', '')))}
>
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
<div className="p-2 border-b">
<h3 className="text-xs font-medium">Layout {type.replace('type', '')}</h3>
</div>
<div className="p-2">
<LayoutPreview type={type} />
</div>
</div>
</div>
))}
</div>
</div>
)
}
export default NewSlide

View file

@ -1,82 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import ImageEditor from "../ImageEditor";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import SlideFooter from "./SlideFooter";
interface Type1LayoutProps {
title: string;
description: string;
slideId: string | null;
images: string[];
slideIndex: number;
image_prompts?: string[] | null;
properties?: null | any;
}
const Type1Layout = ({
title,
description,
images,
slideId,
slideIndex,
image_prompts,
properties,
}: Type1LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
return (
<div
className="slide-container w-full rounded-sm max-w-[1280px] shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] max-h-[720px] flex items-center aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-id={slideId}
data-slide-index={slideIndex}
data-slide-type="1"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3 sm:gap-8 md:gap-12 lg:gap-16 w-full">
<div className=" flex flex-col w-full items-start justify-center space-y-1 md:space-y-2 lg:space-y-6">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description-body`}
type="description-body"
content={description}
/>
</div>
<ImageEditor
elementId={`slide-${slideIndex}-image`}
slideIndex={slideIndex}
initialImage={images[0]}
title={title}
promptContent={image_prompts?.[0]}
properties={properties}
/>
{/* {imagePosition === 'left' ? (
<>
<ImageSection />
<ContentSection />
</>
) : (
<>
<ContentSection />
<ImageSection />
</>
)} */}
</div>
<SlideFooter />
</div>
);
};
export default Type1Layout;

View file

@ -1,358 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreVertical, Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useSelector } from "react-redux";
import { numberTranslations } from "../../utils/others";
import { RootState } from "@/store/store";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type2LayoutProps {
title: string;
body: Array<{
heading: string;
description: string;
}>;
slideId: string | null;
slideIndex: number;
language: string;
design_index: number;
}
const Type2Layout = ({
title,
body,
slideId,
slideIndex,
design_index,
language,
}: Type2LayoutProps) => {
const { currentColors } = useSelector(
(state: RootState) => state.theme
);
const { handleAddItem, handleDeleteItem, handleVariantChange } =
useSlideOperations(slideIndex);
const onAddItem = () => {
if (body.length < 4) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const onDeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
const VariantMenu = () => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="absolute top-0 -left-7 hidden md:block p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50">
<MoreVertical className="w-4 h-4 text-black" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[180px] p-2">
<DropdownMenuItem
onClick={() => handleVariantChange({ variant: 1 })}
className={`px-3 py-2 cursor-pointer ${design_index === 1 ? "bg-blue-50" : ""
}`}
>
Default Layout
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleVariantChange({ variant: 2 })}
className={`px-3 py-2 cursor-pointer ${design_index === 2 ? "bg-blue-50" : ""
}`}
>
Numbered Layout
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleVariantChange({ variant: 3 })}
className={`px-3 py-2 cursor-pointer ${design_index === 3 ? "bg-blue-50" : ""
}`}
>
Timeline Layout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
const isGridLayout = body.length >= 4;
const renderContent = () => {
if (design_index === 3) {
return (
<div className="w-full flex flex-col relative group mt-4 lg:mt-16">
<div className="absolute -inset-[2px] border-2 border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
{/* Three Dots Icon */}
<VariantMenu />
{/* Plus Icon */}
<button
onClick={onAddItem}
className="absolute top-1/2 -right-4 -translate-y-1/2 p-1 rounded-md bg-white shadow-sm opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
{/* Timeline Header with Numbers and Line */}
<div className="relative flex justify-between w-[85%] mx-auto items-center mb-8 px-8">
{/* Horizontal Line */}
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="line"
data-element-id={`slide-${slideIndex}-horizontal-line`}
className="absolute top-1/2 w-[87%] left-1/2 -translate-x-1/2 h-[2px] "
style={{
backgroundColor: currentColors.iconBg,
}}
/>
{/* Timeline Numbers */}
{body.map((_, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="filledbox"
data-element-id={`slide-${slideIndex}-timeline-number-${index}`}
key={`timeline-${index}`}
className="relative z-10 w-12 h-12 rounded-full px-1 text-white flex items-center justify-center font-bold text-lg"
style={{
backgroundColor: currentColors.iconBg,
}}
>
<span
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-timeline-number-text-${index}`}
>
{numberTranslations[language][index || 0]}
</span>
</div>
))}
</div>
{/* Timeline Content */}
<div className="flex justify-between gap-8">
{body.map((item, index) => (
<div
key={`${body.length}-${index}`}
className="flex-1 text-center relative"
>
<ElementMenu index={index} handleDeleteItem={onDeleteItem} />
<div className="space-y-4">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
))}
</div>
</div>
);
}
if (isGridLayout) {
return (
<div
className={`grid grid-cols-1 lg:grid-cols-2 relative group ${design_index === 2 ? "gap-4 lg:gap-8" : "gap-6 md:gap-12"
} mt-4 lg:mt-12`}
>
{/* Hover Border and Icons for entire layout */}
<div className="absolute -inset-[2px] border-2 border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
{/* Three Dots Icon */}
<VariantMenu />
{body.map((item, index) => (
<div
key={index}
data-slide-element
data-slide-index={slideIndex}
data-element-type={design_index === 2 ? "slide-box" : ""}
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow:
design_index === 2
? "0 2px 10px 0 rgba(43, 43, 43, 0.2)"
: "",
}}
className={`w-full relative group ${design_index === 2
? "slide-box shadow-lg rounded-lg p-3 lg:p-6"
: ""
}`}
>
<div className="flex gap-3 ">
{design_index === 2 && (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-item-${index}-number`}
className=" text-[32px] leading-[40px] px-1 font-bold mb-4"
style={{
color: currentColors.iconBg,
}}
>
{
numberTranslations[
language as keyof typeof numberTranslations
][index]
}
</div>
)}
<ElementMenu index={index} handleDeleteItem={onDeleteItem} />
<div className="space-y-2">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
bodyIdx={index}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
bodyIdx={index}
/>
</div>
</div>
</div>
))}
</div>
);
}
// Horizontal layout for 2-3 items
return (
<div
className={`flex flex-col lg:flex-row mt-4 lg:mt-12 w-full relative group ${design_index === 2 ? "gap-4 lg:gap-8" : "gap-12"
}`}
>
{/* Hover Border and Icons for entire layout */}
<div className="absolute -inset-[2px] hidden lg:block border-2 border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
{/* Three Dots Icon */}
<VariantMenu />
{/* Plus Icon */}
{body.length < 4 && (
<button
onClick={onAddItem}
className="absolute top-1/2 -right-4 hidden lg:block -translate-y-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
)}
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
key={`${body.length}-${index}`}
style={{
boxShadow:
design_index === 2 ? "0 2px 10px 0 rgba(43, 43, 43, 0.2)" : "",
}}
className={`w-full relative ${design_index === 2
? "slide-box shadow-lg rounded-lg p-3 lg:p-6"
: ""
}`}
>
<ElementMenu index={index} handleDeleteItem={onDeleteItem} />
{design_index === 2 && (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-item-${index}-number`}
className=" text-[32px] leading-[40px] font-semibold lg:mb-4"
style={{
color: currentColors.iconBg,
}}
>
{
numberTranslations[
language as keyof typeof numberTranslations
][index]
}
</div>
)}
<div className="space-y-2 lg:space-y-4">
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
))}
</div>
);
};
return (
<div
className="slide-container rounded-sm max-w-[1280px]w-full shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-element-type="slide-container"
data-slide-type="2"
data-element-id={`slide-${slideIndex}-container`}
data-design-index={design_index}
>
<div className="text-center lg:pb-8 w-full">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
isAlingCenter={true}
content={title}
/>
</div>
{renderContent()}
<SlideFooter />
</div>
);
};
export default Type2Layout;

View file

@ -1,147 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import ImageEditor from "../ImageEditor";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
import ElementMenu from "../ElementMenu";
import { Plus } from "lucide-react";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type4LayoutProps {
title: string;
body: Array<{
heading: string;
description: string;
}>;
slideId: string | null;
images: string[];
slideIndex: number;
image_prompts?: string[] | null;
properties?: null | any;
}
const Type4Layout = ({
title,
body,
slideId,
images,
slideIndex,
image_prompts,
properties,
}: Type4LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const {
handleAddItem,
handleDeleteItem,
handleImageChange,
handleDeleteImage,
} = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 3) {
handleImageChange({ imageUrl: "", imageIndex: slideIndex });
handleAddItem({
item: { heading: "Enter Heading", description: "Enter Description" },
});
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
handleDeleteImage({ imageIndex: index });
}
};
const getGridCols = (length: number) => {
switch (length) {
case 1: return 'lg:grid-cols-1';
case 2: return 'lg:grid-cols-2';
case 3: return 'lg:grid-cols-3';
case 4: return 'lg:grid-cols-4';
// Add more cases as needed
default: return 'lg:grid-cols-1';
}
}
return (
<div
className="slide-container shadow-lg rounded-sm w-full max-w-[1280px] px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] font-inter flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="4"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="text-center mb-4 lg:mb-16 w-full">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
isAlingCenter={true}
content={title}
/>
</div>
<div
className={`grid grid-cols-1 lg:grid-cols-2 ${getGridCols(body.length)} gap-3 lg:gap-6 w-full relative group`}
>
<div className="absolute -inset-[2px] border-2 border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute -bottom-4 left-1/2 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-black" />
</button>
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={index}
className="flex slide-box flex-col w-full rounded-lg overflow-hidden relative group"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<ImageEditor
elementId={`slide-${slideIndex}-item-${index}-image`}
slideIndex={slideIndex}
initialImage={images[index]}
className="max-md:h-[140px] max-lg:h-[180px] h-48 w-full rounded-t-lg rounded-b-none"
title={item.heading}
promptContent={image_prompts?.[index]}
imageIdx={index}
properties={properties}
/>
<div className="space-y-2 p-3 lg:p-6">
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
))}
</div>
<SlideFooter />
</div>
);
};
export default Type4Layout;

View file

@ -1,73 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import { RootState } from "@/store/store";
import { useSelector } from "react-redux";
import AllChart from "./AllChart";
import SlideFooter from "./SlideFooter";
interface Type5LayoutProps {
title: string;
description: string;
slideId: string | null;
chartComponent?: React.ReactNode;
graphData?: any;
slideIndex: number;
isFullSizeGraph?: boolean;
}
const Type5Layout = ({
title,
description,
slideId,
chartComponent,
graphData,
slideIndex,
isFullSizeGraph = false,
}: Type5LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
return (
<div
className="slide-container font-inter rounded-sm w-full max-w-[1280px] px-3 py-[10px] sm:px-12 lg:px-20 sm:py-[40px] lg:py-[86px] shadow-lg max-h-[720px] flex flex-col items-center justify-center aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="5"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
isAlingCenter={false}
/>
<div
className={`flex w-full items-center ${isFullSizeGraph
? " flex-col mt-4 lg:mt-10 gap-2 sm:gap-4 md:gap-6 lg:gap-10"
: "mt-4 lg:mt-16 gap-4 sm:gap-8 md:gap-12 lg:gap-16 "
} `}
>
<div className={` w-full`}>
<AllChart chartData={graphData} slideIndex={slideIndex} />
</div>
<div className={` w-full text-center`}>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description-body`}
type="description-body"
isAlingCenter={isFullSizeGraph}
content={description}
/>
</div>
</div>
<SlideFooter />
</div>
);
};
export default Type5Layout;

View file

@ -1,139 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import { Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useDispatch, useSelector } from "react-redux";
import { numberTranslations } from "../../utils/others";
import { RootState } from "@/store/store";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type6LayoutProps {
title: string;
description: string;
body: Array<{
heading: string;
description: string;
}>;
slideId: string | null;
slideIndex: number;
language: string;
}
const Type6Layout = ({
title,
description,
body,
slideId,
slideIndex,
language,
}: Type6LayoutProps) => {
const dispatch = useDispatch();
const { currentColors } = useSelector((state: RootState) => state.theme);
const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 3) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
return (
<div
className="slide-container rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-container"
data-slide-type="6"
data-element-id={`slide-${slideIndex}-container`}
data-slide-id={slideId}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="flex flex-col lg:flex-row gap-4 sm:gap-18 d:gap-16 items-center w-full">
{/* Left section - Description */}
<div className="lg:w-1/2 lg:space-y-8">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description`}
type="description"
content={description}
/>
</div>
{/* Right section - Numbered items */}
<div className="lg:w-1/2 relative group">
<div className="absolute -inset-[2px] border-2 hidden md:block border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute -bottom-4 left-1/2 hidden md:block -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-black" />
</button>
<div className="space-y-3 lg:space-y-6">
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={`${body.length}-${index}`}
className="slide-box rounded-lg p-3 lg:p-6 relative group"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<div className="flex gap-6">
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-item-${index}-number`}
className=" text-[26px] lg:text-[32px] leading-[40px] px-1 font-bold mb-4"
style={{
color: currentColors.iconBg,
}}
>
{numberTranslations[language][index || 0]}
</div>
<div className="space-y-1">
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
<SlideFooter />
</div>
);
};
export default Type6Layout;

View file

@ -1,209 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import IconsEditor from "../IconsEditor";
import { Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type2LayoutProps {
title: string;
body: Array<{
heading: string;
description: string;
}>;
icons: string[];
slideIndex: number;
slideId: string | null;
icon_queries?: Array<{ queries: string[] }> | null;
}
const Type7Layout = ({
title,
body,
icons,
slideIndex,
slideId,
icon_queries,
}: Type2LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 4) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
const getGridCols = (length: number) => {
switch (length) {
case 1: return 'lg:grid-cols-1';
case 2: return 'lg:grid-cols-2';
case 3: return 'lg:grid-cols-3';
case 4: return 'lg:grid-cols-4';
case 5: return 'lg:grid-cols-5';
case 6: return 'lg:grid-cols-6';
case 7: return 'lg:grid-cols-7';
// Add more cases as needed
default: return 'lg:grid-cols-1';
}
}
const isGridLayout = body.length >= 4;
const renderContent = () => {
if (isGridLayout) {
return (
<div
className={`grid grid-cols-1 ${body.length > 4 ? 'md:grid-cols-3' : 'md:grid-cols-2'} gap-4 sm:gap-6 lg:gap-8 mt-4 lg:mt-12 w-full relative group`}
>
<div className="absolute hidden lg:block -inset-[2px] border-2 border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute hidden lg:block -bottom-4 left-1/2 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
key={`${body.length}-${index}`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className={` w-full slide-box rounded-lg p-3 lg:p-6 relative group`}
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<div className="flex items-start gap-2 mg:gap-4">
<div className="flex-shrink-0 lg:w-16">
<IconsEditor
hasBg={true}
backgroundColor={currentColors.iconBg}
icon={icons[index]}
index={index}
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-icon-${index}`}
icon_prompt={icon_queries?.[index]?.queries || []}
/>
</div>
<div>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
bodyIdx={index}
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
bodyIdx={index}
content={item.description}
/>
</div>
</div>
</div>
))}
</div>
);
}
// Horizontal layout for 2-3 items
return (
<div
className={`grid grid-cols-1 sm:grid-cols-2 ${getGridCols(body.length)} w-full gap-3 lg:gap-8 mt-4 lg:mt-12 relative group`}
>
<div className="absolute -inset-[2px] border-2 hidden lg:block border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute -bottom-4 hidden lg:block left-1/2 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
key={`${body.length}-${index}`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className={`w-full slide-box rounded-lg p-3 lg:p-6 relative group`}
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<IconsEditor
hasBg={true}
backgroundColor={currentColors.iconBg}
icon={icons[index]}
index={index}
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-icon-${index}`}
icon_prompt={icon_queries?.[index]?.queries || []}
/>
<div className="lg:space-y-4 mt-2 lg:mt-4">
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
))}
</div>
);
};
return (
<div
className="slide-container rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-type="7"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
data-slide-id={slideId}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="text-center sm:pb-2 lg:pb-8 w-full">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
isAlingCenter={true}
content={title}
/>
</div>
{renderContent()}
<SlideFooter />
</div>
);
};
export default Type7Layout;

View file

@ -1,185 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import IconsEditor from "../IconsEditor";
import { Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type6LayoutProps {
title: string;
description: string;
body: Array<{
heading: string;
description: string;
}>;
icons: string[];
slideId: string | null;
slideIndex: number;
icon_queries?: Array<{ queries: string[] }> | null;
}
const Type8Layout = ({
title,
description,
body,
icons,
slideIndex,
slideId,
icon_queries,
}: Type6LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 3) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
return (
<div
className="slide-container shadow-lg w-full max-w-[1280px] rounded-sm font-inter px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="8"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-8 lg:gap-16 items-center w-full">
{/* Left section - Description */}
<div className="space-y-2 lg:space-y-6">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description`}
type="description"
content={description}
/>
</div>
{/* Right section - Numbered items */}
<div className=" relative group ">
<div className="absolute -inset-[2px] hidden md:block border-2 border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute -bottom-4 left-1/2 hidden md:block -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
<div className="space-y-4 lg:space-y-8">
{body && body.length > 0 && body.length === 2
? body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={`${body.length}-${index}`}
className="slide-box rounded-lg p-3 lg:p-6 relative group"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<IconsEditor
icon={icons[index]}
index={index}
backgroundColor={currentColors.iconBg}
hasBg={true}
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-icon-${index}`}
icon_prompt={icon_queries?.[index]?.queries || []}
/>
<div className="space-y-1 lg:space-y-3 lg:mt-3">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
bodyIdx={index}
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
bodyIdx={index}
content={item.description}
/>
</div>
</div>
))
: body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={`${body.length}-${index}`}
className="slide-box rounded-lg p-3 lg:p-6 relative group"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<div className="flex items-start gap-4">
<div className="w-[32px] md:w-[64px] h-[32px] md:h-[64px]">
<IconsEditor
className="rounded-lg"
icon={icons[index]}
index={index}
backgroundColor={currentColors.iconBg}
hasBg={true}
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-icon-${index}`}
icon_prompt={icon_queries?.[index]?.queries || []}
/>
</div>
<div className="lg:space-y-3 ">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
bodyIdx={index}
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
bodyIdx={index}
content={item.description}
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
<SlideFooter />
</div>
);
};
export default Type8Layout;

View file

@ -1,139 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import { Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useSelector } from "react-redux";
import { numberTranslations } from "../../utils/others";
import { RootState } from "@/store/store";
import AllChart from "./AllChart";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type9LayoutProps {
title: string;
body: Array<{
heading: string;
description: string;
}>;
graphData?: any;
slideId: string | null;
language: string;
slideIndex: number;
}
const Type9Layout = ({
title,
body,
graphData,
slideId,
slideIndex,
language,
}: Type9LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 3) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
return (
<div
className="slide-container rounded-sm w-full max-w-[1280px] font-inter px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] shadow-lg flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-type="9"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
data-slide-id={slideId}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="grid grid-cols-1 lg:grid-cols-2 w-full items-center gap-2 sm:gap-4 md:gap-8 lg:gap-16">
{/* Left section - Chart */}
<div className="space-y-2 lg:space-y-14">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
/>
<div className=" flex items-center justify-center">
<div className="w-full">
<AllChart chartData={graphData} slideIndex={slideIndex} />
</div>
</div>
</div>
{/* Right section - Numbered items */}
<div className=" relative group">
<div className="absolute -inset-[2px] border-2 hidden lg:block border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute left-1/2 hidden lg:block -bottom-4 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
<div className="space-y-4 lg:space-y-8">
{body.length > 0 &&
body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={`${body.length}-${index}`}
className="slide-box relative rounded-lg p-3 lg:p-6"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<div className="flex gap-3 lg:gap-6 ">
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-item-${index}-number`}
className=" text-[32px] leading-[40px] px-1 font-bold mb-4"
style={{
color: currentColors.iconBg,
}}
>
{numberTranslations[language][index || 0]}
</div>
<div className="lg:space-y-2 ">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
bodyIdx={index}
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
bodyIdx={index}
content={item.description}
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
<SlideFooter />
</div>
);
};
export default Type9Layout;

View file

@ -1,7 +1,7 @@
"use client";
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import dynamic from 'next/dynamic';
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import * as z from 'zod';
export interface LayoutInfo {
@ -98,8 +98,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
const module = await import(`@/presentation-layouts/${groupData.groupName}/${file}`);
if (!module.default) {
toast({
title: `${file} has no default export`,
toast.error(`${file} has no default export`, {
description: 'Please ensure the layout file exports a default component',
});
console.warn(`${file} has no default export`);
@ -107,8 +106,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
}
if (!module.Schema) {
toast({
title: `${file} has no Schema export`,
toast.error(`${file} has no Schema export`, {
description: 'Please ensure the layout file exports a Schema',
});
console.warn(`${file} has no Schema export`);

View file

@ -13,7 +13,6 @@
"use client";
import styles from "../styles/main.module.css";
import { useEffect, useState, useRef, useMemo } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { OverlayLoader } from "@/components/ui/overlay-loader";
@ -23,13 +22,12 @@ import { useDispatch, useSelector } from "react-redux";
import { useRouter } from "next/navigation";
import { RootState } from "@/store/store";
import { Button } from "@/components/ui/button";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import MarkdownRenderer from "./MarkdownRenderer";
import { getIconFromFile } from "../../utils/others";
import { ChevronRight, PanelRightOpen, X } from "lucide-react";
import ToolTip from "@/components/ToolTip";
import Header from "@/app/dashboard/components/Header";
import { useLayout } from "../../context/LayoutContext";
// Types
interface LoadingState {
@ -124,11 +122,7 @@ const DocumentsPreviewPage: React.FC = () => {
});
} catch (error) {
console.error('Error reading files:', error);
toast({
title: "Error",
description: "Failed to read document content",
variant: "destructive",
});
toast.error("Failed to read document content");
}
setDownloadingDocuments([]);
}
@ -157,11 +151,7 @@ const DocumentsPreviewPage: React.FC = () => {
router.push("/outline");
} catch (error) {
console.error("Error in presentation creation:", error);
toast({
title: "Error in presentation creation.",
description: "Please try again.",
variant: "destructive",
});
toast.error("Error in presentation creation. Please try again.");
setShowLoading({
message: "Error in presentation creation.",
show: true,
@ -214,7 +204,7 @@ const DocumentsPreviewPage: React.FC = () => {
if (!isOpen) return null;
return (
<div className={`${styles.sidebar} fixed xl:relative w-full z-50 xl:z-auto
<div className={`border-r border-gray-200 fixed xl:relative w-full z-50 xl:z-auto
transition-all duration-300 ease-in-out max-w-[200px] md:max-w-[300px] h-[85vh] rounded-md p-5`}>
<X
onClick={() => setIsOpen(false)}
@ -230,7 +220,7 @@ const DocumentsPreviewPage: React.FC = () => {
<div
key={key}
onClick={() => updateSelectedDocument(key)}
className={`${selectedDocument === key ? styles.selected_border : ""
className={`${selectedDocument === key ? 'border border-blue-500' : ""
} flex p-2 rounded-sm gap-2 items-center cursor-pointer`}
>
<img
@ -251,7 +241,7 @@ const DocumentsPreviewPage: React.FC = () => {
};
return (
<div className={`${styles.wrapper} min-h-screen flex flex-col w-full`}>
<div className={`bg-white/90 min-h-screen flex flex-col w-full`}>
<OverlayLoader
show={showLoading.show}
text={showLoading.message}

View file

@ -1,39 +0,0 @@
.wrapper {
background: rgba(233, 232, 248, 1);
}
.sidebar {
border-right: 1px solid rgba(221, 220, 237, 1);
background: white;
}
.report_icon_box {
background: rgba(234, 241, 255, 1);
}
.selected_border {
border: 1px solid rgba(0, 0, 255, 1);
}
.unselected_border {
border: 1px solid rgba(237, 237, 237, 1);
}
.custom_scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom_scrollbar::-webkit-scrollbar-thumb {
background-color: #4A90E2;
border-radius: 6px;
cursor: pointer;
}
.custom_scrollbar::-webkit-scrollbar-track {
background-color: #F0F0F0;
}
.custom_scrollbar::-webkit-scrollbar-corner {
background-color: transparent;
}

View file

@ -1,56 +0,0 @@
import { useDispatch } from "react-redux";
import {
addSlideBodyItem,
deleteSlideBodyItem,
updateSlideVariant,
updateSlideImage,
deleteSlideImage,
// Import other slide operation actions as needed
} from "@/store/slices/presentationGeneration";
export const useSlideOperations = (slideIndex: number) => {
const dispatch = useDispatch();
const handleAddItem = ({ item }: { item: any }) => {
dispatch(addSlideBodyItem({ index: slideIndex, item }));
};
const handleDeleteItem = ({ itemIndex }: { itemIndex: number }) => {
dispatch(deleteSlideBodyItem({ index: slideIndex, itemIdx: itemIndex }));
};
const handleVariantChange = ({ variant }: { variant: number }) => {
dispatch(updateSlideVariant({ index: slideIndex, variant }));
};
const handleImageChange = ({
imageUrl,
imageIndex,
}: {
imageUrl: string;
imageIndex: number;
}) => {
dispatch(
updateSlideImage({
index: slideIndex,
imageIdx: imageIndex,
image: imageUrl,
})
);
};
const handleDeleteImage = ({ imageIndex }: { imageIndex: number }) => {
dispatch(deleteSlideImage({ index: slideIndex, imageIdx: imageIndex }));
};
// Add other common slide operations here
return {
handleAddItem,
handleDeleteItem,
handleVariantChange,
handleImageChange,
handleDeleteImage,
};
};

View file

@ -35,7 +35,7 @@ export const useGroupLayouts = () => {
// Render slide content with group validation, automatic Tiptap text editing, and editable images/icons
const renderSlideContent = useMemo(() => {
return (slide: any, isEditMode: boolean = true) => {
return (slide: any, isEditMode: boolean) => {
const Layout = getGroupLayout(slide.layout, slide.layout_group);
if (!Layout) {
return (
@ -55,6 +55,7 @@ export const useGroupLayouts = () => {
isEditMode={isEditMode}
>
<TiptapTextReplacer
key={slide.id}
slideData={slide.content}
slideIndex={slide.index}
isEditMode={isEditMode}

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import { setOutlines, SlideOutline } from "@/store/slices/presentationGeneration";
import { jsonrepair } from "jsonrepair";
import { StreamState } from "../types/index";
@ -56,11 +56,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
eventSource.close();
} catch (error) {
console.error("Error parsing accumulated chunks:", error);
toast({
title: "Error",
description: "Failed to parse presentation data",
variant: "destructive",
});
toast.error("Failed to parse presentation data");
eventSource.close();
}
accumulatedChunks = "";
@ -76,19 +72,11 @@ export const useOutlineStreaming = (presentationId: string | null) => {
eventSource.onerror = () => {
setStreamState({ isStreaming: false, isLoading: false });
eventSource.close();
toast({
title: "Connection Error",
description: "Failed to connect to the server. Please try again.",
variant: "destructive",
});
toast.error("Failed to connect to the server. Please try again.");
};
} catch (error) {
setStreamState({ isStreaming: false, isLoading: false });
toast({
title: "Error",
description: "Failed to initialize connection",
variant: "destructive",
});
toast.error("Failed to initialize connection");
}
};

View file

@ -1,7 +1,7 @@
import { useState, useCallback } from "react";
import { useDispatch } from "react-redux";
import { useRouter } from "next/navigation";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import { clearPresentationData, setPresentationData, SlideOutline } from "@/store/slices/presentationGeneration";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { useLayout } from "../../context/LayoutContext";
@ -27,19 +27,15 @@ export const usePresentationGeneration = (
const validateInputs = useCallback(() => {
if (!outlines || outlines.length === 0) {
toast({
title: "No Outlines",
toast.error("No Outlines", {
description: "Please wait for outlines to load before generating presentation",
variant: "destructive",
});
return false;
}
if (!selectedLayoutGroup) {
toast({
title: "Select Layout Group",
toast.error("Select Layout Group", {
description: "Please select a layout group before generating presentation",
variant: "destructive",
});
return false;
}
@ -101,10 +97,8 @@ export const usePresentationGeneration = (
}
} catch (error) {
console.error("Error in data generation", error);
toast({
title: "Generation Error",
toast.error("Generation Error", {
description: "Failed to generate presentation. Please try again.",
variant: "destructive",
});
} finally {
setLoadingState(DEFAULT_LOADING_STATE);

View file

@ -8,7 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { DashboardApi } from "@/app/dashboard/api/dashboard";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
@ -40,11 +40,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
setContentLoading(false);
} catch (error) {
setError(true);
toast({
title: "Error",
description: "Failed to load presentation",
variant: "destructive",
});
toast.error("Failed to load presentation");
console.error("Error fetching user slides:", error);
setContentLoading(false);
}

View file

@ -1,12 +1,9 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Menu,
Palette,
SquareArrowOutUpRight,
Play,
Loader2,
ExternalLink,
} from "lucide-react";
import React, { useState } from "react";
import Wrapper from "@/components/Wrapper";
@ -16,38 +13,20 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import UserAccount from "../../components/UserAccount";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { OverlayLoader } from "@/components/ui/overlay-loader";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { useDispatch, useSelector } from "react-redux";
import Link from "next/link";
import { ThemeType } from "@/app/(presentation-generator)/upload/type";
import {
setTheme,
setThemeColors,
defaultColors,
serverColors,
} from "../../store/themeSlice";
import CustomThemeSettings from "../../components/CustomThemeSettings";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RootState } from "@/store/store";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import ThemeSelector from "./ThemeSelector";
import Modal from "./Modal";
import Announcement from "@/components/Announcement";
import { getFontLink, getStaticFileUrl } from "../../utils/others";
import { getStaticFileUrl } from "../../utils/others";
import { PptxPresentationModel } from "@/types/pptx_models";
import HeaderNav from "../../components/HeaderNab";
const Header = ({
@ -61,66 +40,10 @@ const Header = ({
const [showLoader, setShowLoader] = useState(false);
const router = useRouter();
const [showCustomThemeModal, setShowCustomThemeModal] = useState(false);
const [showDownloadModal, setShowDownloadModal] = useState(false);
const [downloadPath, setDownloadPath] = useState("");
const { currentTheme, currentColors } = useSelector(
(state: RootState) => state.theme
);
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
const dispatch = useDispatch();
const handleThemeSelect = async (value: string) => {
if (isStreaming) return;
if (value === "custom") {
setShowCustomThemeModal(true);
return;
} else {
const themeType = value as ThemeType;
const themeColors = serverColors[themeType] || defaultColors[themeType];
if (themeColors) {
try {
// Update UI
dispatch(setTheme(themeType));
dispatch(setThemeColors({ ...themeColors, theme: themeType }));
// Set CSS variables
const root = document.documentElement;
root.style.setProperty(
`--${themeType}-slide-bg`,
themeColors.slideBg
);
root.style.setProperty(
`--${themeType}-slide-title`,
themeColors.slideTitle
);
root.style.setProperty(
`--${themeType}-slide-heading`,
themeColors.slideHeading
);
root.style.setProperty(
`--${themeType}-slide-description`,
themeColors.slideDescription
);
root.style.setProperty(
`--${themeType}-slide-box`,
themeColors.slideBox
);
} catch (error) {
console.error("Failed to update theme:", error);
toast({
title: "Error updating theme",
description:
"Failed to update the presentation theme. Please try again.",
variant: "destructive",
});
}
}
}
};
const get_presentation_pptx_model = async (id: string): Promise<PptxPresentationModel> => {
const response = await fetch(`/api/presentation_to_pptx_model?id=${id}`);
@ -148,11 +71,9 @@ const Header = ({
} catch (error) {
console.error("Export failed:", error);
setShowLoader(false);
toast({
title: "Having trouble exporting!",
toast.error("Having trouble exporting!", {
description:
"We are having trouble exporting your presentation. Please try again.",
variant: "default",
});
} finally {
setShowLoader(false);
@ -176,19 +97,16 @@ const Header = ({
if (response.ok) {
const { path: pdfPath } = await response.json();
const staticFileUrl = getStaticFileUrl(pdfPath);
window.open(staticFileUrl, '_blank');
window.open(pdfPath, '_blank');
} else {
throw new Error("Failed to export PDF");
}
} catch (err) {
console.error(err);
toast({
title: "Having trouble exporting!",
toast.error("Having trouble exporting!", {
description:
"We are having trouble exporting your presentation. Please try again.",
variant: "default",
});
} finally {
setShowLoader(false);
@ -212,15 +130,8 @@ const Header = ({
<img src="/pptx.svg" alt="pptx export" width={30} height={30} />
Export as PPTX
</Button>
{/* <div className={`w-full ${mobile ? "bg-white py-2 rounded-lg" : ""}`}>
<JSPowerPointExtractor />
</div> */}
<p className={`text-sm pt-3 border-t border-gray-300 ${mobile ? "border-none text-white font-semibold" : ""}`}>
Font Used:
<a className={`text-blue-500 flex items-center gap-1 ${mobile ? "mt-2 py-2 px-4 bg-white rounded-lg w-fit" : ""}`} href={getFontLink(currentColors.fontFamily).link || ''} target="_blank" rel="noopener noreferrer">
{getFontLink(currentColors.fontFamily).name || ''} <ExternalLink className="w-4 h-4" />
</a>
</p>
</div>
);
@ -282,81 +193,19 @@ const Header = ({
{isStreaming && (
<Loader2 className="animate-spin text-white font-bold w-6 h-6" />
)}
<Select value={currentTheme} onValueChange={handleThemeSelect}>
<SelectTrigger className="w-[160px] bg-[#6358fd] text-white border-none hover:bg-[#5146E5] transition-colors">
<div className="flex items-center gap-2">
<Palette className="w-4 h-4" />
<span>Change Theme</span>
</div>
</SelectTrigger>
<SelectContent className="w-[300px] p-0">
<ThemeSelector
onSelect={handleThemeSelect}
selectedTheme={currentTheme}
/>
</SelectContent>
</Select>
{/* Custom Theme Modal */}
<Modal
isOpen={showCustomThemeModal}
onClose={() => setShowCustomThemeModal(false)}
title="Custom Theme Colors"
>
<CustomThemeSettings
onClose={() => setShowCustomThemeModal(false)}
presentationId={presentation_id}
/>
</Modal>
<MenuItems mobile={false} />
<UserAccount />
<HeaderNav />
</div>
{/* Mobile Menu */}
<div className="lg:hidden flex items-center gap-4">
<UserAccount />
<Sheet>
<SheetTrigger asChild>
<button className="text-white">
<Menu className="h-6 w-6" />
</button>
</SheetTrigger>
<SheetContent side="right" className="bg-[#5146E5] border-none p-4">
<div className="flex flex-col gap-6 mt-10">
<Select onValueChange={handleThemeSelect}>
<SelectTrigger className="w-full bg-[#6358fd] flex justify-center gap-2 text-white border-none">
<Palette className="w-4 h-4 mr-2" />
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light Theme</SelectItem>
<SelectItem value="dark">Dark Theme</SelectItem>
<SelectItem value="royal_blue">Royal Blue Theme</SelectItem>
<SelectItem value="cream">Cream Theme</SelectItem>
<SelectItem value="dark_pink">Dark Pink Theme</SelectItem>
<SelectItem value="light_red">Light Red Theme</SelectItem>
<SelectItem value="faint_yellow">
Faint Yellow Theme
</SelectItem>
<SelectItem value="custom">Custom Theme</SelectItem>
</SelectContent>
</Select>
<MenuItems mobile={true} />
</div>
</SheetContent>
</Sheet>
<HeaderNav />
</div>
</Wrapper>
{/* Download Modal */}
<Modal
isOpen={showDownloadModal}
onClose={() => setShowDownloadModal(false)}
title="File Downloaded"
>
<div className="text-center">
<p className="text-gray-600">Your file is saved at:</p>
<p className="font-mono text-sm mt-2 break-all">{downloadPath}</p>
</div>
</Modal>
</div>
);
};

View file

@ -6,7 +6,6 @@ import { Skeleton } from "@/components/ui/skeleton";
import PresentationMode from "../../components/PresentationMode";
import SidePanel from "../components/SidePanel";
import SlideContent from "../components/SlideContent";
import LoadingState from "../../components/LoadingState";
import Header from "../components/Header";
import { Button } from "@/components/ui/button";
import { AlertCircle, Loader2 } from "lucide-react";
@ -18,6 +17,7 @@ import {
useAutoSave
} from "../hooks";
import { PresentationPageProps } from "../types";
import LoadingState from "./LoadingState";
const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id }) => {
// State management
@ -27,10 +27,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
const [error, setError] = useState(false);
const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false);
// Redux state
const { currentTheme, currentColors } = useSelector(
(state: RootState) => state.theme
);
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
@ -43,7 +40,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
});
// Custom hooks
const { fetchUserSlides, handleDeleteSlide } = usePresentationData(
const { fetchUserSlides } = usePresentationData(
presentation_id,
setLoading,
setError
@ -72,9 +69,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
fetchUserSlides
);
const onDeleteSlide = (index: number) => {
handleDeleteSlide(index, presentationData);
};
const onSlideChange = (newSlide: number) => {
handleSlideChange(newSlide, presentationData);
@ -86,7 +81,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
<PresentationMode
slides={presentationData?.slides!}
currentSlide={currentSlide}
currentTheme={currentTheme}
isFullscreen={isFullscreen}
onFullscreenToggle={toggleFullscreen}
onExit={handlePresentExit}
@ -132,7 +127,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
<div
style={{
background: currentColors.background,
background: '#c8c7c9',
}}
className="flex flex-1 relative pt-6"
>
@ -172,7 +167,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
slide={slide}
index={index}
presentationId={presentation_id}
onDeleteSlide={onDeleteSlide}
/>
))}
</>

View file

@ -45,9 +45,7 @@ const SidePanel = ({
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
const { currentTheme, currentColors } = useSelector(
(state: RootState) => state.theme
);
const dispatch = useDispatch();
// Use the centralized group layouts hook
@ -159,16 +157,10 @@ const SidePanel = ({
`}
>
<div
data-theme={currentTheme}
style={{
backgroundColor: currentColors.slideBg,
}}
className="min-w-[300px] max-w-[300px] h-[calc(100vh-120px)] rounded-[20px] hide-scrollbar overflow-hidden slide-theme shadow-xl"
className="min-w-[300px] bg-white max-w-[300px] h-[calc(100vh-120px)] rounded-[20px] hide-scrollbar overflow-hidden slide-theme shadow-xl"
>
<div
style={{
backgroundColor: currentColors.slideBg,
}}
className="sticky top-0 z-40 px-6 py-4"
>
<div className="flex items-center justify-between gap-4">

View file

@ -8,28 +8,26 @@ import {
} from "@/components/ui/popover";
import { Textarea } from "@/components/ui/textarea";
import { SendHorizontal } from "lucide-react";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import ToolTip from "@/components/ToolTip";
import { RootState } from "@/store/store";
import { useDispatch, useSelector } from "react-redux";
import { addSlide, updateSlide } from "@/store/slices/presentationGeneration";
import NewSlide from "../../components/slide_layouts/NewSlide";
import { getEmptySlideContent } from "../../utils/NewSlideContent";
import { deletePresentationSlide, updateSlide } from "@/store/slices/presentationGeneration";
import { useGroupLayouts } from "../../hooks/useGroupLayouts";
import NewSlide from "../../components/NewSlide";
interface SlideContentProps {
slide: any;
index: number;
presentationId: string;
onDeleteSlide: (index: number) => void;
}
const SlideContent = ({
slide,
index,
presentationId,
onDeleteSlide,
}: SlideContentProps) => {
const dispatch = useDispatch();
const [isUpdating, setIsUpdating] = useState(false);
@ -47,11 +45,7 @@ const SlideContent = ({
) as HTMLInputElement;
const value = element?.value;
if (!value?.trim()) {
toast({
title: "Error",
description: "Please enter a prompt before submitting",
variant: "destructive",
});
toast.error("Please enter a prompt before submitting");
return;
}
setIsUpdating(true);
@ -66,45 +60,25 @@ const SlideContent = ({
if (response) {
console.log("response", response);
dispatch(updateSlide({ index: slide.index, slide: response }));
toast({
title: "Success",
description: "Slide updated successfully",
});
toast.success("Slide updated successfully");
}
} catch (error) {
console.error("Error updating slide:", error);
toast({
title: "Error",
description: "Failed to update slide. Please try again.",
variant: "destructive",
});
toast.error("Failed to update slide. Please try again.");
} finally {
setIsUpdating(false);
}
};
const handleNewSlide = (type: number, index: number) => {
const newSlide: Slide = getEmptySlideContent(
type,
index + 1,
presentationData?.id!
);
dispatch(addSlide({ slide: newSlide, index: index + 1 }));
setShowNewSlideSelection(false);
// Scroll to the newly added slide after a short delay to ensure it's rendered
setTimeout(() => {
const newSlideElement = document.getElementById(`slide-${newSlide.id}`);
if (newSlideElement) {
newSlideElement.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}, 100);
const onDeleteSlide = async () => {
try {
dispatch(deletePresentationSlide(slide.index));
} catch (error) {
console.error("Error deleting slide:", error);
}
};
// Scroll to the new slide when streaming and new slides are being generated
useEffect(() => {
if (
@ -139,16 +113,16 @@ const SlideContent = ({
{isStreaming && (
<Loader2 className="w-8 h-8 absolute right-2 top-2 z-30 text-blue-800 animate-spin" />
)}
<div data-layout={slide.layout} data-group={slide.layout_group} className={` w-full group mb-6`}>
<div data-layout={slide.layout} data-group={slide.layout_group} className={` w-full group `}>
{/* render slides */}
{loading ? <div className="flex flex-col bg-white aspect-video items-center justify-center h-full">
<Loader2 className="w-8 h-8 animate-spin" />
</div> : slideContent}
{/* {!showNewSlideSelection && (
{!showNewSlideSelection && (
<div className="group-hover:opacity-100 hidden md:block opacity-0 transition-opacity my-4 duration-300">
<ToolTip content="Add new slide below">
{!isStreaming && (
{!isStreaming && !loading && (
<div
onClick={() => setShowNewSlideSelection(true)}
className=" bg-white shadow-md w-[80px] py-2 border hover:border-[#5141e5] duration-300 flex items-center justify-center rounded-lg cursor-pointer mx-auto"
@ -159,16 +133,18 @@ const SlideContent = ({
</ToolTip>
</div>
)}
{showNewSlideSelection && (
{showNewSlideSelection && !loading && (
<NewSlide
onSelectLayout={(type) => handleNewSlide(type, slide.index)}
index={index}
group={slide.layout_group}
setShowNewSlideSelection={setShowNewSlideSelection}
presentationId={presentationId}
/>
)} */}
{!isStreaming && (
)}
{!isStreaming && !loading && (
<ToolTip content="Delete slide">
<div
onClick={() => onDeleteSlide(slide.index)}
onClick={onDeleteSlide}
className="absolute top-2 z-20 sm:top-4 right-2 sm:right-4 hidden md:block transition-transform"
>
<Trash2 className="text-gray-500 text-xl cursor-pointer" />

View file

@ -1,155 +0,0 @@
import React from "react";
const ThemeSelector = ({
onSelect,
selectedTheme,
}: {
onSelect: (theme: string) => void;
selectedTheme: string;
}) => {
return (
<div className="grid grid-cols-2 gap-3 p-3">
<button
onClick={() => onSelect("light")}
className="group focus:outline-none"
>
<ThemePreview
theme="Light"
color="#F5F5F5"
isSelected={selectedTheme === "light"}
/>
</button>
<button
onClick={() => onSelect("dark")}
className="group focus:outline-none"
>
<ThemePreview
theme="Dark"
color="#1E1E1E"
isSelected={selectedTheme === "dark"}
/>
</button>
<button
onClick={() => onSelect("cream")}
className="group focus:outline-none"
>
<ThemePreview
theme="Cream"
color="#F9F6F0"
isSelected={selectedTheme === "cream"}
/>
</button>
<button
onClick={() => onSelect("royal_blue")}
className="group focus:outline-none"
>
<ThemePreview
theme="Royal Blue"
color="#091433"
isSelected={selectedTheme === "royal_blue"}
/>
</button>
<button
onClick={() => onSelect("faint_yellow")}
className="group focus:outline-none"
>
<ThemePreview
theme="Faint Yellow"
color="#F8F4E8"
isSelected={selectedTheme === "faint_yellow"}
/>
</button>
<button
onClick={() => onSelect("light_red")}
className="group focus:outline-none"
>
<ThemePreview
theme="Light Red"
color="#FFFAFA"
isSelected={selectedTheme === "light_red"}
/>
</button>
<button
onClick={() => onSelect("dark_pink")}
className="group focus:outline-none"
>
<ThemePreview
theme="Dark Pink"
color="#F9E8FF"
isSelected={selectedTheme === "dark_pink"}
/>
</button>
<button
onClick={() => onSelect("custom")}
className="group focus:outline-none"
>
<div className="flex flex-col items-center gap-1 w-full">
<div
className={`w-full h-16 rounded-lg shadow-sm transition-all p-2 flex flex-col justify-between
bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500
${
selectedTheme === "custom"
? "ring-2 ring-[#5146E5] scale-95"
: "hover:scale-105"
}`}
>
<div className="w-12 h-1.5 rounded bg-white/20"></div>
<div className="space-y-1">
<div className="w-16 h-1.5 rounded bg-white/30"></div>
<div className="w-12 h-1.5 rounded bg-white/20"></div>
</div>
</div>
<span
className={`text-xs font-medium ${
selectedTheme === "custom" ? "text-[#5146E5]" : "text-gray-600"
}`}
>
Custom
</span>
</div>
</button>
</div>
);
};
export default ThemeSelector;
const ThemePreview = ({
theme,
color,
isSelected,
}: {
theme: string;
color: string;
isSelected: boolean;
}) => (
<div
className={`flex flex-col items-center gap-1 w-full ${
isSelected ? "scale-95" : ""
}`}
>
<div
className={`w-full h-16 rounded-t-lg rounded-r-lg border shadow-sm transition-all p-2 flex flex-col justify-between
${
isSelected
? "ring-2 ring-[#5146E5] scale-95"
: "hover:scale-105"
}`}
style={{ backgroundColor: color }}
>
<div className="w-12 h-1.5 rounded bg-white/20"></div>
<div className="space-y-1">
<div className="w-16 h-1.5 rounded bg-white/30"></div>
<div className="w-12 h-1.5 rounded bg-white/20"></div>
</div>
</div>
<span
className={`text-xs font-medium ${
isSelected ? "text-[#5146E5]" : "text-gray-600"
}`}
>
{theme}
</span>
</div>
);

View file

@ -13,7 +13,7 @@ export const useAutoSave = ({
debounceMs = 2000,
enabled = true,
}: UseAutoSaveOptions = {}) => {
const { presentationData } = useSelector(
const { presentationData, isStreaming, isLoading } = useSelector(
(state: RootState) => state.presentationGeneration
);
@ -61,7 +61,7 @@ export const useAutoSave = ({
// Effect to trigger auto-save when presentation data changes
useEffect(() => {
if (!enabled || !presentationData) return;
if (!enabled || !presentationData || isStreaming || isLoading) return;
// Trigger debounced save
debouncedSave(presentationData);

View file

@ -1,9 +1,8 @@
import { useCallback } from "react";
import { useCallback, useEffect } from 'react';
import { useDispatch } from "react-redux";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import { DashboardApi } from "@/app/dashboard/api/dashboard";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { setPresentationData, deletePresentationSlide } from "@/store/slices/presentationGeneration";
import { setPresentationData } from "@/store/slices/presentationGeneration";
export const usePresentationData = (
presentationId: string,
@ -12,7 +11,6 @@ export const usePresentationData = (
) => {
const dispatch = useDispatch();
const fetchUserSlides = useCallback(async () => {
try {
const data = await DashboardApi.getPresentation(presentationId);
@ -22,35 +20,17 @@ export const usePresentationData = (
}
} catch (error) {
setError(true);
toast({
title: "Error",
description: "Failed to load presentation",
variant: "destructive",
});
toast.error("Failed to load presentation");
console.error("Error fetching user slides:", error);
setLoading(false);
}
}, [presentationId, dispatch, setLoading, setError]);
const handleDeleteSlide = useCallback(async (index: number, presentationData: any) => {
dispatch(deletePresentationSlide(index));
try {
await PresentationGenerationApi.deleteSlide(
presentationId,
presentationData?.slides[index].id!
);
} catch (error) {
console.error("Error deleting slide:", error);
toast({
title: "Error",
description: "Failed to delete slide",
variant: "destructive",
});
}
}, [presentationId, dispatch]);
useEffect(() => {
fetchUserSlides();
}, [fetchUserSlides]);
return {
fetchUserSlides,
handleDeleteSlide,
};
};

View file

@ -1,7 +1,8 @@
import { useEffect, useRef } from "react";
import { useDispatch } from "react-redux";
import { setPresentationData, setStreaming } from "@/store/slices/presentationGeneration";
import { useDispatch, useSelector } from "react-redux";
import { clearPresentationData, setPresentationData, setStreaming } from "@/store/slices/presentationGeneration";
import { jsonrepair } from "jsonrepair";
import { RootState } from "@/store/store";
export const usePresentationStreaming = (
presentationId: string,
@ -10,6 +11,8 @@ export const usePresentationStreaming = (
setError: (error: boolean) => void,
fetchUserSlides: () => void
) => {
const { presentationData } = useSelector((state: RootState) => state.presentationGeneration);
const dispatch = useDispatch();
const previousSlidesLength = useRef(0);
@ -19,6 +22,7 @@ export const usePresentationStreaming = (
const initializeStream = async () => {
dispatch(setStreaming(true));
dispatch(clearPresentationData());
eventSource = new EventSource(
`/api/v1/ppt/presentation/stream?presentation_id=${presentationId}`
@ -98,7 +102,9 @@ export const usePresentationStreaming = (
if (stream) {
initializeStream();
} else {
fetchUserSlides();
if(!presentationData || presentationData.slides.length === 0){
fetchUserSlides();
}
}
return () => {

View file

@ -1,164 +0,0 @@
import { ThemeType } from "@/app/(presentation-generator)/upload/type";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export const defaultColors = {
light: {
background: "#c8c7c9",
slideBg: "#F2F2F2",
slideTitle: "#000000",
slideHeading: "#1a1a1a",
slideDescription: "#333333",
slideBox: "#ffffff",
iconBg: "#1F1F2D",
chartColors: ["#1F1F2D", "#3F3F5D", "#62628E", "#8F8FB2", "#C0C0D3"],
fontFamily: "var(--font-inter)",
},
dark: {
background: "#000000",
slideBg: "#1E1E1E",
slideTitle: "#ffffff",
slideHeading: "#f5f5f5",
slideDescription: "#e0e0e0",
slideBox: "#2d2d2d",
iconBg: "#5E8CF0",
chartColors: ["#5E8CF0", "#8800ff", "#b200ff", "#d700ff", "#ef00ff"],
fontFamily: "var(--font-inter)",
},
faint_yellow: {
background: "#d9cebc",
slideBg: "#F8F4E8",
slideTitle: "#2C1810",
slideHeading: "#4A3728",
slideDescription: "#665E57",
slideBox: "#FFFFFF",
iconBg: "#281810",
chartColors: ["#281810", "#4A3728", "#665E57", "#665E57", "#665E57"],
fontFamily: "var(--font-inter)",
},
custom: {
background: "#63ceff",
slideBg: "#F4F4F4",
slideTitle: "#1A1A1A",
slideHeading: "#2D2D2D",
slideDescription: "#4A4A4A",
slideBox: "#d8c6c6",
iconBg: "#281810",
chartColors: ["#281810", "#4A3728", "#665E57", "#665E57", "#665E57"],
fontFamily: "var(--font-inter)",
},
cream: {
background: "#DDCFBB",
slideBg: "#F9F6F0",
slideTitle: "#484237",
slideHeading: "#484237",
slideDescription: "#595F6C",
slideBox: "#EEE9DD",
iconBg: "#A6825B",
chartColors: ["#765939", "#A6825B", "#B89B7C", "#CAB49D", "#DBCDBD"],
fontFamily: "var(--font-fraunces)",
},
royal_blue: {
background: "#010103",
slideBg: "#091433",
slideTitle: "#ffffff",
slideHeading: "#ffffff",
slideDescription: "#E6E6E6",
slideBox: "#29136C",
iconBg: "#5E8CF0",
chartColors: ["#5E8CF0", "#496CEB", "#f051b5", "#F7A8FF", "#FCD8FF"],
fontFamily: "var(--font-instrument-sans)",
},
light_red: {
background: "#F8E9E8",
slideBg: "#FFFAFA",
slideTitle: "#181D27",
slideHeading: "#252B37",
slideDescription: "#595F6C",
slideBox: "#F3E8E8",
iconBg: "#F0695F",
chartColors: [
"#F0695F",
"#450808",
"#8F1010",
"#C1392F",
"#EC5555",
"#F49E9E",
],
fontFamily: "var(--font-montserrat)",
},
dark_pink: {
background: "#F3AEED",
slideBg: "#F9E8FF",
slideTitle: "#261827",
slideHeading: "#252B37",
slideDescription: "#6A596C",
slideBox: "#F0D4F7",
iconBg: "#D02CE5",
chartColors: ["#D02CE5", "#B414C9", "#6E1886", "#A724CC", "#C65FE3"],
fontFamily: "var(--font-inria-serif)",
},
};
// Store the server-provided colors
export const serverColors: { [key in ThemeType]?: ThemeColors } = {};
export interface ThemeColors {
background: string;
slideBg: string;
slideTitle: string;
slideHeading: string;
slideDescription: string;
slideBox: string;
iconBg: string;
chartColors: string[];
fontFamily: string;
}
interface ThemeState {
currentTheme: ThemeType;
currentColors: ThemeColors;
isLoading: boolean;
}
const initialState: ThemeState = {
currentTheme: ThemeType.Light,
currentColors: defaultColors.light,
isLoading: false,
};
const themeSlice = createSlice({
name: "theme",
initialState,
reducers: {
setTheme: (state, action: PayloadAction<ThemeType>) => {
state.currentTheme = action.payload;
// Use server colors if available, otherwise fall back to default
state.currentColors =
serverColors[action.payload] || defaultColors[action.payload];
},
setThemeColors: (
state,
action: PayloadAction<Partial<ThemeColors> & { theme: ThemeType }>
) => {
const newColors = { ...state.currentColors, ...action.payload };
state.currentColors = newColors;
state.currentTheme = action.payload.theme;
// Store the colors for this theme
serverColors[action.payload.theme] = newColors;
},
setLoadingState: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
loadSavedTheme: (state, action: PayloadAction<any>) => {
if (action.payload.name === "custom") {
state.currentTheme = ThemeType.Custom;
state.currentColors = action.payload.colors;
serverColors.custom = action.payload.colors;
}
},
},
});
export const { setTheme, setThemeColors, setLoadingState, loadSavedTheme } =
themeSlice.actions;
export default themeSlice.reducer;

View file

@ -1,148 +0,0 @@
/* Light Theme */
.slide-theme[data-theme="light"] {
--slide-bg: var(--light-slide-bg);
--slide-title: var(--light-slide-title);
--slide-heading: var(--light-slide-heading);
--slide-description: var(--light-slide-description);
--slide-box: var(--light-slide-box);
}
/* Dark Theme */
.slide-theme[data-theme="dark"] {
--slide-bg: var(--dark-slide-bg);
--slide-title: var(--dark-slide-title);
--slide-heading: var(--dark-slide-heading);
--slide-description: var(--dark-slide-description);
--slide-box: var(--dark-slide-box);
}
/* Classic Theme */
.slide-theme[data-theme="faint_yellow"] {
--slide-bg: var(--faint_yellow-slide-bg);
--slide-title: var(--faint_yellow-slide-title);
--slide-heading: var(--faint_yellow-slide-heading);
--slide-description: var(--faint_yellow-slide-description);
--slide-box: var(--faint_yellow-slide-box);
}
/* Custom Theme */
.slide-theme[data-theme="custom"] {
--slide-bg: var(--custom-slide-bg);
--slide-title: var(--custom-slide-title);
--slide-heading: var(--custom-slide-heading);
--slide-description: var(--custom-slide-description);
--slide-box: var(--custom-slide-box);
}
/* Royal Blue Theme */
.slide-theme[data-theme="royal_blue"] {
--slide-bg: var(--royal_blue-slide-bg);
--slide-title: var(--royal_blue-slide-title);
--slide-heading: var(--royal_blue-slide-heading);
--slide-description: var(--royal_blue-slide-description);
--slide-box: var(--royal_blue-slide-box);
}
/* Light Red Theme */
.slide-theme[data-theme="light_red"] {
--slide-bg: var(--light_red-slide-bg);
--slide-title: var(--light_red-slide-title);
--slide-heading: var(--light_red-slide-heading);
--slide-description: var(--light_red-slide-description);
--slide-box: var(--light_red-slide-box);
}
/* Dark Pink Theme */
.slide-theme[data-theme="dark_pink"] {
--slide-bg: var(--dark_pink-slide-bg);
--slide-title: var(--dark_pink-slide-title);
--slide-heading: var(--dark_pink-slide-heading);
--slide-description: var(--dark_pink-slide-description);
--slide-box: var(--dark_pink-slide-box);
}
/* Cream Theme */
.slide-theme[data-theme="cream"] {
--slide-bg: var(--cream-slide-bg);
--slide-title: var(--cream-slide-title);
--slide-heading: var(--cream-slide-heading);
--slide-description: var(--cream-slide-description);
--slide-box: var(--cream-slide-box);
}
/* Apply theme styles - Make sure these selectors are more specific */
.slide-theme .slide-container {
background-color: var(--slide-bg);
}
.slide-theme .slide-title {
color: var(--slide-title);
}
.slide-theme .slide-heading {
color: var(--slide-heading);
}
.slide-theme .slide-description {
color: var(--slide-description);
}
.slide-theme .slide-box {
background-color: var(--slide-box);
}
/* Add to your existing CSS */
.slide-theme {
transition: background-color 0.3s ease, color 0.3s ease;
}
.slide-theme .border-color{
border-color: var(--slide-box);
}
@keyframes progress {
0% { width: 5%; }
20% { width: 25%; }
50% { width: 45%; }
75% { width: 75%; }
90% { width: 85%; }
100% { width: 90%; }
}
@keyframes fade-in {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
.animate-progress {
animation: progress 20s ease-out forwards;
}
.animate-fade-in {
animation: fade-in 0.5s ease-out forwards;
}
@keyframes border-dance {
0% {
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
}
100% {
background-position: 100% 0, 100% 100%, 0 100%, 0 0;
}
}
.animate-border {
background-image:
linear-gradient(90deg, #5141e5 50%, transparent 50%), /* top */
linear-gradient(90deg, #5141e5 50%, transparent 50%), /* right */
linear-gradient(90deg, #5141e5 50%, transparent 50%), /* bottom */
linear-gradient(90deg, #5141e5 50%, transparent 50%); /* left */
background-repeat: repeat-x, repeat-y, repeat-x, repeat-y;
background-size: 15px 2px, 2px 15px, 15px 2px, 2px 15px;
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
animation: border-dance 6s infinite linear;
}

View file

@ -1,184 +0,0 @@
"use client";
import React, { useEffect, useState } from "react";
import { Card } from "@/components/ui/card";
import { defaultColors, setTheme, ThemeColors } from "../store/themeSlice";
import Header from "@/app/dashboard/components/Header";
import Wrapper from "@/components/Wrapper";
import { useDispatch } from "react-redux";
import { useRouter } from "next/navigation";
import { ThemeType } from "../upload/type";
import { Button } from "@/components/ui/button";
import { toast } from "@/hooks/use-toast";
interface ThemeCardProps {
name: string;
font: string;
colors: ThemeColors;
selected: boolean;
onClick: () => void;
}
const ThemeCard = ({
name,
font,
colors,
selected,
onClick,
}: ThemeCardProps) => {
return (
<div
className="cursor-pointer group"
style={{ fontFamily: font }}
onClick={onClick}
>
<Card
className={` p-3 md:p-6 h-[120px] md:h-[210px] transition-all duration-200 border-2 ${selected ? " border-4 border-blue-400" : "hover:border-primary"
}`}
style={{ background: colors.slideBg }}
>
<div
className="rounded-lg p-6 h-full"
style={{ background: colors.slideBox }}
>
<div className="flex justify-start items-center h-full">
<div className="space-y-3">
<h3
style={{ color: colors.slideTitle }}
className="text-xl font-semibold"
>
{name}
</h3>
<p
style={{ color: colors.slideDescription }}
className="text-sm text-muted-foreground"
>
This is the body paragraph
</p>
</div>
</div>
</div>
</Card>
</div>
);
};
const ThemePage = () => {
const themes = [
{
name: "Dark Theme",
colors: defaultColors.dark,
type: "dark",
font: "var(--font-inter)",
},
{
name: "Royal Blue Theme",
colors: defaultColors.royal_blue,
type: "royal_blue",
font: "var(--font-instrument-sans)",
},
{
name: "Creme Theme",
colors: defaultColors.cream,
type: "cream",
font: "var(--font-fraunces)",
},
{
name: "Light Red Theme",
colors: defaultColors.light_red,
type: "light_red",
font: "var(--font-montserrat)",
},
{
name: "Dark Pink Theme",
colors: defaultColors.dark_pink,
type: "dark_pink",
font: "var(--font-inria-serif)",
},
{
name: "Light Theme",
colors: defaultColors.light,
type: "light",
font: "var(--font-inter)",
},
{
name: "Faint Yellow Theme",
colors: defaultColors.faint_yellow,
type: "faint_yellow",
font: "var(--font-inter)",
},
];
const dispatch = useDispatch();
const router = useRouter();
const [selectedTheme, setSelectedTheme] = useState<ThemeType | null>(null);
const handleThemeClick = async (theme: ThemeColors, type: string) => {
setSelectedTheme(type as ThemeType);
};
const handleSubmit = () => {
if (!selectedTheme) {
toast({
title: "Please select a theme",
variant: "destructive",
});
return;
}
dispatch(setTheme(selectedTheme as ThemeType));
router.push("/outline");
};
return (
<div>
<Header />
<Wrapper className="py-8 md:w-[90%] xl:w-[70%]">
<h1 className="text-3xl font-bold mb-8">Select a Theme</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pb-16">
{themes.map((theme, index) => (
<ThemeCard
key={index}
name={theme.name}
font={theme.font}
colors={theme.colors}
selected={selectedTheme === theme.type}
onClick={() => handleThemeClick(theme.colors, theme.type)}
/>
))}
</div>
<Button
onClick={handleSubmit}
className="bg-[#5146E5] fixed bottom-4 left-0 right-0 max-w-[1100px] mx-auto w-full rounded-[32px] text-base sm:text-lg py-4 sm:py-6 transition-all duration-300 font-switzer font-semibold hover:bg-[#5146E5]/80 text-white mt-4"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height={35}
width={35}
viewBox="0 0 25 25"
fill="none"
>
<g clipPath="url(#clip0_1960_939)">
<path
d="M21.217 9.57008L21.463 9.00408C21.8955 8.0028 22.6876 7.2 23.683 6.75408L24.442 6.41508C24.5341 6.37272 24.6121 6.30485 24.6668 6.21951C24.7214 6.13417 24.7505 6.03494 24.7505 5.93358C24.7505 5.83222 24.7214 5.73299 24.6668 5.64765C24.6121 5.56231 24.5341 5.49444 24.442 5.45208L23.725 5.13308C22.7046 4.67446 21.8989 3.84196 21.474 2.80708L21.221 2.19608C21.1838 2.10144 21.119 2.02018 21.035 1.96291C20.951 1.90563 20.8517 1.875 20.75 1.875C20.6483 1.875 20.549 1.90563 20.465 1.96291C20.381 2.02018 20.3162 2.10144 20.279 2.19608L20.026 2.80608C19.6015 3.84116 18.7962 4.67401 17.776 5.13308L17.058 5.45308C16.9662 5.49556 16.8885 5.56342 16.834 5.64865C16.7795 5.73389 16.7506 5.83293 16.7506 5.93408C16.7506 6.03523 16.7795 6.13428 16.834 6.21951C16.8885 6.30474 16.9662 6.3726 17.058 6.41508L17.818 6.75308C18.8132 7.19945 19.6049 8.00261 20.037 9.00408L20.283 9.57008C20.463 9.98408 21.036 9.98408 21.217 9.57008ZM6.55 16.8761H8.704L9.304 15.3761H12.196L12.796 16.8761H14.95L11.75 8.87608H9.75L6.55 16.8761ZM10.75 11.7611L11.396 13.3761H10.104L10.75 11.7611ZM15.75 16.8761V8.87608H17.75V16.8761H15.75ZM3.75 3.87608C3.48478 3.87608 3.23043 3.98144 3.04289 4.16897C2.85536 4.35651 2.75 4.61086 2.75 4.87608V20.8761C2.75 21.1413 2.85536 21.3957 3.04289 21.5832C3.23043 21.7707 3.48478 21.8761 3.75 21.8761H21.75C22.0152 21.8761 22.2696 21.7707 22.4571 21.5832C22.6446 21.3957 22.75 21.1413 22.75 20.8761V11.8761H20.75V19.8761H4.75V5.87608H14.75V3.87608H3.75Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_1960_939">
<rect
width="30"
height="30"
fill="white"
transform="translate(0.75 0.876953)"
/>
</clipPath>
</defs>
</svg>
Generate Outline
</Button>
</Wrapper>
</div>
);
};
export default ThemePage;

View file

@ -1,24 +0,0 @@
import { Skeleton } from '@/components/ui/skeleton'
import React from 'react'
const loading = () => {
return (
<div className='h-screen'>
<Skeleton className='h-24 w-full' />
<div className='max-w-7xl mx-auto'>
<Skeleton className='h-10 mt-10 w-60' />
<div className=' mt-10 mx-auto grid grid-cols-2 gap-6'>
{
Array.from({ length: 6 }).map((_, index) => (
<Skeleton key={index} className='h-[210px] w-full' />
))
}
</div>
</div>
</div>
)
}
export default loading

View file

@ -1,16 +0,0 @@
import React from 'react'
import ThemePage from './ThemePage'
import { Metadata } from 'next'
export const metadata: Metadata = {
title: "Theme Selection | Presenton.ai",
description: "Select a Presenton theme which will be suitable for your presentation",
}
const page = () => {
return (
<ThemePage />
)
}
export default page

View file

@ -2,7 +2,7 @@
import React, { useRef, useState } from 'react'
import { File, X, Upload } from 'lucide-react'
import { useToast } from '@/hooks/use-toast'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
interface FileWithId extends File {
@ -17,7 +17,6 @@ interface SupportingDocProps {
const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
const [isDragging, setIsDragging] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const { toast } = useToast()
// Convert Files to FileWithId with proper type checking
const filesWithIds: FileWithId[] = files.map(file => {
@ -57,19 +56,15 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
const invalidFiles = droppedFiles.filter(file => !validTypes.includes(file.type));
if (invalidFiles.length > 0) {
toast({
title: 'Invalid file type',
toast.error('Invalid file type', {
description: 'Please upload only PDF, TXT, PPTX, or DOCX files',
variant: 'destructive',
});
return;
}
if (hasPdf && droppedFiles.some(file => file.type === 'application/pdf')) {
toast({
title: 'Multiple PDF files are not allowed',
toast.error('Multiple PDF files are not allowed', {
description: 'Please select only one PDF file',
variant: 'destructive',
});
return;
}
@ -82,8 +77,7 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
const updatedFiles = [...files, ...validFiles]
onFilesChange(updatedFiles)
toast({
title: 'Files selected',
toast.success('Files selected', {
description: `${validFiles.length} file(s) have been added`,
})
}
@ -102,8 +96,7 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
const updatedFiles = [...files, ...validFiles]
onFilesChange(updatedFiles)
toast({
title: 'Files selected',
toast.success('Files selected', {
description: `${validFiles.length} file(s) have been added`,
})
}

View file

@ -0,0 +1,115 @@
import React from 'react'
import { Button } from '@/components/ui/button'
import { toast } from 'sonner'
const ToastTesting = () => {
return (
<div className="p-8 space-y-4">
<h2 className="text-2xl font-bold mb-6">Toast Testing - All Variants</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{/* Success Toast */}
<Button
onClick={() => toast.success('Success! Operation completed successfully', {
description: 'Your data has been saved.',
duration: 4000,
richColors: true,
})}
className="bg-green-600 hover:bg-green-700"
>
Success Toast
</Button>
{/* Error Toast */}
<Button
onClick={() => toast.error('Error! Something went wrong', {
description: 'Please try again later.',
duration: 5000,
richColors: true,
})}
className="bg-red-600 hover:bg-red-700"
>
Error Toast
</Button>
{/* Info Toast */}
<Button
onClick={() => toast.info('Information', {
description: 'Here is some useful information for you.',
duration: 4000,
richColors: true,
})}
className="bg-blue-600 hover:bg-blue-700"
>
Info Toast
</Button>
{/* Warning Toast */}
<Button
onClick={() => toast.warning('Warning! Please be careful', {
description: 'This action cannot be undone.',
duration: 4000,
richColors: true,
})}
className="bg-yellow-600 hover:bg-yellow-700"
>
Warning Toast
</Button>
{/* Loading Toast */}
<Button
onClick={() => {
const loadingToast = toast.loading('Processing...', {
description: 'Please wait while we process your request.',
});
// Simulate loading completion after 3 seconds
setTimeout(() => {
toast.dismiss(loadingToast);
toast.success('Processing completed!');
}, 3000);
}}
className="bg-indigo-600 hover:bg-indigo-700"
>
Loading Toast
</Button>
{/* Promise Toast */}
<Button
onClick={() => {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve('Success!') : reject('Failed!');
}, 2000);
});
toast.promise(promise, {
loading: 'Uploading file...',
success: 'File uploaded successfully!',
error: 'Failed to upload file',
});
}}
className="bg-teal-600 hover:bg-teal-700"
>
Promise Toast
</Button>
</div>
</div>
)
}
export default ToastTesting

View file

@ -20,12 +20,12 @@ import { LanguageType, PresentationConfig } from "../type";
import SupportingDoc from "./SupportingDoc";
import { Button } from "@/components/ui/button";
import { ChevronRight } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { toast } from "sonner";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { OverlayLoader } from "@/components/ui/overlay-loader";
import Wrapper from "@/components/Wrapper";
import { setPptGenUploadState } from "@/store/slices/presentationGenUpload";
import { useLayout } from "../../context/LayoutContext";
import ToastTesting from "./ToastTesting";
// Types for loading state
interface LoadingState {
@ -39,7 +39,6 @@ interface LoadingState {
const UploadPage = () => {
const router = useRouter();
const dispatch = useDispatch();
const { toast } = useToast();
// State management
const [files, setFiles] = useState<File[]>([]);
@ -72,18 +71,12 @@ const UploadPage = () => {
*/
const validateConfiguration = (): boolean => {
if (!config.language || !config.slides) {
toast({
title: "Please select number of Slides & Language",
variant: "destructive",
});
toast.error("Please select number of Slides & Language");
return false;
}
if (!config.prompt.trim() && files.length === 0) {
toast({
title: "No Prompt or Document Provided",
variant: "destructive",
});
toast.error("No Prompt or Document Provided");
return false;
}
return true;
@ -177,10 +170,8 @@ const UploadPage = () => {
duration: 0,
showProgress: false,
});
toast({
title: "Error",
toast.error("Error", {
description: "Failed to generate presentation. Please try again.",
variant: "destructive",
});
};
@ -200,6 +191,7 @@ const UploadPage = () => {
onConfigChange={handleConfigChange}
/>
</div>
<div className="relative">
<PromptInput
value={config.prompt}

View file

@ -1,151 +0,0 @@
import { randomChartGenerator } from "@/lib/utils";
import { Slide } from "../types/slide";
import { generateRandomId } from "./others";
const randomGraph = (presentation_id: string) => {
const randomData = randomChartGenerator();
return {
id: generateRandomId(),
name: "Sales Performance",
type: "bar",
presentation: presentation_id,
postfix: "",
data: randomData,
};
};
export const getEmptySlideContent = (
type: number,
index: number,
presentation_id: string
): Slide => {
const baseSlide: Slide = {
id: generateRandomId(),
type,
index,
design_index: 1,
properties: null,
images: [],
icons: [],
graph_id: null,
presentation: presentation_id,
content: {
title: "",
body: "",
infographics: [],
},
};
const graph = randomGraph(presentation_id);
switch (type) {
case 1:
return {
...baseSlide,
images: [""],
content: {
title: "New Title",
body: "Add your description here",
image_prompts: [""],
},
};
case 2:
return {
...baseSlide,
content: {
title: "New Title",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
},
};
case 4:
return {
...baseSlide,
images: ["", "", ""],
content: {
title: "New Title",
body: [
{ heading: "First Item", description: "Add description" },
{ heading: "Second Item", description: "Add description" },
{ heading: "Third Item", description: "Add description" },
],
image_prompts: ["", "", ""],
},
};
case 5:
return {
...baseSlide,
graph_id: graph.id,
content: {
graph: graph,
title: "New Title",
body: "Add your description here",
},
};
case 6:
return {
...baseSlide,
content: {
title: "New Title",
description: "Add your description here",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
},
};
case 7:
return {
...baseSlide,
icons: ["", "", ""],
content: {
title: "New Title",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
icon_queries: [
{
queries: [""],
},
],
},
};
case 8:
return {
...baseSlide,
icons: ["", "", ""],
content: {
title: "New Title",
description: "Add your description here",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
icon_queries: [
{
queries: [""],
},
],
},
};
case 9:
return {
...baseSlide,
graph_id: graph.id,
content: {
graph: graph,
title: "New Subheading",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
},
};
default:
return baseSlide;
}
};

View file

@ -1,46 +0,0 @@
import { formatLargeNumber } from "@/lib/utils";
import { Chart } from "@/store/slices/presentationGeneration";
export const formatTooltipValue = (localChartData: Chart, value: number) => {
const formattedValue = formatLargeNumber(value);
if (localChartData.postfix) {
return `${formattedValue}${localChartData.postfix}`;
}
return formattedValue;
};
export const transformedData = (localChartData: Chart) => {
if (!localChartData) return [];
if (!localChartData.data || localChartData.data.categories.length === 0)
return [];
if (localChartData && localChartData.type === "pie") {
return localChartData.data.categories.map((category, index) => ({
name: category,
value: localChartData.data.series[0].data[index],
actualValue: localChartData.data.series[0].data[index],
seriesName: localChartData.data.series[0].name || "Series 1",
}));
} else {
return localChartData.data.categories.map((category, index) => {
const dataPoint: any = { name: category };
localChartData.data.series.forEach((serie) => {
const seriesName = serie.name || "Series";
dataPoint[seriesName] = serie.data[index];
dataPoint[`${seriesName}Value`] = serie.data[index];
});
return dataPoint;
});
}
};
export const formatYAxisTick = (value: number) => {
if (value >= 1_000_000_000_000) {
return `${(value / 1_000_000_000_000).toFixed(0)}T`;
} else if (value >= 1_000_000_000) {
return `${(value / 1_000_000_000).toFixed(0)}B`;
} else if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(0)}M`;
} else if (value >= 1_000) {
return `${(value / 1_000).toFixed(0)}k`;
}
return value.toString();
};

View file

@ -1,18 +0,0 @@
// Store Chart Data Type
export interface StoreChartData {
id: string;
name: string;
type: 'bar' | 'line' | 'pie';
style: {
};
presentation: string;
postfix: string;
data: {
categories: string[];
series: Array<{
name?: string;
data: number[];
}>;
};
}

View file

@ -1,285 +0,0 @@
import * as z from 'zod';
import fs from 'fs';
import * as path from 'path';
interface LayoutInfo {
id: string;
name: string;
description: string;
json_schema: Record<string, any>;
group: string;
}
interface LayoutGroup {
id: string;
ordered: boolean;
slides: string[];
}
interface LayoutStructure {
name: string;
ordered: boolean;
slides: LayoutInfo[];
}
interface GroupedLayoutsResponse {
group: string;
files: string[];
}
// Cache for layouts to avoid repeated file system operations
let layoutsCache: LayoutStructure[] | null = null;
/**
* Dynamically imports a layout file and extracts its schema and metadata
*/
async function extractLayoutFromFile(filePath: string, fileName: string, groupName: string): Promise<LayoutInfo | null> {
try {
// Import the layout module dynamically
const module = await import(filePath);
// Check if the module has a Schema export
if (!module.Schema) {
console.warn(`No Schema export found in ${fileName}`);
return null;
}
// Extract layout metadata (optional)
const layoutId = module.layoutId || fileName.replace(/\.tsx?$/, '').toLowerCase().replace(/layout$/, '');
const layoutName = module.layoutName || fileName.replace(/\.tsx?$/, '').replace(/([A-Z])/g, ' $1').trim();
const layoutDescription = module.layoutDescription || `${layoutName} layout for presentations`;
// Convert Zod schema to JSON schema
const jsonSchema = z.toJSONSchema(module.Schema, {
override: (ctx) => {
delete ctx.jsonSchema.default;
},
});
return {
id: layoutId,
name: layoutName,
description: layoutDescription,
json_schema: jsonSchema,
group: groupName
};
} catch (error: unknown) {
console.error(`Error extracting layout from ${fileName}:`, error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Failed to extract schema from ${fileName}: ${errorMessage}`);
}
}
/**
* Gets all layout files from the grouped presentation-layouts directory
*/
async function getGroupedLayoutFiles(): Promise<GroupedLayoutsResponse[]> {
const layoutsDirectory = path.join(process.cwd(), 'presentation-layouts');
if (!fs.existsSync(layoutsDirectory)) {
throw new Error(`Layouts directory not found at ${layoutsDirectory}`);
}
const items = fs.readdirSync(layoutsDirectory, { withFileTypes: true });
const groupDirectories = items
.filter(item => item.isDirectory())
.map(dir => dir.name);
const allLayouts: GroupedLayoutsResponse[] = [];
for (const groupName of groupDirectories) {
try {
const groupPath = path.join(layoutsDirectory, groupName);
const groupFiles = fs.readdirSync(groupPath);
// Filter for TypeScript/TSX files, excluding setting.json and other non-layout files
const layoutFiles = groupFiles.filter(file =>
(file.endsWith('.ts') || file.endsWith('.tsx')) &&
file !== 'setting.json' &&
!file.startsWith('.') &&
!file.includes('.test.') &&
!file.includes('.spec.')
);
if (layoutFiles.length > 0) {
allLayouts.push({
group: groupName,
files: layoutFiles
});
}
} catch (error) {
console.error(`Error reading group directory ${groupName}:`, error);
// Continue with other groups even if one fails
}
}
return allLayouts;
}
/**
* Extracts layout groups from layoutGroup.ts file in presentation-layouts directory
*/
async function extractLayoutGroups(): Promise<LayoutGroup[]> {
try {
const layoutGroupPath = path.join(process.cwd(), 'presentation-layouts', 'layoutGroup.ts');
if (!fs.existsSync(layoutGroupPath)) {
throw new Error('layoutGroup.ts file not found in presentation-layouts directory');
}
const module = await import(layoutGroupPath);
// Extract all exported layout groups
const layoutGroups: LayoutGroup[] = [];
Object.keys(module).forEach(key => {
const exportedItem = module[key];
// Check if it's a layout group object
if (exportedItem &&
typeof exportedItem === 'object' &&
exportedItem.id &&
Array.isArray(exportedItem.slides)) {
layoutGroups.push({
id: exportedItem.id,
ordered: exportedItem.ordered || false,
slides: exportedItem.slides
});
}
});
if (layoutGroups.length === 0) {
throw new Error('No valid layout groups found in layoutGroup.ts');
}
return layoutGroups;
} catch (error) {
console.error('Error extracting layout groups:', error);
throw error;
}
}
/**
* Maps layout information to layout groups
*/
function mapLayoutsToGroups(
layoutInfos: LayoutInfo[],
layoutGroups: LayoutGroup[]
): LayoutStructure[] {
return layoutGroups.map(group => {
const groupSlides: LayoutInfo[] = [];
// Map slides in the group to their layout info
group.slides.forEach(slideId => {
const layoutInfo = layoutInfos.find(layout =>
layout.id === slideId ||
layout.id.replace('-', '') === slideId.replace('-', '') ||
layout.id.toLowerCase() === slideId.toLowerCase()
);
if (layoutInfo) {
groupSlides.push(layoutInfo);
} else {
console.warn(`Layout info not found for slide ID: ${slideId}`);
}
});
return {
name: group.id,
ordered: group.ordered,
slides: groupSlides
};
});
}
/**
* Main function to extract all layouts dynamically from grouped structure
*/
export async function extractLayouts(): Promise<LayoutStructure[]> {
// Return cached layouts if available
if (layoutsCache) {
return layoutsCache;
}
try {
// Get all grouped layout files
const groupedLayoutFiles = await getGroupedLayoutFiles();
if (groupedLayoutFiles.length === 0) {
throw new Error('No layout files found in the presentation-layouts directory');
}
// Extract layout information from each file in each group
const allLayoutPromises: Promise<LayoutInfo | null>[] = [];
for (const groupData of groupedLayoutFiles) {
for (const fileName of groupData.files) {
const filePath = path.join(process.cwd(), 'presentation-layouts', groupData.group, fileName);
allLayoutPromises.push(extractLayoutFromFile(filePath, fileName, groupData.group));
}
}
const layoutResults = await Promise.all(allLayoutPromises);
// Filter out null results (files without valid schemas)
const validLayouts = layoutResults.filter((layout): layout is LayoutInfo => layout !== null);
if (validLayouts.length === 0) {
throw new Error('No valid schemas found in any layout files');
}
// Extract layout groups
const layoutGroups = await extractLayoutGroups();
// Map layouts to groups
const mappedLayouts = mapLayoutsToGroups(validLayouts, layoutGroups);
// Cache the results
layoutsCache = mappedLayouts;
return mappedLayouts;
} catch (error) {
console.error('Error extracting layouts:', error);
throw error;
}
}
/**
* Clears the layout cache (useful for development/testing)
*/
export function clearLayoutCache(): void {
layoutsCache = null;
}
/**
* Gets a specific layout by ID
*/
export async function getLayoutById(layoutId: string): Promise<LayoutInfo | null> {
const layouts = await extractLayouts();
for (const group of layouts) {
const layout = group.slides.find(slide => slide.id === layoutId);
if (layout) {
return layout;
}
}
return null;
}
/**
* Gets all available layout IDs
*/
export async function getAllLayoutIds(): Promise<string[]> {
const layouts = await extractLayouts();
const ids: string[] = [];
layouts.forEach(group => {
group.slides.forEach(slide => {
ids.push(slide.id);
});
});
return ids;
}

View file

@ -52,170 +52,10 @@ export function removeUUID(fileName: string) {
}
export function generateRandomId(): string {
const length = 36;
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
let id = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * chars.length);
id += chars[randomIndex];
}
return id;
}
export const getFontLink = (fontName: string) => {
if (!fontName) {
return { link: '', name: '' };
}
if (fontName.includes('instrument')) {
return { link: 'https://fonts.google.com/specimen/Instrument+Sans', name: 'Instrument Sans' }
}
if (fontName.includes('fraunces')) {
return { link: 'https://fonts.google.com/specimen/Fraunces', name: 'Fraunces' }
}
if (fontName.includes('montserrat')) {
return { link: 'https://fonts.google.com/specimen/Montserrat', name: 'Montserrat' }
}
if (fontName.includes('inria-serif')) {
return { link: 'https://fonts.google.com/specimen/Inria+Serif', name: 'Inria Serif' }
}
if (fontName.includes('inter')) {
return { link: 'https://fonts.google.com/specimen/Inter', name: 'Inter' }
}
else {
return { link: '', name: '' };
}
}
export const numberTranslations: any = {
// Key languages
"English (English)": ["01", "02", "03", "04", "05"],
"English(English)": ["01", "02", "03", "04", "05"],
English: ["01", "02", "03", "04", "05"],
"Spanish (Español)": ["01", "02", "03", "04", "05"],
"French (Français)": ["01", "02", "03", "04", "05"],
"German (Deutsch)": ["01", "02", "03", "04", "05"],
"Portuguese (Português)": ["01", "02", "03", "04", "05"],
"Italian (Italiano)": ["01", "02", "03", "04", "05"],
"Dutch (Nederlands)": ["01", "02", "03", "04", "05"],
"Russian (Русский)": ["01", "02", "03", "04", "05"],
"Chinese (Simplified & Traditional - 中文, 汉语/漢語)": [
"一",
"二",
"三",
"四",
"五",
],
"Japanese (日本語)": ["一", "二", "三", "四", "五"],
"Korean (한국어)": ["일", "이", "삼", "사", "오"],
"Arabic (العربية)": ["١", "٢", "٣", "٤", "٥"],
"Hindi (हिन्दी)": ["०१", "०२", "०३", "०४", "०५"],
"Bengali (বাংলা)": ["০১", "০২", "০৩", "", "০৫"],
// European Languages
"Polish (Polski)": ["01", "02", "03", "04", "05"],
"Czech (Čeština)": ["01", "02", "03", "04", "05"],
"Slovak (Slovenčina)": ["01", "02", "03", "04", "05"],
"Hungarian (Magyar)": ["01", "02", "03", "04", "05"],
"Romanian (Română)": ["01", "02", "03", "04", "05"],
"Bulgarian (Български)": ["01", "02", "03", "04", "05"],
"Greek (Ελληνικά)": ["α΄", "β΄", "γ΄", "δ΄", "ε΄"],
"Serbian (Српски)": ["01", "02", "03", "04", "05"],
"Croatian (Hrvatski)": ["01", "02", "03", "04", "05"],
"Bosnian (Bosanski)": ["01", "02", "03", "04", "05"],
"Slovenian (Slovenščina)": ["01", "02", "03", "04", "05"],
"Finnish (Suomi)": ["01", "02", "03", "04", "05"],
"Swedish (Svenska)": ["01", "02", "03", "04", "05"],
"Danish (Dansk)": ["01", "02", "03", "04", "05"],
"Norwegian (Norsk)": ["01", "02", "03", "04", "05"],
"Icelandic (Íslenska)": ["01", "02", "03", "04", "05"],
"Lithuanian (Lietuvių)": ["01", "02", "03", "04", "05"],
"Latvian (Latviešu)": ["01", "02", "03", "04", "05"],
"Estonian (Eesti)": ["01", "02", "03", "04", "05"],
"Maltese (Malti)": ["01", "02", "03", "04", "05"],
"Welsh (Cymraeg)": ["01", "02", "03", "04", "05"],
"Irish (Gaeilge)": ["01", "02", "03", "04", "05"],
"Scottish Gaelic (Gàidhlig)": ["01", "02", "03", "04", "05"],
// Middle Eastern and Central Asian Languages
"Hebrew (עברית)": ["א׳", "ב׳", "ג׳", "ד׳", "ה׳"],
"Persian/Farsi (فارسی)": ["۱", "۲", "۳", "۴", "۵"],
"Turkish (Türkçe)": ["01", "02", "03", "04", "05"],
"Kurdish (Kurdî / کوردی)": ["١", "٢", "٣", "٤", "٥"],
"Pashto (پښتو)": ["١", "٢", "٣", "٤", "٥"],
"Dari (دری)": ["١", "٢", "٣", "٤", "٥"],
"Uzbek (Oʻzbek)": ["01", "02", "03", "04", "05"],
"Kazakh (Қазақша)": ["01", "02", "03", "04", "05"],
"Tajik (Тоҷикӣ)": ["01", "02", "03", "04", "05"],
"Turkmen (Türkmençe)": ["01", "02", "03", "04", "05"],
"Azerbaijani (Azərbaycan dili)": ["01", "02", "03", "04", "05"],
// South Asian Languages
"Urdu (اردو)": ["١", "٢", "٣", "٤", "٥"],
"Tamil (தமிழ்)": ["௧", "௨", "௩", "௪", "௫"],
"Telugu (తెలుగు)": ["౧", "౨", "౩", "౪", "౫"],
"Marathi (मराठी)": ["०१", "०२", "०३", "०४", "०५"],
"Punjabi (ਪੰਜਾਬੀ / پنجابی)": ["", "੦੨", "੦੩", "", "੦੫"],
"Gujarati (ગુજરાતી)": ["૦૧", "૦૨", "૦૩", "૦૪", "૦૫"],
"Malayalam (മലയാളം)": ["൧", "൨", "൩", "൪", "൫"],
"Kannada (ಕನ್ನಡ)": ["೧", "೨", "೩", "೪", "೫"],
"Odia (ଓଡ଼ିଆ)": ["୧", "", "୩", "୪", "୫"],
"Sinhala (සිංහල)": ["෧", "෨", "෩", "෪", "෫"],
"Nepali (नेपाली)": ["०१", "०२", "०३", "०४", "०५"],
// East and Southeast Asian Languages
"Thai (ไทย)": ["๑", "๒", "๓", "๔", "๕"],
"Vietnamese (Tiếng Việt)": ["01", "02", "03", "04", "05"],
"Lao (ລາວ)": ["໑", "໒", "໓", "໔", "໕"],
"Khmer (ភាសាខ្មែរ)": ["១", "២", "៣", "៤", "៥"],
"Burmese (မြန်မာစာ)": ["၁", "၂", "၃", "၄", "၅"],
"Tagalog/Filipino (Tagalog/Filipino)": ["01", "02", "03", "04", "05"],
"Javanese (Basa Jawa)": ["01", "02", "03", "04", "05"],
"Sundanese (Basa Sunda)": ["01", "02", "03", "04", "05"],
"Malay (Bahasa Melayu)": ["01", "02", "03", "04", "05"],
"Mongolian (Монгол)": ["01", "02", "03", "04", "05"],
// African Languages
"Swahili (Kiswahili)": ["01", "02", "03", "04", "05"],
"Hausa (Hausa)": ["01", "02", "03", "04", "05"],
"Yoruba (Yoruba)": ["01", "02", "03", "04", "05"],
"Igbo (Igbo)": ["01", "02", "03", "04", "05"],
"Amharic (አማርኛ)": ["፩", "፪", "፫", "፬", "፭"],
"Zulu (isiZulu)": ["01", "02", "03", "04", "05"],
"Xhosa (isiXhosa)": ["01", "02", "03", "04", "05"],
"Shona (ChiShona)": ["01", "02", "03", "04", "05"],
"Somali (Soomaaliga)": ["01", "02", "03", "04", "05"],
// Indigenous and Lesser-Known Languages
"Basque (Euskara)": ["01", "02", "03", "04", "05"],
"Catalan (Català)": ["01", "02", "03", "04", "05"],
"Galician (Galego)": ["01", "02", "03", "04", "05"],
"Quechua (Runasimi)": ["01", "02", "03", "04", "05"],
"Nahuatl (Nāhuatl)": ["01", "02", "03", "04", "05"],
"Hawaiian (ʻŌlelo Hawaiʻi)": ["01", "02", "03", "04", "05"],
"Maori (Te Reo Māori)": ["01", "02", "03", "04", "05"],
"Tahitian (Reo Tahiti)": ["01", "02", "03", "04", "05"],
"Samoan (Gagana Samoa)": ["01", "02", "03", "04", "05"],
};
export const ThemeImagePrompt = {
light:
"Classy and modern with a corporate and minimalist touch. Tone is serious yet elegant, using a palette of light, white, and cool gray colors.",
dark: "Luxurious and futuristic with a simple, clean design. Professional yet elegant using a color scheme of dark, black, and high contrast.",
faint_yellow:
"Fresh young creatively vibrant style, utilizing a playful mixture of light colors like orange, salmon, and pastel purple, all set against a warm gradient.",
cream:
"Elegant with a classic and professional look. Subtle and minimalist using a warm palette of cream, beige, and light beige colors.",
royal_blue:
"Playful and creative, bold and loud with a futuristic touch, using a gradient of vibrant colors including blue, purple, and royal blue.",
light_red:
"Fun and organic with a playful and inspirational aesthetic, featuring pastel colors like pink, coral, and orange for a vibrant and warm feel.",
dark_pink:
" Inspirational and creative with a youthful and playful tone, featuring light, pastel colors including blue, pink, and purple, all blending in a vibrant gradient.",
custom: "",
};
export function sanitizeFilename(input: string, replacement = '') {

View file

@ -8,6 +8,7 @@ import { NextResponse, NextRequest } from 'next/server';
export async function POST(req: NextRequest) {
const { id, title } = await req.json();
console.log('path', process.env.APP_DATA_DIRECTORY);
if (!id) {
return NextResponse.json({ error: "Missing Presentation ID" }, { status: 400 });
}
@ -26,7 +27,8 @@ export async function POST(req: NextRequest) {
});
browser.close();
const sanitizedTitle = sanitizeFilename(title);
const destinationPath = path.join(process.env.APP_DATA_DIRECTORY!, `${sanitizedTitle}.pdf`);
const destinationPath = path.join(process.env.APP_DATA_DIRECTORY!,'exports', `${sanitizedTitle}.pdf`);
console.log('destinationPath', destinationPath);
await fs.promises.writeFile(destinationPath, pdfBuffer);
return NextResponse.json({

View file

@ -4,6 +4,10 @@ import puppeteer, { Browser, ElementHandle } from "puppeteer";
import { ElementAttributes, SlideAttributesResult } from "@/types/element_attibutes";
import { convertElementAttributesToPptxSlides } from "@/utils/pptx_models_utils";
import { PptxPresentationModel } from "@/types/pptx_models";
import fs from "fs";
import path from "path";
import crypto from "crypto";
import sharp from "sharp";
// Interface for getAllChildElementsAttributes function arguments
interface GetAllChildElementsAttributesArgs {
@ -12,6 +16,8 @@ interface GetAllChildElementsAttributesArgs {
depth?: number;
inheritedFont?: ElementAttributes['font'];
inheritedBackground?: ElementAttributes['background'];
inheritedBorderRadius?: number[];
screenshotsDir: string;
}
@ -24,8 +30,19 @@ export async function GET(request: NextRequest) {
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
// Ensure screenshots directory exists
const tempDir = process.env.TEMP_DIRECTORY;
if (!tempDir) {
console.warn('TEMP_DIRECTORY environment variable not set, skipping screenshot');
return undefined;
}
const screenshotsDir = path.join(tempDir, 'screenshots');
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true });
}
const slides = await getSlides(browser, id);
const slides_attributes = await getSlidesAttributes(slides);
const slides_attributes = await getSlidesAttributes(slides, screenshotsDir);
const slides_pptx_models = convertElementAttributesToPptxSlides(slides_attributes.elements, slides_attributes.backgroundColors);
const presentation_pptx_model: PptxPresentationModel = {
slides: slides_pptx_models,
@ -56,10 +73,14 @@ async function getPresentationId(request: NextRequest) {
return id;
}
async function getSlidesAttributes(slides: ElementHandle<Element>[]) {
const slideResults = await Promise.all(slides.map(async (slide) => {
return await getAllChildElementsAttributes({ element: slide });
}));
async function getSlidesAttributes(slides: ElementHandle<Element>[], screenshotsDir: string) {
const slideResults: SlideAttributesResult[] = [];
//? Can't use Promise.all because of the screenshot
//? taking screenshot with mess up position of elements
for (const slide of slides) {
const result = await getAllChildElementsAttributes({ element: slide, screenshotsDir });
slideResults.push(result);
}
const elements = slideResults.map(result => result.elements);
const backgroundColors = slideResults.map(result => result.backgroundColor);
@ -93,9 +114,10 @@ async function getPresentationPage(browser: Browser, id: string) {
page.on('console', (msg) => {
const type = msg.type();
const text = msg.text();
console.log(`${type}: ${text}`);
});
await page.setViewport({ width: 1640, height: 720, deviceScaleFactor: 1 });
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 });
await page.goto(`http://localhost/presentation?id=${id}`, {
waitUntil: "networkidle0",
timeout: 60000,
@ -104,7 +126,7 @@ async function getPresentationPage(browser: Browser, id: string) {
}
async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground }: GetAllChildElementsAttributesArgs): Promise<SlideAttributesResult> {
async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground, inheritedBorderRadius, screenshotsDir }: GetAllChildElementsAttributesArgs): Promise<SlideAttributesResult> {
// Get rootRect if not provided (first call)
const currentRootRect = rootRect || await element.evaluate((el) => {
const rect = el.getBoundingClientRect();
@ -116,6 +138,43 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
};
});
// Check if this element is SVG or canvas or table
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
if (tagName === 'svg' || tagName === 'canvas' || tagName === 'table') {
return {
elements: [],
backgroundColor: undefined
};
// // Get basic attributes for the element
// const attributes = await getElementAttributes(element);
// // Take screenshot of SVG/canvas/table element with accurate colors and opacity
// const screenshotPath = await takeElementScreenshot(element, screenshotsDir);
// // Update image source to point to the screenshot
// if (screenshotPath) {
// attributes.imageSrc = screenshotPath;
// }
// // Adjust position relative to root
// if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) {
// attributes.position = {
// left: attributes.position.left - currentRootRect.left,
// top: attributes.position.top - currentRootRect.top,
// width: attributes.position.width,
// height: attributes.position.height,
// };
// }
// // Return early without processing children for these elements
// return {
// elements: [attributes],
// backgroundColor: undefined
// };
}
// Get direct children only (not all descendants)
const directChildElementHandles = await element.$$(':scope > *');
@ -134,6 +193,10 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
if (inheritedBackground && !attributes.background && attributes.shadow) {
attributes.background = inheritedBackground;
}
// Apply inherited border radius if element doesn't have it
if (inheritedBorderRadius && !attributes.borderRadius) {
attributes.borderRadius = inheritedBorderRadius;
}
// Adjust position relative to root
if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) {
@ -155,6 +218,8 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
depth: depth + 1,
inheritedFont: attributes.font || inheritedFont,
inheritedBackground: attributes.background || inheritedBackground,
inheritedBorderRadius: attributes.borderRadius || inheritedBorderRadius,
screenshotsDir,
});
allResults.push(...childResults.elements.map(attr => ({ attributes: attr, depth: depth + 1 })));
}
@ -232,7 +297,6 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
}
// Do not edit this function, it is used to get the attributes of an element
async function getElementAttributes(element: ElementHandle<Element>): Promise<ElementAttributes> {
const attributes = await element.evaluate((el: Element) => {
@ -333,6 +397,7 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
return borderWidth === 0 ? undefined : {
color: borderColorResult.hex,
width: isNaN(borderWidth) ? undefined : borderWidth,
opacity: borderColorResult.opacity,
};
}
@ -384,7 +449,6 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
for (let i = 0; i < shadows.length; i++) {
const shadowStr = shadows[i];
console.log(`Analyzing shadow ${i}: "${shadowStr}"`);
// Parse the shadow to check if it has meaningful values
const shadowParts = shadowStr.split(' ');
@ -582,6 +646,38 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
};
}
function parseLineHeight(computedStyles: CSSStyleDeclaration, el: Element) {
const lineHeight = computedStyles.lineHeight;
const innerText = el.textContent || '';
// Check if text is multiline by looking for newline characters or checking if text wraps due to bounds
const htmlEl = el as HTMLElement;
// Get font size for comparison
const fontSize = parseFloat(computedStyles.fontSize);
const computedLineHeight = parseFloat(computedStyles.lineHeight);
// Estimate single line height (use computed line height if available, otherwise use font size * 1.2)
const singleLineHeight = !isNaN(computedLineHeight) ? computedLineHeight : fontSize * 1.2;
// Check for multiline text
const hasExplicitLineBreaks = innerText.includes('\n') || innerText.includes('\r') || innerText.includes('\r\n');
const hasTextWrapping = htmlEl.offsetHeight > singleLineHeight * 2; // Allow some tolerance
const hasOverflow = htmlEl.scrollHeight > htmlEl.clientHeight;
const isMultiline = hasExplicitLineBreaks || hasTextWrapping || hasOverflow;
// Only return line height if text is multiline
if (isMultiline && lineHeight && lineHeight !== 'normal') {
const parsedLineHeight = parseFloat(lineHeight);
if (!isNaN(parsedLineHeight)) {
return parsedLineHeight;
}
}
return undefined;
}
function parseMargin(computedStyles: CSSStyleDeclaration) {
const marginTop = parseFloat(computedStyles.marginTop);
const marginBottom = parseFloat(computedStyles.marginBottom);
@ -657,6 +753,8 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
const font = parseFont(computedStyles);
const lineHeight = parseLineHeight(computedStyles, el);
const margin = parseMargin(computedStyles);
const padding = parsePadding(computedStyles);
@ -690,6 +788,7 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
padding,
zIndex: zIndexValue,
textAlign: textAlign !== 'left' ? textAlign : undefined,
lineHeight,
borderRadius: borderRadiusValue,
imageSrc: imageSrc || undefined,
objectFit,
@ -705,3 +804,53 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
});
return attributes;
}
async function takeElementScreenshot(element: ElementHandle<Element>, screenshotsDir: string): Promise<string | undefined> {
try {
// Check element visibility and dimensions
const elementInfo = await element.evaluate((el) => {
const rect = el.getBoundingClientRect();
const styles = window.getComputedStyle(el);
// Check if element is visible
const isVisible = styles.visibility !== 'hidden' &&
styles.display !== 'none' &&
styles.opacity !== '0';
if (!isVisible || rect.width <= 0 || rect.height <= 0) {
return null;
}
return {
width: rect.width,
height: rect.height
};
});
if (!elementInfo) {
console.warn('Element is not visible or has invalid dimensions, skipping screenshot');
return undefined;
}
// Generate unique filename
const uuid = crypto.randomUUID();
const filename = `${uuid}.png`;
const filePath = path.join(screenshotsDir, filename);
// Take screenshot of the element with accurate colors and opacity
// This captures the element exactly as rendered in the browser with all CSS styles applied
await element.screenshot({
path: filePath as `${string}.png`,
type: 'png',
omitBackground: true // Use transparent background for better quality
});
console.log(`Screenshot saved: ${filePath}`);
return filePath;
} catch (error) {
console.error('Error taking element screenshot:', error);
return undefined;
}
}

View file

@ -22,14 +22,13 @@ export async function POST(request: NextRequest) {
const buffer = Buffer.from(bytes);
// Create uploads directory if it doesn't exist
const uploadsDir = path.join(userDataDir, "images");
const uploadsDir = path.join(userDataDir, "uploads");
fs.mkdirSync(uploadsDir, { recursive: true });
console.log("uploadsDir", uploadsDir);
// Generate unique filename
const filename = `${crypto.randomBytes(16).toString("hex")}.png`;
const filePath = path.join(uploadsDir, filename);
console.log("filePath", filePath);
// Write file to disk
fs.writeFileSync(filePath, buffer);

View file

@ -3,9 +3,9 @@
import Wrapper from "@/components/Wrapper";
import React from "react";
import Link from "next/link";
import UserAccount from "@/app/(presentation-generator)/components/UserAccount";
import BackBtn from "@/components/BackBtn";
import { usePathname } from "next/navigation";
import HeaderNav from "@/app/(presentation-generator)/components/HeaderNab";
const Header = () => {
const pathname = usePathname();
return (
@ -23,7 +23,7 @@ const Header = () => {
</Link>
</div>
<div className="flex items-center gap-3 sm:gap-5 md:gap-10">
<UserAccount />
<HeaderNav />
</div>
</div>
</Wrapper>

View file

@ -9,7 +9,7 @@ import {
PopoverContent,
} from "@/components/ui/popover";
import { useRouter } from "next/navigation";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import { useGroupLayouts } from "@/app/(presentation-generator)/hooks/useGroupLayouts";
export const PresentationCard = ({
@ -39,35 +39,22 @@ export const PresentationCard = ({
e.preventDefault();
e.stopPropagation();
toast({
title: "Deleting presentation",
toast.loading("Deleting presentation", {
description: "Please wait while we delete the presentation",
variant: "default",
});
const response = await DashboardApi.deletePresentation(id);
if (response) {
toast({
title: "Presentation deleted",
toast.success("Presentation deleted", {
description: "The presentation has been deleted successfully",
variant: "default",
});
if (onDeleted) {
onDeleted(id);
}
} else {
toast({
title: "Error",
description: "Failed to delete presentation",
variant: "destructive",
});
toast.error("Error deleting presentation");
}
};
return (
<Card
onClick={handlePreview}
@ -82,11 +69,11 @@ export const PresentationCard = ({
{new Date(created_at).toLocaleDateString()}
</p>
<Popover>
<PopoverTrigger onClick={(e) => e.stopPropagation()}>
<button className="w-6 h-6 rounded-full flex items-center justify-center text-gray-500 hover:text-gray-700" >
<PopoverTrigger className="w-6 h-6 rounded-full flex items-center justify-center text-gray-500 hover:text-gray-700" onClick={(e) => e.stopPropagation()}>
<DotsVerticalIcon className="w-4 h-4 text-gray-500" />
<DotsVerticalIcon className="w-4 h-4 text-gray-500" />
</button>
</PopoverTrigger>
<PopoverContent align="end" className="bg-white w-[200px]">
<button

View file

@ -2,7 +2,6 @@
@tailwind components;
@tailwind utilities;
@import '../app/(presentation-generator)/styles/themes.css';
body {
font-family: Arial, Helvetica, sans-serif;

View file

@ -1,137 +0,0 @@
# Layout Preview Studio
A modular, responsive layout preview system for viewing and testing presentation layout components with realistic sample data.
## ✨ Features
- **Dynamic Layout Discovery**: Automatically discovers and loads layout components
- **Interactive Navigation**: Easy navigation with quick select grid
- **Beautiful Presentation Display**: Mock browser frame with professional styling
- **Detailed Information Panel**: Layout metadata and sample data inspection
- **Responsive Design**: Mobile-friendly with collapsible sidebar
- **Professional Loading States**: Skeleton loaders and error handling
- **Type Safety**: Full TypeScript support with shared types
## 🏗️ Architecture
The system is built with a modular architecture for maintainability and reusability:
```
layout-preview/
├── components/ # Modular UI components
│ ├── LayoutNavigation.tsx # Navigation controls & layout selector
│ ├── LayoutDisplay.tsx # Main layout preview area
│ ├── LayoutInfoPanel.tsx # Information and data structure display
│ └── LoadingStates.tsx # Loading, error, and empty states
├── hooks/ # Custom React hooks
│ └── useLayoutLoader.ts # Layout loading logic and state management
├── utils/ # Utility functions
│ └── sampleDataGenerator.ts # Realistic sample data generation
├── types/ # Shared TypeScript types
│ └── index.ts # Common interfaces and types
├── page.tsx # Main page component
└── README.md # This file
```
## 🧩 Components
### LayoutNavigation
- Previous/Next navigation buttons
- Layout counter and current layout info
- Quick select grid for fast switching
- Responsive design with mobile optimization
### LayoutDisplay
- Mock browser frame presentation
- Layout rendering with sample data
- Professional shadow and styling effects
- Empty state with helpful messaging
### LayoutInfoPanel
- Layout metadata display
- Collapsible sample data viewer
- Copy to clipboard functionality
- Position tracking in collection
### LoadingStates
- Loading spinner with animated dots
- Error state with retry functionality
- Empty state with helpful instructions
- Skeleton loading animations
## 🎯 Custom Hooks
### useLayoutLoader
Handles all layout loading logic:
- Fetches layout files from API
- Imports and validates components
- Generates realistic sample data
- Provides retry functionality
- Manages loading and error states
## 🛠️ Utilities
### sampleDataGenerator
Intelligent sample data generation:
- Context-aware field value generation
- Support for images, emails, phones, URLs
- Realistic business content
- Zod schema parsing and validation
- Array and object handling
## 📱 Responsive Design
The layout preview system is fully responsive:
- **Desktop**: Sidebar navigation with main preview area
- **Tablet**: Collapsible navigation panels
- **Mobile**: Stacked layout with touch-friendly controls
## 🎨 Styling
Built with:
- **Tailwind CSS**: Utility-first styling
- **shadcn/ui**: Consistent component library
- **Gradient Backgrounds**: Modern visual appeal
- **Glass Morphism**: Backdrop blur effects
- **Smooth Animations**: Hover and transition effects
## 🔧 Usage
The system automatically discovers layout components that export:
```typescript
// Layout component
export default function MyLayout({ data }: { data: any }) {
return <div>{/* Your layout */}</div>
}
// Zod schema for type safety and sample data generation
export const Schema = z.object({
title: z.string(),
description: z.string(),
// ... other fields
})
```
## 🚀 Getting Started
1. **Add Layout Components**: Place your layout files in the appropriate directory
2. **Export Requirements**: Ensure each layout exports both a default component and Schema
3. **Navigate**: Use the navigation controls or quick select grid
4. **Inspect**: View layout information and sample data structure
5. **Test**: See how your layouts render with realistic data
## 🎯 Benefits
- **Modular Architecture**: Easy to maintain and extend
- **Type Safety**: Full TypeScript support prevents runtime errors
- **Beautiful UI**: Professional design that's pleasant to use
- **Developer Experience**: Quick feedback loop for layout development
- **Responsive**: Works on all device sizes
- **Accessible**: Keyboard navigation and screen reader support
## 📈 Performance
- **Lazy Loading**: Components are imported only when needed
- **Optimized Rendering**: Efficient re-renders with React best practices
- **Minimal Bundle**: Modular structure enables tree shaking
- **Caching**: Sample data generation is memoized

View file

@ -70,7 +70,7 @@ const GroupLayoutPreview = () => {
{/* Layout Grid */}
<main className="max-w-7xl mx-auto px-6 py-8">
<div className="space-y-8">
{layoutGroup.layouts.map((layout, index) => {
{layoutGroup.layouts.map((layout: any, index: number) => {
const { component: LayoutComponent, sampleData, name, fileName } = layout
return (

View file

@ -1,8 +1,8 @@
'use client'
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types'
import { toast } from '@/hooks/use-toast'
import { toast } from 'sonner'
interface UseGroupLayoutLoaderReturn {
layoutGroup: LayoutGroup | null
@ -11,21 +11,38 @@ interface UseGroupLayoutLoaderReturn {
retry: () => void
}
// Global cache to store layout groups and avoid re-fetching
const layoutGroupCache = new Map<string, LayoutGroup>()
const loadingGroupsCache = new Set<string>()
export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => {
const [layoutGroup, setLayoutGroup] = useState<LayoutGroup | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const hasMountedRef = useRef(false)
const loadGroupLayouts = async () => {
// Check cache first
if (layoutGroupCache.has(groupSlug)) {
setLayoutGroup(layoutGroupCache.get(groupSlug)!)
setLoading(false)
setError(null)
return
}
// Prevent multiple simultaneous requests for the same group
if (loadingGroupsCache.has(groupSlug)) {
return
}
try {
setLoading(true)
setError(null)
setLayoutGroup(null)
loadingGroupsCache.add(groupSlug)
const response = await fetch('/api/layouts')
if (!response.ok) {
toast({
title: 'Error loading layouts',
toast.error('Error loading layouts', {
description: response.statusText,
})
return
@ -57,8 +74,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`)
if (!module.default) {
toast({
title: `${layoutName} has no default export`,
toast.error(`${layoutName} has no default export`, {
description: 'Please ensure the layout file exports a default component',
})
console.warn(`${layoutName} has no default export`)
@ -66,8 +82,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
}
if (!module.Schema) {
toast({
title: `${layoutName} is missing required Schema export`,
toast.error(`${layoutName} is missing required Schema export`, {
description: 'Please ensure the layout file exports a Schema',
})
console.error(`${layoutName} is missing required Schema export`)
@ -76,6 +91,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
// Use empty object to let schema apply its default values
const sampleData = module.Schema.parse({})
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
const layoutInfo: LayoutInfo = {
name: layoutName,
@ -83,7 +99,8 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
schema: module.Schema,
sampleData,
fileName,
groupName: targetGroupData.groupName
groupName: targetGroupData.groupName,
layoutId
}
groupLayouts.push(layoutInfo)
@ -98,13 +115,15 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
if (module.default && module.Schema) {
const sampleData = module.Schema.parse({})
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
const layoutInfo: LayoutInfo = {
name: layoutName,
component: module.default,
schema: module.Schema,
sampleData,
fileName,
groupName: targetGroupData.groupName
groupName: targetGroupData.groupName,
layoutId
}
groupLayouts.push(layoutInfo)
} else {
@ -117,17 +136,20 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
}
if (groupLayouts.length === 0) {
toast({
title: 'No valid layouts found',
toast.error('No valid layouts found', {
description: `No valid layouts found in "${groupSlug}" group.`,
})
setError(`No valid layouts found in "${groupSlug}" group.`)
} else {
setLayoutGroup({
const group: LayoutGroup = {
groupName: targetGroupData.groupName,
layouts: groupLayouts,
settings: groupSettings
})
}
// Cache the result
layoutGroupCache.set(groupSlug, group)
setLayoutGroup(group)
setError(null)
}
@ -136,15 +158,19 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
setError(error instanceof Error ? error.message : 'Failed to load group layouts')
} finally {
setLoading(false)
loadingGroupsCache.delete(groupSlug)
}
}
const retry = () => {
// Clear cache for this group to force reload
layoutGroupCache.delete(groupSlug)
loadGroupLayouts()
}
useEffect(() => {
if (groupSlug) {
if (groupSlug && !hasMountedRef.current) {
hasMountedRef.current = true
loadGroupLayouts()
}
}, [groupSlug])

View file

@ -2,7 +2,7 @@
import { useState, useEffect } from 'react'
import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types'
import { toast } from '@/hooks/use-toast'
import { toast } from 'sonner'
interface UseLayoutLoaderReturn {
layoutGroups: LayoutGroup[]
@ -25,8 +25,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
const response = await fetch('/api/layouts')
if (!response.ok) {
toast({
title: 'Error loading layouts',
toast.error('Error loading layouts', {
description: response.statusText,
})
return
@ -50,21 +49,16 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`)
if (!module.default) {
toast({
title: `${layoutName} has no default export`,
toast.error(`${layoutName} has no default export`, {
description: 'Please ensure the layout file exports a default component',
})
console.warn(`${layoutName} has no default export`)
continue
}
if (!module.Schema) {
toast({
title: `${layoutName} is missing required Schema export`,
toast.error(`${layoutName} is missing required Schema export`, {
description: 'Please ensure the layout file exports a Schema',
})
console.error(`${layoutName} is missing required Schema export`)
continue
@ -73,6 +67,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
// 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$/, '')
const layoutInfo: LayoutInfo = {
name: layoutName,
@ -80,7 +75,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
schema: module.Schema,
sampleData,
fileName,
groupName: groupData.groupName
groupName: groupData.groupName,
layoutId
}
groupLayouts.push(layoutInfo)
@ -97,13 +93,15 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
if (module.default && module.Schema) {
// Use empty object to let schema apply its default values
const sampleData = module.Schema.parse({})
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
const layoutInfo: LayoutInfo = {
name: layoutName,
component: module.default,
schema: module.Schema,
sampleData,
fileName,
groupName: groupData.groupName
groupName: groupData.groupName,
layoutId
}
groupLayouts.push(layoutInfo)
allLayouts.push(layoutInfo)
@ -126,10 +124,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
}
if (allLayouts.length === 0) {
toast({
title: 'No valid layouts found',
toast.error('No valid layouts found', {
description: 'Make sure your layout 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.')
} else {

View file

@ -6,6 +6,7 @@ export interface LayoutInfo {
sampleData: any
fileName: string
groupName: string
layoutId: string
}
export interface GroupSetting {

View file

@ -3,9 +3,9 @@ import localFont from "next/font/local";
import { Fraunces, Montserrat, Inria_Serif, Roboto, Instrument_Sans } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";
import { Toaster } from "@/components/ui/toaster";
import { FooterProvider } from "./(presentation-generator)/context/footerContext";
import { LayoutProvider } from "./(presentation-generator)/context/LayoutContext";
import { Toaster } from "sonner";
const fraunces = Fraunces({
subsets: ["latin"],
@ -105,13 +105,11 @@ export default function RootLayout({
<Providers>
<LayoutProvider>
<FooterProvider>
{children}
</FooterProvider>
</LayoutProvider>
</Providers>
<Toaster />
<Toaster position="top-center" richColors={true} />
</body>
</html>
);

View file

@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react";
import Header from "../dashboard/components/Header";
import Wrapper from "@/components/Wrapper";
import { Settings, Key, Loader2, Check, ChevronsUpDown } from "lucide-react";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import { RootState } from "@/store/store";
import { useSelector } from "react-redux";
import { handleSaveLLMConfig } from "@/utils/storeHelpers";
@ -156,22 +156,16 @@ const SettingsPage = () => {
setIsLoading(true);
await pullOllamaModels();
}
toast({
title: "Success",
description: "Configuration saved successfully",
});
toast.success("Configuration saved successfully");
setIsLoading(false);
router.back();
} catch (error) {
console.error("Error:", error);
toast({
title: "Error",
description:
error instanceof Error
? error.message
: "Failed to save configuration",
variant: "destructive",
});
toast.error(
error instanceof Error
? error.message
: "Failed to save configuration"
);
setIsLoading(false);
}
};
@ -284,23 +278,18 @@ const SettingsPage = () => {
const isModelAvailable = data.includes(llmConfig.CUSTOM_MODEL);
if (!isModelAvailable) {
setLlmConfig({ ...llmConfig, CUSTOM_MODEL: "" });
toast({
title: "Model Unavailable",
description: `The selected model "${llmConfig.CUSTOM_MODEL}" is no longer available. Please select a different model.`,
variant: "destructive",
});
toast.error(
`The selected model "${llmConfig.CUSTOM_MODEL}" is no longer available. Please select a different model.`
);
}
}
} catch (error) {
console.error("Error fetching custom models:", error);
// Don't set customModelsChecked to true on error, so the button remains visible
setCustomModels([]);
toast({
title: "Error",
description:
"Failed to fetch available models. Please check your URL and API key.",
variant: "destructive",
});
toast.error(
"Failed to fetch available models. Please check your URL and API key."
);
} finally {
setCustomModelsLoading(false);
}
@ -385,15 +374,15 @@ const SettingsPage = () => {
key={provider}
onClick={() => changeProvider(provider)}
className={`relative p-4 rounded-lg border-2 transition-all duration-200 ${llmConfig.LLM === provider
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-blue-200 hover:bg-gray-50"
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-blue-200 hover:bg-gray-50"
}`}
>
<div className="flex items-center justify-center gap-3">
<span
className={`font-medium text-center ${llmConfig.LLM === provider
? "text-blue-700"
: "text-gray-700"
? "text-blue-700"
: "text-gray-700"
}`}
>
{provider === "openai"
@ -770,8 +759,8 @@ const SettingsPage = () => {
customModelsLoading || !llmConfig.CUSTOM_LLM_URL
}
className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 font-semibold ${customModelsLoading || !llmConfig.CUSTOM_LLM_URL
? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500"
: "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20"
? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500"
: "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20"
}`}
>
{customModelsLoading ? (
@ -804,8 +793,8 @@ const SettingsPage = () => {
customModelsLoading || !llmConfig.CUSTOM_LLM_URL
}
className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 font-semibold ${customModelsLoading || !llmConfig.CUSTOM_LLM_URL
? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500"
: "bg-white border-gray-600 text-gray-600 hover:bg-gray-50 focus:ring-2 focus:ring-gray-500/20"
? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500"
: "bg-white border-gray-600 text-gray-600 hover:bg-gray-50 focus:ring-2 focus:ring-gray-500/20"
}`}
>
{customModelsLoading ? (
@ -995,10 +984,10 @@ const SettingsPage = () => {
(llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL)
}
className={`mt-8 w-full font-semibold py-3 px-4 rounded-lg transition-all duration-500 ${isLoading ||
(llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL) ||
(llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL)
? "bg-gray-400 cursor-not-allowed"
: "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200"
(llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL) ||
(llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL)
? "bg-gray-400 cursor-not-allowed"
: "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200"
} text-white`}
>
{isLoading ? (

View file

@ -1,6 +1,12 @@
import React from 'react'
import SettingPage from './SettingPage'
export const metadata = {
title: 'Settings | Presenton',
description: 'Settings page',
}
const page = () => {
return (
<SettingPage />
)

View file

@ -1,7 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { toast } from "@/hooks/use-toast";
import { toast } from "sonner";
import {
Info,
ExternalLink,
@ -276,22 +276,20 @@ export default function Home() {
setIsLoading(true);
await pullOllamaModels();
}
toast({
title: "Success",
description: "Configuration saved successfully",
});
toast.success("Configuration saved successfully");
setIsLoading(false);
router.push("/upload");
} catch (error) {
console.error("Error:", error);
toast({
title: "Error",
description:
error instanceof Error
? error.message
: "Failed to save configuration",
variant: "destructive",
});
toast.error(
error instanceof Error
? error.message
: "Failed to save configuration",
{
description:
"Failed to save configuration",
}
);
setIsLoading(false);
}
};
@ -413,12 +411,13 @@ export default function Home() {
console.error("Error fetching custom models:", error);
// Don't set customModelsChecked to true on error, so the button remains visible
setCustomModels([]);
toast({
title: "Error",
description:
"Failed to fetch available models. Please check your URL and API key.",
variant: "destructive",
});
toast.error(
"Failed to fetch available models. Please check your URL and API key.",
{
description:
"Failed to fetch available models. Please check your URL and API key.",
}
);
} finally {
setCustomModelsLoading(false);
}

View file

@ -1,141 +0,0 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View file

@ -1,59 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View file

@ -1,7 +0,0 @@
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

View file

@ -1,50 +0,0 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View file

@ -1,36 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View file

@ -1,114 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRightIcon />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View file

@ -1,213 +0,0 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months
),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next
),
month_caption: cn(
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
defaultClassNames.month_caption
),
dropdowns: cn(
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
defaultClassNames.dropdown_root
),
dropdown: cn(
"bg-popover absolute inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-[--cell-size] select-none",
defaultClassNames.week_number_header
),
week_number: cn(
"text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day
),
range_start: cn(
"bg-accent rounded-l-md",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-[--cell-size] items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

View file

@ -1,261 +0,0 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeftIcon className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRightIcon className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

View file

@ -1,29 +0,0 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { cn } from "@/lib/utils"
import { CheckIcon } from "@radix-ui/react-icons"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View file

@ -1,199 +0,0 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { cn } from "@/lib/utils"
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View file

@ -1,118 +0,0 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View file

@ -1,200 +0,0 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "@/lib/utils"
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View file

@ -1,178 +0,0 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View file

@ -1,29 +0,0 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

View file

@ -1,70 +0,0 @@
"use client"
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { cn } from "@/lib/utils"
import { MinusIcon } from "@radix-ui/react-icons"
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
))
InputOTP.displayName = "InputOTP"
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
))
InputOTPGroup.displayName = "InputOTPGroup"
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
return (
<div
ref={ref}
className={cn(
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-1 ring-ring",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
)
})
InputOTPSlot.displayName = "InputOTPSlot"
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<MinusIcon />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

View file

@ -1,255 +0,0 @@
"use client"
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { cn } from "@/lib/utils"
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu {...props} />
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group {...props} />
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal {...props} />
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return <MenubarPrimitive.RadioGroup {...props} />
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

View file

@ -1,127 +0,0 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { ChevronDownIcon } from "@radix-ui/react-icons"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

View file

@ -1,9 +1,8 @@
import { cn } from "@/lib/utils"
import { Loader } from "./loader"
import { ProgressBar } from "./progress-bar"
import { useEffect, useRef } from "react"
import anime from "animejs"
import Image from "next/image"
import { useEffect, useState } from "react"
interface OverlayLoaderProps {
text?: string
className?: string
@ -23,27 +22,13 @@ export const OverlayLoader = ({
onProgressComplete,
extra_info
}: OverlayLoaderProps) => {
const overlayRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (show && overlayRef.current && contentRef.current) {
// Animate overlay fade in
anime({
targets: overlayRef.current,
opacity: [0, 1],
duration: 300,
easing: 'easeInOutQuad'
});
// Animate content scale and fade in
anime({
targets: contentRef.current,
scale: [0.9, 1],
opacity: [0, 1],
duration: 400,
easing: 'easeOutQuad'
});
if (show) {
setIsVisible(true);
} else {
setIsVisible(false);
}
}, [show]);
@ -51,14 +36,16 @@ export const OverlayLoader = ({
return (
<div
ref={overlayRef}
className="fixed inset-0 bg-black/70 z-50 flex items-center justify-center opacity-0"
className={cn(
"fixed inset-0 bg-black/70 z-50 flex items-center justify-center transition-opacity duration-300",
isVisible ? "opacity-100" : "opacity-0"
)}
>
<div
ref={contentRef}
className={cn(
"flex flex-col items-center justify-center px-6 pt-0 pb-8 rounded-xl bg-[#030303] shadow-2xl",
"min-w-[280px] sm:min-w-[330px] border border-white/10 opacity-0",
"min-w-[280px] sm:min-w-[330px] border border-white/10 transition-all duration-400 ease-out",
isVisible ? "opacity-100 scale-100" : "opacity-0 scale-90",
className
)}

View file

@ -1,116 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
import { ChevronLeftIcon, ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeftIcon className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRightIcon className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View file

@ -1,6 +1,5 @@
'use client'
import React, { useEffect, useState, useRef } from 'react';
import anime from 'animejs';
interface ProgressBarProps {
duration: number;
@ -11,22 +10,8 @@ export const ProgressBar = ({ duration, onComplete }: ProgressBarProps) => {
const [progress, setProgress] = useState(0);
const progressInterval = useRef<NodeJS.Timeout | null>(null);
const startTime = useRef<number>(Date.now());
const progressBarRef = useRef<HTMLDivElement>(null);
const gradientRef = useRef<anime.AnimeInstance | null>(null);
useEffect(() => {
// Animate gradient
if (progressBarRef.current) {
gradientRef.current = anime({
targets: progressBarRef.current,
backgroundPosition: ['0% 50%', '100% 50%'],
duration: 2000,
loop: true,
direction: 'alternate',
easing: 'linear'
});
}
const updateProgress = () => {
const currentTime = Date.now();
const elapsedTime = currentTime - startTime.current;
@ -56,9 +41,6 @@ export const ProgressBar = ({ duration, onComplete }: ProgressBarProps) => {
if (progressInterval.current) {
clearInterval(progressInterval.current);
}
if (gradientRef.current) {
gradientRef.current.pause();
}
};
}, [duration, onComplete]);
@ -70,16 +52,29 @@ export const ProgressBar = ({ duration, onComplete }: ProgressBarProps) => {
</div>
<div className="w-full bg-white rounded-full h-2 overflow-hidden">
<div
ref={progressBarRef}
className="h-full bg-gradient-to-r from-[#9034EA] via-[#5146E5] to-[#9034EA] rounded-full"
className="h-full bg-gradient-to-r from-[#9034EA] via-[#5146E5] to-[#9034EA] rounded-full animate-gradient transition-all duration-300 ease-out"
style={{
width: `${progress}%`,
backgroundSize: '200% 100%',
transition: 'width 0.3s ease-out'
}}
/>
</div>
<style jsx>{`
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.animate-gradient {
animation: gradient 2s linear infinite;
}
`}</style>
</div>
);
};

View file

@ -1,44 +0,0 @@
"use client"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
import { DragHandleDots2Icon } from "@radix-ui/react-icons"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<DragHandleDots2Icon className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View file

@ -1,772 +0,0 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { ViewVerticalIcon } from "@radix-ui/react-icons"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
)
SidebarProvider.displayName = "SidebarProvider"
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}
>(
(
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className
)}
ref={ref}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
/>
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
)
}
)
Sidebar.displayName = "Sidebar"
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<ViewVerticalIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
})
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
})
SidebarRail.displayName = "SidebarRail"
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"main">
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex w-full flex-1 flex-col bg-background",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...props}
/>
)
})
SidebarInset.displayName = "SidebarInset"
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className
)}
{...props}
/>
)
})
SidebarInput.displayName = "SidebarInput"
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarFooter.displayName = "SidebarFooter"
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
})
SidebarSeparator.displayName = "SidebarSeparator"
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
})
SidebarContent.displayName = "SidebarContent"
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
})
SidebarGroup.displayName = "SidebarGroup"
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
})
SidebarGroupLabel.displayName = "SidebarGroupLabel"
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarGroupAction.displayName = "SidebarGroupAction"
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
))
SidebarGroupContent.displayName = "SidebarGroupContent"
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
))
SidebarMenu.displayName = "SidebarMenu"
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
))
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className
)}
{...props}
/>
)
})
SidebarMenuAction.displayName = "SidebarMenuAction"
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuSub.displayName = "SidebarMenuSub"
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View file

@ -12,6 +12,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
duration={2000}
richColors={true}
toastOptions={{
classNames: {
toast:

View file

@ -1,129 +0,0 @@
"use client"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Cross2Icon } from "@radix-ui/react-icons"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-2 z-[100] flex max-h-screen w-full flex-col-reverse p-4 right-0 sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-green-50 text-green-700 ",
success: "border bg-green-50 text-green-700 border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive/90 text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-bold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm font-medium opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View file

@ -1,35 +0,0 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View file

@ -1,61 +0,0 @@
"use client"
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

View file

@ -1,194 +0,0 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

View file

@ -1,39 +0,0 @@
import { useState, useEffect } from "react";
export const useTypewriter = (text: string, speed = 50, enabled = true) => {
const [displayText, setDisplayText] = useState("");
const [isCursorVisible, setIsCursorVisible] = useState(true);
const [index, setIndex] = useState(0);
// Reset when text changes or enabled status changes
useEffect(() => {
if (enabled) {
setDisplayText("");
setIndex(0);
setIsCursorVisible(true);
} else {
// When disabled, immediately show full text and hide cursor
setDisplayText(text);
setIsCursorVisible(false);
}
}, [text, enabled]);
// Only run the typing effect when enabled
useEffect(() => {
if (!enabled) return;
if (index >= text.length) {
setIsCursorVisible(false);
return;
}
const timeout = setTimeout(() => {
setDisplayText((prev) => prev + text.charAt(index));
setIndex((prev) => prev + 1);
}, speed);
return () => clearTimeout(timeout);
}, [index, text, speed, enabled]);
return { displayText, isCursorVisible };
};

Some files were not shown because too many files have changed in this diff Show more