Commit 20ef3e14 authored by Grant's avatar Grant
Browse files

feat: Share URLs

parent 82edb61a
Loading
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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
@@ -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
+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

+5 −0
Original line number Diff line number Diff line
@@ -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"
+13 −32
Original line number Diff line number Diff line
@@ -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) => {
  /**
@@ -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 }) => {
@@ -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 && (
@@ -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>
  );
};
+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