)}
);
diff --git a/editron-starters/bolt-qwik/src/components/starter/hero/hero.tsx b/editron-starters/bolt-qwik/src/components/starter/hero/hero.tsx
index 119c6df5..fcd15da4 100644
--- a/editron-starters/bolt-qwik/src/components/starter/hero/hero.tsx
+++ b/editron-starters/bolt-qwik/src/components/starter/hero/hero.tsx
@@ -2,6 +2,23 @@ import { component$ } from '@builder.io/qwik';
import styles from './hero.module.css';
import ImgThunder from '../../../media/thunder.png?jsx';
+export interface ConfettiOptions {
+ particleCount?: number;
+ angle?: number;
+ spread?: number;
+ startVelocity?: number;
+ decay?: number;
+ gravity?: number;
+ drift?: number;
+ ticks?: number;
+ origin?: { x?: number; y?: number };
+ colors?: string[];
+ shapes?: string[];
+ scalar?: number;
+ zIndex?: number;
+ disableForReducedMotion?: boolean;
+}
+
export default component$(() => {
return (
@@ -29,7 +46,7 @@ export default component$(() => {
};
function loadConfetti() {
- return new Promise<(opts: any) => void>((resolve, reject) => {
+ return new Promise<(opts: ConfettiOptions) => void>((resolve, reject) => {
if ((globalThis as any).confetti) {
return resolve((globalThis as any).confetti as any);
}
diff --git a/env.ts b/env.ts
index 1b72efcd..01e54a46 100644
--- a/env.ts
+++ b/env.ts
@@ -2,23 +2,31 @@ import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
- server: {
- DATABASE_URL: z.string().url(),
- AUTH_SECRET: z.string().min(1).optional(),
- GEMINI_API_KEY: z.string().optional(),
- GROQ_API_KEY: z.string().optional(),
- MISTRAL_API_KEY: z.string().optional(),
- AUTH_GITHUB_ID: z.string().optional(),
- AUTH_GITHUB_SECRET: z.string().optional(),
- AUTH_GOOGLE_ID: z.string().optional(),
- AUTH_GOOGLE_SECRET: z.string().optional(),
- },
- client: {
- NEXT_PUBLIC_COLLAB_SERVER_URL: z.string().optional(),
- },
- experimental__runtimeEnv: {
- NEXT_PUBLIC_COLLAB_SERVER_URL: process.env.NEXT_PUBLIC_COLLAB_SERVER_URL,
- },
- skipValidation: !!process.env.SKIP_ENV_VALIDATION,
- emptyStringAsUndefined: true,
-});
+ server: {
+ DATABASE_URL: z.string().url(),
+
+ // ❗ IMPORTANT: optional hata diya
+ AUTH_SECRET: z.string().min(1),
+
+ GEMINI_API_KEY: z.string().optional(),
+ GROQ_API_KEY: z.string().optional(),
+ MISTRAL_API_KEY: z.string().optional(),
+
+ AUTH_GITHUB_ID: z.string().optional(),
+ AUTH_GITHUB_SECRET: z.string().optional(),
+ AUTH_GOOGLE_ID: z.string().optional(),
+ AUTH_GOOGLE_SECRET: z.string().optional(),
+ },
+
+ client: {
+ NEXT_PUBLIC_COLLAB_SERVER_URL: z.string().optional(),
+ },
+
+ experimental__runtimeEnv: {
+ NEXT_PUBLIC_COLLAB_SERVER_URL:
+ process.env.NEXT_PUBLIC_COLLAB_SERVER_URL,
+ },
+
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
+ emptyStringAsUndefined: true,
+});
\ No newline at end of file
diff --git a/lib/api-utils.ts b/lib/api-utils.ts
index 976e8b4b..b990fb2e 100644
--- a/lib/api-utils.ts
+++ b/lib/api-utils.ts
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { z } from "zod";
-import { Ratelimit } from "@upstash/ratelimit";
+import { Ratelimit, type Duration } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// --- Rate Limiter ---
@@ -26,7 +26,7 @@ function getRedisRatelimit(maxRequests: number, windowMs: number): Ratelimit | n
new Ratelimit({
redis: Redis.fromEnv(),
// @upstash/ratelimit allows durations like "10 s", "60000 ms"
- limiter: Ratelimit.slidingWindow(maxRequests, `${windowMs} ms` as any),
+ limiter: Ratelimit.slidingWindow(maxRequests, `${windowMs} ms` as Duration),
})
);
}
@@ -119,8 +119,41 @@ export function handleApiError(error: unknown, context: string): NextResponse {
}
// --- IP Extraction ---
-export function getClientIp(request: Request): string {
- const forwarded = request.headers.get("x-forwarded-for");
- if (forwarded) return forwarded.split(",")[0].trim();
+const TRUSTED_PROXIES = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
+
+export function getClientIp(request: Request | any): string {
+ // Next.js NextRequest has an 'ip' property which is securely populated by the platform
+ if (request.ip) {
+ const ip = request.ip.trim();
+ if (ip.length > 0) return ip;
+ }
+
+ // Identify the direct connection IP (only available in Node.js environments, not Edge)
+ const remoteAddr = (request.socket?.remoteAddress || request.connection?.remoteAddress || "").trim();
+
+ // Determine if we should trust proxy headers
+ // If remoteAddr is unavailable (e.g. Edge runtime), we assume the platform handles trust.
+ const isTrustedProxy = !remoteAddr || TRUSTED_PROXIES.has(remoteAddr);
+
+ if (isTrustedProxy) {
+ // Prioritize x-real-ip as it is typically set by the reverse proxy/load balancer
+ const realIpHeader = request.headers.get("x-real-ip");
+ if (realIpHeader) {
+ const realIp = realIpHeader.trim();
+ if (realIp.length > 0) return realIp;
+ }
+
+ // Fallback to x-forwarded-for
+ const forwardedHeader = request.headers.get("x-forwarded-for");
+ if (forwardedHeader) {
+ const forwarded = forwardedHeader.split(",")[0].trim();
+ if (forwarded.length > 0) return forwarded;
+ }
+ }
+
+ if (remoteAddr.length > 0) {
+ return remoteAddr;
+ }
+
return "unknown";
}
diff --git a/lib/encryption.ts b/lib/encryption.ts
new file mode 100644
index 00000000..282e2872
--- /dev/null
+++ b/lib/encryption.ts
@@ -0,0 +1,58 @@
+import crypto from "crypto";
+
+const ALGORITHM = "aes-256-gcm";
+const IV_LENGTH = 16;
+const SALT_LENGTH = 64;
+const TAG_LENGTH = 16;
+const KEY_LENGTH = 32;
+
+// The ENCRYPTION_KEY should be exactly 32 bytes long.
+// If it's not set, we'll fall back to a dummy key for local development to prevent crashes,
+// but in production this MUST be set.
+const envKey = process.env.ENCRYPTION_KEY || "fallback_dummy_key_for_dev_only_123!";
+let secretKey: Buffer;
+
+if (Buffer.byteLength(envKey) === KEY_LENGTH) {
+ secretKey = Buffer.from(envKey);
+} else {
+ // If it's not exactly 32 bytes, we hash it to force it to 32 bytes.
+ secretKey = crypto.createHash('sha256').update(String(envKey)).digest();
+}
+
+/**
+ * Encrypts a string using AES-256-GCM
+ */
+export function encrypt(text: string): string {
+ const iv = crypto.randomBytes(IV_LENGTH);
+ const cipher = crypto.createCipheriv(ALGORITHM, secretKey, iv);
+
+ let encrypted = cipher.update(text, "utf8", "hex");
+ encrypted += cipher.final("hex");
+
+ const authTag = cipher.getAuthTag();
+
+ // Return iv:authTag:encrypted
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
+}
+
+/**
+ * Decrypts a string using AES-256-GCM
+ */
+export function decrypt(encryptedText: string): string {
+ const parts = encryptedText.split(":");
+ if (parts.length !== 3) {
+ throw new Error("Invalid encrypted text format");
+ }
+
+ const iv = Buffer.from(parts[0], "hex");
+ const authTag = Buffer.from(parts[1], "hex");
+ const encrypted = parts[2];
+
+ const decipher = crypto.createDecipheriv(ALGORITHM, secretKey, iv);
+ decipher.setAuthTag(authTag);
+
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
+ decrypted += decipher.final("utf8");
+
+ return decrypted;
+}
diff --git a/lib/user-data.ts b/lib/user-data.ts
index 594685b2..d74be3f9 100644
--- a/lib/user-data.ts
+++ b/lib/user-data.ts
@@ -10,7 +10,7 @@ export const getUserById = async (id: string) => {
});
return user;
} catch (error) {
- console.log(error);
+ console.error(error);
return null;
}
};
@@ -22,7 +22,7 @@ export const getUserByEmail = async (email: string) => {
});
return user;
} catch (error) {
- console.log(error);
+ console.error(error);
return null;
}
};
@@ -36,7 +36,7 @@ export const getAccountByUserId = async (userId: string) => {
});
return account;
} catch (error) {
- console.log(error);
+ console.error(error);
return null;
}
};
diff --git a/lib/yjs.ts b/lib/yjs.ts
index c69bb85b..ddd56842 100644
--- a/lib/yjs.ts
+++ b/lib/yjs.ts
@@ -62,7 +62,21 @@ export function getOrCreateYDoc(roomId: string, token: string) {
console.log(`[Yjs] Room ${roomId} mapped. Synced:`, isSynced as boolean);
});
provider.on('status', (event: unknown) => {
- console.log(`[Yjs] Room ${roomId} status:`, (event as { status: 'connected' | 'disconnected' | 'connecting' }).status);
+ const status = (event as {
+ status: 'connected' | 'disconnected' | 'connecting'
+ }).status;
+
+ console.log(`[Yjs] Room ${roomId} status:`, status);
+
+ // Attempt recovery after reconnect
+ if (status === "connected") {
+ console.log("[Yjs] Reconnected successfully");
+
+ // Force preview refresh after reconnect
+ window.dispatchEvent(
+ new CustomEvent("yjs-reconnected")
+ );
+ }
});
return { doc, provider };
diff --git a/modules/auth/components/logout-button.tsx b/modules/auth/components/logout-button.tsx
index 8cf0e053..1fb5f5ab 100644
--- a/modules/auth/components/logout-button.tsx
+++ b/modules/auth/components/logout-button.tsx
@@ -1,23 +1,25 @@
"use client";
import React from 'react'
-import { LogoutButtonProps } from '../types'
import { useRouter } from 'next/navigation'
import { signOut } from 'next-auth/react';
+import { Button } from '@/components/ui/button';
+import { LogOut } from 'lucide-react';
-const LogoutButton = ({ children }: LogoutButtonProps) => {
+const LogoutButton = () => {
const router = useRouter();
const onLogout = async () => {
await signOut()
router.refresh()
}
return (
-
+
+ Logout
+
)
}
diff --git a/modules/auth/types.ts b/modules/auth/types.ts
index 6ec262c5..532a2fa0 100644
--- a/modules/auth/types.ts
+++ b/modules/auth/types.ts
@@ -1,3 +1,4 @@
export interface LogoutButtonProps {
- children?: React.ReactNode
+ children?: React.ReactNode;
+ className?: string;
}
\ No newline at end of file
diff --git a/modules/dashboard/actions/index.ts b/modules/dashboard/actions/index.ts
index a1dcd85a..2c2679f2 100644
--- a/modules/dashboard/actions/index.ts
+++ b/modules/dashboard/actions/index.ts
@@ -73,7 +73,7 @@ export const getAllPlaygroundForUser = async () => {
return playground;
} catch (error) {
- console.log(error);
+ console.error(error);
}
};
@@ -140,7 +140,7 @@ export const deleteProjectById = async (id: string) => {
revalidatePath("/dashboard");
} catch (error) {
- console.log(error);
+ console.error(error);
}
};
@@ -170,7 +170,7 @@ export const editProjectById = async (
revalidatePath("/dashboard");
} catch (error) {
- console.log(error);
+ console.error(error);
throw error;
}
};
diff --git a/modules/dashboard/components/dashboard-sidebar.tsx b/modules/dashboard/components/dashboard-sidebar.tsx
index e513a691..0c6fa34c 100644
--- a/modules/dashboard/components/dashboard-sidebar.tsx
+++ b/modules/dashboard/components/dashboard-sidebar.tsx
@@ -31,8 +31,11 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
+ useSidebar,
} from "@/components/ui/sidebar"
import Image from "next/image"
+import { LogOut } from "lucide-react"
+import LogoutButton from "@/modules/auth/components/logout-button"
// Define the interface for a single playground item, icon is now a string
interface PlaygroundData {
@@ -56,6 +59,7 @@ const lucideIconMap: Record
= {
export function DashboardSidebar({ initialPlaygroundData }: { initialPlaygroundData: PlaygroundData[] }) {
const pathname = usePathname()
+ const { isMobile } = useSidebar()
const [starredPlaygrounds] = useState(initialPlaygroundData.filter((p) => p.starred))
const [recentPlaygrounds] = useState(initialPlaygroundData)
@@ -192,6 +196,16 @@ export function DashboardSidebar({ initialPlaygroundData }: { initialPlaygroundD
+ {isMobile && (
+
+
+
+
+ Logout
+
+
+
+ )}
diff --git a/modules/dashboard/components/empty-state.tsx b/modules/dashboard/components/empty-state.tsx
index 377d1986..feadab4d 100644
--- a/modules/dashboard/components/empty-state.tsx
+++ b/modules/dashboard/components/empty-state.tsx
@@ -7,7 +7,7 @@ const EmptyState = () => {
{
body: JSON.stringify(body),
});
+ // 1. Check content type first to safely prevent standard HTML parser crash
+ const contentType = response.headers.get("content-type");
+ if (!contentType || !contentType.includes("application/json")) {
+ throw new Error("Received an invalid server response. If running locally, make sure your collaboration server is active.");
+ }
+
+ // 2. Safe to parse now that we know it is valid JSON
const data = await response.json();
+ // 3. Evaluate internal error messages returned from the API handler
if (!response.ok) {
throw new Error(data.error || "Failed to import repository");
}
@@ -217,4 +225,4 @@ const GithubImportDialog = ({ children }: { children: React.ReactNode }) => {
);
};
-export default GithubImportDialog;
+export default GithubImportDialog;
\ No newline at end of file
diff --git a/modules/dashboard/components/project-table.tsx b/modules/dashboard/components/project-table.tsx
index 0d47fa91..4f7ce6d7 100644
--- a/modules/dashboard/components/project-table.tsx
+++ b/modules/dashboard/components/project-table.tsx
@@ -3,6 +3,7 @@
import Image from "next/image"
import { format } from "date-fns"
import type { Project } from "../types"
+import type { Playground } from "@prisma/client"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
@@ -43,9 +44,9 @@ import { MarkedToggleButton } from "./marked-toggle"
interface ProjectTableProps {
projects: Project[]
- onUpdateProject?: (id: string, data: { title: string; description: string }) => Promise
- onDeleteProject?: (id: string) => Promise
- onDuplicateProject?: (id: string) => Promise
+ onUpdateProject?: (id: string, data: { title: string; description: string }) => Promise
+ onDeleteProject?: (id: string) => Promise
+ onDuplicateProject?: (id: string) => Promise
}
interface EditProjectData {
diff --git a/modules/dashboard/components/template-selecting-modal.tsx b/modules/dashboard/components/template-selecting-modal.tsx
index f46021cb..19cf51a9 100644
--- a/modules/dashboard/components/template-selecting-modal.tsx
+++ b/modules/dashboard/components/template-selecting-modal.tsx
@@ -13,7 +13,7 @@ import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import type { TemplateCategory } from "@/lib/templates/types";
-import { getTemplateSummaries } from "@/lib/templates/actions";
+
import type { TemplateKey } from "@/lib/template";
import {
ChevronRight,
diff --git a/modules/home/code-line.tsx b/modules/home/code-line.tsx
index 06da7478..a6ae8c92 100644
--- a/modules/home/code-line.tsx
+++ b/modules/home/code-line.tsx
@@ -2,38 +2,44 @@
"use client";
import React from 'react';
+const highlightCode = (code: string) => {
+ return code
+ .replace(/\b(?:import|from|export|default|return|const|new)\b/g, '$&')
+ .replace(/'[^']*'/g, '$&')
+ .replace(/"[^"]*"/g, '$&')
+ .replace(/\b(?:Editron|console|editor)\b/g, '$&');
+}
+
+// Ensure the text is properly escaped to avoid HTML injection
+const escapeHtml = (unsafe: string) => {
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+const highlight = (text: string) => {
+ const escaped = escapeHtml(text);
+
+ // Comments (simple // for now)
+ if (escaped.includes('//')) {
+ const parts = escaped.split('//');
+ return <>{'//' + parts[1]}>;
+ }
+
+ return ;
+};
+
export const CodeLine = ({ line }: { line: string }) => {
// Basic replacements to simulate syntax highlighting
// Note: This is a very simplistic implementation and should be replaced with a proper library like prismjs or shiki in production
- const highlight = (text: string) => {
- // We use a series of replacements. Order matters to avoid replacing inside already replaced spans.
- // A better approach for robust highlighting is tokenization.
-
- const highlighted = text;
-
- // Comments (simple // for now)
- if (highlighted.includes('//')) {
- const parts = highlighted.split('//');
- return <>{'//' + parts[1]}>;
- }
-
- return ;
- };
-
- const highlightCode = (code: string) => {
- return code
- .replace(/import|from|export|default|return|const|new/g, '$&')
- .replace(/'[^']*'/g, '$&')
- .replace(/"[^"]*"/g, '$&')
- .replace(/Editron|console|editor/g, '$&');
- }
-
const [highlighted, setHighlighted] = React.useState(line);
React.useEffect(() => {
setHighlighted(highlight(line));
-// eslint-disable-next-line react-hooks/exhaustive-deps
}, [line]);
return highlighted;
diff --git a/modules/home/features.tsx b/modules/home/features.tsx
index 0604745d..fa59e6bb 100644
--- a/modules/home/features.tsx
+++ b/modules/home/features.tsx
@@ -88,9 +88,9 @@ const features: Feature[] = [
export function Features() {
return (
- {features.map((feature, index) => (
+ {features.map((feature) => (
}
title={feature.title}
diff --git a/modules/home/header.tsx b/modules/home/header.tsx
index 2c1f7aef..ae0fe427 100644
--- a/modules/home/header.tsx
+++ b/modules/home/header.tsx
@@ -1,12 +1,14 @@
"use client";
+
import { useState } from "react";
import Link from "next/link";
import Image from "next/image";
+
import { ThemeToggle } from "@/components/ui/toggle-theme";
import UserButton from "../auth/components/user-button";
-import { cn as _cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { useCurrentUser } from "../auth/hooks/use-current-user";
+import ShortcutModal from "@/components/ShortcutModal";
import { Menu } from "lucide-react";
@@ -18,83 +20,91 @@ import {
export function Header() {
const user = useCurrentUser();
- const [open, setOpen] = useState(false);
- return (
-
+
+
+ {/* ✅ FIXED Shortcut Modal */}
+
+ >
);
}
-function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
+function NavLink({ href, children }: any) {
return (
+
{
return playground;
} catch (error) {
- console.log(error)
+ throw error;
}
}
@@ -192,7 +192,6 @@ export const SaveUpdatedCode = async (
return updatedPlayground;
} catch (error) {
- console.log("SaveUpdatedCode error:", error);
throw error;
}
};
@@ -205,7 +204,7 @@ export const deleteProjectById = async (id:string)=>{
})
revalidatePath("/dashboard")
} catch (error) {
- console.log(error)
+ throw error;
}
}
@@ -219,7 +218,7 @@ export const editProjectById = async (id:string,data:{title:string , description
})
revalidatePath("/dashboard")
} catch (error) {
- console.log(error)
+ throw error;
}
}
@@ -246,9 +245,8 @@ export const duplicateProjectById = async (id: string) => {
template: originalPlayground.template,
userId,
templateFiles: {
- // @ts-ignore
create: originalPlayground.templateFiles.map((file) => ({
- content: file.content,
+ content: file.content as Prisma.InputJsonValue,
})),
},
},
diff --git a/modules/playground/components/ai-chat-panel.tsx b/modules/playground/components/ai-chat-panel.tsx
index dfc86196..b5774d0e 100644
--- a/modules/playground/components/ai-chat-panel.tsx
+++ b/modules/playground/components/ai-chat-panel.tsx
@@ -10,6 +10,12 @@ import {
SheetDescription,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
import {
Bot,
Send,
@@ -21,6 +27,7 @@ import {
Zap,
Code2,
ChevronDown,
+ Download,
} from "lucide-react";
import {
useAI,
@@ -34,6 +41,10 @@ import { useFileExplorer } from "@/modules/playground/hooks/useFileExplorer";
import { toast } from "sonner";
import type { TemplateFolder } from "@/modules/playground/lib/path-to-json";
import { useChat } from "@ai-sdk/react";
+import {
+ exportChatAsMarkdown,
+ exportChatAsJSON,
+} from "@/modules/playground/lib/chat-export";
interface AIChatPanelProps {
templateData: TemplateFolder | null;
@@ -172,9 +183,7 @@ export default function AIChatPanel({
// Debug: log all parts to see what v3 sends
if (rawParts.length > 0) {
const toolParts = rawParts.filter((p) => typeof (p as Record).type === "string" && ((p as Record).type as string).startsWith("tool-"));
- if (toolParts.length > 0) {
- console.log("[AIChatPanel] Tool parts in last message:", JSON.stringify(toolParts, null, 2));
- }
+
}
for (const rawPart of rawParts) {
@@ -299,7 +308,6 @@ export default function AIChatPanel({
// Mark as processed BEFORE calling addToolResult to prevent re-execution on re-render
processedToolCallIds.current.add(toolCallId);
- console.log(`[AIChatPanel] Executed tool ${toolName} (${toolCallId}), result:`, result.slice(0, 100));
addToolResult({
toolCallId,
@@ -316,6 +324,34 @@ export default function AIChatPanel({
}
};
+ const handleExportMarkdown = () => {
+ if (messages.length === 0) {
+ toast.error("No messages to export");
+ return;
+ }
+ try {
+ exportChatAsMarkdown(messages);
+ toast.success("Chat exported as Markdown");
+ } catch (error) {
+ console.error("Export error:", error);
+ toast.error("Failed to export chat");
+ }
+ };
+
+ const handleExportJSON = () => {
+ if (messages.length === 0) {
+ toast.error("No messages to export");
+ return;
+ }
+ try {
+ exportChatAsJSON(messages);
+ toast.success("Chat exported as JSON");
+ } catch (error) {
+ console.error("Export error:", error);
+ toast.error("Failed to export chat");
+ }
+ };
+
const clearChat = () => setMessages([]);
const currentProvider = PROVIDERS.find((p) => p.id === provider) || PROVIDERS[0];
@@ -335,9 +371,32 @@ export default function AIChatPanel({