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:
commit
bc5d772733
109 changed files with 1260 additions and 7987 deletions
24
package-lock.json
generated
Normal file
24
package-lock.json
generated
Normal 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
5
package.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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`,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -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[];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 = '') {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import '../app/(presentation-generator)/styles/themes.css';
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface LayoutInfo {
|
|||
sampleData: any
|
||||
fileName: string
|
||||
groupName: string
|
||||
layoutId: string
|
||||
}
|
||||
|
||||
export interface GroupSetting {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
duration={2000}
|
||||
richColors={true}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 }
|
||||
Binary file not shown.
|
|
@ -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 }
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue