diff --git a/.clasp.json b/.clasp.json index 93e6d50..304a7d0 100644 --- a/.clasp.json +++ b/.clasp.json @@ -1,7 +1,7 @@ { + "scriptId": "1IzIEW1uGP4WWdowrlYyNEwhasdXObby_Lik-xwRXn3uWFUitm19SHAhU", "rootDir": "dist", - "scriptId": "1EBKJNiPs_XxntcBHxMKJgfBr7089AHhT3boz7_Lkc6AD0hjsQavkIjxz", "parentId": [ - "1UAVn7sP1pTutyOM2i95jxQz6sjVr2ZrxhG82uLIdd70" + "1NVNWs4Wdj2ib6hhPgq1kzA-BMtydvk_SnxoK1K2yX5o" ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 37b4a80..8f34e1f 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "google-plugin", - "version": "1.0.0", + "version": "1.1.0", "type": "module", "scripts": { "dev": "vite", "lint": "eslint .", - "login": "clasp login", - "setup": "rimraf .clasp.json && mkdirp dist && clasp create --type sheets --title \"My React Project\" --rootDir ./dist && mv ./dist/.clasp.json ./.clasp.json && rimraf dist", - "open": "clasp open --addon", - "push": "clasp push", + "login": "npx clasp login", + "setup": "rimraf .clasp.json && mkdirp dist && npx clasp create --type sheets --title \"My React Project\" --rootDir ./dist && mv ./dist/.clasp.json ./.clasp.json && rimraf dist", + "open": "npx clasp open --addon", + "push": "npx clasp push", "setup:https": "mkdirp certs && mkcert -key-file ./certs/key.pem -cert-file ./certs/cert.pem localhost 127.0.0.1", "build:dev": "tsc && vite build --mode development", "build:test": "tsc && NODE_ENV=test vite build --mode test", diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts new file mode 100644 index 0000000..693f51b --- /dev/null +++ b/src/analytics/analytics.ts @@ -0,0 +1,63 @@ +import httpClient from './httpClient'; +class Analytics { + + public sendEvent(eventName: string, eventID:string, errorMessage?: string, diagramType?:string, userLoginState: boolean = true) { + const analyticsID = getAnalyticsID(); + const pluginID= "google-docs-plugin"; + const pluginSource = 'googledocs'; + const payload = { + analyticsID, + pluginID, + eventName, + eventID, + userLoginState, + pluginSource, + errorMessage, + diagramType + }; + + httpClient.post('/rest-api/plugins/pulse', payload).catch(error => { + if (error.code !== 'ERR_NETWORK') { + console.error('Failed to send analytics event:', error); + } + }); + } + + + public trackLogin() { + this.sendEvent('Google Docs Plugin Logged In','GOOGLE_DOCS_PLUGIN_LOGIN'); + } + + public trackLogout() { + this.sendEvent('Google Docs Logged Out','GOOGLE_DOCS_PLUGIN_LOGOUT', undefined, undefined, false); + } + + public trackBrowseDiagram() { + this.sendEvent('Google Docs Browse Diagram','GOOGLE_DOCS_PLUGIN_BROWSE_DIAGRAM'); + } + + public trackNewDiagram() { + this.sendEvent('Google Docs New Diagram', 'GOOGLE_DOCS_PLUGIN_NEW_DIAGRAM'); + } + + public trackEditDiagram() { + this.sendEvent('Google Docs Edit Diagram', 'GOOGLE_DOCS_PLUGIN_EDIT_DIAGRAM'); + } + + public trackUpdateAllDiagrams() { + this.sendEvent('Google Docs Update All Diagrams', 'GOOGLE_DOCS_PLUGIN_UPDATE_ALL_DIAGRAMS'); + } +} + +function getAnalyticsID() { + const STORAGE_KEY = 'MERMAIDCHART_ANALYTICS_ID'; + + let id = localStorage.getItem(STORAGE_KEY); + if (!id) { + id = crypto.randomUUID(); + localStorage.setItem(STORAGE_KEY, id); + } + return id; +} + +export default new Analytics(); \ No newline at end of file diff --git a/src/analytics/httpClient.ts b/src/analytics/httpClient.ts new file mode 100644 index 0000000..82847d1 --- /dev/null +++ b/src/analytics/httpClient.ts @@ -0,0 +1,26 @@ +import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; +import { prodUrl } from '../config/urls'; + +const ANALYTICS_BASE_URL = prodUrl; + +const httpClient: AxiosInstance = axios.create({ + baseURL: ANALYTICS_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, + timeout: 10000, // 10 second timeout +}); + +httpClient.interceptors.response.use( + (response: AxiosResponse) => response, + (error: AxiosError) => { + if (error.code === 'ERR_NETWORK') { + console.warn('Analytics endpoint unreachable - continuing without tracking'); + return Promise.resolve({ data: null, status: 200, statusText: 'OK', headers: {}, config: error.config }); + } + console.error('HTTP Client error:', error); + return Promise.reject(error); + } +); + +export default httpClient; \ No newline at end of file diff --git a/src/client/components/button.module.css b/src/client/components/button.module.css index 3557db1..4143a3b 100644 --- a/src/client/components/button.module.css +++ b/src/client/components/button.module.css @@ -5,13 +5,14 @@ padding: 0.5rem; outline: none; border-radius: 0.375rem; - border: 1px solid rgb(207, 207, 211); - background-color: white; + border: none; + background-color: #E80962; cursor: pointer; + color: white; } .button:hover { - background-color: rgb(0, 66, 235); + background-color: #B20E45; color: white; transition: all 0.2s; } diff --git a/src/client/create-diagram-dialog/components/create-diagram-dialog.tsx b/src/client/create-diagram-dialog/components/create-diagram-dialog.tsx index 9e42d87..7a02c6e 100644 --- a/src/client/create-diagram-dialog/components/create-diagram-dialog.tsx +++ b/src/client/create-diagram-dialog/components/create-diagram-dialog.tsx @@ -1,13 +1,20 @@ import { useEffect, useState } from 'react'; -import { buildUrl, handleDialogClose } from '../../utils/helpers'; +import { + buildUrl, + handleDialogClose, + compressBase64Image, +} from '../../utils/helpers'; import { serverFunctions } from '../../utils/serverFunctions'; import useAuth from '../../hooks/useAuth'; -import { CircularProgress, Container, Typography } from '@mui/material'; +import { CircularProgress, Container, Typography, Box } from '@mui/material'; import { showAlertDialog } from '../../utils/alert'; +import LoadingOverlay from '../../components/loading-overlay'; const CreateDiagramDialog = () => { const { authState, authStatus } = useAuth(); const [diagramsUrl, setDiagramsUrl] = useState(''); + const [isInserting, setIsInserting] = useState(false); + const [iframeLoading, setIframeLoading] = useState(true); useEffect(() => { if (!authState?.authorized) return; @@ -22,6 +29,13 @@ const CreateDiagramDialog = () => { const handleMessage = async (e: MessageEvent) => { const action = e.data.action; if (action === 'save') { + if (isInserting) { + console.log('Already inserting diagram, ignoring duplicate click'); + return; + } + + setIsInserting(true); + const data = e.data.data; const metadata = new URLSearchParams({ projectID: data.projectID, @@ -31,14 +45,17 @@ const CreateDiagramDialog = () => { }); try { + const compressedImage = await compressBase64Image(data.diagramImage); + await serverFunctions.insertBase64ImageWithMetadata( - data.diagramImage, + compressedImage, metadata.toString() ); handleDialogClose(); } catch (error) { console.error('Error inserting image with metadata', error); showAlertDialog('Error inserting image, please try again'); + setIsInserting(false); } } }; @@ -48,20 +65,23 @@ const CreateDiagramDialog = () => { return () => { window.removeEventListener('message', handleMessage); }; - }, []); + }, [isInserting]); + + const handleIframeLoad = () => { + setIframeLoading(false); + }; if (authStatus === 'idle' || authStatus === 'loading') { return ( - + ); } @@ -77,28 +97,67 @@ const CreateDiagramDialog = () => { height: '96.5vh', }} > - + Error - + Something went wrong. Please try again later. ); } - return ( -
-