Repository: getvim/vim-canvas-demo-app
Pull request #23: Add storage
Author: login: AnatBardach1
Created: 2025-03-03T13:01:08Z | Merged: 2025-03-06T08:34:48Z

to deploy d1 db
npx wrangler d1 execute canvas-demo-db --remote --file=./sql/settings.sql


Diff:
diff --git a/vim-canvas-demo-app-react/functions/api/settings.ts b/vim-canvas-demo-app-react/functions/api/settings.ts
new file mode 100644
index 0000000..b0553c7
--- /dev/null
+++ b/vim-canvas-demo-app-react/functions/api/settings.ts
@@ -0,0 +1,57 @@
+import { Env } from "../context-env";
+import { Settings } from "../../types";
+import { jwtDecode } from "jwt-decode";
+
+const ORGANIZATION_ID_CLAIM = "https://getvim.com/organizationId";
+
+const extractOrgId = (token: string) => {
+  if (!token) {
+    return Response.json({ error: "Unauthorized" }, { status: 401 });
+  }
+  const decodedToken = jwtDecode<{ [key: string]: string }>(token);
+  return decodedToken?.[ORGANIZATION_ID_CLAIM];
+};
+
+export const onRequestPost: PagesFunction<Env> = async (context) => {
+  const { request, env } = context;
+  const { DB } = env;
+  const token = context.request.headers.get("Authorization");
+  const organizationId = extractOrgId(token);
+
+  try {
+    const data = await request.json<Settings>();
+    const { theme_color } = data;
+
+    const result = await DB.prepare(
+      "INSERT INTO settings (organization_id, theme_color, created_at) VALUES (?, ?, ?) \
+       ON CONFLICT (organization_id) \
+       DO UPDATE SET \
+       theme_color = excluded.theme_color,\
+       updated_at = excluded.updated_at"
+    )
+      .bind(organizationId, theme_color, new Date().toISOString())
+      .run();
+
+    return Response.json({ success: true, id: result.meta.last_row_id });
+  } catch (error) {
+    return Response.json({ error: (error as Error).message }, { status: 500 });
+  }
+};
+
+export const onRequestGet: PagesFunction<Env> = async (context) => {
+  const { env } = context;
+  const { DB } = env;
+  const token = context.request.headers.get("Authorization");
+  const organizationId = extractOrgId(token);
+  try {
+    const result = await DB.prepare(
+      "SELECT * FROM settings WHERE organization_id = ?"
+    )
+      .bind(organizationId)
+      .first();
+
+    return Response.json(result);
+  } catch (error) {
+    return Response.json({ error: (error as Error).message }, { status: 500 });
+  }
+};
diff --git a/vim-canvas-demo-app-react/functions/context-env.ts b/vim-canvas-demo-app-react/functions/context-env.ts
index 1f43193..d26f45e 100644
--- a/vim-canvas-demo-app-react/functions/context-env.ts
+++ b/vim-canvas-demo-app-react/functions/context-env.ts
@@ -6,4 +6,5 @@ export interface Env {
   VIM_AUTHORIZE_ENDPOINT?: string;
   VIM_ISSUER?: string;
   CLIENT_SECRET_FALLBACK?: string;
+  DB: D1Database;
 }
