From 2c5f7cc446482daed792d118a56eddcc7ea74e4a Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 3 Jan 2025 16:35:29 -0500 Subject: [PATCH] Add eventfeed cards --- dashboard/src/App.jsx | 16 +- .../src/components/dashboard/EventFeed.jsx | 2 + .../components/dashboard/MiniEventFeed.jsx | 435 ++++++++++++++++++ 3 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 dashboard/src/components/dashboard/MiniEventFeed.jsx diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx index ca3cc47..114fbbd 100644 --- a/dashboard/src/App.jsx +++ b/dashboard/src/App.jsx @@ -30,6 +30,7 @@ import TypeformDashboard from "@/components/dashboard/TypeformDashboard"; import MiniStatCards from "@/components/dashboard/MiniStatCards"; import MiniRealtimeAnalytics from "@/components/dashboard/MiniRealtimeAnalytics"; import MiniSalesChart from "@/components/dashboard/MiniSalesChart"; +import MiniEventFeed from "@/components/dashboard/MiniEventFeed"; // Public layout const PublicLayout = () => ( @@ -68,9 +69,10 @@ const SmallLayout = () => { const STATS_SCALE = 1.65; const ANALYTICS_SCALE = 1.65; const SALES_SCALE = 1.65; + const FEED_SCALE = 1.65; return ( -
+
@@ -132,6 +134,18 @@ const SmallLayout = () => {
+ + {/* Event Feed at bottom */} +
+
+ +
+
); }; diff --git a/dashboard/src/components/dashboard/EventFeed.jsx b/dashboard/src/components/dashboard/EventFeed.jsx index b90eb8b..30a091a 100644 --- a/dashboard/src/components/dashboard/EventFeed.jsx +++ b/dashboard/src/components/dashboard/EventFeed.jsx @@ -913,6 +913,8 @@ const EventDialog = ({ event, children }) => { ); }; +export { EventDialog }; + const EventCard = ({ event }) => { const eventType = EVENT_TYPES[event.metric_id] || { label: "Unknown Event", diff --git a/dashboard/src/components/dashboard/MiniEventFeed.jsx b/dashboard/src/components/dashboard/MiniEventFeed.jsx new file mode 100644 index 0000000..eda7788 --- /dev/null +++ b/dashboard/src/components/dashboard/MiniEventFeed.jsx @@ -0,0 +1,435 @@ +import React, { useState, useEffect, useCallback, useRef } from "react"; +import axios from "axios"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Package, + Truck, + UserPlus, + XCircle, + DollarSign, + Activity, + AlertCircle, + FileText, +} from "lucide-react"; +import { format } from "date-fns"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { EventDialog } from "./EventFeed.jsx"; + +const METRIC_IDS = { + PLACED_ORDER: "Y8cqcF", + SHIPPED_ORDER: "VExpdL", + ACCOUNT_CREATED: "TeeypV", + CANCELED_ORDER: "YjVMNg", + NEW_BLOG_POST: "YcxeDr", + PAYMENT_REFUNDED: "R7XUYh", +}; + +const EVENT_TYPES = { + [METRIC_IDS.PLACED_ORDER]: { + label: "Order Placed", + color: "bg-green-500 dark:bg-green-600", + textColor: "text-green-600 dark:text-green-400", + }, + [METRIC_IDS.SHIPPED_ORDER]: { + label: "Order Shipped", + color: "bg-blue-500 dark:bg-blue-600", + textColor: "text-blue-600 dark:text-blue-400", + }, + [METRIC_IDS.ACCOUNT_CREATED]: { + label: "New Account", + color: "bg-purple-500 dark:bg-purple-600", + textColor: "text-purple-600 dark:text-purple-400", + }, + [METRIC_IDS.CANCELED_ORDER]: { + label: "Order Canceled", + color: "bg-red-500 dark:bg-red-600", + textColor: "text-red-600 dark:text-red-400", + }, + [METRIC_IDS.PAYMENT_REFUNDED]: { + label: "Payment Refunded", + color: "bg-orange-500 dark:bg-orange-600", + textColor: "text-orange-600 dark:text-orange-400", + }, + [METRIC_IDS.NEW_BLOG_POST]: { + label: "New Blog Post", + color: "bg-indigo-500 dark:bg-indigo-600", + textColor: "text-indigo-600 dark:text-indigo-400", + }, +}; + +const EVENT_ICONS = { + [METRIC_IDS.PLACED_ORDER]: Package, + [METRIC_IDS.SHIPPED_ORDER]: Truck, + [METRIC_IDS.ACCOUNT_CREATED]: UserPlus, + [METRIC_IDS.CANCELED_ORDER]: XCircle, + [METRIC_IDS.PAYMENT_REFUNDED]: DollarSign, + [METRIC_IDS.NEW_BLOG_POST]: FileText, +}; + +// Loading State Component +const LoadingState = () => ( +
+ {[...Array(4)].map((_, i) => ( + + +
+ +
+ + +
+
+
+ +
+ + +
+
+
+ ))} +
+); + +// Empty State Component +const EmptyState = () => ( + + +
+ +
+

+ No activity yet +

+

+ Recent activity will appear here as it happens +

