diff --git a/webapp/app.ironcalc.com/frontend/package-lock.json b/webapp/app.ironcalc.com/frontend/package-lock.json index ebeb880..f97fc49 100644 --- a/webapp/app.ironcalc.com/frontend/package-lock.json +++ b/webapp/app.ironcalc.com/frontend/package-lock.json @@ -13,6 +13,7 @@ "@ironcalc/workbook": "file:../../IronCalc/", "@mui/material": "^6.4", "lucide-react": "^0.473.0", + "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -2487,6 +2488,14 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", diff --git a/webapp/app.ironcalc.com/frontend/package.json b/webapp/app.ironcalc.com/frontend/package.json index 19a2f96..d13e54c 100644 --- a/webapp/app.ironcalc.com/frontend/package.json +++ b/webapp/app.ironcalc.com/frontend/package.json @@ -16,6 +16,7 @@ "@ironcalc/workbook": "file:../../IronCalc/", "@mui/material": "^6.4", "lucide-react": "^0.473.0", + "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/webapp/app.ironcalc.com/frontend/src/components/FileBar.tsx b/webapp/app.ironcalc.com/frontend/src/components/FileBar.tsx index b929f20..6a296c3 100644 --- a/webapp/app.ironcalc.com/frontend/src/components/FileBar.tsx +++ b/webapp/app.ironcalc.com/frontend/src/components/FileBar.tsx @@ -1,13 +1,12 @@ import styled from "@emotion/styled"; import type { Model } from "@ironcalc/workbook"; import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook"; -import { CircleCheck } from "lucide-react"; import { useRef, useState } from "react"; -// import { IronCalcIcon, IronCalcLogo } from "./../icons"; import { FileMenu } from "./FileMenu"; import { ShareButton } from "./ShareButton"; +import ShareWorkbookDialog from "./ShareWorkbookDialog"; import { WorkbookTitle } from "./WorkbookTitle"; -import { downloadModel, shareModel } from "./rpc"; +import { downloadModel } from "./rpc"; import { updateNameSelectedWorkbook } from "./storage"; export function FileBar(properties: { @@ -18,7 +17,8 @@ export function FileBar(properties: { onDelete: () => void; }) { const hiddenInputRef = useRef(null); - const [toast, setToast] = useState(false); + const [isDialogOpen, setIsDialogOpen] = useState(false); + return ( @@ -53,37 +53,17 @@ export function FileBar(properties: { type="text" style={{ position: "absolute", left: -9999, top: -9999 }} /> -
- {toast ? ( - - - - URL copied to clipboard - - - ) : ( - "" +
+ + setIsDialogOpen(true)} /> + {isDialogOpen && ( + setIsDialogOpen(false)} + onModelUpload={properties.onModelUpload} + model={properties.model} + /> )} -
- { - const model = properties.model; - const bytes = model.toBytes(); - const fileName = model.getName(); - const hash = await shareModel(bytes, fileName); - const value = `${location.origin}/?model=${hash}`; - if (hiddenInputRef.current) { - hiddenInputRef.current.value = value; - hiddenInputRef.current.select(); - document.execCommand("copy"); - setToast(true); - setTimeout(() => setToast(false), 5000); - } - console.log(value); - }} - /> + ); } @@ -117,14 +97,6 @@ const HelpButton = styled("div")` } `; -const Toast = styled("div")` - font-weight: 400; - font-size: 12px; - color: #9e9e9e; - display: flex; - align-items: center; -`; - const Divider = styled("div")` margin: 0px 8px 0px 16px; height: 12px; @@ -141,3 +113,17 @@ const FileBarWrapper = styled("div")` position: relative; justify-content: space-between; `; + +const DialogContainer = styled("div")` + position: relative; + display: inline-block; + button { + margin-bottom: 8px; + } + .MuiDialog-root { + position: absolute; + top: 100%; + left: 0; + transform: translateY(8px); + } +`; diff --git a/webapp/app.ironcalc.com/frontend/src/components/ShareWorkbookDialog.tsx b/webapp/app.ironcalc.com/frontend/src/components/ShareWorkbookDialog.tsx new file mode 100644 index 0000000..bb7d950 --- /dev/null +++ b/webapp/app.ironcalc.com/frontend/src/components/ShareWorkbookDialog.tsx @@ -0,0 +1,202 @@ +import type { Model } from "@ironcalc/workbook"; +import { Button, Dialog, TextField, styled } from "@mui/material"; +import { Check, Copy, GlobeLock } from "lucide-react"; +import { QRCodeSVG } from "qrcode.react"; +import { useEffect, useState } from "react"; +import { shareModel } from "./rpc"; + +function ShareWorkbookDialog(properties: { + onClose: () => void; + onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise; + model?: Model; +}) { + const [url, setUrl] = useState(""); + const [copied, setCopied] = useState(false); + + useEffect(() => { + const generateUrl = async () => { + if (properties.model) { + const bytes = properties.model.toBytes(); + const fileName = properties.model.getName(); + const hash = await shareModel(bytes, fileName); + setUrl(`${location.origin}/?model=${hash}`); + } + }; + generateUrl(); + }, [properties.model]); + + useEffect(() => { + let timeoutId: ReturnType; + if (copied) { + timeoutId = setTimeout(() => { + setCopied(false); + }, 2000); + } + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [copied]); + + const handleClose = () => { + properties.onClose(); + }; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(url); + setCopied(true); + } catch (err) { + console.error("Failed to copy text: ", err); + } + }; + + return ( + { + if (event.code === "Escape") { + handleClose(); + } + }} + > + + + {" "} + + + + + {copied ? : } + {copied ? "Copied!" : "Copy URL"} + + + + + + + Anyone with the link will be able to access a copy of this workbook + + + ); +} + +const DialogWrapper = styled(Dialog)` + .MuiDialog-paper { + width: 440px; + position: absolute; + top: 44px; + right: 0px; + margin: 10px; + max-width: calc(100% - 20px); + } + .MuiBackdrop-root { + background-color: transparent; + } +`; + +const DialogContent = styled("div")` + padding: 20px; + display: flex; + flex-direction: row; + gap: 12px; + height: 80px; +`; + +const URLWrapper = styled("div")` + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + justify-content: space-between; +`; + +const StyledTextField = styled(TextField)` + margin: 0px; + .MuiInputBase-root { + max-height: 36px; + font-size: 14px; + padding-top: 0px; + } + .MuiOutlinedInput-input { + text-overflow: ellipsis; + padding: 8px; + } +`; + +const StyledButton = styled(Button)` + display: flex; + flex-direction: row; + gap: 4px; + background-color: #eeeeee; + height: 36px; + color: #616161; + box-shadow: none; + font-size: 14px; + text-transform: capitalize; + gap: 10px; + &:hover { + background-color: #e0e0e0; + box-shadow: none; + } + &:active { + background-color: #d4d4d4; + box-shadow: none; + } +`; + +const StyledCopy = styled(Copy)` + width: 16px; +`; + +const StyledCheck = styled(Check)` + width: 16px; +`; + +const QRCodeWrapper = styled("div")` + min-height: 80px; + min-width: 80px; + background-color: grey; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + + @media (max-width: 600px) { + display: none; + } +`; + +const UploadFooter = styled("div")` + height: 44px; + border-top: 1px solid #e0e0e0; + font-size: 12px; + font-weight: 400; + color: #757575; + display: flex; + align-items: center; + font-family: Inter; + gap: 8px; + padding: 0px 12px; + svg { + max-width: 16px; + } +`; + +export default ShareWorkbookDialog;