diff --git a/vim-canvas-demo-app-react/package.json b/vim-canvas-demo-app-react/package.json
index f3c1e94..3ed58ae 100644
--- a/vim-canvas-demo-app-react/package.json
+++ b/vim-canvas-demo-app-react/package.json
@@ -5,7 +5,7 @@
   "type": "module",
   "scripts": {
     "dev:vite": "vite",
-    "dev:functions": "wrangler pages dev --proxy=5173",
+    "dev:functions": "wrangler pages dev --proxy=5173 --d1=DB:canvas-demo-db",
     "dev": "concurrently --raw -i \"npm:dev:functions\" \"npm:dev:vite\" ",
     "build": "tsc -b && vite build",
     "lint": "eslint .",
diff --git a/vim-canvas-demo-app-react/sql/settings.sql b/vim-canvas-demo-app-react/sql/settings.sql
new file mode 100644
index 0000000..53ebadb
--- /dev/null
+++ b/vim-canvas-demo-app-react/sql/settings.sql
@@ -0,0 +1,5 @@
+CREATE TABLE settings (
+    organization_id INTEGER PRIMARY KEY,
+    theme_color VARCHAR(255) NOT NULL,
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+)
\ No newline at end of file
diff --git a/vim-canvas-demo-app-react/src/components/settings-app/BrandCustomization.tsx b/vim-canvas-demo-app-react/src/components/settings-app/BrandCustomization.tsx
index c1835c3..aae8cfd 100644
--- a/vim-canvas-demo-app-react/src/components/settings-app/BrandCustomization.tsx
+++ b/vim-canvas-demo-app-react/src/components/settings-app/BrandCustomization.tsx
@@ -1,37 +1,74 @@
-import React, { useCallback, useState } from "react";
-import { useOrganizationContext } from "@/hooks/useOrganizationContext";
-import { saveSettings } from "../../utils/storage";
+import React, { useCallback, useEffect, useState } from "react";
+import { useAuthTokenData } from "@/hooks/useAuthTokenData";
+import { saveSettings, loadSettings } from "../../utils/settings-api";
 import { VimConnectPreview } from "./VimConnectPreview";
 import { ColorPicker } from "./ColorPicker";
 
 export const BrandCustomization: React.FC = () => {
   const [appColor, setAppColor] = useState<string>("#00FFE1");
-  const organizationId = useOrganizationContext();
+  const { idToken } = useAuthTokenData();
+  const [isLoading, setIsLoading] = useState<boolean>(true);
+  const [isSaveButtonDisabled, setIsSaveButtonDisabled] =
+    useState<boolean>(true);
 
   const handleSave = useCallback(() => {
-    saveSettings({ [organizationId]: { appColor } });
-  }, [organizationId, appColor]);
+    saveSettings(
+      {
+        theme_color: appColor,
+      },
+      idToken
+    );
+    setIsSaveButtonDisabled(true);
+  }, [appColor, idToken]);
 
