Files
dashboard/examples DO NOT USE OR EDIT/EXAMPLE ONLY EventFeed.jsx
2024-12-21 09:49:53 -05:00

1029 lines
38 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useMemo, useCallback } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
ShoppingCart,
UserPlus,
AlertCircle,
Package,
DollarSign,
FileText,
XCircle,
ChevronRight,
Tag,
Box,
Activity,
RefreshCcw,
} from "lucide-react";
import { DateTime } from "luxon";
import { Skeleton } from "@/components/ui/skeleton";
const LoadingState = () => (
<div className="flex items-center justify-center p-8">
<div className="space-y-4 w-full">
{[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
))}
</div>
</div>
);
const formatCurrency = (value) => {
if (!value || isNaN(value)) return "$0.00";
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
};
const formatShipMethod = (method) => {
if (!method) return "Standard Shipping";
return method
.replace("usps_", "USPS ")
.replace("ups_", "UPS ")
.replace("fedex_", "FedEx ")
.replace(/_/g, " ")
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
const formatShipMethodSimple = (method) => {
if (!method) return 'Standard';
if (method.includes('usps')) return 'USPS';
if (method.includes('fedex')) return 'FedEx';
if (method.includes('ups')) return 'UPS';
return 'Standard';
};
const PromotionalInfo = ({ details }) => {
if (!details?.PromosUsedReg?.length && !details?.PointsDiscount) return null;
return (
<div className="mt-4 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h4 className="text-sm font-medium text-green-800 dark:text-green-400 mb-2">
<span className="cursor-help">Savings Applied</span>
</h4>
<div className="space-y-2">
{Array.isArray(details.PromosUsedReg) &&
details.PromosUsedReg.map(([code, amount], index) => (
<div key={index} className="flex justify-between text-sm">
<span className="font-mono text-green-700 dark:text-green-300 cursor-help">
{code}
</span>
<span className="font-medium text-green-700 dark:text-green-300 cursor-help">
-{formatCurrency(amount)}
</span>
</div>
))}
{details.PointsDiscount > 0 && (
<div className="flex justify-between text-sm">
<span className="text-green-700 dark:text-green-300 cursor-help">
Points Discount
</span>
<span className="font-medium text-green-700 dark:text-green-300 cursor-help">
-{formatCurrency(details.PointsDiscount)}
</span>
</div>
)}
</div>
</div>
);
};
const EmptyState = () => (
<div className="h-full flex flex-col items-center justify-center py-16 px-4">
<div className="bg-gray-100 dark:bg-gray-800 rounded-full p-3 mb-4">
<Activity className="h-8 w-8 text-gray-400 dark:text-gray-500" />
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
No activity yet today
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 text-center max-w-sm">
Recent activity will appear here as it happens
</p>
</div>
);
const ProductCard = ({ product }) => (
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg mb-3 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<div className="flex items-start space-x-3">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<p className="font-medium text-sm text-gray-900 dark:text-gray-100 truncate">
{product.ProductName || 'Unnamed Product'}
</p>
{product.ItemStatus === "Pre-Order" && (
<span className="px-2 py-0.5 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-xs cursor-help">
Pre-order
</span>
)}
</div>
<div className="mt-1 flex flex-wrap gap-2">
{product.Brand && (
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 cursor-help">
<Tag className="w-3 h-3 mr-1" />
<span>{product.Brand}</span>
</div>
)}
{product.SKU && (
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 cursor-help">
<Box className="w-3 h-3 mr-1" />
<span>SKU: {product.SKU}</span>
</div>
)}
</div>
</div>
<div className="text-right flex-shrink-0">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 cursor-help">
{formatCurrency(product.ItemPrice)}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 cursor-help">
Qty: {product.Quantity || product.QuantityOrdered || 1}
</div>
{product.RowTotal && (
<div className="text-xs font-medium text-gray-600 dark:text-gray-300 cursor-help">
Total: {formatCurrency(product.RowTotal)}
</div>
)}
</div>
</div>
</div>
);
const OrderSummary = ({ details }) => (
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2 cursor-help">
Subtotal
</h4>
<div className="space-y-1">
<div className="flex justify-between">
<span className="cursor-help">
Items ({details.Items?.length || 0})
</span>
<span>{formatCurrency(details.Subtotal)}</span>
</div>
{details.PointsDiscount > 0 && (
<div className="flex justify-between text-green-600">
<span className="cursor-help">Points Discount</span>
<span>-{formatCurrency(details.PointsDiscount)}</span>
</div>
)}
{details.TotalDiscounts > 0 && (
<div className="flex justify-between text-green-600">
<span className="cursor-help">Discounts</span>
<span>-{formatCurrency(details.TotalDiscounts)}</span>
</div>
)}
</div>
</div>
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2 cursor-help">
Shipping
</h4>
<div className="space-y-1">
<div className="flex justify-between">
<span className="cursor-help">Shipping Cost</span>
<span>{formatCurrency(details.ShippingTotal)}</span>
</div>
{details.RushFee > 0 && (
<div className="flex justify-between">
<span className="cursor-help">Rush Fee</span>
<span>{formatCurrency(details.RushFee)}</span>
</div>
)}
{details.SalesTax > 0 && (
<div className="flex justify-between">
<span className="cursor-help">Sales Tax</span>
<span>{formatCurrency(details.SalesTax)}</span>
</div>
)}
</div>
</div>
</div>
<div className="pt-3 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center">
<div>
<span className="text-sm font-medium">Total</span>
{details.TotalSavings > 0 && (
<div className="text-xs text-green-600 cursor-help">
You saved {formatCurrency(details.TotalSavings)}
</div>
)}
</div>
<span className="text-lg font-bold">
{formatCurrency(details.TotalAmount)}
</span>
</div>
{details.Payments?.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<h4 className="text-sm font-medium text-gray-500 mb-2 cursor-help">
Payment Details
</h4>
<div className="space-y-1">
{details.Payments.map(([method, amount], index) => (
<div key={index} className="flex justify-between text-sm">
<span className="cursor-help">{method}</span>
<span>{formatCurrency(amount)}</span>
</div>
))}
</div>
</div>
)}
</div>
<PromotionalInfo details={details} />
</div>
);
const ShippingInfo = ({ details }) => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-500 cursor-help">
Shipping Address
</h4>
<div className="text-sm space-y-1">
<div className="font-medium cursor-help">
{details.ShippingName || 'Customer'}
</div>
<div>{details.ShippingStreet1}</div>
{details.ShippingStreet2 && <div>{details.ShippingStreet2}</div>}
<div className="cursor-help">
{details.ShippingCity}, {details.ShippingState} {details.ShippingZip}
</div>
{details.ShippingCountry !== "US" && (
<div className="cursor-help">{details.ShippingCountry}</div>
)}
</div>
</div>
{details.TrackingNumber && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-500 cursor-help">
Tracking Information
</h4>
<div className="text-sm space-y-1">
<div className="font-medium cursor-help">
{formatShipMethod(details.ShipMethod)}
</div>
<div className="font-mono text-blue-600 dark:text-blue-400 cursor-pointer hover:underline">
{details.TrackingNumber}
</div>
</div>
</div>
)}
</div>
);
const OrderStatusTags = ({ details }) => (
<div className="flex flex-wrap gap-2">
{details.HasPreorder && (
<span className="px-2 py-1 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 rounded-full text-xs cursor-help">
Includes Pre-order
</span>
)}
{details.LocalPickup && (
<span className="px-2 py-1 bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300 rounded-full text-xs cursor-help">
Local Pickup
</span>
)}
{details.IsOnHold && (
<span className="px-2 py-1 bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 rounded-full text-xs cursor-help">
On Hold
</span>
)}
{details.HasDigiItem && (
<span className="px-2 py-1 bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 rounded-full text-xs cursor-help">
Digital Items
</span>
)}
{details.HasNotions && (
<span className="px-2 py-1 bg-pink-100 dark:bg-pink-900/20 text-pink-800 dark:text-pink-300 rounded-full text-xs cursor-help">
Includes Notions
</span>
)}
{details.HasDigitalGC && (
<span className="px-2 py-1 bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800 dark:text-indigo-300 rounded-full text-xs cursor-help">
Gift Card
</span>
)}
{details.StillOwes && (
<span className="px-2 py-1 bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300 rounded-full text-xs cursor-help">
Payment Due
</span>
)}
{details.PaymentsHasCustomerCredit && (
<span className="px-2 py-1 bg-teal-100 dark:bg-teal-900/20 text-teal-800 dark:text-teal-300 rounded-full text-xs cursor-help">
Store Credit Used
</span>
)}
{details.WasHeld && (
<span className="px-2 py-1 bg-orange-100 dark:bg-orange-900/20 text-orange-800 dark:text-orange-300 rounded-full text-xs cursor-help">
Was On Hold
</span>
)}
{details.IsShorted && (
<span className="px-2 py-1 bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300 rounded-full text-xs cursor-help">
Partial Shipment
</span>
)}
</div>
);
const EventFeed = () => {
const [events, setEvents] = useState([]);
const [selectedEvent, setSelectedEvent] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [lastUpdate, setLastUpdate] = useState(null);
// Updated fetchEvents function to handle a simpler response
const fetchEvents = async () => {
try {
setIsLoading(true);
const response = await fetch("/api/klaviyo/events/feed");
if (!response.ok) {
throw new Error(`Failed to fetch events: ${response.status}`);
}
const data = await response.json();
// Handle both possible response formats
const eventData = data.data || data;
if (!Array.isArray(eventData)) {
throw new Error('Invalid event data format - expected an array');
}
// Process the events
const processedEvents = eventData
.filter(event => event && event.type && event.timestamp) // Filter out invalid events
.map(event => ({
id: event.id,
type: event.type.toLowerCase(),
timestamp: DateTime.fromISO(event.timestamp),
details: {
OrderId: event.details?.OrderId || event.details?.FromOrder || 'Unknown',
ShippingName: event.details?.ShippingName || event.details?.EmailAddress || 'Unknown Customer',
EmailAddress: event.details?.EmailAddress || '',
TotalAmount: parseFloat(event.details?.TotalAmount || event.details?.PaymentAmount) || 0,
Items: Array.isArray(event.details?.Items) ? event.details.Items.map(item => ({
...item,
ItemPrice: parseFloat(item.ItemPrice) || 0,
Quantity: parseInt(item.Quantity) || 1,
RowTotal: parseFloat(item.RowTotal) || parseFloat(item.ItemPrice) * (parseInt(item.Quantity) || 1)
})) : [],
HasPreorder: event.details?.HasPreorder || event.details?.Items?.some(item => item.ItemStatus === 'Pre-Order'),
LocalPickup: !!event.details?.LocalPickup,
IsOnHold: !!event.details?.IsOnHold,
TrackingNumber: event.details?.TrackingNumber,
ShipMethod: event.details?.ShipMethod,
PaymentMethod: event.details?.PaymentMethod,
...event.details
}
}))
.sort((a, b) => b.timestamp - a.timestamp);
setEvents(processedEvents);
setLastUpdate(DateTime.now());
setError(null);
} catch (err) {
console.error("[EVENT FEED] Error:", err);
setError(err.message);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchEvents();
const interval = setInterval(fetchEvents, 60000);
return () => clearInterval(interval);
}, []);
const getEventIcon = (type) => {
switch (type) {
case "placed_order":
return <ShoppingCart className="h-5 w-5" />;
case "shipped_order":
return <Package className="h-5 w-5" />;
case "account_created":
return <UserPlus className="h-5 w-5" />;
case "canceled_order":
return <XCircle className="h-5 w-5" />;
case "payment_refunded":
return <DollarSign className="h-5 w-5" />;
case "new_blog_post":
return <FileText className="h-5 w-5" />;
default:
return <AlertCircle className="h-5 w-5" />;
}
};
const getEventColor = (type) => {
switch (type) {
case "placed_order":
return "bg-green-500";
case "shipped_order":
return "bg-purple-500";
case "account_created":
return "bg-blue-500";
case "canceled_order":
return "bg-red-500";
case "payment_refunded":
return "bg-orange-500";
case "new_blog_post":
return "bg-yellow-500";
default:
return "bg-gray-500";
}
};
const getEventTitle = (type) => {
switch (type) {
case "placed_order":
return "Order Placed";
case "shipped_order":
return "Order Shipped";
case "account_created":
return "Account Created";
case "canceled_order":
return "Order Canceled";
case "payment_refunded":
return "Payment Refunded";
case "new_blog_post":
return "New Blog Post";
default:
return "Event";
}
};
const ShipmentStatus = ({ status, quantity }) => {
const getStatusInfo = (status) => {
switch (status?.toLowerCase()) {
case "shipped":
return {
color: "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400",
};
case "partial":
return {
color: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400",
};
case "pre-order":
return {
color: "bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400",
};
case "refunded":
return {
color: "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400",
};
default:
return {
color: "bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400",
};
}
};
const statusInfo = getStatusInfo(status);
return (
<span className={`px-2 py-1 rounded-full text-xs cursor-help ${statusInfo.color}`}>
{status} {quantity > 1 && `(${quantity})`}
</span>
);
};
const renderEventInlineInfo = useCallback((event) => {
const details = event.details || {};
switch (event.type) {
case 'placed_order':
return (
<div className="space-y-2">
<div className="flex items-center">
<span className="font-medium">{details.ShippingName}</span>
<span className="mx-2"></span>
<span className="text-gray-500">#{details.OrderId}</span>
</div>
<div className="flex items-center text-sm">
<span className="font-medium text-green-600">
{formatCurrency(details.TotalAmount)}
</span>
<span className="mx-2"></span>
<span>{details.Items?.length || 0} items</span>
</div>
<OrderStatusTags details={details} />
</div>
);
case 'shipped_order':
return (
<>
<div className="flex items-center">
<span className="font-medium">{details.ShippingName}</span>
<span className="mx-2"></span>
<span className="text-gray-500">#{details.OrderId}</span>
</div>
<div className="flex items-center text-sm">
<span>{formatShipMethodSimple(details.ShipMethod)}</span>
{details.TrackingNumber && (
<>
<span className="mx-2"></span>
<span className="font-mono">{details.TrackingNumber}</span>
</>
)}
</div>
</>
);
case "account_created":
return (
<>
<div className="font-medium">
{details.FirstName || ''} {details.LastName || ''}
</div>
<div className="text-sm text-gray-500">{details.EmailAddress}</div>
</>
);
case "canceled_order":
return (
<>
<div className="flex items-center">
<span className="font-medium">{details.ShippingName}</span>
<span className="mx-2"></span>
<span className="text-gray-500">#{details.OrderId}</span>
</div>
<div className="text-sm text-red-500">
{details.CancelReason || details.CancelMessage || "No reason provided"}
</div>
</>
);
case "payment_refunded":
return (
<>
<div className="flex items-center">
<span className="font-medium">
{details.FirstName || ''} {details.LastName || ''}
</span>
<span className="mx-2"></span>
<span className="text-gray-500">Order #{details.FromOrder}</span>
</div>
<div className="flex items-center text-sm">
<span className="font-medium text-orange-600">
{formatCurrency(details.PaymentAmount)}
</span>
<span className="mx-2"></span>
<span>{details.PaymentName}</span>
</div>
</>
);
case "new_blog_post":
return (
<>
<div className="font-medium">{details.title}</div>
<div className="text-sm text-gray-500 line-clamp-1">
{details.description}
</div>
</>
);
default:
return (
<div className="text-sm text-gray-500">No details available</div>
);
}
}, []);
const renderEventDetails = (event) => {
const details = event.details || {};
switch (event.type) {
case 'placed_order':
return (
<div className="space-y-6">
<OrderStatusTags details={details} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<ShippingInfo details={details} />
{details.BillingName && details.BillingName !== details.ShippingName && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-500">Billing Address</h4>
<div className="text-sm space-y-1">
<div className="font-medium">{details.BillingName}</div>
<div>{details.BillingStreet1}</div>
{details.BillingStreet2 && <div>{details.BillingStreet2}</div>}
<div>
{details.BillingCity}, {details.BillingState} {details.BillingZip}
</div>
</div>
</div>
)}
</div>
{details.Items?.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-500 mb-3">
Order Items ({details.Items.length})
</h4>
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{details.Items.map((item, index) => (
<div key={index} className="flex items-start space-x-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
{item.ImgThumb && (
<div className="w-20 h-20 flex-shrink-0">
<img
src={item.ImgThumb}
alt={item.ProductName}
className="w-full h-full object-cover rounded-lg border border-gray-200 dark:border-gray-600"
/>
</div>
)}
<div className="flex-grow min-w-0">
<div className="flex items-center space-x-2">
<h4 className="font-medium text-sm">{item.ProductName}</h4>
{item.ItemStatus === 'Pre-Order' && (
<span className="px-2 py-0.5 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-xs">
Pre-order {item.ItemEta && `(ETA: ${item.ItemEta})`}
</span>
)}
</div>
<div className="mt-1 text-sm text-gray-500">
<div>SKU: {item.SKU}</div>
{item.Brand && <div>Brand: {item.Brand}</div>}
<div className="mt-2">
Qty: {item.Quantity} × {formatCurrency(item.ItemPrice)}
</div>
</div>
{item.Categories?.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{item.Categories.map((category, idx) => (
<span
key={idx}
className="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded text-xs"
>
{category}
</span>
))}
</div>
)}
</div>
<div className="text-right flex-shrink-0">
<div className="font-medium">
{formatCurrency(item.RowTotal)}
</div>
</div>
</div>
))}
</div>
</div>
)}
<OrderSummary details={details} />
</div>
);
case "shipped_order":
return (
<div className="space-y-6">
<div className="bg-purple-50 dark:bg-purple-900/20 p-4 rounded-lg">
<div className="flex items-center space-x-3">
<Package className="h-6 w-6 text-purple-500" />
<div>
<h3 className="font-medium">Shipment Details</h3>
<p className="text-sm text-gray-500">
Shipped via {formatShipMethod(details.ShipMethod)}
</p>
</div>
</div>
{details.TrackingNumber && (
<div className="mt-3 text-sm">
<span className="text-gray-500">Tracking Number: </span>
<span className="font-mono font-medium">
{details.TrackingNumber}
</span>
</div>
)}
</div>
<ShippingInfo details={details} />
{details.Items?.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-500 mb-3">
Shipped Items ({details.Items.length})
</h4>
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{details.Items.map((item, index) => (
<ProductCard
key={index}
product={{
...item,
Quantity: item.QuantitySent,
Status:
item.QuantityBackordered > 0 ? "Partial" : "Shipped",
}}
/>
))}
</div>
</div>
)}
<OrderSummary details={details} />
</div>
);
case "account_created":
return (
<div className="space-y-6">
<div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-lg">
<div className="flex items-center justify-center space-x-4">
<div className="bg-blue-100 dark:bg-blue-900/50 p-3 rounded-full">
<UserPlus className="h-8 w-8 text-blue-500" />
</div>
<div className="text-center">
<h3 className="text-lg font-medium">
{details.FirstName} {details.LastName}
</h3>
<p className="text-gray-500">{details.EmailAddress}</p>
</div>
</div>
</div>
</div>
);
case "canceled_order":
return (
<div className="space-y-6">
<div className="bg-red-50 dark:bg-red-900/20 p-4 rounded-lg">
<div className="flex items-center space-x-3">
<XCircle className="h-6 w-6 text-red-500" />
<div>
<h3 className="font-medium">Cancellation Details</h3>
{(details.CancelReason || details.CancelMessage) && (
<p className="text-sm text-red-600 mt-1">
{details.CancelReason || details.CancelMessage}
</p>
)}
</div>
</div>
</div>
{details.Items?.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-500 mb-3">
Canceled Items ({details.Items.length})
</h4>
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{details.Items.map((item, index) => (
<ProductCard
key={index}
product={{
...item,
Quantity: item.QuantityHadOrdered,
}}
/>
))}
</div>
</div>
)}
<OrderSummary details={details} />
</div>
);
case "payment_refunded":
return (
<div className="space-y-6">
<div className="bg-orange-50 dark:bg-orange-900/20 p-4 rounded-lg">
<div className="flex items-center space-x-3">
<RefreshCcw className="h-6 w-6 text-orange-500" />
<div className="flex-1">
<h3 className="font-medium">Refund Details</h3>
<div className="mt-2 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-500">Customer</span>
<span className="font-medium">
{details.FirstName} {details.LastName}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Original Order</span>
<span className="font-medium">#{details.FromOrder}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Payment Method</span>
<span className="font-medium">{details.PaymentName}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Refund Amount</span>
<span className="font-medium text-orange-600">
{formatCurrency(details.PaymentAmount)}
</span>
</div>
</div>
</div>
</div>
</div>
{details.OrderMessage && (
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<h4 className="text-sm font-medium text-gray-500 mb-2">
Refund Reason
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
{details.OrderMessage}
</p>
</div>
)}
{details.EmailAddress && (
<div className="text-sm text-gray-500">
<span className="mr-2">Email:</span>
<span className="font-medium">{details.EmailAddress}</span>
</div>
)}
{/* If there are refunded items */}
{details.RefundedItems?.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-500 mb-3">
Refunded Items ({details.RefundedItems.length})
</h4>
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{details.RefundedItems.map((item, index) => (
<ProductCard
key={index}
product={{
...item,
Status: "Refunded",
}}
/>
))}
</div>
</div>
)}
</div>
);
case "new_blog_post":
return (
<div className="space-y-6">
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-6 rounded-lg">
<div className="flex flex-col items-center text-center">
<div className="bg-yellow-100 dark:bg-yellow-900/50 p-3 rounded-full mb-4">
<FileText className="h-8 w-8 text-yellow-600" />
</div>
<h3 className="text-xl font-medium mb-2">{details.title}</h3>
<div className="prose dark:prose-invert max-w-none">
{details.description && (
<div
className="text-gray-600 dark:text-gray-300"
dangerouslySetInnerHTML={{
__html: details.description.replace(/\n/g, "<br />"),
}}
/>
)}
</div>
</div>
</div>
</div>
);
default:
return (
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<pre className="overflow-auto text-sm">
{JSON.stringify(details, null, 2)}
</pre>
</div>
);
}
};
const renderContent = useMemo(() => {
if (isLoading && events.length === 0) {
return <LoadingState />;
}
if (error) {
return (
<div className="flex flex-col items-center justify-center p-6 text-center">
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Error Loading Feed
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
{error}
</p>
<button
onClick={fetchEvents}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
>
Try Again
</button>
</div>
);
}
if (!events || events.length === 0) {
return <EmptyState />;
}
return (
<div className="max-h-[500px] md:max-h-[887px] lg:max-h-[610px] xl:max-h-[420px] overflow-auto scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent">
{events.map((event) => (
<div
key={event.id}
className="flex items-center space-x-4 pt-4 pb-4 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer border-b dark:border-gray-800 last:border-b-0 transition-colors px-4"
onClick={() => setSelectedEvent(event)}
>
<div className={`rounded-full p-2 ${getEventColor(event.type)}`}>
{getEventIcon(event.type)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{getEventTitle(event.type)}
</p>
<div className="text-sm text-gray-500 dark:text-gray-400">
{renderEventInlineInfo(event)}
</div>
</div>
<div className="flex items-center">
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{event.timestamp instanceof DateTime
? event.timestamp.toFormat("h:mm a")
: DateTime.fromISO(event.timestamp).toFormat("h:mm a")}
</span>
<ChevronRight className="h-5 w-5 text-gray-400 dark:text-gray-600 ml-2" />
</div>
</div>
))}
</div>
);
}, [events, isLoading, error, renderEventInlineInfo]);
return (
<>
<Card className="bg-white dark:bg-gray-900 p-6">
<CardHeader className="p-0 pb-4">
<div className="flex justify-between items-center">
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Activity Feed
</CardTitle>
{lastUpdate && (
<span className="text-sm text-gray-500 dark:text-gray-400">
Last updated: {lastUpdate.toFormat("hh:mm a")}
</span>
)}
</div>
</CardHeader>
<CardContent className="p-0">{renderContent}</CardContent>
</Card>
{selectedEvent && (
<Dialog
open={!!selectedEvent}
onOpenChange={() => setSelectedEvent(null)}
>
<DialogContent className="max-w-2xl p-6 bg-white dark:bg-gray-900 max-h-[90vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<div className="flex items-center space-x-3">
<div className={`rounded-full p-2 ${getEventColor(selectedEvent.type)}`}>
{getEventIcon(selectedEvent.type)}
</div>
<div>
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
{getEventTitle(selectedEvent.type)}
</DialogTitle>
<div className="text-sm text-gray-500 dark:text-gray-400">
{selectedEvent.timestamp.toFormat('MMMM d, yyyy - h:mm a')}
</div>
</div>
</div>
</DialogHeader>
<div className="mt-6 overflow-y-auto flex-grow pr-2">
{renderEventDetails(selectedEvent)}
</div>
</DialogContent>
</Dialog>
)}
</>
);
};
export default EventFeed;