+
+
+); + +const EventCard = ({ event }) => { + const eventType = EVENT_TYPES[event.metric_id]; + if (!eventType) return null; + + const Icon = EVENT_ICONS[event.metric_id] || Package; + const details = event.event_properties || {}; + + const getBgGradient = (type) => { + switch (type) { + case METRIC_IDS.PLACED_ORDER: + return 'from-green-900 to-green-800'; + case METRIC_IDS.SHIPPED_ORDER: + return 'from-blue-900 to-blue-800'; + case METRIC_IDS.ACCOUNT_CREATED: + return 'from-purple-900 to-purple-800'; + case METRIC_IDS.CANCELED_ORDER: + return 'from-red-900 to-red-800'; + case METRIC_IDS.PAYMENT_REFUNDED: + return 'from-orange-900 to-orange-800'; + case METRIC_IDS.NEW_BLOG_POST: + return 'from-indigo-900 to-indigo-800'; + default: + return 'from-gray-900 to-gray-800'; + } + }; + + return ( + + + +
+ + {eventType.label} + + {event.datetime && ( + + {format(new Date(event.datetime), "h:mm a")} + + )} +
+
+
+ +
+ + + {event.metric_id === METRIC_IDS.PLACED_ORDER && ( + <> +
+ {details.ShippingName} +
+
+
+ #{details.OrderId} • {formatCurrency(details.TotalAmount)} +
+
+ {(details.IsOnHold || details.OnHoldReleased || details.StillOwes || details.LocalPickup || details.HasPreorder || details.HasNotions || details.OnlyDigitalGC || details.HasDigitalGC || details.HasDigiItem || details.OnlyDigiItem) && ( +
+ {details.IsOnHold && ( + + On Hold + + )} + {details.OnHoldReleased && ( + + Hold Released + + )} + {details.StillOwes && ( + + Owes + + )} + {details.LocalPickup && ( + + Local + + )} + {details.HasPreorder && ( + + Pre-order + + )} + {details.HasNotions && ( + + Notions + + )} + {(details.OnlyDigitalGC || details.HasDigitalGC) && ( + + eGift Card + + )} + {(details.HasDigiItem || details.OnlyDigiItem) && ( + + Digital + + )} +
+ )} + + )} + + {event.metric_id === METRIC_IDS.SHIPPED_ORDER && ( + <> +
+ {details.ShippingName} +
+
+
+ #{details.OrderId} • {formatShipMethodSimple(details.ShipMethod)} +
+
+
+ {details.ShippingStreet1}, {details.ShippingCity}, {details.ShippingState} {details.ShippingZip} +
+ + )} + + {event.metric_id === METRIC_IDS.ACCOUNT_CREATED && ( + <> +
+ {details.FirstName} {details.LastName} +
+
+ {details.EmailAddress} +
+ + )} + + {event.metric_id === METRIC_IDS.CANCELED_ORDER && ( + <> +
+ {details.ShippingName} +
+
+
+ #{details.OrderId} • {formatCurrency(details.TotalAmount)} +
+
+
+ {details.CancelReason} +
+ + )} + + {event.metric_id === METRIC_IDS.PAYMENT_REFUNDED && ( + <> +
+ {details.ShippingName} +
+
+
+ #{details.FromOrder} • {formatCurrency(details.PaymentAmount)} +
+
+
+ via {details.PaymentName} +
+ + )} + + {event.metric_id === METRIC_IDS.NEW_BLOG_POST && ( + <> +
+ {details.title} +
+
+ {details.description} +
+ + )} +
+ + + ); +}; + +const DEFAULT_METRICS = Object.values(METRIC_IDS); + +const MiniEventFeed = ({ + selectedMetrics = DEFAULT_METRICS, +}) => { + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [lastUpdate, setLastUpdate] = useState(null); + const scrollRef = useRef(null); + + const fetchEvents = useCallback(async () => { + try { + setError(null); + + if (events.length === 0) { + setLoading(true); + } + + const response = await axios.get("/api/klaviyo/events/feed", { + params: { + timeRange: "today", + metricIds: JSON.stringify(selectedMetrics), + }, + }); + + const processedEvents = (response.data.data || []).map((event) => ({ + ...event, + datetime: event.attributes?.datetime || event.datetime, + event_properties: { + ...event.event_properties, + datetime: event.attributes?.datetime || event.datetime, + }, + })); + + setEvents(processedEvents); + setLastUpdate(new Date()); + + // Scroll to the right after events are loaded + if (scrollRef.current) { + setTimeout(() => { + scrollRef.current.scrollTo({ + left: scrollRef.current.scrollWidth, + behavior: 'instant' + }); + }, 0); + } + } catch (error) { + console.error("Error fetching events:", error); + setError(error.message); + } finally { + setLoading(false); + } + }, [selectedMetrics]); + + useEffect(() => { + fetchEvents(); + const interval = setInterval(fetchEvents, 60000); + return () => clearInterval(interval); + }, [fetchEvents]); + + return ( +
+
+
+
+ {loading && !events.length ? ( + + ) : error ? ( + + + Error + + Failed to load event feed: {error} + + + ) : !events || events.length === 0 ? ( +
+ +
+ ) : ( + events.map((event) => ( + + )) + )} +
+
+
+
+ ); +}; + +export default MiniEventFeed; + +// Helper Functions +const formatCurrency = (amount) => { + // Convert to number if it's a string + const num = typeof amount === "string" ? parseFloat(amount) : amount; + // Handle negative numbers + const absNum = Math.abs(num); + // Format to 2 decimal places and add negative sign if needed + return `${num < 0 ? "-" : ""}$${absNum.toFixed(2)}`; +}; + +const formatShipMethodSimple = (method) => { + if (!method) return "Digital"; + if (method.includes("usps")) return "USPS"; + if (method.includes("fedex")) return "FedEx"; + if (method.includes("ups")) return "UPS"; + return "Standard"; +}; \ No newline at end of file