+  useEffect(() => {
+    const fetchSettings = async () => {
+      setIsLoading(true);
+      const settings = await loadSettings(idToken);
+      setAppColor((prev) =>
+        settings?.theme_color ? settings.theme_color : prev
+      );
+      setIsLoading(false);
+    };
+
+    if (idToken) {
+      fetchSettings();
+    }
+  }, [idToken]);
+
+  if (isLoading) {
+    return <div>Loading...</div>;
+  }
+
+  const handleColorChange = (color: string) => {
+    setAppColor(color);
+    setIsSaveButtonDisabled(false);
+  };
 
   return (
-    <div className="w-full p-5 font-proxima rounded-xl bg-white">
+    <div className="w-full p-5 font-proxima rounded-xl bg-white h-full">
       <div className="font-proxima text-sm leading-4 font-bold text-left mb-1">
         Customize Demo Canvas in-app experience to your organization branding
       </div>
 
-      <div className="grid grid-cols-[267px_9px_268px] gap-4 mt-5">
+      <div className="grid grid-cols-[267px_9px_268px] gap-4 mt-5 h-[95%]">
         <div>
           <div>
             <label className="text-left text-sm font-bold">
               Select your app color:
             </label>
 
-            <ColorPicker color={appColor} onChange={setAppColor} />
+            <ColorPicker color={appColor} onChange={handleColorChange} />
           </div>
 
           <button
-            className="mt-5 bg-gray-300 hover:bg-gray-400 text-white text-center text-sm font-medium rounded-sm w-[100px] h-[34px]"
+            className="mt-5 hover:bg-gray-400 text-white text-center text-sm font-medium rounded-sm w-[100px] h-[34px]"
             onClick={handleSave}
+            disabled={isSaveButtonDisabled}
+            style={{
+              cursor: isSaveButtonDisabled ? "not-allowed" : "pointer",
+              backgroundColor: isSaveButtonDisabled ? "#D1D5DB" : "#001C36",
+            }}
           >
             Save
           </button>
diff --git a/vim-canvas-demo-app-react/src/hooks/useAuthTokenData.tsx b/vim-canvas-demo-app-react/src/hooks/useAuthTokenData.tsx
new file mode 100644
index 0000000..18005d4
--- /dev/null
+++ b/vim-canvas-demo-app-react/src/hooks/useAuthTokenData.tsx
@@ -0,0 +1,14 @@
+import { useEffect, useState } from "react";
+import { useVimSettingsSdkContext } from "./useVimSettingsSdkContext";
+
+export const useAuthTokenData = () => {
+  const vimSettingsSDk = useVimSettingsSdkContext();
+  const [idToken, setIdToken] = useState<string>("");
+
+  useEffect(() => {
+    const idToken = vimSettingsSDk.getIdToken();
+    setIdToken(idToken);
+  }, [setIdToken, vimSettingsSDk]);
+
+  return { idToken };
+};
diff --git a/vim-canvas-demo-app-react/src/hooks/useOrganizationContext.tsx b/vim-canvas-demo-app-react/src/hooks/useOrganizationContext.tsx
deleted file mode 100644
index c685cef..0000000
--- a/vim-canvas-demo-app-react/src/hooks/useOrganizationContext.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { useEffect, useState } from "react";
-import { useVimSettingsSdkContext } from "./useVimSettingsSdkContext";
-import { jwtDecode } from "jwt-decode";
-
-const ORGANIZATION_ID_CLAIM = "https://getvim.com/organizationId";
-
-export const useOrganizationContext = () => {
-  const [organizationId, setOrganizationId] = useState<string>("");
-  const vimSettingsSDk = useVimSettingsSdkContext();
-
-  useEffect(() => {
-    const idToken = vimSettingsSDk.getIdToken();
-    const decodedToken = jwtDecode<{ [key: string]: string }>(idToken);
-    const organizationId = decodedToken?.[ORGANIZATION_ID_CLAIM];
-    setOrganizationId(organizationId);
-  }, [vimSettingsSDk]);
-
-  return organizationId;
-};
diff --git a/vim-canvas-demo-app-react/src/utils/settings-api.tsx b/vim-canvas-demo-app-react/src/utils/settings-api.tsx
new file mode 100644
index 0000000..a066d29
--- /dev/null
+++ b/vim-canvas-demo-app-react/src/utils/settings-api.tsx
@@ -0,0 +1,20 @@
+import { Settings } from "../../types";
+
+export const saveSettings = async (settings: Settings, token: string) => {
+  await fetch("api/settings", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+      Authorization: token,
+    },
+    body: JSON.stringify(settings),
+  });
+};
+
+export const loadSettings = async (token: string): Promise<Settings> => {
+  const res = await fetch("/api/settings", {
+    method: "GET",
+    headers: { Authorization: token },
+  });
+  return res.json();
+};
diff --git a/vim-canvas-demo-app-react/src/utils/storage.tsx b/vim-canvas-demo-app-react/src/utils/storage.tsx
deleted file mode 100644
index 258f4e3..0000000
--- a/vim-canvas-demo-app-react/src/utils/storage.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-export const saveSettings = (settings: object) => {
-  console.log("call backend to save settings: ", settings);
-};
-
-export const loadSettings = (): object => {
-  console.log("call backend to load settings");
-  return { "123": { appColor: "#00FFE1" } };
-};
diff --git a/vim-canvas-demo-app-react/tsconfig.functions.json b/vim-canvas-demo-app-react/tsconfig.functions.json
index 63c0eb9..13a3990 100644
--- a/vim-canvas-demo-app-react/tsconfig.functions.json
+++ b/vim-canvas-demo-app-react/tsconfig.functions.json
@@ -6,5 +6,5 @@
     "lib": ["esnext"],
     "types": ["@cloudflare/workers-types"]
   },
-  "include": ["functions"]
+  "include": ["functions", "types"]
 }
diff --git a/vim-canvas-demo-app-react/types/index.js b/vim-canvas-demo-app-react/types/index.js
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/vim-canvas-demo-app-react/types/index.js
@@ -0,0 +1 @@
+export {};
diff --git a/vim-canvas-demo-app-react/types/index.ts b/vim-canvas-demo-app-react/types/index.ts
new file mode 100644
index 0000000..67e6c0d
--- /dev/null
+++ b/vim-canvas-demo-app-react/types/index.ts
@@ -0,0 +1,5 @@
+export interface Settings {
+  id?: string;
+  organization_id?: string;
+  theme_color: string;
+}
diff --git a/vim-canvas-demo-app-react/wrangler.toml b/vim-canvas-demo-app-react/wrangler.toml
new file mode 100644
index 0000000..28a4977
--- /dev/null
+++ b/vim-canvas-demo-app-react/wrangler.toml
@@ -0,0 +1,4 @@
+[[d1_databases]]
+binding = "DB"
+database_name = "canvas-demo-db"
+database_id = "a184dff5-29fc-4436-b7c6-64db9ba45b58"
\ No newline at end of file


