Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ba1784e
refactor: enhance TreeNode and TreeView to support rootExpand prop fo…
JMauclair Oct 30, 2025
9c59e6e
Merge branch 'main' of https://github.com/Klickbee/klickbee-cms into …
JMauclair Oct 30, 2025
ed701db
chore: add generated CSS file to .gitignore
JMauclair Nov 4, 2025
ebe81e0
feat: implement CSS generation functionality for builder components
JMauclair Nov 5, 2025
e1fe251
feat: add event handlers and className prop to builder components for…
JMauclair Nov 5, 2025
5a60791
feat: enhance ComponentRenderer to support click and drag event handl…
JMauclair Nov 5, 2025
25e2f96
feat: add breakpoint style handling functions for responsive componen…
JMauclair Nov 5, 2025
086a2f3
feat: integrate BreakpointProvider and enhance breakpoint handling in…
JMauclair Nov 5, 2025
bde42b0
feat: create Zustand store for managing active breakpoints in builder…
JMauclair Nov 5, 2025
cf60cae
feat: add ComponentName type to currentComponent store for improved t…
JMauclair Nov 5, 2025
9e1646e
feat: add BreakpointStyleProps type for improved breakpoint styling m…
JMauclair Nov 5, 2025
de8589f
feat: add name property to various component definitions for improved…
JMauclair Nov 5, 2025
68fcec9
feat: add new components support to enhance the component library
JMauclair Nov 5, 2025
f873127
feat: enhance BuilderComponent with name property and support for bre…
JMauclair Nov 5, 2025
76b9e4b
feat: update useFooterEditor to support BreakpointStyleProps for impr…
JMauclair Nov 5, 2025
7ac6e29
feat: update useHeaderEditor to utilize BreakpointStyleProps for enha…
JMauclair Nov 5, 2025
211d1c0
feat: refactor PageRenderer to improve component structure and add ID…
JMauclair Nov 5, 2025
a4ac930
feat: update .gitignore to ignore all generated files in the app dire…
JMauclair Nov 5, 2025
bf2fbc6
feat: add generated.css and update layout to import it
JMauclair Nov 5, 2025
0affdf4
refactor: replace trash and plus button by dropdown for UX purpose
JMauclair Nov 5, 2025
4477b68
fix: missing component name property in DnD of component tab to viewport
JMauclair Nov 6, 2025
d007426
feat: enhance drag-and-drop functionality with DragOverlay for better UX
JMauclair Nov 6, 2025
6915c26
feat: add generated.css to .gitignore to prevent tracking of generate…
JMauclair Nov 6, 2025
90409c3
feat: import generated.css in globals.css for styling enhancements
JMauclair Nov 6, 2025
5fd97fd
feat: simplify style prop in Section component by removing unused pro…
JMauclair Nov 6, 2025
ca3e03a
feat: enhance mapStylePropsToCss to support breakpoint styles and imp…
JMauclair Nov 6, 2025
026817b
feat: enhance CSS generation to support breakpoint-specific styles in…
JMauclair Nov 6, 2025
cf479b2
feat: update default breakpoint width to 1440 in BreakpointContext
JMauclair Nov 6, 2025
e2f636f
feat: integrate mapStylePropsToCss for dynamic styling in SubmitButton
JMauclair Nov 6, 2025
e60431a
feat: integrate useCssGeneration in page header, footer, and actions …
JMauclair Nov 6, 2025
67541a2
feat: update CSS import for new page structure in generated.css
JMauclair Nov 6, 2025
dfdc746
feat: refactor component rendering logic to integrate active breakpoi…
JMauclair Nov 6, 2025
f3f7063
feat: integrate active breakpoint handling in Builder and Layers comp…
JMauclair Nov 6, 2025
c630718
feat: optimize import statements and clean up unused code in Layers c…
JMauclair Nov 6, 2025
d92bbbe
feat: wrap img element in a div for improved styling and layout control
JMauclair Nov 6, 2025
aa62319
feat: enhance component selection styling with updated border thickness
JMauclair Nov 6, 2025
7768ad8
feat: reduce border thickness for selected and drop target states in …
JMauclair Nov 10, 2025
4a74b37
feat: enhance size and spacing handling with default values and impro…
JMauclair Nov 10, 2025
eb00bff
feat: update media query logic to use max-width for breakpoint handling
JMauclair Nov 11, 2025
a0aa89f
feat: add CSS generation for header and footer components in usePageH…
JMauclair Nov 11, 2025
5712145
feat: implement PagePresenter component for dynamic page rendering wi…
JMauclair Nov 11, 2025
35278d7
feat: extend sizeUnits to include viewport width and height units
JMauclair Nov 11, 2025
6475a73
feat: enhance to support custom unit handling and add TextInput compo…
JMauclair Nov 11, 2025
c0c6a4d
feat: refine CSS generation for builder container by removing width p…
JMauclair Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ src/generated
public/builder/uploads/logo/*
WARP.md
/public/uploads/media/*
/src/app/generated/*
/src/app/generated.css
112 changes: 112 additions & 0 deletions src/app/admin/[adminKey]/builder/present/[id]/[breakpoint]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"use client";

import React, { Usable } from "react";
import { BreakpointProvider } from "@/feature/builder/contexts/BreakpointContext";
import { usePageFooterByPage } from "@/feature/page/_footer/queries/usePageFooter";
import { usePageHeaderByPage } from "@/feature/page/_header/queries/usePageHeader";
import { usePageById } from "@/feature/page/queries/usePageById";
import { PageLight } from "@/feature/page/types/page";
import { PageFooter, PageHeader } from "@/generated/prisma";
import { PageRenderer } from "@/public/renderer/PageRenderer";

export default function PagePresenter({
params,
}: {
params: { id: string; breakpoint: string };
}) {
const { id: idParam, breakpoint: bpParam } = React.use(
params as unknown as Usable<{ id: string; breakpoint: string }>,
);

// Support builder opening with "new" when no page exists yet
if (idParam === "new") {
const bpNum = parseInt(bpParam || "0", 10) || 0;
return (
<div style={{ padding: 16 }}>
<h2>Preview (new page)</h2>
<div
style={{
width: bpNum ? `${bpNum}px` : "100%",
maxWidth: bpNum ? `${bpNum}px` : "100%",
margin: "0 auto",
border: "1px solid #e5e7eb",
background: "white",
minHeight: 400,
}}
/>
</div>
);
}

const pageId = Number(idParam);
const breakpoint = Number(bpParam) || 0;
const bpName = breakpoint ? `bp-${breakpoint}` : "default";

const {
error: pageError,
data: page,
isLoading: pageLoading,
} = usePageById(pageId);
const { data: pageHeader } = usePageHeaderByPage(
pageId > 0 ? pageId : undefined,
);
const { data: pageFooter } = usePageFooterByPage(
pageId > 0 ? pageId : undefined,
);

if (pageLoading) {
return <div style={{ padding: 16 }}>Loading preview…</div>;
}

// Handle possible auth error response shape returned by server helpers
if (!page) {
return <div style={{ padding: 16 }}>Unable to load page.</div>;
}
if (pageError) {
return (
<div style={{ padding: 16 }}>
Error loading page: {pageError.message}
</div>
);
}

// page is expected to be a Page object at this point
const content = (page as PageLight).content;

return (
<div style={{ padding: 16 }}>
<h1
style={{
marginBottom: 8,
padding: 24,
color: "#fff",
textAlign: "center",
backgroundColor: "black",
}}
>
Preview - Page {pageId} - {breakpoint}px
</h1>
<div
style={{
width: breakpoint ? `${breakpoint}px` : "100%",
maxWidth: breakpoint ? `${breakpoint}px` : "100%",
margin: "0 auto",
background: "white",
border: "1px solid #e5e7eb",
minHeight: 200,
overflow: "hidden",
}}
>
<BreakpointProvider
value={{ name: bpName, width: breakpoint || 1440 }}
>
<PageRenderer
content={content}
footerContent={(pageFooter as PageHeader)?.content}
headerContent={(pageHeader as PageFooter)?.content}
/>
</BreakpointProvider>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "./generated.css";

.dark {
--background: oklch(0.145 0 0);
Expand Down
1 change: 1 addition & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import "./globals.css";
import "./generated.css";
import { NextIntlClientProvider } from "next-intl";
import { getLocale } from "next-intl/server";
import QueryProvider from "@/providers/QueryProvider";
Expand Down
77 changes: 77 additions & 0 deletions src/feature/builder/actions/cssActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use server";

import { promises as fs } from "fs";
import path from "path";

export async function generateCssAction({
css,
pageId,
fileName,
}: {
css: string;
pageId: number | string;
fileName: string; // sanitized filename computed client-side
}): Promise<{ ok: boolean; file?: string; error?: string }> {
if (!css || !fileName || (!pageId && pageId !== 0)) {
return { ok: false, error: "Missing css, fileName, or pageId" };
}

try {
const cwd = process.cwd();
const appDir = path.join(cwd, "src", "app");
const generatedDir = path.join(appDir, "generated");
const generated = path.join(appDir, "generated.css");
const targetCssPath = path.join(generatedDir, fileName);

// Ensure generated directory exists
await fs.mkdir(generatedDir, { recursive: true });

// Write or replace the per-page CSS file
await fs.writeFile(targetCssPath, css, "utf8");

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

To mitigate this vulnerability, server-side validation of fileName must be performed before constructing and using any file path based on it. The best way to fix this is to ensure that the generated path (after joining generatedDir and fileName and normalizing) is still inside generatedDir, preventing path traversal. Steps:

  1. Use path.resolve to combine generatedDir and fileName, producing an absolute, normalized path.
  2. Ensure that the resolved path begins with the absolute path for generatedDir (for example, using startsWith).
  3. If the check fails, return an error and do not use the path.
  4. Optionally, add a regex check to ensure fileName is a "safe" filename (e.g., only alphanumerics, dashes, underscore, .css extension)—but the path containment check is more general and robust.
    Modify only the section between lines 24 and 30 of src/feature/builder/actions/cssActions.ts.
    No additional dependencies are needed; all can be done with Node.js built-in path.

Suggested changeset 1
src/feature/builder/actions/cssActions.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/feature/builder/actions/cssActions.ts b/src/feature/builder/actions/cssActions.ts
--- a/src/feature/builder/actions/cssActions.ts
+++ b/src/feature/builder/actions/cssActions.ts
@@ -21,7 +21,12 @@
 		const appDir = path.join(cwd, "src", "app");
 		const generatedDir = path.join(appDir, "generated");
 		const generated = path.join(appDir, "generated.css");
-		const targetCssPath = path.join(generatedDir, fileName);
+		// Compute target CSS path and validate it to prevent path traversal
+		const targetCssPath = path.resolve(generatedDir, fileName);
+		// Ensure resulting path is within generatedDir
+		if (!targetCssPath.startsWith(path.resolve(generatedDir) + path.sep)) {
+			return { ok: false, error: "Invalid file name" };
+		}
 
 		// Ensure generated directory exists
 		await fs.mkdir(generatedDir, { recursive: true });
EOF
@@ -21,7 +21,12 @@
const appDir = path.join(cwd, "src", "app");
const generatedDir = path.join(appDir, "generated");
const generated = path.join(appDir, "generated.css");
const targetCssPath = path.join(generatedDir, fileName);
// Compute target CSS path and validate it to prevent path traversal
const targetCssPath = path.resolve(generatedDir, fileName);
// Ensure resulting path is within generatedDir
if (!targetCssPath.startsWith(path.resolve(generatedDir) + path.sep)) {
return { ok: false, error: "Invalid file name" };
}

// Ensure generated directory exists
await fs.mkdir(generatedDir, { recursive: true });
Copilot is powered by AI and may make mistakes. Always verify output.

// Update globals.css to import this CSS
let globals = "";
try {
globals = await fs.readFile(generated, "utf8");
} catch (_e) {
// If globals.css doesn't exist, create it
globals = "";
}

const pageIdStr = String(pageId);
const importRegex = new RegExp(
String.raw`^\s*@import\s+["']\.\/generated\/page-${pageIdStr}-[^"']+["'];\s*$`,
"gm",
);

// Remove any existing imports for this page id
globals = globals.replace(importRegex, "").trimEnd() + "\n";

const newImport = `@import "./generated/${fileName}";`;

if (!globals.includes(newImport)) {
// Append the new import near the top, after existing imports if any
const lines = globals.split(/\r?\n/);
let insertIndex = 0;
while (
insertIndex < lines.length &&
lines[insertIndex].trim().startsWith("@import")
) {
insertIndex++;
}
lines.splice(insertIndex, 0, newImport);
globals = lines.join("\n");
await fs.writeFile(generated, globals, "utf8");
}

return {
ok: true,
file: `src/app/generated/${fileName}`,
};
} catch (error) {
return {
ok: false,
error: (error as { message: string })?.message ?? String(error),
};
}
}
21 changes: 20 additions & 1 deletion src/feature/builder/components/Builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { useAdminKey } from "@/feature/admin-key/lib/utils";
import BuilderLeftSidebar from "@/feature/builder/components/ui/BuilderLeftSidebar";
import BuilderPreview from "@/feature/builder/components/ui/BuilderPreview";
import BuilderRightSidebar from "@/feature/builder/components/ui/BuilderRightSidebar";
import { useActiveBreakpointStore } from "@/feature/builder/store/storeActiveBreakpoint";
import { Breakpoint } from "@/feature/builder/types/breakpoint";
import { useAddPage } from "@/feature/page/hooks/useAddPage";
import { useSetting } from "@/feature/settings/queries/useSettings";

export default function BuilderComponent() {
const searchParams = useSearchParams();
Expand All @@ -15,6 +18,8 @@ export default function BuilderComponent() {
const action = searchParams.get("action");
const hasProcessedAction = useRef(false);
const { addPage } = useAddPage();
const { active: activeBreakpoint, setActive: setActiveBreakpoint } =
useActiveBreakpointStore();

// Function to clean up the URL after the action
const clearActionFromUrl = useCallback(() => {
Expand Down Expand Up @@ -50,11 +55,25 @@ export default function BuilderComponent() {
[addPage, clearActionFromUrl],
);

const breakPoints = useSetting("builder_breakpoints");

useEffect(() => {
if (action && !hasProcessedAction.current) {
executeAction(action);
}
}, [action, executeAction]);
if (!activeBreakpoint) {
const bps: Breakpoint[] = breakPoints?.data?.value
? JSON.parse(breakPoints.data.value)
: [];
if (bps.length > 0) {
// set the biggest breakpoint as active
const sortedBps = bps.sort(
(a, b) => (b.width || 0) - (a.width || 0),
);
setActiveBreakpoint(sortedBps[0]);
}
}
}, [action, executeAction, breakPoints]);

return (
<div className="flex-1 flex">
Expand Down
26 changes: 22 additions & 4 deletions src/feature/builder/components/builder_components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@ import { BuilderComponent } from "@/feature/builder/types/components/components"

interface ButtonProps {
component: BuilderComponent;
className?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
onDragLeave?: React.DragEventHandler<HTMLElement>;
onDragOver?: React.DragEventHandler<HTMLElement>;
}

export const Button: React.FC<ButtonProps> = ({ component }) => {
export const Button: React.FC<ButtonProps> = ({
component,
className,
onClick,
onDragLeave,
onDragOver,
}) => {
// Extract content with sensible fallbacks
const text = (component.props?.content?.text as string) || "Button";
const href = (component.props?.content?.href as string) || "";
Expand Down Expand Up @@ -79,8 +89,11 @@ export const Button: React.FC<ButtonProps> = ({ component }) => {
...mapStylePropsToCss(component.props?.style),
};

const className =
const classNameLocal =
"inline-flex items-center gap-2 rounded-md font-medium focus:outline-none focus:ring-2 focus:ring-offset-2";
const mergedClassName = [classNameLocal, className]
.filter(Boolean)
.join(" ");

const handleClick = React.useCallback(
(e?: React.MouseEvent) => {
Expand All @@ -103,9 +116,14 @@ export const Button: React.FC<ButtonProps> = ({ component }) => {

return (
<ButtonShadcn
className={className}
className={mergedClassName}
data-href={href || undefined}
onClick={handleClick}
onClick={(e) => {
handleClick(e);
if (onClick) onClick(e);
}}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
style={commonStyle}
type="button"
>
Expand Down
19 changes: 17 additions & 2 deletions src/feature/builder/components/builder_components/ui/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@ import { BuilderComponent } from "../../../types/components/components";

interface CheckboxProps {
component: BuilderComponent;
className?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
onDragLeave?: React.DragEventHandler<HTMLElement>;
onDragOver?: React.DragEventHandler<HTMLElement>;
}

export const Checkbox: React.FC<CheckboxProps> = ({ component }) => {
export const Checkbox: React.FC<CheckboxProps> = ({
component,
className,
onClick,
onDragLeave,
onDragOver,
}) => {
// Default checkbox properties if not provided
const label = (component.props?.content?.label as string) || "Checkbox";
const name =
Expand All @@ -18,7 +28,12 @@ export const Checkbox: React.FC<CheckboxProps> = ({ component }) => {

return (
<div
className="flex items-start"
className={["flex items-start", className]
.filter(Boolean)
.join(" ")}
onClick={onClick}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
style={{
order: component.order || 0, // Use order property for positioning
...mapStylePropsToCss(component.props?.style),
Expand Down
20 changes: 18 additions & 2 deletions src/feature/builder/components/builder_components/ui/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,29 @@ import { ComponentRenderer } from "../../../lib/renderers/ComponentRenderer";

interface ContainerProps {
component: BuilderComponent;
className?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
onDragLeave?: React.DragEventHandler<HTMLElement>;
onDragOver?: React.DragEventHandler<HTMLElement>;
}

export const Container: React.FC<ContainerProps> = ({ component }) => {
export const Container: React.FC<ContainerProps> = ({
component,
className,
onClick,
onDragLeave,
onDragOver,
}) => {
// Get the setTargetComponent function from context
const dragDropContext = useContext(DragDropContext);
return (
<div
className="relative container"
className={["relative builder-container", className]
.filter(Boolean)
.join(" ")}
onClick={onClick}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
style={{
order: component.order || 0, // Use order property for positioning
...mapStylePropsToCss(component.props?.style),
Expand Down Expand Up @@ -59,6 +74,7 @@ export const Container: React.FC<ContainerProps> = ({ component }) => {
);
}
}}
region={"content"}
/>
))}
</>
Expand Down
Loading
Loading