Loading .gitlab-ci.yml +2 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ eslint: image: node:24-alpine before_script: - corepack enable yarn && corepack prepare --activate - apk add --update --no-cache make g++ jpeg-dev cairo-dev giflib-dev pango-dev libtool autoconf automake script: - yarn workspaces focus --all - yarn run lint:ci Loading Loading @@ -67,6 +68,7 @@ jest server: DATABASE_URL: postgres://canvas@postgres/canvas before_script: - corepack enable yarn && corepack prepare --activate - apk add --update --no-cache make g++ jpeg-dev cairo-dev giflib-dev pango-dev libtool autoconf automake script: - apk update && apk add git - yarn workspaces focus --all Loading Dockerfile +1 −0 Original line number Diff line number Diff line FROM node:24-alpine AS base # add openssl as root RUN apk add --no-cache openssl RUN apk add --update --no-cache make g++ jpeg-dev cairo-dev giflib-dev pango-dev libtool autoconf automake ARG VERSION Loading packages/client/src/components/Templating/Template.tsx +5 −0 Original line number Diff line number Diff line Loading @@ -148,6 +148,11 @@ const $Template = () => { instance.current.setOption("style", style); }, [style]); useEffect(() => { instance.current?.setOption("x", x); instance.current?.setOption("y", y); }, [x, y]); return ( <div id="template" Loading packages/client/src/components/Toolbar/CanvasMeta.tsx +13 −32 Original line number Diff line number Diff line Loading @@ -11,6 +11,8 @@ import { CanvasCore } from "../../lib/canvas"; import { useEffect, useState } from "react"; import { type ClientConfig } from "@sc07-canvas/lib/net"; import network from "../../lib/network"; import { ShareModal } from "./ShareModal"; import { useDialog } from "@/contexts/DialogContext"; const getTimeLeft = (available: number, config: ClientConfig) => { /** Loading Loading @@ -81,7 +83,7 @@ const OnlineCount = () => { export const CanvasMeta = () => { const [available, setAvailable] = useState(-1); const { canvasPosition, cursor, config } = useAppContext<true>(); const { isOpen, onOpen, onOpenChange } = useDisclosure(); const Dialog = useDialog(); useEffect(() => { const handle = ({ available }: { available: number }) => { Loading @@ -101,7 +103,15 @@ export const CanvasMeta = () => { <div id="canvas-meta" className="toolbar-box"> {canvasPosition && ( <span> <button className="btn-link" onClick={onOpen}> <button className="btn-link" onClick={() => Dialog.dispatch({ action: "OPEN_DIALOG", dialog: { id: "SHARE_MODAL" }, }) } > ({canvasPosition.x}, {canvasPosition.y}) </button> {cursor.x !== undefined && cursor.y !== undefined && ( Loading @@ -128,36 +138,7 @@ export const CanvasMeta = () => { </span> </span> </div> <ShareModal isOpen={isOpen} onOpenChange={onOpenChange} /> </> ); }; const ShareModal = ({ isOpen, onOpenChange, }: { isOpen: boolean; onOpenChange: () => void; }) => { // TODO: implement sharemodal return ( <Modal isOpen={isOpen} onOpenChange={onOpenChange}> <ModalContent> {() => ( <> <ModalHeader className="flex flex-col gap-1"> share modal </ModalHeader> <ModalBody> <p>share the current zoom level & position as a url</p> <p> params would be not a hash so the server can generate an oembed </p> </ModalBody> <ShareModal /> </> )} </ModalContent> </Modal> ); }; packages/client/src/components/Toolbar/ShareModal.tsx 0 → 100644 +151 −0 Original line number Diff line number Diff line import { useDialog } from "@/contexts/DialogContext"; import { CanvasCore } from "@/lib/canvas"; import { KeybindManager } from "@/lib/keybinds"; import { Template, type ITemplateState } from "@/lib/template"; import { Alert, Link, Modal, ModalBody, ModalContent, ModalHeader, } from "@heroui/react"; import React, { useCallback, useEffect, useRef, type MouseEvent } from "react"; import { Button } from "../core/Button"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCopy } from "@fortawesome/free-solid-svg-icons"; import { toast } from "react-toastify"; import { useTemplateContext } from "@/contexts/TemplateContext"; export const ShareModal = () => { const Dialog = useDialog(); const template = useTemplateContext(); const canvas = useRef<CanvasCore>(null); useEffect(() => { const open = () => { Dialog.dispatch({ action: "OPEN_DIALOG", dialog: { id: "SHARE_MODAL" } }); }; KeybindManager.on("SNAPSHOT", open); return () => { KeybindManager.off("SNAPSHOT", open); }; }, []); useEffect(() => { canvas.current = CanvasCore.get(false); }, [Dialog.state.dialogs]); const url = useCallback((withTemplate: boolean) => { if (canvas.current) { return String( ShareURL( canvas.current?.panZoomTransformToCanvas(), withTemplate ? (Template.getState() ?? undefined) : undefined, ), ); } return null; }, []); return ( <Modal isOpen={"SHARE_MODAL" in Dialog.state.dialogs} onOpenChange={(open) => open ? Dialog.dispatch({ action: "OPEN_DIALOG", dialog: { id: "SHARE_MODAL" }, }) : Dialog.dispatch({ action: "CLOSE_DIALOG", dialogId: "SHARE_MODAL" }) } > <ModalContent> {() => ( <> <ModalHeader className="flex flex-col gap-1">Share</ModalHeader> <ModalBody> <p>Share your view of the canvas!</p> {canvas.current ? ( <> <CopyButton url={url(false)}>Just position</CopyButton> <CopyButton isDisabled={!template.enable} url={url(true)}> With template </CopyButton> </> ) : ( <Alert color="primary"> Unable to get canvas context, try refreshing </Alert> )} </ModalBody> </> )} </ModalContent> </Modal> ); }; const CopyButton = ({ url, ...props }: React.PropsWithChildren<Parameters<typeof Button>[0]> & { url: string | null; }) => { const copy = useCallback( (e: MouseEvent) => { if (!url) return; if (e.ctrlKey || e.altKey || e.shiftKey) return; e.preventDefault(); navigator.clipboard.writeText(url); toast.success("Copied to clipboard!"); }, [url], ); return ( <Button variant="flat" onClick={copy} startContent={<FontAwesomeIcon icon={faCopy} />} as={Link} href={url ?? undefined} {...props} > {props.children} </Button> ); }; const ShareURL = ( { canvasX: x, canvasY: y, zoom, }: { canvasX: number; canvasY: number; zoom: number }, template?: ITemplateState, ): URL => { let path = `/share/${x},${y}`; if (typeof zoom === "number") { path += `/z=${zoom}`; } if (template) { const params = new URLSearchParams(); params.set("template", template.url); params.set("x", String(template.x)); params.set("y", String(template.y)); params.set("w", String(template.width)); params.set("style", String(template.style)); path += "?" + params; } return new URL(path, window.location.href); }; Loading
.gitlab-ci.yml +2 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ eslint: image: node:24-alpine before_script: - corepack enable yarn && corepack prepare --activate - apk add --update --no-cache make g++ jpeg-dev cairo-dev giflib-dev pango-dev libtool autoconf automake script: - yarn workspaces focus --all - yarn run lint:ci Loading Loading @@ -67,6 +68,7 @@ jest server: DATABASE_URL: postgres://canvas@postgres/canvas before_script: - corepack enable yarn && corepack prepare --activate - apk add --update --no-cache make g++ jpeg-dev cairo-dev giflib-dev pango-dev libtool autoconf automake script: - apk update && apk add git - yarn workspaces focus --all Loading
Dockerfile +1 −0 Original line number Diff line number Diff line FROM node:24-alpine AS base # add openssl as root RUN apk add --no-cache openssl RUN apk add --update --no-cache make g++ jpeg-dev cairo-dev giflib-dev pango-dev libtool autoconf automake ARG VERSION Loading
packages/client/src/components/Templating/Template.tsx +5 −0 Original line number Diff line number Diff line Loading @@ -148,6 +148,11 @@ const $Template = () => { instance.current.setOption("style", style); }, [style]); useEffect(() => { instance.current?.setOption("x", x); instance.current?.setOption("y", y); }, [x, y]); return ( <div id="template" Loading
packages/client/src/components/Toolbar/CanvasMeta.tsx +13 −32 Original line number Diff line number Diff line Loading @@ -11,6 +11,8 @@ import { CanvasCore } from "../../lib/canvas"; import { useEffect, useState } from "react"; import { type ClientConfig } from "@sc07-canvas/lib/net"; import network from "../../lib/network"; import { ShareModal } from "./ShareModal"; import { useDialog } from "@/contexts/DialogContext"; const getTimeLeft = (available: number, config: ClientConfig) => { /** Loading Loading @@ -81,7 +83,7 @@ const OnlineCount = () => { export const CanvasMeta = () => { const [available, setAvailable] = useState(-1); const { canvasPosition, cursor, config } = useAppContext<true>(); const { isOpen, onOpen, onOpenChange } = useDisclosure(); const Dialog = useDialog(); useEffect(() => { const handle = ({ available }: { available: number }) => { Loading @@ -101,7 +103,15 @@ export const CanvasMeta = () => { <div id="canvas-meta" className="toolbar-box"> {canvasPosition && ( <span> <button className="btn-link" onClick={onOpen}> <button className="btn-link" onClick={() => Dialog.dispatch({ action: "OPEN_DIALOG", dialog: { id: "SHARE_MODAL" }, }) } > ({canvasPosition.x}, {canvasPosition.y}) </button> {cursor.x !== undefined && cursor.y !== undefined && ( Loading @@ -128,36 +138,7 @@ export const CanvasMeta = () => { </span> </span> </div> <ShareModal isOpen={isOpen} onOpenChange={onOpenChange} /> </> ); }; const ShareModal = ({ isOpen, onOpenChange, }: { isOpen: boolean; onOpenChange: () => void; }) => { // TODO: implement sharemodal return ( <Modal isOpen={isOpen} onOpenChange={onOpenChange}> <ModalContent> {() => ( <> <ModalHeader className="flex flex-col gap-1"> share modal </ModalHeader> <ModalBody> <p>share the current zoom level & position as a url</p> <p> params would be not a hash so the server can generate an oembed </p> </ModalBody> <ShareModal /> </> )} </ModalContent> </Modal> ); };
packages/client/src/components/Toolbar/ShareModal.tsx 0 → 100644 +151 −0 Original line number Diff line number Diff line import { useDialog } from "@/contexts/DialogContext"; import { CanvasCore } from "@/lib/canvas"; import { KeybindManager } from "@/lib/keybinds"; import { Template, type ITemplateState } from "@/lib/template"; import { Alert, Link, Modal, ModalBody, ModalContent, ModalHeader, } from "@heroui/react"; import React, { useCallback, useEffect, useRef, type MouseEvent } from "react"; import { Button } from "../core/Button"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCopy } from "@fortawesome/free-solid-svg-icons"; import { toast } from "react-toastify"; import { useTemplateContext } from "@/contexts/TemplateContext"; export const ShareModal = () => { const Dialog = useDialog(); const template = useTemplateContext(); const canvas = useRef<CanvasCore>(null); useEffect(() => { const open = () => { Dialog.dispatch({ action: "OPEN_DIALOG", dialog: { id: "SHARE_MODAL" } }); }; KeybindManager.on("SNAPSHOT", open); return () => { KeybindManager.off("SNAPSHOT", open); }; }, []); useEffect(() => { canvas.current = CanvasCore.get(false); }, [Dialog.state.dialogs]); const url = useCallback((withTemplate: boolean) => { if (canvas.current) { return String( ShareURL( canvas.current?.panZoomTransformToCanvas(), withTemplate ? (Template.getState() ?? undefined) : undefined, ), ); } return null; }, []); return ( <Modal isOpen={"SHARE_MODAL" in Dialog.state.dialogs} onOpenChange={(open) => open ? Dialog.dispatch({ action: "OPEN_DIALOG", dialog: { id: "SHARE_MODAL" }, }) : Dialog.dispatch({ action: "CLOSE_DIALOG", dialogId: "SHARE_MODAL" }) } > <ModalContent> {() => ( <> <ModalHeader className="flex flex-col gap-1">Share</ModalHeader> <ModalBody> <p>Share your view of the canvas!</p> {canvas.current ? ( <> <CopyButton url={url(false)}>Just position</CopyButton> <CopyButton isDisabled={!template.enable} url={url(true)}> With template </CopyButton> </> ) : ( <Alert color="primary"> Unable to get canvas context, try refreshing </Alert> )} </ModalBody> </> )} </ModalContent> </Modal> ); }; const CopyButton = ({ url, ...props }: React.PropsWithChildren<Parameters<typeof Button>[0]> & { url: string | null; }) => { const copy = useCallback( (e: MouseEvent) => { if (!url) return; if (e.ctrlKey || e.altKey || e.shiftKey) return; e.preventDefault(); navigator.clipboard.writeText(url); toast.success("Copied to clipboard!"); }, [url], ); return ( <Button variant="flat" onClick={copy} startContent={<FontAwesomeIcon icon={faCopy} />} as={Link} href={url ?? undefined} {...props} > {props.children} </Button> ); }; const ShareURL = ( { canvasX: x, canvasY: y, zoom, }: { canvasX: number; canvasY: number; zoom: number }, template?: ITemplateState, ): URL => { let path = `/share/${x},${y}`; if (typeof zoom === "number") { path += `/z=${zoom}`; } if (template) { const params = new URLSearchParams(); params.set("template", template.url); params.set("x", String(template.x)); params.set("y", String(template.y)); params.set("w", String(template.width)); params.set("style", String(template.style)); path += "?" + params; } return new URL(path, window.location.href); };