Reviews:
user: login: ashercoren
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T16:09:37Z
user: login: AnatBardach1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T11:01:47Z
user: login: AnatBardach1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T11:21:38Z
user: login: ashercoren
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:03:27Z
user: login: ashercoren
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:04:18Z
user: login: AnatBardach1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:49:53Z
user: login: ashercoren
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T14:00:27Z
user: login: ashercoren
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T14:02:32Z

Review comments:
diff_hunk: @@ -0,0 +1,26 @@
+import { Env } from "../context-env";
+import { Settings } from "../../types";
+
+export const onRequestPost: PagesFunction<Env> = async (context) => {
+  const { request, env } = context;
+  const { DB } = env;
+
+  try {
+    const data = (await request.json()) as Settings;
path: vim-canvas-demo-app-react/functions/api/settings.ts
user: login: ashercoren
body: ```suggestion
    const data = await request.json<Settings>();
```
created_at: 2025-03-03T16:04:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,6 @@
+CREATE TABLE settings (
+    organization_id INTEGER PRIMARY KEY,
+    theme_color VARCHAR(255) NOT NULL,
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
path: vim-canvas-demo-app-react/sql/settings.sql
user: login: ashercoren
body: Does this make the updated_at column change on every update?
created_at: 2025-03-03T16:08:18Z
author_association: MEMBER
diff_hunk: @@ -9,9 +9,21 @@ export const BrandCustomization: React.FC = () => {
   const organizationId = useOrganizationContext();
 
   const handleSave = useCallback(() => {
-    saveSettings({ [organizationId]: { appColor } });
+    saveSettings({
+      organization_id: organizationId,
+      theme_color: appColor,
+    });
   }, [organizationId, appColor]);
 
+  useEffect(() => {
+    const fetchSettings = async () => {
+      const settings = await loadSettings(organizationId);
+      setAppColor((prev) =>
+        settings?.theme_color ? settings.theme_color : prev
path: vim-canvas-demo-app-react/src/components/settings-app/BrandCustomization.tsx
user: login: ashercoren
body: What will happen if the user will change the color before the settings are loaded?
created_at: 2025-03-03T16:09:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,6 @@
+CREATE TABLE settings (
+    organization_id INTEGER PRIMARY KEY,
+    theme_color VARCHAR(255) NOT NULL,
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
path: vim-canvas-demo-app-react/sql/settings.sql
user: login: AnatBardach1
body: for this configuration it doesnt
do you think we need it for the demo canvas? or maybe I'll remove this field..
wdyt?
@ashercoren 
created_at: 2025-03-05T11:01:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,9 +9,21 @@ export const BrandCustomization: React.FC = () => {
   const organizationId = useOrganizationContext();
 
   const handleSave = useCallback(() => {
-    saveSettings({ [organizationId]: { appColor } });
+    saveSettings({
+      organization_id: organizationId,
+      theme_color: appColor,
+    });
   }, [organizationId, appColor]);
 
+  useEffect(() => {
+    const fetchSettings = async () => {
+      const settings = await loadSettings(organizationId);
+      setAppColor((prev) =>
+        settings?.theme_color ? settings.theme_color : prev
path: vim-canvas-demo-app-react/src/components/settings-app/BrandCustomization.tsx
user: login: AnatBardach1
body: nothing..
prev and settings?.theme_color will be with the same value
created_at: 2025-03-05T11:21:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,9 +9,21 @@ export const BrandCustomization: React.FC = () => {
   const organizationId = useOrganizationContext();
 
   const handleSave = useCallback(() => {
-    saveSettings({ [organizationId]: { appColor } });
+    saveSettings({
+      organization_id: organizationId,
+      theme_color: appColor,
+    });
   }, [organizationId, appColor]);
 
+  useEffect(() => {
+    const fetchSettings = async () => {
+      const settings = await loadSettings(organizationId);
+      setAppColor((prev) =>
+        settings?.theme_color ? settings.theme_color : prev
path: vim-canvas-demo-app-react/src/components/settings-app/BrandCustomization.tsx
user: login: ashercoren
body: How will they be the same? If the user selects something other than the saved color?
created_at: 2025-03-05T12:03:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,6 @@
+CREATE TABLE settings (
+    organization_id INTEGER PRIMARY KEY,
+    theme_color VARCHAR(255) NOT NULL,
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
path: vim-canvas-demo-app-react/sql/settings.sql
user: login: ashercoren
body: If you have a field (which is a good thing), have it update automatically.
If it doesn't update, remove it
created_at: 2025-03-05T12:04:18Z
author_association: MEMBER
diff_hunk: @@ -9,9 +9,21 @@ export const BrandCustomization: React.FC = () => {
   const organizationId = useOrganizationContext();
 
   const handleSave = useCallback(() => {
-    saveSettings({ [organizationId]: { appColor } });
+    saveSettings({
+      organization_id: organizationId,
+      theme_color: appColor,
+    });
   }, [organizationId, appColor]);
 
+  useEffect(() => {
+    const fetchSettings = async () => {
+      const settings = await loadSettings(organizationId);
+      setAppColor((prev) =>
+        settings?.theme_color ? settings.theme_color : prev
path: vim-canvas-demo-app-react/src/components/settings-app/BrandCustomization.tsx
user: login: AnatBardach1
body: when the user selects a color it changes the state of `appColor` (line 41)
so prev will be the selected color, if he saves it -> the loadSettings will return what he saved
if he doesn't save it will change the color from the loadSettings 
created_at: 2025-03-05T12:49:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,17 +1,45 @@
-import React, { useCallback, useState } from "react";
+import React, { useCallback, useEffect, useState } from "react";
 import { useOrganizationContext } from "@/hooks/useOrganizationContext";
-import { saveSettings } from "../../utils/storage";
+import { saveSettings, loadSettings } from "../../utils/settings-api";
 import { VimConnectPreview } from "./VimConnectPreview";
 import { ColorPicker } from "./ColorPicker";
 
 export const BrandCustomization: React.FC = () => {
   const [appColor, setAppColor] = useState<string>("#00FFE1");
   const organizationId = useOrganizationContext();
+  const [isLoading, setIsLoading] = useState<boolean>(true);
+  const [isSaveButtonDisabled, setIsSaveButtonDisabled] =
+    useState<boolean>(true);
 
   const handleSave = useCallback(() => {
-    saveSettings({ [organizationId]: { appColor } });
+    saveSettings({
+      organization_id: organizationId,
+      theme_color: appColor,
+    });
+    setIsSaveButtonDisabled(true);
   }, [organizationId, appColor]);
 
+  useEffect(() => {
+    const fetchSettings = async () => {
+      setIsLoading(true);
+      const settings = await loadSettings(organizationId);
path: vim-canvas-demo-app-react/src/components/settings-app/BrandCustomization.tsx
user: login: ashercoren
body: Let's do it the correct way - send the idtoken to the backend and have it parse it and use the org id
created_at: 2025-03-05T14:02:30Z
author_association: MEMBER

PR comments:
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-03-05T11:23:28Z
body: ## Deploying vim-canvas-demo-app-staging with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>d0df02b</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://7885254a.vim-canvas-demo-app-staging.pages.dev'>https://7885254a.vim-canvas-demo-app-staging.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://add-storage.vim-canvas-demo-app-staging.pages.dev'>https://add-storage.vim-canvas-demo-app-staging.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/59bdb4e22f71d50cd67b45aa7bd53dff/pages/view/vim-canvas-demo-app-staging/7885254a-0fb2-44b9-b981-366c741e8239)

author_association: NONE
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-03-05T11:24:11Z
body: ## Deploying vim-canvas-demo-app with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>d0df02b</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://b09cbce2.vim-canvas-demo-app.pages.dev'>https://b09cbce2.vim-canvas-demo-app.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://add-storage.vim-canvas-demo-app.pages.dev'>https://add-storage.vim-canvas-demo-app.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/59bdb4e22f71d50cd67b45aa7bd53dff/pages/view/vim-canvas-demo-app/b09cbce2-7e7a-49f7-b689-6ccc28eb5c87)

author_association: NONE
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-03-05T11:24:15Z
body: ## Deploying vim-canvas-demo-app-dev with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>d0df02b</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://63a2833b.vim-canvas-demo-app-dev.pages.dev'>https://63a2833b.vim-canvas-demo-app-dev.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://add-storage.vim-canvas-demo-app-dev.pages.dev'>https://add-storage.vim-canvas-demo-app-dev.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/59bdb4e22f71d50cd67b45aa7bd53dff/pages/view/vim-canvas-demo-app-dev/63a2833b-1ca2-4fcb-b6b0-f33ae364d401)

author_association: NONE