diff --git a/app/(dashboard)/_components/board-card/footer.tsx b/app/(dashboard)/_components/board-card/footer.tsx index 1583973..dc3c176 100644 --- a/app/(dashboard)/_components/board-card/footer.tsx +++ b/app/(dashboard)/_components/board-card/footer.tsx @@ -26,9 +26,9 @@ export const Footer = ({ onClick(); }; return ( -
-

{title}

-

+

+

{title}

+

{authorLabel}, {createdAtLabel}

diff --git a/app/(dashboard)/_components/board-list.tsx b/app/(dashboard)/_components/board-list.tsx index 9e4f6a8..ab25f0f 100644 --- a/app/(dashboard)/_components/board-list.tsx +++ b/app/(dashboard)/_components/board-list.tsx @@ -1,7 +1,7 @@ "use client"; import { useSearchParams } from "next/navigation"; -import { useMemo, useEffect } from "react"; +import { useMemo, useEffect, useState } from "react"; import { EmptySearch } from "./empty-search"; import { EmptyFavorites } from "./empty-favorites"; import { EmptyBoards } from "./empty-boards"; @@ -9,6 +9,7 @@ import { useQuery } from "convex/react"; import { api } from "@/convex/_generated/api"; import { BoardCard } from "./board-card"; import { NewBoardButton } from "./new-board-button"; +import { useOrganization } from "@clerk/nextjs"; interface BoardListProps { orgId: string; @@ -16,6 +17,8 @@ interface BoardListProps { export const BoardList = ({ orgId }: BoardListProps) => { const searchParams = useSearchParams(); + const { organization } = useOrganization(); + const [isOrgSwitching, setIsOrgSwitching] = useState(false); // Inject keyframes dynamically since no global CSS is used useEffect(() => { @@ -38,6 +41,17 @@ export const BoardList = ({ orgId }: BoardListProps) => { }; }, []); + // Handle organization switching + useEffect(() => { + if (organization?.id !== orgId) { + setIsOrgSwitching(true); + const timer = setTimeout(() => { + setIsOrgSwitching(false); + }, 1000); + return () => clearTimeout(timer); + } + }, [organization?.id, orgId]); + const search = useMemo( () => searchParams.get("search") || undefined, [searchParams] @@ -49,7 +63,11 @@ export const BoardList = ({ orgId }: BoardListProps) => { const data = useQuery(api.boards.get, { orgId, search, favorites }); - if (data === undefined) { + // Debug logging + // console.log("BoardList - Search:", search, "Favorites:", favorites, "Data:", data); + + // Show loading state when organization is switching + if (isOrgSwitching || data === undefined) { return (
{/* Beautiful Background */} @@ -74,8 +92,11 @@ export const BoardList = ({ orgId }: BoardListProps) => {
-

+

{favorites ? "Favorite Boards" : "Team Boards"} + {isOrgSwitching && ( +
+ )}

@@ -102,7 +123,7 @@ export const BoardList = ({ orgId }: BoardListProps) => { } return ( -
+
@@ -124,12 +145,38 @@ export const BoardList = ({ orgId }: BoardListProps) => { />
-
-

- {favorites ? "Favorite Boards" : "Team Boards"} -

-
- +
+ {/* Header with search context */} +
+

+ {search ? `Search Results` : favorites ? "Favorite Boards" : "Team Boards"} +

+ + {/* Search query display */} + {search && ( +

+ Found {data.length} {data.length === 1 ? 'board' : 'boards'} matching “{search}” +

+ )} + + {/* Mobile-friendly board count for non-search views */} + {!search && ( +

+ {data.length} {data.length === 1 ? 'board' : 'boards'} +

+ )} +
+ + {/* Responsive Grid */} +
+ {/* Only show NewBoardButton when not searching */} + {!search && } {data.map((board) => ( { - const router = useRouter(); const { organization } = useOrganization(); const { mutate, pending } = useApiMutation(api.board.create); @@ -24,21 +22,22 @@ export const EmptyBoards = () => { }) .then((id) => { toast.success("Board Created"); - router.push(`/board/${id}`); + // Use window.location.href to ensure a full page load with proper auth sync + window.location.href = `/board/${id}`; }) .catch(() => toast.error("Failed to create board")); }; return ( -
+
Empty -

Create your first board!

-

- Start by creating a board for your organization. +

Create your first board!

+

+ Start by creating a board for your organization to begin collaborating.

-
diff --git a/app/(dashboard)/_components/empty-favorites.tsx b/app/(dashboard)/_components/empty-favorites.tsx index 4522765..1154450 100644 --- a/app/(dashboard)/_components/empty-favorites.tsx +++ b/app/(dashboard)/_components/empty-favorites.tsx @@ -1,13 +1,48 @@ import Image from "next/image"; +import { Heart, Star } from "lucide-react"; export const EmptyFavorites = () => { + const handleViewAllBoards = () => { + window.location.href = "/"; + }; + return ( -
- Empty -

No favorite Boards !

-

- Try favoriting a board . +

+
+ No favorite boards +
+ +
+
+ +

+ No favorite boards yet! +

+ +

+ Mark boards as favorites by clicking the star icon on any board. + Your favorite boards will appear here for quick access.

+ + + +
+

How to favorite a board:

+
+ + + Click the star icon + + On any board card + Quick access +
+
); }; diff --git a/app/(dashboard)/_components/empty-search.tsx b/app/(dashboard)/_components/empty-search.tsx index 5c14baa..47ae1ea 100644 --- a/app/(dashboard)/_components/empty-search.tsx +++ b/app/(dashboard)/_components/empty-search.tsx @@ -1,13 +1,57 @@ import Image from "next/image"; +import { useSearchParams } from "next/navigation"; +import { Search, RefreshCw } from "lucide-react"; export const EmptySearch = () => { + const searchParams = useSearchParams(); + const search = searchParams.get("search") || ""; + + const handleClearSearch = () => { + window.location.href = "/"; + }; + return ( -
- Empty -

No results Found !

-

- Try Searching for something else . +

+
+ No search results +
+ +
+
+ +

+ No results found +

+ +

+ {search ? ( + <> + No boards found matching “{search}”. + Try a different search term or check your spelling. + + ) : ( + "Try searching for something else or browse all boards." + )}

+ +
+ +
+ +
+

Search tips:

+
+ Try board names + Check spelling + Use fewer words +
+
); }; diff --git a/app/(dashboard)/_components/navbar.tsx b/app/(dashboard)/_components/navbar.tsx index bb35eb7..f3e6efb 100644 --- a/app/(dashboard)/_components/navbar.tsx +++ b/app/(dashboard)/_components/navbar.tsx @@ -6,61 +6,88 @@ import { useOrganization, } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { SearchInput } from "./search-input"; import { InviteButton } from "./invite-button"; export const Navbar = () => { const { organization } = useOrganization(); const { user } = useUser(); + const [previousOrgId, setPreviousOrgId] = useState(null); + + // Handle organization changes - refresh when org switches + useEffect(() => { + if (organization?.id && previousOrgId && organization.id !== previousOrgId) { + // Organization changed, refresh the page + // console.log("Organization changed from", previousOrgId, "to", organization.id); + // Clear any search parameters when switching organizations + const currentUrl = new URL(window.location.href); + currentUrl.search = ''; + window.location.href = currentUrl.toString(); + } + if (organization?.id) { + setPreviousOrgId(organization.id); + } + }, [organization?.id, previousOrgId]); // Log user information for debugging useEffect(() => { - console.log("Current User:", user); - }, [user]); + // console.log("Current User:", user); + // console.log("Current Organization:", organization); + }, [user, organization]); return ( -
- {/* Search Input for large screens */} -
- -
+ <> +
+ {/* Search Input - Takes available space but has max width */} +
+ +
- {/* Organization Switcher for small screens */} -
- + -
+ }} + /> +
+ + {/* Spacer to push buttons to the right on desktop */} +
- {/* Invite Button if an organization is selected */} - {organization ? ( - - ) : ( -

No organization selected

- )} + {/* Invite Button if an organization is selected */} + {organization && ( +
+ +
+ )} + + {/* User Button */} +
+ +
+
- {/* User Button */} - -
+ ); }; diff --git a/app/(dashboard)/_components/new-board-button.tsx b/app/(dashboard)/_components/new-board-button.tsx index 7d6dede..a1aee9e 100644 --- a/app/(dashboard)/_components/new-board-button.tsx +++ b/app/(dashboard)/_components/new-board-button.tsx @@ -5,28 +5,34 @@ import { useApiMutation } from "@/hooks/use-api-mutation"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { Plus } from "lucide-react"; -import { useRouter } from "next/navigation"; interface NewBoardButtonProps { orgId: string; disabled?: boolean; } export const NewBoardButton = ({ orgId, disabled }: NewBoardButtonProps) => { - const router = useRouter(); const { mutate, pending } = useApiMutation(api.board.create); const onClick = () => { + toast.loading("Creating board...", { id: "create-board" }); + mutate({ orgId, title: "Untitled", }) .then((id) => { - toast.success("Board created successfully"); - - router.push(`/board/${id}`); + // console.log("Board created with ID:", id); + toast.success("Board created successfully", { id: "create-board" }); + + if (id) { + // Use window.location.href to ensure a full page load with proper auth sync + window.location.href = `/board/${id}`; + } else { + toast.error("Failed to get board ID", { id: "create-board" }); + } }) - .catch((err) => { - console.error("Failed to create board", err); - toast.error("Failed to create board"); + .catch(() => { + // console.error("Failed to create board", err); + toast.error("Failed to create board", { id: "create-board" }); }); }; return ( @@ -34,14 +40,14 @@ export const NewBoardButton = ({ orgId, disabled }: NewBoardButtonProps) => { disabled={pending || disabled} onClick={onClick} className={cn( - "col-span-1 flex aspect-[100/127] flex-col items-center justify-center rounded-lg bg-blue-600 py-6 hover:bg-blue-800", + "col-span-1 flex aspect-[100/127] flex-col items-center justify-center rounded-lg bg-blue-600 py-4 md:py-6 hover:bg-blue-800 transition-colors touch-manipulation", (pending || disabled) && "cursor-not-allowed opacity-75 hover:bg-blue-600" )} >
- -

New Board

+ +

New Board

); }; diff --git a/app/(dashboard)/_components/org-sidebar.tsx b/app/(dashboard)/_components/org-sidebar.tsx index 683d73c..663333c 100644 --- a/app/(dashboard)/_components/org-sidebar.tsx +++ b/app/(dashboard)/_components/org-sidebar.tsx @@ -6,7 +6,7 @@ import { Poppins } from "next/font/google"; import { cn } from "@/lib/utils"; import { OrganizationSwitcher } from "@clerk/nextjs"; import { Button } from "@/components/ui/button"; -import { LayoutDashboard, Star, Sparkles, Grid3X3 } from "lucide-react"; +import { LayoutDashboard, Star, Sparkles } from "lucide-react"; import { useSearchParams } from "next/navigation"; const font = Poppins({ diff --git a/app/(dashboard)/_components/search-input.tsx b/app/(dashboard)/_components/search-input.tsx index ad9e717..b4f1416 100644 --- a/app/(dashboard)/_components/search-input.tsx +++ b/app/(dashboard)/_components/search-input.tsx @@ -1,6 +1,6 @@ "use client"; -import { Search } from "lucide-react"; +import { Search, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { Input } from "@/components/ui/input"; import { ChangeEvent, useEffect, useState } from "react"; @@ -25,12 +25,35 @@ function useDebounce(value: string, delay: number): string { export const SearchInput = () => { const router = useRouter(); const [value, setValue] = useState(""); + const [isExpanded, setIsExpanded] = useState(false); + const [isMobile, setIsMobile] = useState(false); const debouncedValue = useDebounce(value, 500); + useEffect(() => { + const checkDevice = () => { + setIsMobile(window.innerWidth < 1024); // lg breakpoint + }; + + checkDevice(); + window.addEventListener("resize", checkDevice); + return () => window.removeEventListener("resize", checkDevice); + }, []); + const handleChange = (e: ChangeEvent) => { setValue(e.target.value); }; + const handleSearchClick = () => { + if (isMobile) { + setIsExpanded(true); + } + }; + + const handleClose = () => { + setIsExpanded(false); + setValue(""); + }; + useEffect(() => { const url = queryString.stringifyUrl( { @@ -45,15 +68,86 @@ export const SearchInput = () => { router.push(url); }, [debouncedValue, router]); + // Mobile expanded search in navbar + if (isMobile && isExpanded) { + return ( +
+
+
+ + +
+ +
+ {value && ( +
+
+ +

+ Searching for “{value}” +

+
+

+ Close search to see results below +

+
+ )} +
+ ); + } + + // Desktop version or mobile collapsed return (
- - + {isMobile ? ( + // Mobile search button + + ) : ( + // Desktop search input +
+ + + {value && ( +
+ + {value.length > 0 ? 'Searching...' : ''} + + +
+ )} +
+ )}
); }; diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 4e2a1db..10a7e3a 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -37,41 +37,50 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => { return (
- -
-
- + {/* Mobile/Tablet Sidebar - Hidden on mobile, overlay on tablet */} +
+ +
+ + {/* Main Content Area */} +
+
+ {/* Organization Sidebar - Hidden on mobile */} +
+ +
+
{/* Beautiful Background */}
{/* Subtle Gradient Overlay */}
- {/* Dotted Pattern */} + {/* Dotted Pattern - Smaller on mobile */}
- {/* Animated Blurred Orbs */} + {/* Animated Blurred Orbs - Smaller on mobile */}
-
-
+
+
{children}
diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index 768e925..0be87c2 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -3,14 +3,20 @@ import { useOrganization } from "@clerk/nextjs"; import { EmptyOrg } from "./_components/empty-org"; import { BoardList } from "./_components/board-list"; +import { MobileOptimization } from "@/components/mobile-optimization"; const DashboardPage = () => { const { organization } = useOrganization(); return ( -
- {!organization ? : } -
+ <> + +
+
+ {!organization ? : } +
+
+ ); }; diff --git a/app/api/liveblocks-auth/route.ts b/app/api/liveblocks-auth/route.ts index a7f2010..fd00631 100644 --- a/app/api/liveblocks-auth/route.ts +++ b/app/api/liveblocks-auth/route.ts @@ -12,10 +12,10 @@ const liveBlocks = new Liveblocks({ export async function POST(request: Request) { const authorization = await auth(); const user = await currentUser(); - console.log("AUTH_INFO", { - authorization, - user, - }); + // console.log("AUTH_INFO", { + // authorization, + // user, + // }); if (!authorization || !user) { return new Response("Unauthorized", { status: 403 }); } @@ -23,21 +23,21 @@ export async function POST(request: Request) { const { room } = await request.json(); const board = await convex.query(api.board.get, { id: room }); - console.log("AUTH_INFO", { - room, - board, - boardOrgId: board?.orgId, - userOrgId: authorization.orgId, - }); + // console.log("AUTH_INFO", { + // room, + // board, + // boardOrgId: board?.orgId, + // userOrgId: authorization.orgId, + // }); if (board?.orgId !== authorization.orgId) { return new Response("Unauthorized", { status: 403 }); } - const userInfo = { - name: user.firstName || "Teammate", - picture: user.imageUrl, - }; - console.log({ userInfo }); + // const userInfo = { + // name: user.firstName || "Teammate", + // picture: user.imageUrl, + // }; + // console.log({ userInfo }); const session = liveBlocks.prepareSession(user.id, { userInfo: { @@ -51,6 +51,6 @@ export async function POST(request: Request) { } const { status, body } = await session.authorize(); - console.log({ status, body }, "Allowed"); + // console.log({ status, body }, "Allowed"); return new Response(body, { status }); } diff --git a/app/board/[boardid]/_components/tool-button.tsx b/app/board/[boardid]/_components/tool-button.tsx index aacd471..091093e 100644 --- a/app/board/[boardid]/_components/tool-button.tsx +++ b/app/board/[boardid]/_components/tool-button.tsx @@ -21,7 +21,7 @@ export const ToolButton = ({ isDisabled, }: ToolButtonProps) => { const handleClick = () => { - console.log("ToolButton clicked:", label); + // console.log("ToolButton clicked:", label); onClick(); }; return ( diff --git a/app/board/[boardid]/page.tsx b/app/board/[boardid]/page.tsx index 7ed092f..fed5c13 100644 --- a/app/board/[boardid]/page.tsx +++ b/app/board/[boardid]/page.tsx @@ -7,6 +7,7 @@ import { Id } from "@/convex/_generated/dataModel"; import { Canvas } from "./_components/canvas"; import { Room } from "@/app/room"; import { Loading } from "./_components/loading"; +import { MobileOptimization } from "@/components/mobile-optimization"; interface BoardIdPageProps { params: Promise<{ @@ -33,9 +34,12 @@ const BoardIdPage = ({ params }: BoardIdPageProps) => { } return ( - }> - - + <> + + }> + + + ); }; diff --git a/app/globals.css b/app/globals.css index 26d9832..8cab784 100644 --- a/app/globals.css +++ b/app/globals.css @@ -11,6 +11,78 @@ body { font-family: Arial, Helvetica, sans-serif; } +/* Mobile-specific utilities */ +@media (max-width: 768px) { + .touch-manipulation { + touch-action: manipulation; + } + + /* Larger touch targets on mobile */ + .mobile-touch-target { + min-height: 44px; + min-width: 44px; + } + + /* Prevent zoom on input focus */ + input, select, textarea { + font-size: 16px !important; + } + + /* Smooth scrolling on mobile */ + * { + -webkit-overflow-scrolling: touch; + } + + /* Better scrollbar styling for mobile */ + ::-webkit-scrollbar { + width: 3px; + } + + ::-webkit-scrollbar-track { + background: transparent; + } + + ::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; + } + + ::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.3); + } + + /* Ensure proper viewport height on mobile */ + .mobile-full-height { + height: 100vh; + height: 100dvh; /* Dynamic viewport height for mobile browsers */ + } + + /* Adjust spacing for mobile optimization banner */ + .mobile-content-with-banner { + padding-top: 60px; /* Space for collapsed banner */ + } + + .mobile-content-with-expanded-banner { + padding-top: 140px; /* Space for expanded banner */ + } + + /* Adjust search overlay positioning */ + .mobile-search-overlay { + top: 60px; /* Below the mobile banner */ + } + + .mobile-search-overlay-with-expanded-banner { + top: 140px; /* Below the expanded mobile banner */ + } +} + +/* Extra small screens */ +@media (min-width: 480px) { + .xs\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + @layer base { :root { --background: 0 0% 100%; diff --git a/components/actions.tsx b/components/actions.tsx index 2276bf6..e375128 100644 --- a/components/actions.tsx +++ b/components/actions.tsx @@ -31,6 +31,7 @@ export const Actions = ({ }: ActionProps) => { const { onOpen } = useRenameModal(); const { mutate, pending } = useApiMutation(api.board.remove); + const onCopyLink = () => { navigator.clipboard .writeText(`${window.location.origin}/board/${id}`) @@ -39,9 +40,22 @@ export const Actions = ({ }; const onDelete = () => { + toast.loading("Deleting board...", { id: "delete-board" }); + mutate({ id }) - .then(() => toast.success("Board Deleted")) - .catch(() => toast.error("Failed to delete board")); + .then(() => { + toast.success("Board Deleted", { id: "delete-board" }); + // Use window.location.href to ensure a full page load + window.location.href = "/"; + }) + .catch((error) => { + // console.error("Delete error:", error); + if (error.message?.includes("Unauthorized") || error.message?.includes("authentication")) { + toast.error("Authentication issue. Please try again.", { id: "delete-board" }); + } else { + toast.error("Failed to delete board", { id: "delete-board" }); + } + }); }; return ( @@ -76,6 +90,7 @@ export const Actions = ({ + +
+
+
+ ); + } + + // Expanded banner + return ( +
+
+
+
+ {isMobile ? ( + + ) : ( + + )} +
+ +
+

+ {isMobile ? "Mobile" : "Tablet"} Experience Notice +

+

+ Flowboard is optimized for desktop use. While you can browse boards here, + the full editing experience works best on larger screens. +

+ +
+ + Best on desktop • 1024px+ screen • Mouse/trackpad recommended +
+
+ +
+ + +
+
+
+
+ ); +}; diff --git a/components/modals/rename-modal.tsx b/components/modals/rename-modal.tsx index f5892e6..2b0631b 100644 --- a/components/modals/rename-modal.tsx +++ b/components/modals/rename-modal.tsx @@ -33,8 +33,19 @@ export const RenameModal = () => { .then(() => { toast.success("Board Renamed"); onClose(); + // Reload the page to ensure UI updates + setTimeout(() => { + window.location.reload(); + }, 100); }) - .catch(() => toast.error("Failed to rename board")); + .catch((error) => { + // console.error("Rename error:", error); + if (error.message?.includes("Unauthorized")) { + toast.error("Unable to rename board. Please try again in a moment."); + } else { + toast.error("Failed to rename board"); + } + }); }; return ( diff --git a/convex/board.ts b/convex/board.ts index c68ed36..4b69067 100644 --- a/convex/board.ts +++ b/convex/board.ts @@ -53,6 +53,13 @@ export const update = mutation({ throw new Error("Unauthorized"); } + // Get the board first to check permissions + const existingBoard = await ctx.db.get(args.id); + + if (!existingBoard) { + throw new Error("Board not found"); + } + const title = args.title.trim(); if (!title) { @@ -158,7 +165,16 @@ export const remove = mutation({ if (!identity) { throw new Error("Unauthorized"); } + + // Check if the board exists first + const existingBoard = await ctx.db.get(args.id); + if (!existingBoard) { + throw new Error("Board not found"); + } + const userId = identity.subject; + + // Remove from favorites if it exists const existingFavorite = await ctx.db .query("userFavorites") .withIndex("by_user_board", (q) => @@ -170,7 +186,10 @@ export const remove = mutation({ await ctx.db.delete(existingFavorite._id); } + // Delete the board await ctx.db.delete(args.id); + + return { success: true }; }, }); diff --git a/hooks/use-api-mutation.ts b/hooks/use-api-mutation.ts index ff32b0e..8d2ea22 100644 --- a/hooks/use-api-mutation.ts +++ b/hooks/use-api-mutation.ts @@ -4,12 +4,21 @@ import { useMutation } from "convex/react"; export const useApiMutation = (mutationFunction: any) => { const [pending, setPending] = useState(false); const apiMutation = useMutation(mutationFunction); - const mutate = async (payload: any) => { + + const mutate = async (payload: any): Promise => { setPending(true); + try { - await apiMutation(payload); - } catch (err) { + const result = await apiMutation(payload); + return result; + } catch (err: any) { console.error("Mutation error:", err); + + // Re-throw with better error message for auth issues + if (err.message?.includes("Unauthorized")) { + throw new Error("Authentication required. Please try again."); + } + throw err; } finally { setPending(false); } diff --git a/hooks/use-disable-scroll-bounce.ts b/hooks/use-disable-scroll-bounce.ts new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 57226e7..8d5744b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,11 +18,13 @@ "@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", + "canvg": "^4.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "^1.17.4", "convex-helpers": "^0.1.74", "date-fns": "^4.1.0", + "html2canvas": "^1.4.1", "lucide-react": "^0.469.0", "nanoid": "^5.1.5", "next": "15.1.2", @@ -2626,6 +2628,12 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.0.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.2.tgz", @@ -3187,6 +3195,15 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3313,6 +3330,22 @@ } ] }, + "node_modules/canvg": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-4.0.3.tgz", + "integrity": "sha512-fKzMoMBwus3CWo1Uy8XJc4tqqn98RoRrGV6CsIkaNiQT5lOeHuMh4fOt+LXLzn2Wqtr4p/c2TOLz4xtu4oBlFA==", + "license": "MIT", + "dependencies": { + "@types/raf": "^3.4.0", + "raf": "^3.4.1", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3609,6 +3642,15 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5036,6 +5078,19 @@ "node": ">= 0.4" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -6532,6 +6587,12 @@ "integrity": "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==", "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6894,6 +6955,15 @@ } ] }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -7149,6 +7219,15 @@ "dev": true, "license": "MIT" }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7514,6 +7593,15 @@ "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", "dev": true }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/std-env": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", @@ -7832,6 +7920,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/swr": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz", @@ -7932,6 +8029,15 @@ "node": ">=6" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -8214,6 +8320,15 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 5fb2252..2e79a31 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,13 @@ "@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", + "canvg": "^4.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "^1.17.4", "convex-helpers": "^0.1.74", "date-fns": "^4.1.0", + "html2canvas": "^1.4.1", "lucide-react": "^0.469.0", "nanoid": "^5.1.5", "next": "15.1.2", diff --git a/providers/convex-client-provider.tsx b/providers/convex-client-provider.tsx index 1508b91..a5666e2 100644 --- a/providers/convex-client-provider.tsx +++ b/providers/convex-client-provider.tsx @@ -18,12 +18,15 @@ export const ConvexClientProvider = ({ return ( - {children} + + {children} + );