Skip to content
Merged

sse #43

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const AudienceViewsChart: React.FC<AudienceViewsChartProps> = ({data, tot
<CardTitle>Audience views</CardTitle>
<CardDescription>Event impressions over the last 30 days</CardDescription>
</CardHeader>
<CardContent className="flex h-[320px] flex-col justify-center gap-4">
<CardContent className="flex h-[320px] flex-col justify-center gap-4 pb-2">
{isLoading ? (
<Skeleton className="h-[260px] w-full"/>
) : chartData.length === 0 ? (
Expand All @@ -55,10 +55,15 @@ export const AudienceViewsChart: React.FC<AudienceViewsChartProps> = ({data, tot
<span className="font-semibold text-foreground">{totalViews.toLocaleString("en-LK")}</span>
</div>
<ChartContainer config={CHART_CONFIG} className="h-full w-full">
<LineChart data={chartData} margin={{left: 0, right: 12, top: 4, bottom: 12}}>
<LineChart data={chartData} margin={{left: 0, right: 12, top: 20, bottom: 12}}>
<CartesianGrid vertical={false} strokeDasharray="4 4" className="stroke-border/60"/>
<XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8}/>
<YAxis tickLine={false} axisLine={false} allowDecimals={false}/>
<YAxis
tickLine={false}
axisLine={false}
allowDecimals={false}
domain={['auto', 'auto']}
/>
<ChartTooltip
content={<ChartTooltipContent
nameKey="views"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
"use client";

import React, {useCallback, useMemo} from "react";
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import {Badge} from "@/components/ui/badge";
import {Button} from "@/components/ui/button";
import {cn, formatCurrency} from "@/lib/utils";
import {Share2} from "lucide-react";
import {useEventContext} from "@/providers/EventProvider";
import {toast} from "sonner";
import {animate} from "framer-motion";

const useAnimatedNumber = (value: number | null, duration = 0.6) => {
const [displayValue, setDisplayValue] = useState(() => value ?? 0);
const previousValueRef = useRef<number>(value ?? 0);

useEffect(() => {
if (value === null || value === undefined) {
setDisplayValue(0);
previousValueRef.current = 0;
return;
}

const controls = animate(previousValueRef.current, value, {
duration,
ease: "easeOut",
onUpdate: latest => setDisplayValue(latest),
});

return () => {
controls.stop();
};
}, [value, duration]);

useEffect(() => {
previousValueRef.current = displayValue;
}, [displayValue]);

return displayValue;
};

const formatTimestamp = (timestamp?: Date | null) => {
if (!timestamp) {
Expand Down Expand Up @@ -38,6 +68,8 @@ export const EventRevenueHero: React.FC = () => {

const resolvedRevenue = typeof liveRevenueTotal === "number" ? liveRevenueTotal : null;
const resolvedTickets = typeof liveTicketsSold === "number" ? liveTicketsSold : null;
const animatedRevenue = useAnimatedNumber(resolvedRevenue);
const animatedTickets = useAnimatedNumber(resolvedTickets);

const shareUrl = useMemo(() => {
if (!event?.id) {
Expand Down Expand Up @@ -143,7 +175,7 @@ export const EventRevenueHero: React.FC = () => {
<p className="mt-3 text-3xl font-semibold tracking-tight">
{resolvedRevenue === null
? "--"
: formatCurrency(resolvedRevenue, "LKR", "en-LK")}
: formatCurrency(animatedRevenue, "LKR", "en-LK")}
</p>
<span className="mt-2 block text-xs text-white/70">
Total gross revenue confirmed across all sessions.
Expand All @@ -155,7 +187,7 @@ export const EventRevenueHero: React.FC = () => {
<p className="mt-3 text-3xl font-semibold tracking-tight">
{resolvedTickets === null
? "--"
: resolvedTickets.toLocaleString("en-LK")}
: Math.round(animatedTickets).toLocaleString("en-LK")}
</p>
<span className="mt-2 block text-xs text-white/70">
Confirmed tickets issued for this event.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ export const EventViewsChart: React.FC<{ data: TimeSeriesData[] }> = ({ data })
<CardTitle>Page Views Trend</CardTitle>
<CardDescription>Daily event page views (last 30 days)</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="pb-6">
<ChartContainer config={chartConfig} className="h-[250px] w-full">
<LineChart accessibilityLayer data={data} margin={{ left: 0, right: 12 }}>
<LineChart accessibilityLayer data={data} margin={{ left: 0, right: 12, top: 20, bottom: 8 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
Expand All @@ -37,14 +37,20 @@ export const EventViewsChart: React.FC<{ data: TimeSeriesData[] }> = ({ data })
tickMargin={8}
tickFormatter={formatAxisDate}
/>
<YAxis tickLine={false} axisLine={false} tickMargin={8} />
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
domain={['auto', 'auto']}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Line
dataKey="views"
type="natural"
type="monotone"
stroke="var(--color-chart-1)"
strokeWidth={2}
dot={false}
activeDot={{ r: 4 }}
/>
</LineChart>
</ChartContainer>
Expand Down
52 changes: 40 additions & 12 deletions src/providers/EventProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, {createContext, useContext, useState, useCallback, ReactNode, useEffect, useRef} from "react";
import React, {createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState} from "react";
import {getMyEventById} from "@/lib/actions/eventActions";
import {EventDetailDTO, SessionDetailDTO} from "@/lib/validators/event";
import {OrganizationResponse} from "@/types/oraganizations";
Expand All @@ -11,6 +11,7 @@ import {getSession, getSessionsByEventId} from "@/lib/actions/sessionActions";
import {subscribeToSse} from "@/lib/api";
import {OrderCheckoutSseEvent} from "@/types/order";
import {parseOrderCheckoutEvent} from "@/lib/orderSseUtils";
import {getEventRevenueAnalytics} from "@/lib/actions/analyticsActions";

interface EventContextProps {
event: EventDetailDTO | null;
Expand Down Expand Up @@ -58,6 +59,13 @@ export const EventProvider = ({children, eventId}: EventProviderProps) => {
const {organizations, organization: currentOrganization, switchOrganization} = useOrganization();
const processedOrderIdsRef = useRef<Set<string>>(new Set());

const seedRevenueSnapshot = useCallback((payload: {totalRevenue?: number | null; totalTickets?: number | null}) => {
const {totalRevenue, totalTickets} = payload;
setLiveRevenueTotal(typeof totalRevenue === "number" && Number.isFinite(totalRevenue) ? Math.max(totalRevenue, 0) : 0);
setLiveTicketsSold(typeof totalTickets === "number" && Number.isFinite(totalTickets) ? Math.max(totalTickets, 0) : 0);
setLastRevenueUpdateAt(new Date());
}, []);

const fetchEventData = useCallback(async () => {
if (!eventId) {
setError("No event ID provided");
Expand Down Expand Up @@ -105,7 +113,37 @@ export const EventProvider = ({children, eventId}: EventProviderProps) => {
return;
}

const unsubscribe = subscribeToSse<OrderCheckoutSseEvent>(
let cancelled = false;

const preloadRevenueSnapshot = async () => {
try {
const analytics = await getEventRevenueAnalytics(eventId);
if (cancelled || !analytics) {
return;
}

seedRevenueSnapshot({
totalRevenue: analytics.total_revenue,
totalTickets: analytics.total_tickets_sold,
});
} catch (err) {
console.error("Failed to preload revenue analytics", err);
}
};

preloadRevenueSnapshot();

return () => {
cancelled = true;
};
}, [eventId, seedRevenueSnapshot]);

useEffect(() => {
if (!eventId) {
return;
}

return subscribeToSse<OrderCheckoutSseEvent>(
`/order/sse/checkouts/event/${eventId}`,
{
onMessage: ({event: eventName, data}) => {
Expand Down Expand Up @@ -158,18 +196,8 @@ export const EventProvider = ({children, eventId}: EventProviderProps) => {
},
},
);

return unsubscribe;
}, [eventId]);

const seedRevenueSnapshot = useCallback((payload: {totalRevenue?: number | null; totalTickets?: number | null}) => {
const {totalRevenue, totalTickets} = payload;
setLiveRevenueTotal(typeof totalRevenue === "number" && Number.isFinite(totalRevenue) ? Math.max(totalRevenue, 0) : 0);
setLiveTicketsSold(typeof totalTickets === "number" && Number.isFinite(totalTickets) ? Math.max(totalTickets, 0) : 0);
setLastRevenueUpdateAt(new Date());
}, []);


const fetchDiscounts = useCallback(async () => {
if (!event?.id) return;
setIsLoading(true);
Expand Down