diff --git a/package-lock.json b/package-lock.json
index c3227a1..0cd4877 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@hookform/resolvers": "^3.9.0",
"@supabase/supabase-js": "^2.98.0",
"@types/react-big-calendar": "^1.8.9",
+ "lucide-react": "^0.575.0",
"moment": "^2.30.1",
"next": "^14.2.35",
"next-cloudinary": "^6.13.0",
@@ -33,6 +34,7 @@
"dotenv": "^17.3.1",
"eslint": "^8",
"eslint-config-next": "^14.2.35",
+ "phoenix": "^1.8.4",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
@@ -7569,6 +7571,15 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/lucide-react": {
+ "version": "0.575.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz",
+ "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/luxon": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
@@ -8254,6 +8265,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/phoenix": {
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/phoenix/-/phoenix-1.8.4.tgz",
+ "integrity": "sha512-5wdO9mqU4l0AmcexN8mA0m1BsC4ROv2l55MN31gbxmJPzghc4SxVxnNTh9qaummYa2pbwkoBvG8FezO7cjdr8A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
diff --git a/package.json b/package.json
index bb8083b..6d93cf1 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@hookform/resolvers": "^3.9.0",
"@supabase/supabase-js": "^2.98.0",
"@types/react-big-calendar": "^1.8.9",
+ "lucide-react": "^0.575.0",
"moment": "^2.30.1",
"next": "^14.2.35",
"next-cloudinary": "^6.13.0",
@@ -36,6 +37,7 @@
"dotenv": "^17.3.1",
"eslint": "^8",
"eslint-config-next": "^14.2.35",
+ "phoenix": "^1.8.4",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
@@ -47,4 +49,4 @@
"@clerk/shared": "2.9.2",
"@clerk/clerk-react": "5.12.0"
}
-}
\ No newline at end of file
+}
diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx
index e2ee6e2..ac8758b 100644
--- a/src/app/(dashboard)/layout.tsx
+++ b/src/app/(dashboard)/layout.tsx
@@ -1,31 +1,28 @@
-import Menu from "@/components/Menu";
-import Navbar from "@/components/Navbar";
-import Image from "next/image";
-import Link from "next/link";
+import DashboardShell from "@/components/DashboardShell";
+import { currentUser } from "@clerk/nextjs/server";
+import { redirect } from "next/navigation";
-export default function DashboardLayout({
+export default async function DashboardLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
+ const user = await currentUser();
+
+ if (!user) {
+ redirect("/sign-in");
+ }
+
+ const role = user.publicMetadata.role as string;
+ const userMetadata = {
+ firstName: user.firstName,
+ lastName: user.lastName,
+ role: role
+ };
+
return (
-
- {/* LEFT */}
-
-
-
- SchooLama
-
-
-
- {/* RIGHT */}
-
-
- {children}
-
-
+
+ {children}
+
);
}
diff --git a/src/app/(dashboard)/lesson/page.tsx b/src/app/(dashboard)/lesson/page.tsx
new file mode 100644
index 0000000..1b5380c
--- /dev/null
+++ b/src/app/(dashboard)/lesson/page.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import { useSearchParams } from "next/navigation";
+import WhiteboardCore from "@/components/Whiteboard/WhiteboardCore";
+
+const DashboardWhiteboardPage = () => {
+ const searchParams = useSearchParams();
+ const lessonIdParam = searchParams.get("lessonId");
+ const lessonId = lessonIdParam ? parseInt(lessonIdParam) : null;
+
+ return ;
+};
+
+export default DashboardWhiteboardPage;
diff --git a/src/app/(fullscreen)/board/page.tsx b/src/app/(fullscreen)/board/page.tsx
new file mode 100644
index 0000000..ccf02ad
--- /dev/null
+++ b/src/app/(fullscreen)/board/page.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import { useSearchParams } from "next/navigation";
+import WhiteboardCore from "@/components/Whiteboard/WhiteboardCore";
+
+const FullscreenWhiteboardPage = () => {
+ const searchParams = useSearchParams();
+ const lessonIdParam = searchParams.get("lessonId");
+ const lessonId = lessonIdParam ? parseInt(lessonIdParam) : null;
+
+ return ;
+};
+
+export default FullscreenWhiteboardPage;
diff --git a/src/app/(fullscreen)/layout.tsx b/src/app/(fullscreen)/layout.tsx
new file mode 100644
index 0000000..65d9704
--- /dev/null
+++ b/src/app/(fullscreen)/layout.tsx
@@ -0,0 +1,11 @@
+export default function FullscreenLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/(whiteboard)/lesson/page.tsx b/src/app/(whiteboard)/lesson/page.tsx
deleted file mode 100644
index 4d7de76..0000000
--- a/src/app/(whiteboard)/lesson/page.tsx
+++ /dev/null
@@ -1,218 +0,0 @@
-"use client";
-
-import { useEffect, useState, useRef } from "react";
-import { useSearchParams } from "next/navigation";
-import { Tldraw, createTLStore, TLStore, Editor, getSnapshot, loadSnapshot } from "tldraw";
-import "tldraw/tldraw.css";
-import { getLessonWhiteboard, saveLessonSnapshot, SnapshotType } from "@/lib/whiteboardActions";
-import { toast } from "react-toastify";
-import { useUser } from "@clerk/nextjs";
-
-const WhiteboardPage = () => {
- const searchParams = useSearchParams();
- const lessonIdParam = searchParams.get("lessonId");
- const lessonId = lessonIdParam ? parseInt(lessonIdParam) : null;
-
- const { user } = useUser();
- const role = user?.publicMetadata?.role as string | undefined;
- const isTeacherOrAdmin = role === "teacher" || role === "admin";
-
- const [store, setStore] = useState(null);
- const [loading, setLoading] = useState(true);
- const [activeState, setActiveState] = useState("planned");
- const [whiteboardRecord, setWhiteboardRecord] = useState(null);
- const editorRef = useRef(null);
-
- useEffect(() => {
- const fetchWhiteboard = async () => {
- if (!lessonId) {
- setLoading(false);
- return;
- }
-
- const { success, data } = await getLessonWhiteboard(lessonId);
- if (success && data) {
- setWhiteboardRecord(data);
-
- // Initialize store based on role
- const newStore = createTLStore();
-
- let defaultSnapshot = data.plannedSnapshotData;
- let defaultState: SnapshotType = "planned";
-
- // For students, default to live, or final if live doesn't exist. If neither, show empty.
- if (!isTeacherOrAdmin) {
- if (data.liveSnapshotData) {
- defaultSnapshot = data.liveSnapshotData;
- defaultState = "live";
- } else if (data.finalSnapshotData) {
- defaultSnapshot = data.finalSnapshotData;
- defaultState = "final";
- } else {
- defaultSnapshot = null;
- defaultState = "live"; // Wait for live
- }
- }
-
- if (defaultSnapshot) {
- try {
- loadSnapshot(newStore, defaultSnapshot as any);
- } catch (e) {
- console.error("Failed to load snapshot", e);
- }
- }
- setStore(newStore);
- setActiveState(defaultState);
- }
- setLoading(false);
- };
-
- if (role !== undefined) {
- fetchWhiteboard();
- }
- }, [lessonId, role]);
-
- const handleSave = async (type: SnapshotType) => {
- if (!lessonId || !editorRef.current || !isTeacherOrAdmin) return;
-
- const snapshot = getSnapshot(editorRef.current.store);
- const { success } = await saveLessonSnapshot(lessonId, type, snapshot);
-
- if (success) {
- toast.success(`Successfully saved ${type} snapshot!`);
- setActiveState(type);
- } else {
- toast.error("Failed to save snapshot.");
- }
- };
-
- const handleLoadData = (type: SnapshotType) => {
- if (!whiteboardRecord || !editorRef.current) return;
-
- let snapshotData = null;
- if (type === "planned") snapshotData = whiteboardRecord.plannedSnapshotData;
- if (type === "live") snapshotData = whiteboardRecord.liveSnapshotData;
- if (type === "final") snapshotData = whiteboardRecord.finalSnapshotData;
-
- if (snapshotData) {
- try {
- loadSnapshot(editorRef.current.store, snapshotData as any);
- toast.success(`Loaded ${type} snapshot.`);
- setActiveState(type);
- } catch (e) {
- console.error("Failed to load snapshot", e);
- toast.error("Snapshot data is invalid or corrupted.");
- }
- } else {
- toast.info(`No data found for ${type} snapshot.`);
- }
- };
-
- return (
-
-
- {/* Header Banner */}
-
-
-
- Lesson Whiteboard {lessonId ? `#${lessonId}` : "(No Lesson Selected)"}
-
-
- Current mode: {activeState.toUpperCase()}
-
-
- {!isTeacherOrAdmin && (
-
- View Only
-
- )}
-
-
-
- {loading || role === undefined ? (
-
- Loading whiteboard...
-
- ) : !lessonId ? (
-
- Please provide a ?lessonId URL parameter.
-
- ) : store ? (
-
- {
- editorRef.current = editor;
- if (!isTeacherOrAdmin) {
- editor.updateInstanceState({ isReadonly: true });
- }
- }}
- />
-
- ) : (
-
- Failed to initialize whiteboard.
-
- )}
-
-
-
-
- {isTeacherOrAdmin && (
-
-
Save Actions
-
Save your current canvas state to the database.
-
-
-
-
-
- )}
-
-
-
Load Actions
-
Switch between different saved states for this lesson.
-
- {isTeacherOrAdmin && (
-
- )}
-
-
-
-
-
- );
-};
-
-export default WhiteboardPage;
diff --git a/src/components/BigCalender.tsx b/src/components/BigCalender.tsx
index 791bb99..96c58ca 100644
--- a/src/components/BigCalender.tsx
+++ b/src/components/BigCalender.tsx
@@ -32,7 +32,6 @@ const BigCalendar = ({
{props.event.id && (
diff --git a/src/components/DashboardShell.tsx b/src/components/DashboardShell.tsx
new file mode 100644
index 0000000..33bb5bc
--- /dev/null
+++ b/src/components/DashboardShell.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import { useState } from "react";
+import Menu from "@/components/Menu";
+import Navbar from "@/components/Navbar";
+import Image from "next/image";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+export default function DashboardShell({
+ children,
+ role,
+ userMetadata
+}: {
+ children: React.ReactNode;
+ role: string;
+ userMetadata: any;
+}) {
+ const [isCollapsed, setIsCollapsed] = useState(false);
+
+ const toggleSidebar = () => {
+ setIsCollapsed((prev) => !prev);
+ };
+
+ return (
+
+ {/* LEFT SIDEBAR */}
+
+ {/* Logo Area (Hidden when collapsed, moves to Navbar) */}
+
+
+
+ SchooLama
+
+
+
+ {/* Collapsed Logo (Shows only when collapsed) */}
+
+
+
+
+
+
+
+
+
+ {/* RIGHT MAIN CONTENT */}
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/components/Menu.tsx b/src/components/Menu.tsx
index b7c2fba..406d295 100644
--- a/src/components/Menu.tsx
+++ b/src/components/Menu.tsx
@@ -1,4 +1,3 @@
-import { currentUser } from "@clerk/nextjs/server";
import Image from "next/image";
import Link from "next/link";
@@ -117,26 +116,43 @@ const menuItems = [
},
];
-const Menu = async () => {
- const user = await currentUser();
- const role = user?.publicMetadata.role as string;
+const Menu = ({ role, isCollapsed }: { role: string; isCollapsed: boolean }) => {
return (
-
+
{menuItems.map((i) => (
-
-
+
+
{i.title}
+
{i.items.map((item) => {
if (item.visible.includes(role)) {
return (
-
-
{item.label}
+
+
+ {item.label}
+
+
+ {/* Tooltip on hover when collapsed */}
+ {isCollapsed && (
+
+ {item.label}
+
+ )}
);
}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 0a7b058..cd5260e 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -1,38 +1,73 @@
import { UserButton } from "@clerk/nextjs";
-import { currentUser } from "@clerk/nextjs/server";
import Image from "next/image";
+import Link from "next/link";
+import { Menu as MenuIcon } from "lucide-react";
-const Navbar = async () => {
- const user = await currentUser();
+const Navbar = ({
+ role,
+ userMetadata,
+ toggleSidebar,
+ isCollapsed
+}: {
+ role: string;
+ userMetadata: any;
+ toggleSidebar: () => void;
+ isCollapsed: boolean;
+}) => {
return (
-
- {/* SEARCH BAR */}
-
-
-
+
+ {/* LEFT AREA: Toggle and Animated Logo */}
+
+ {/* Hamburger Toggle */}
+
+
+ {/* Logo (Appears here when sidebar is collapsed) */}
+
+
+
+ SchooLama
+
+
- {/* ICONS AND USER */}
-
-
+
+ {/* RIGHT: ICONS AND USER */}
+
+ {/* SEARCH BAR */}
+
+
+
+
+
+
-
+
- John Doe
+
+ {userMetadata?.firstName} {userMetadata?.lastName}
+
- {user?.publicMetadata?.role as string}
+ {role}
- {/*
*/}
diff --git a/src/components/Whiteboard/WhiteboardCore.tsx b/src/components/Whiteboard/WhiteboardCore.tsx
new file mode 100644
index 0000000..e1b0dd2
--- /dev/null
+++ b/src/components/Whiteboard/WhiteboardCore.tsx
@@ -0,0 +1,257 @@
+"use client";
+
+import { useEffect, useState, useRef, createContext, useContext } from "react";
+import {
+ Tldraw,
+ createTLStore,
+ TLStore,
+ Editor,
+ getSnapshot,
+ loadSnapshot,
+ useDialogs,
+ TldrawUiButton,
+ TldrawUiButtonLabel,
+ TldrawUiButtonIcon,
+ TldrawUiDialogHeader,
+ TldrawUiDialogTitle,
+ TldrawUiDialogCloseButton,
+ TldrawUiDialogBody,
+ TldrawUiIcon
+} from "tldraw";
+import "tldraw/tldraw.css";
+import { getLessonWhiteboard, saveLessonSnapshot, SnapshotType } from "@/lib/whiteboardActions";
+import { toast } from "react-toastify";
+import { useUser } from "@clerk/nextjs";
+
+type WhiteboardContextType = {
+ activeState: SnapshotType;
+ isTeacherOrAdmin: boolean;
+ handleSave: (type: SnapshotType) => void;
+ handleLoadData: (type: SnapshotType) => void;
+ lessonId: number | null;
+ whiteboardRecord: any;
+ isFullscreen: boolean;
+};
+
+const WhiteboardContext = createContext
(null);
+
+const useWhiteboardContext = () => {
+ const ctx = useContext(WhiteboardContext);
+ if (!ctx) throw new Error("Missing WhiteboardContext.Provider");
+ return ctx;
+};
+
+function NativeFileManagerDialog({ onClose }: { onClose: () => void }) {
+ const { activeState, isTeacherOrAdmin, handleSave, handleLoadData, whiteboardRecord } = useWhiteboardContext();
+
+ const renderFileRow = (type: SnapshotType, title: string) => {
+ const hasData = whiteboardRecord && whiteboardRecord[`${type}SnapshotData`];
+ const isActive = activeState === type;
+
+ return (
+
+
+
+ {title}
+ {isActive && ACTIVE}
+
+
+ {hasData ? "Snapshot available" : "No snapshot saved"}
+
+
+
+ {isTeacherOrAdmin && (
+ { handleSave(type); }}>
+ Save Here
+
+ )}
+ { handleLoadData(type); onClose(); }}>
+ Load
+
+
+
+ );
+ };
+
+ return (
+ <>
+
+ File Manager
+
+
+
+
+ Manage your lesson whiteboard snapshots below.
+ {!isTeacherOrAdmin && " As a student, you can only load available snapshots."}
+
+ {renderFileRow("planned", "Planned")}
+ {renderFileRow("live", "Live")}
+ {renderFileRow("final", "Final")}
+
+ >
+ )
+}
+
+const CustomSharePanel = () => {
+ const { addDialog } = useDialogs();
+ const { isFullscreen, lessonId } = useWhiteboardContext();
+
+ return (
+
+ {!isFullscreen && lessonId && (
+ {
+ window.open(`/board?lessonId=${lessonId}`, '_blank');
+ }}
+ >
+
+
+ )}
+ addDialog({ component: NativeFileManagerDialog })}
+ >
+
+
+
+ );
+};
+
+const WhiteboardCore = ({ lessonId, isFullscreen = false }: { lessonId: number | null, isFullscreen?: boolean }) => {
+ const { user } = useUser();
+ const role = user?.publicMetadata?.role as string | undefined;
+ const isTeacherOrAdmin = role === "teacher" || role === "admin";
+
+ const [store, setStore] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [activeState, setActiveState] = useState("planned");
+ const [whiteboardRecord, setWhiteboardRecord] = useState(null);
+ const editorRef = useRef(null);
+
+ useEffect(() => {
+ const fetchWhiteboard = async () => {
+ if (!lessonId) {
+ setLoading(false);
+ return;
+ }
+
+ const { success, data } = await getLessonWhiteboard(lessonId);
+ if (success && data) {
+ setWhiteboardRecord(data);
+
+ // Initialize store based on role
+ const newStore = createTLStore();
+
+ let defaultSnapshot = data.plannedSnapshotData;
+ let defaultState: SnapshotType = "planned";
+
+ // For students, default to live, or final if live doesn't exist. If neither, show empty.
+ if (!isTeacherOrAdmin) {
+ if (data.liveSnapshotData) {
+ defaultSnapshot = data.liveSnapshotData;
+ defaultState = "live";
+ } else if (data.finalSnapshotData) {
+ defaultSnapshot = data.finalSnapshotData;
+ defaultState = "final";
+ } else {
+ defaultSnapshot = null;
+ defaultState = "live"; // Wait for live
+ }
+ }
+
+ if (defaultSnapshot) {
+ try {
+ loadSnapshot(newStore, defaultSnapshot as any);
+ } catch (e) {
+ console.error("Failed to load snapshot", e);
+ }
+ }
+ setStore(newStore);
+ setActiveState(defaultState);
+ }
+ setLoading(false);
+ };
+
+ if (role !== undefined) {
+ fetchWhiteboard();
+ }
+ }, [lessonId, role]);
+
+ const handleSave = async (type: SnapshotType) => {
+ if (!lessonId || !editorRef.current || !isTeacherOrAdmin) return;
+
+ const snapshot = getSnapshot(editorRef.current.store);
+ const { success } = await saveLessonSnapshot(lessonId, type, snapshot);
+
+ if (success) {
+ toast.success(`Successfully saved ${type} snapshot!`);
+ setActiveState(type);
+ setWhiteboardRecord((prev: any) => ({
+ ...prev,
+ [`${type}SnapshotData`]: snapshot
+ }));
+ } else {
+ toast.error("Failed to save snapshot.");
+ }
+ };
+
+ const handleLoadData = (type: SnapshotType) => {
+ if (!whiteboardRecord || !editorRef.current) return;
+
+ let snapshotData = null;
+ if (type === "planned") snapshotData = whiteboardRecord.plannedSnapshotData;
+ if (type === "live") snapshotData = whiteboardRecord.liveSnapshotData;
+ if (type === "final") snapshotData = whiteboardRecord.finalSnapshotData;
+
+ if (snapshotData) {
+ try {
+ loadSnapshot(editorRef.current.store, snapshotData as any);
+ toast.success(`Loaded ${type} snapshot.`);
+ setActiveState(type);
+ } catch (e) {
+ console.error("Failed to load snapshot", e);
+ toast.error("Snapshot data is invalid or corrupted.");
+ }
+ } else {
+ toast.info(`No data found for ${type} snapshot.`);
+ }
+ };
+
+ return (
+
+ {loading || role === undefined ? (
+
+ Loading whiteboard...
+
+ ) : !lessonId ? (
+
+ Please provide a ?lessonId URL parameter.
+
+ ) : store ? (
+
+
+ {
+ editorRef.current = editor;
+ if (!isTeacherOrAdmin) {
+ editor.updateInstanceState({ isReadonly: true });
+ }
+ }}
+ />
+
+
+ ) : (
+
+ Failed to initialize whiteboard.
+
+ )}
+
+ );
+};
+
+export default WhiteboardCore;