Add filters
This commit is contained in:
@@ -40,6 +40,14 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
const METRIC_IDS = {
|
const METRIC_IDS = {
|
||||||
PLACED_ORDER: "Y8cqcF",
|
PLACED_ORDER: "Y8cqcF",
|
||||||
@@ -1121,6 +1129,23 @@ const EventFeed = ({
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [lastUpdate, setLastUpdate] = useState(null);
|
const [lastUpdate, setLastUpdate] = useState(null);
|
||||||
|
const [activeEventTypes, setActiveEventTypes] = useState({
|
||||||
|
[METRIC_IDS.PLACED_ORDER]: true,
|
||||||
|
[METRIC_IDS.SHIPPED_ORDER]: true,
|
||||||
|
[METRIC_IDS.ACCOUNT_CREATED]: true,
|
||||||
|
[METRIC_IDS.CANCELED_ORDER]: true,
|
||||||
|
[METRIC_IDS.PAYMENT_REFUNDED]: true,
|
||||||
|
[METRIC_IDS.NEW_BLOG_POST]: true,
|
||||||
|
});
|
||||||
|
const [orderFilters, setOrderFilters] = useState({
|
||||||
|
hasPreorder: false,
|
||||||
|
localPickup: false,
|
||||||
|
isOnHold: false,
|
||||||
|
hasDigiItem: false,
|
||||||
|
hasNotions: false,
|
||||||
|
hasGiftCard: false,
|
||||||
|
stillOwes: false,
|
||||||
|
});
|
||||||
|
|
||||||
const fetchEvents = useCallback(async () => {
|
const fetchEvents = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -1168,40 +1193,357 @@ const EventFeed = ({
|
|||||||
};
|
};
|
||||||
}, [fetchEvents]);
|
}, [fetchEvents]);
|
||||||
|
|
||||||
|
const filteredEvents = useMemo(() => {
|
||||||
|
// Check if any order property filters are active
|
||||||
|
const hasActiveOrderFilters = Object.values(orderFilters).some(filter => filter);
|
||||||
|
|
||||||
|
return events.filter(event => {
|
||||||
|
// First check event type filter
|
||||||
|
if (!activeEventTypes[event.metric_id]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check order property filters if any are active
|
||||||
|
if (hasActiveOrderFilters) {
|
||||||
|
if (event.metric_id !== METRIC_IDS.PLACED_ORDER) return false;
|
||||||
|
|
||||||
|
const details = event.event_properties || {};
|
||||||
|
if (orderFilters.hasPreorder && !details.HasPreorder) return false;
|
||||||
|
if (orderFilters.localPickup && !details.LocalPickup) return false;
|
||||||
|
if (orderFilters.isOnHold && !details.IsOnHold) return false;
|
||||||
|
if (orderFilters.hasDigiItem && !details.HasDigiItem) return false;
|
||||||
|
if (orderFilters.hasNotions && !details.HasNotions) return false;
|
||||||
|
if (orderFilters.hasGiftCard && !details.HasDigitalGC) return false;
|
||||||
|
if (orderFilters.stillOwes && !details.StillOwes) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [events, activeEventTypes, orderFilters]);
|
||||||
|
|
||||||
|
// Calculate counts for event types and order properties
|
||||||
|
const counts = useMemo(() => {
|
||||||
|
const eventTypeCounts = {
|
||||||
|
[METRIC_IDS.PLACED_ORDER]: 0,
|
||||||
|
[METRIC_IDS.SHIPPED_ORDER]: 0,
|
||||||
|
[METRIC_IDS.ACCOUNT_CREATED]: 0,
|
||||||
|
[METRIC_IDS.CANCELED_ORDER]: 0,
|
||||||
|
[METRIC_IDS.PAYMENT_REFUNDED]: 0,
|
||||||
|
[METRIC_IDS.NEW_BLOG_POST]: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderPropertyCounts = {
|
||||||
|
hasPreorder: 0,
|
||||||
|
localPickup: 0,
|
||||||
|
isOnHold: 0,
|
||||||
|
hasDigiItem: 0,
|
||||||
|
hasNotions: 0,
|
||||||
|
hasGiftCard: 0,
|
||||||
|
stillOwes: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
events.forEach(event => {
|
||||||
|
// Count event types
|
||||||
|
if (event.metric_id) {
|
||||||
|
eventTypeCounts[event.metric_id]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count order properties
|
||||||
|
if (event.metric_id === METRIC_IDS.PLACED_ORDER) {
|
||||||
|
const details = event.event_properties || {};
|
||||||
|
if (details.HasPreorder) orderPropertyCounts.hasPreorder++;
|
||||||
|
if (details.LocalPickup) orderPropertyCounts.localPickup++;
|
||||||
|
if (details.IsOnHold) orderPropertyCounts.isOnHold++;
|
||||||
|
if (details.HasDigiItem) orderPropertyCounts.hasDigiItem++;
|
||||||
|
if (details.HasNotions) orderPropertyCounts.hasNotions++;
|
||||||
|
if (details.HasDigitalGC) orderPropertyCounts.hasGiftCard++;
|
||||||
|
if (details.StillOwes) orderPropertyCounts.stillOwes++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventTypes: eventTypeCounts,
|
||||||
|
orderProperties: orderPropertyCounts,
|
||||||
|
};
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
const handleOrderPropertyClick = (property) => {
|
||||||
|
setOrderFilters(prev => {
|
||||||
|
// If clicking the active filter, clear all filters
|
||||||
|
if (prev[property]) {
|
||||||
|
return {
|
||||||
|
hasPreorder: false,
|
||||||
|
localPickup: false,
|
||||||
|
isOnHold: false,
|
||||||
|
hasDigiItem: false,
|
||||||
|
hasNotions: false,
|
||||||
|
hasGiftCard: false,
|
||||||
|
stillOwes: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Otherwise, set only this filter to true
|
||||||
|
return {
|
||||||
|
hasPreorder: property === 'hasPreorder',
|
||||||
|
localPickup: property === 'localPickup',
|
||||||
|
isOnHold: property === 'isOnHold',
|
||||||
|
hasDigiItem: property === 'hasDigiItem',
|
||||||
|
hasNotions: property === 'hasNotions',
|
||||||
|
hasGiftCard: property === 'hasGiftCard',
|
||||||
|
stillOwes: property === 'stillOwes',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="p-6 pb-0">
|
<CardHeader className="p-6 pb-0">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
|
||||||
</div>
|
|
||||||
{lastUpdate && (
|
{lastUpdate && (
|
||||||
<span className="text-sm text-muted-foreground">
|
<CardDescription>
|
||||||
Last updated: {format(lastUpdate, "hh:mm a")}
|
Last updated: {format(lastUpdate, "hh:mm a")}
|
||||||
</span>
|
</CardDescription>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={activeEventTypes[METRIC_IDS.PLACED_ORDER] ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setActiveEventTypes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[METRIC_IDS.PLACED_ORDER]: !prev[METRIC_IDS.PLACED_ORDER]
|
||||||
|
}))}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Package className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Orders</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{counts.eventTypes[METRIC_IDS.PLACED_ORDER]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={activeEventTypes[METRIC_IDS.SHIPPED_ORDER] ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setActiveEventTypes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[METRIC_IDS.SHIPPED_ORDER]: !prev[METRIC_IDS.SHIPPED_ORDER]
|
||||||
|
}))}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Truck className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Shipments</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{counts.eventTypes[METRIC_IDS.SHIPPED_ORDER]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={activeEventTypes[METRIC_IDS.ACCOUNT_CREATED] ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setActiveEventTypes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[METRIC_IDS.ACCOUNT_CREATED]: !prev[METRIC_IDS.ACCOUNT_CREATED]
|
||||||
|
}))}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<UserPlus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Accounts</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{counts.eventTypes[METRIC_IDS.ACCOUNT_CREATED]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={activeEventTypes[METRIC_IDS.CANCELED_ORDER] ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setActiveEventTypes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[METRIC_IDS.CANCELED_ORDER]: !prev[METRIC_IDS.CANCELED_ORDER]
|
||||||
|
}))}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<XCircle className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Cancellations</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{counts.eventTypes[METRIC_IDS.CANCELED_ORDER]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={activeEventTypes[METRIC_IDS.PAYMENT_REFUNDED] ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setActiveEventTypes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[METRIC_IDS.PAYMENT_REFUNDED]: !prev[METRIC_IDS.PAYMENT_REFUNDED]
|
||||||
|
}))}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<DollarSign className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Refunds</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{counts.eventTypes[METRIC_IDS.PAYMENT_REFUNDED]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={activeEventTypes[METRIC_IDS.NEW_BLOG_POST] ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setActiveEventTypes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[METRIC_IDS.NEW_BLOG_POST]: !prev[METRIC_IDS.NEW_BLOG_POST]
|
||||||
|
}))}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Blog Posts</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{counts.eventTypes[METRIC_IDS.NEW_BLOG_POST]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Order Property Filters */}
|
||||||
|
<div className="flex flex-wrap gap-2 justify-center mt-4">
|
||||||
|
<span
|
||||||
|
onClick={() => handleOrderPropertyClick('hasPreorder')}
|
||||||
|
className={`px-2 py-1 ${
|
||||||
|
orderFilters.hasPreorder
|
||||||
|
? 'bg-blue-800 text-blue-100'
|
||||||
|
: 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300'
|
||||||
|
} rounded-full text-xs font-medium cursor-help`}
|
||||||
|
>
|
||||||
|
Pre-order {counts.orderProperties.hasPreorder > 0 && `(${counts.orderProperties.hasPreorder})`}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleOrderPropertyClick('localPickup')}
|
||||||
|
className={`px-2 py-1 ${
|
||||||
|
orderFilters.localPickup
|
||||||
|
? 'bg-purple-800 text-purple-100'
|
||||||
|
: 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300'
|
||||||
|
} rounded-full text-xs font-medium cursor-help`}
|
||||||
|
>
|
||||||
|
Local {counts.orderProperties.localPickup > 0 && `(${counts.orderProperties.localPickup})`}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleOrderPropertyClick('isOnHold')}
|
||||||
|
className={`px-2 py-1 ${
|
||||||
|
orderFilters.isOnHold
|
||||||
|
? 'bg-yellow-800 text-yellow-100'
|
||||||
|
: 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300'
|
||||||
|
} rounded-full text-xs font-medium cursor-help`}
|
||||||
|
>
|
||||||
|
On Hold {counts.orderProperties.isOnHold > 0 && `(${counts.orderProperties.isOnHold})`}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleOrderPropertyClick('hasDigiItem')}
|
||||||
|
className={`px-2 py-1 ${
|
||||||
|
orderFilters.hasDigiItem
|
||||||
|
? 'bg-green-800 text-green-100'
|
||||||
|
: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
|
||||||
|
} rounded-full text-xs font-medium cursor-help`}
|
||||||
|
>
|
||||||
|
Digital {counts.orderProperties.hasDigiItem > 0 && `(${counts.orderProperties.hasDigiItem})`}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleOrderPropertyClick('hasNotions')}
|
||||||
|
className={`px-2 py-1 ${
|
||||||
|
orderFilters.hasNotions
|
||||||
|
? 'bg-pink-800 text-pink-100'
|
||||||
|
: 'bg-pink-100 dark:bg-pink-900/20 text-pink-800 dark:text-pink-300'
|
||||||
|
} rounded-full text-xs font-medium cursor-help`}
|
||||||
|
>
|
||||||
|
Notions {counts.orderProperties.hasNotions > 0 && `(${counts.orderProperties.hasNotions})`}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleOrderPropertyClick('hasGiftCard')}
|
||||||
|
className={`px-2 py-1 ${
|
||||||
|
orderFilters.hasGiftCard
|
||||||
|
? 'bg-indigo-800 text-indigo-100'
|
||||||
|
: 'bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800 dark:text-indigo-300'
|
||||||
|
} rounded-full text-xs font-medium cursor-help`}
|
||||||
|
>
|
||||||
|
Gift Card {counts.orderProperties.hasGiftCard > 0 && `(${counts.orderProperties.hasGiftCard})`}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleOrderPropertyClick('stillOwes')}
|
||||||
|
className={`px-2 py-1 ${
|
||||||
|
orderFilters.stillOwes
|
||||||
|
? 'bg-red-800 text-red-100'
|
||||||
|
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
|
||||||
|
} rounded-full text-xs font-medium cursor-help`}
|
||||||
|
>
|
||||||
|
Owes {counts.orderProperties.stillOwes > 0 && `(${counts.orderProperties.stillOwes})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="p-6 pt-4 flex-1 overflow-hidden">
|
<CardContent className="p-6 pt-4 flex-1 overflow-hidden">
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
{loading && !events.length ? (
|
{loading && !events.length ? (
|
||||||
<div className="space-y-4 w-full">
|
<LoadingState />
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<div key={i} className="flex items-center space-x-4 p-4 border-b dark:border-gray-800">
|
|
||||||
<div className="rounded-full">
|
|
||||||
<Skeleton className="h-10 w-10 rounded-full" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0 space-y-2">
|
|
||||||
<Skeleton className="h-4 w-[200px]" />
|
|
||||||
<Skeleton className="h-3 w-[150px]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Skeleton className="h-3 w-16" />
|
|
||||||
<Skeleton className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="flex flex-col items-center justify-center p-6 text-center">
|
<div className="flex flex-col items-center justify-center p-6 text-center">
|
||||||
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
|
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
|
||||||
@@ -1218,7 +1560,7 @@ const EventFeed = ({
|
|||||||
Try Again
|
Try Again
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : !events || events.length === 0 ? (
|
) : !filteredEvents || filteredEvents.length === 0 ? (
|
||||||
<div className="h-full flex flex-col items-center justify-center py-16 px-4">
|
<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">
|
<div className="bg-gray-100 dark:bg-gray-800 rounded-full p-3 mb-4">
|
||||||
<Activity className="h-8 w-8 text-muted-foreground" />
|
<Activity className="h-8 w-8 text-muted-foreground" />
|
||||||
@@ -1232,7 +1574,7 @@ const EventFeed = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||||
{events.map((event) => (
|
{filteredEvents.map((event) => (
|
||||||
<EventCard key={event.id} event={event} />
|
<EventCard key={event.id} event={event} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user