diff --git a/package-lock.json b/package-lock.json
index 13256ffe..76492d3f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,6 +42,7 @@
"drizzle-orm": "^0.44.5",
"esbuild": "^0.25.8",
"eslint-config-next": "13.4.3",
+ "framer-motion": "^12.23.24",
"lucide-react": "^0.233.0",
"next": "^15.4.2",
"next-auth": "^4.22.1",
@@ -50,6 +51,7 @@
"pg": "^8.11.1",
"postcss": "^8.5.6",
"react": "18.3.1",
+ "react-confetti": "^6.4.0",
"react-dom": "18.3.1",
"react-hook-form": "^7.53.2",
"tailwind-merge": "^1.13.2",
@@ -9615,6 +9617,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.23.24",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
+ "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.23.23",
+ "motion-utils": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/from": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
@@ -11292,6 +11321,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/motion-dom": {
+ "version": "12.23.23",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
+ "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.23.6"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
+ "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -12656,6 +12700,21 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-confetti": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.4.0.tgz",
+ "integrity": "sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "tween-functions": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-day-picker": {
"version": "8.10.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
@@ -14372,6 +14431,12 @@
"node": "*"
}
},
+ "node_modules/tween-functions": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
+ "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==",
+ "license": "BSD"
+ },
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
diff --git a/package.json b/package.json
index ee18b476..922bc3e2 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"drizzle-orm": "^0.44.5",
"esbuild": "^0.25.8",
"eslint-config-next": "13.4.3",
+ "framer-motion": "^12.23.24",
"lucide-react": "^0.233.0",
"next": "^15.4.2",
"next-auth": "^4.22.1",
@@ -56,6 +57,7 @@
"pg": "^8.11.1",
"postcss": "^8.5.6",
"react": "18.3.1",
+ "react-confetti": "^6.4.0",
"react-dom": "18.3.1",
"react-hook-form": "^7.53.2",
"tailwind-merge": "^1.13.2",
diff --git a/src/app/portfolio/[slug]/components/BookSurveyModal.tsx b/src/app/portfolio/[slug]/components/BookSurveyModal.tsx
index 9d021d03..5a5f112e 100644
--- a/src/app/portfolio/[slug]/components/BookSurveyModal.tsx
+++ b/src/app/portfolio/[slug]/components/BookSurveyModal.tsx
@@ -19,6 +19,7 @@ interface BookSurveyModalProps {
propertyId: bigint;
portfolioId: bigint;
address: string;
+ onSuccess?: () => void; // ✅ fix: properly declare optional callback
}
export default function BookSurveyModal({
@@ -27,6 +28,7 @@ export default function BookSurveyModal({
propertyId,
portfolioId,
address,
+ onSuccess, // ✅ fix: remove “?:” here, we already declared it optional in interface
}: BookSurveyModalProps) {
const [formData, setFormData] = useState({
dealname: "",
@@ -54,6 +56,7 @@ export default function BookSurveyModal({
console.log("✅ Deal created successfully:", data);
console.log("HUBSPOT DEAL ID MADE", data.dealId);
onOpenChange(false);
+ if (onSuccess) onSuccess(); // 👈 trigger confetti toast
},
onError: (error) => {
console.error("❌ Deal creation failed:", error);
@@ -119,5 +122,12 @@ export default function BookSurveyModal({
+
);
}
+
+
+// TODO: check if bug is happening and why.
+// TODO: Ask khalim what we want to do, maybe a list of Hubspot DB record? if someone presses twice, currently just updates
+// TODO: Make a sexy toast that the deal has been processed
+// TODO: Show khalim a demo and other clean ups for good user experience
\ No newline at end of file
diff --git a/src/app/portfolio/[slug]/components/BookingSuccessToast.tsx b/src/app/portfolio/[slug]/components/BookingSuccessToast.tsx
new file mode 100644
index 00000000..7b9410d8
--- /dev/null
+++ b/src/app/portfolio/[slug]/components/BookingSuccessToast.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import Confetti from "react-confetti";
+import { CheckCircle } from "lucide-react";
+
+interface BookingSuccessToastProps {
+ show: boolean;
+ onClose: () => void;
+ message?: string;
+ subtext?: string;
+}
+
+export default function BookingSuccessToast({
+ show,
+ onClose,
+ message = "Booking Confirmed!",
+ subtext = "You’re all set. 🎉",
+}: BookingSuccessToastProps) {
+ const [confetti, setConfetti] = useState(false);
+
+ useEffect(() => {
+ if (show) {
+ setConfetti(true);
+ const timer = setTimeout(() => {
+ setConfetti(false);
+ onClose();
+ }, 4000);
+ return () => clearTimeout(timer);
+ }
+ }, [show, onClose]);
+
+ return (
+ <>
+
+ {show && (
+
+
+
+
+
+
{message}
+
{subtext}
+
+
+ )}
+
+
+ {confetti && (
+
+ )}
+ >
+ );
+}
diff --git a/src/app/portfolio/[slug]/components/propertyTableColumns.tsx b/src/app/portfolio/[slug]/components/propertyTableColumns.tsx
index 8b8d7803..a3e35337 100644
--- a/src/app/portfolio/[slug]/components/propertyTableColumns.tsx
+++ b/src/app/portfolio/[slug]/components/propertyTableColumns.tsx
@@ -22,6 +22,7 @@ import {
PropertyWithRelations,
} from "@/app/db/schema/property";
import BookSurveyModal from "./BookSurveyModal";
+import BookingSuccessToast from "./BookingSuccessToast";
import { useState } from "react";
@@ -218,6 +219,7 @@ export const columns: ColumnDef[] = [
id: "actions",
cell: ({ row }) => {
const [openModal, setOpenModal] = useState(false);
+ const [showToast, setShowToast] = useState(false);
const property = row.original;
const propertyId = property.id;
const portfolioId = property.portfolioId;
@@ -271,8 +273,17 @@ export const columns: ColumnDef[] = [
propertyId={propertyId}
portfolioId={portfolioId}
address={"ask khalim"}
+ onSuccess={() => setShowToast(true)}
/>
)}
+
+ {/* 💥 Toast */}
+ setShowToast(false)}
+ message="Deal Created Successfully!"
+ subtext="Your HubSpot deal is ready. 🎉"
+ />
);
},