Skip to content
Merged
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 @@ -15,7 +15,9 @@ import {
import { api } from "~/trpc/react";
import AttendancesBarChart from "./event-data/AttendancesBarChart";
import AttendancesMobile from "./event-data/AttendancesMobile";
import FoundPie from "./event-data/HowFound";
import PopularityRanking from "./event-data/PopularityRanking";
import RankingRating from "./event-data/RatingRanking";
import TypePie from "./event-data/TypePie";
import { WeekdayPopularityRadar } from "./event-data/WeekdayPopularityRadar";

Expand Down Expand Up @@ -107,6 +109,12 @@ export default function EventDemographics() {
})
.sort((a, b) => (a.tag > b.tag ? 1 : a.tag < b.tag ? -1 : 0)); // ensure same order of tags

const { data: feedback } = api.event.getFeedback.useQuery({
startDate: activeSemester?.startDate ?? null,
endDate: activeSemester?.endDate ?? null,
includeHackathons,
});

return (
<div className="my-6">
{events && (
Expand Down Expand Up @@ -148,6 +156,7 @@ export default function EventDemographics() {
{filteredEvents && filteredEvents.length > 0 ? (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2">
<PopularityRanking events={filteredEvents} />
{feedback && <RankingRating feedback={feedback} />}

{/* visible on large/medium screens */}
<AttendancesBarChart
Expand All @@ -161,6 +170,7 @@ export default function EventDemographics() {
/>

<TypePie events={filteredEvents} />
<FoundPie found={(feedback ?? []).map((f) => f.howHear)} />
<WeekdayPopularityRadar events={filteredEvents} />
</div>
) : (
Expand Down
198 changes: 198 additions & 0 deletions apps/blade/src/app/admin/club/data/_components/event-data/HowFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"use client";

import type { PieSectorDataItem } from "recharts/types/polar/Pie";
import { useEffect, useMemo, useState } from "react";
import { Cell, Label, Pie, PieChart, Sector } from "recharts";

import type { ChartConfig } from "@forge/ui/chart";
import { FORMS } from "@forge/consts";
import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card";
import {
ChartContainer,
ChartStyle,
ChartTooltip,
ChartTooltipContent,
} from "@forge/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@forge/ui/select";

export default function FoundPie({ found }: { found: string[] }) {
const id = "pie-interactive";

// get amount of each tag
const foundCount: Record<string, number> = {};
found.forEach((t) => {
foundCount[t] = (foundCount[t] ?? 0) + 1;
});

const totalEvents = found.length;

const foundData = Object.entries(foundCount).map(([t, count]) => ({
name: t,
amount: count,
percentage: (totalEvents > 0 ? (count / totalEvents) * 100 : 0).toFixed(2),
}));

const [activeLevel, setActiveLevel] = useState(
foundData[0] ? foundData[0].name : null,
);

const activeIndex = useMemo(
() => foundData.findIndex((item) => item.name === activeLevel),
[activeLevel, foundData],
);
const founds = useMemo(() => foundData.map((item) => item.name), [foundData]);

useEffect(() => {
if (!foundData.some((item) => item.name === activeLevel)) {
setActiveLevel(foundData[0]?.name ?? null);
}
}, [foundData, activeLevel]);

// set up chart config
const baseConfig: ChartConfig = {
events: { label: "events" },
};
let colorIdx = 0;
found.forEach((t) => {
if (!baseConfig[t]) {
baseConfig[t] = {
label: t,
color:
FORMS.ADMIN_PIE_CHART_COLORS[
colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length
],
};
colorIdx++;
}
});

return (
<Card data-chart={id} className="flex flex-col">
<ChartStyle id={id} config={baseConfig} />
<CardHeader className="flex-col items-start gap-4 space-y-0 pb-0">
<div className="grid gap-1">
<CardTitle className="text-xl">Referred By</CardTitle>
</div>
<Select
value={activeLevel ? activeLevel : undefined}
onValueChange={setActiveLevel}
>
<SelectTrigger
className="ml-auto h-7 rounded-lg pl-2.5"
aria-label="Select a value"
>
<SelectValue placeholder="Select month" />
</SelectTrigger>
<SelectContent align="end" className="rounded-xl">
{founds.map((key) => {
const config = baseConfig[key];

if (!config) {
return null;
}

return (
<SelectItem
key={key}
value={key}
className="rounded-lg [&_span]:flex"
>
<div className="flex items-center gap-2 text-xs">
<span
className="flex h-3 w-3 shrink-0 rounded-sm"
style={{
backgroundColor: config.color,
}}
/>
{config.label}
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</CardHeader>
<CardContent className="mt-4 flex flex-1 justify-center pb-0">
<ChartContainer
id={id}
config={baseConfig}
className="mx-auto aspect-square w-full max-w-[300px]"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Pie
data={foundData}
dataKey="amount"
nameKey="name"
innerRadius={60}
strokeWidth={5}
activeIndex={activeIndex}
activeShape={({
outerRadius = 0,
...props
}: PieSectorDataItem) => (
<g>
<Sector {...props} outerRadius={outerRadius + 10} />
<Sector
{...props}
outerRadius={outerRadius + 25}
innerRadius={outerRadius + 12}
/>
</g>
)}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-2xl font-bold"
>
{foundData[activeIndex]?.percentage.toLocaleString()}%
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy ?? 0) + 24}
className="fill-muted-foreground"
>
Events
</tspan>
</text>
);
}
}}
/>
{foundData.map((_, index) => (
<Cell
key={`cell-${index}`}
fill={
FORMS.ADMIN_PIE_CHART_COLORS[
index % FORMS.ADMIN_PIE_CHART_COLORS.length
]
}
/>
))}
</Pie>
</PieChart>
</ChartContainer>
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useState } from "react";

import { FORMS } from "@forge/consts";
import { Button } from "@forge/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card";

interface Feedback {
event: string;
howHear: string;
rating: number;
}

export default function RatingRanking({ feedback }: { feedback: Feedback[] }) {
const [displayFullList, setDisplayFullList] = useState<boolean>(false);

const aggregate = new Map<string, { total: number; count: number }>();
feedback.forEach((f) => {
if (!aggregate.has(f.event)) aggregate.set(f.event, { total: 0, count: 0 });
const cur = aggregate.get(f.event);
if (cur)
aggregate.set(f.event, {
total: cur.total + f.rating,
count: cur.count + 1,
});
});
const averages = Array.from(aggregate.entries()).map(([k, v]) => {
return { name: k, average: v.total / v.count };
});

const topEvents = averages.sort((a, b) => b.average - a.average).slice(0, 10);

const handleClick = () => setDisplayFullList((prev) => !prev);

return (
<Card className="md:col-span-2 lg:col-span-2">
<CardHeader>
<CardTitle className="text-xl">Best Rated Events</CardTitle>
</CardHeader>
<CardContent>
{topEvents.length > 0 ? (
<ol className="mb-4 flex flex-col gap-2">
{(displayFullList ? topEvents : topEvents.slice(0, 3)).map(
(event, index: number) => (
<li
key={event.name}
className={`flex justify-between text-sm ${FORMS.RANKING_STYLES[index] ?? "text-gray-400 md:text-base lg:text-base"}`}
>
<span className="me-4">
{index + 1}. {event.name}
</span>
<span>Average Rating: {event.average}</span>
</li>
),
)}
</ol>
) : (
<p className="mb-14 mt-10 text-center text-slate-300">
No rating data found
</p>
)}
<div className="flex justify-center">
{topEvents.length > 3 && ( // no need for show more toggle if there are 3 or less events
<Button variant="secondary" onClick={handleClick}>
{displayFullList ? "Show less" : "Show more"}
</Button>
)}
</div>
</CardContent>
</Card>
);
}
30 changes: 27 additions & 3 deletions apps/blade/src/app/admin/club/events/_components/events-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { DeleteEventButton } from "./delete-event";
import { EventDetailsButton } from "./event-details";
import { UpdateEventButton } from "./update-event";
import { ViewAttendanceButton } from "./view-attendance-button";
import { ViewFeedbackButton } from "./view-feedback-button";
import { ViewRatingButton } from "./view-rating-button";

type Event = ReturnEvent;
type SortField = keyof Event;
Expand Down Expand Up @@ -148,6 +150,12 @@ export function EventsTable() {
setSortOrder={setSortOrder}
/>
</TableHead>
<TableHead className="text-center">
<Label>Rating</Label>
</TableHead>
<TableHead className="text-center">
<Label>Feedback</Label>
</TableHead>
<TableHead className="text-center">
<Label>Event Details</Label>
</TableHead>
Expand All @@ -166,7 +174,7 @@ export function EventsTable() {
<TableRow>
<TableCell
className="text- bg-muted/50 font-bold sm:text-center"
colSpan={8}
colSpan={10}
>
Upcoming Events
</TableCell>
Expand All @@ -192,6 +200,14 @@ export function EventsTable() {
/>
</TableCell>

<TableCell className="text-center">
<ViewRatingButton event={event} />
</TableCell>

<TableCell className="text-center">
<ViewFeedbackButton event={event} />
</TableCell>

<TableCell className="text-center">
<EventDetailsButton
event={{ ...event, hackathonName: undefined }}
Expand All @@ -214,7 +230,7 @@ export function EventsTable() {
<TableRow>
<TableCell
className="bg-muted/50 text-left font-bold sm:text-center"
colSpan={8}
colSpan={10}
>
Previous Events
</TableCell>
Expand All @@ -240,6 +256,14 @@ export function EventsTable() {
/>
</TableCell>

<TableCell className="text-center">
<ViewRatingButton event={event} />
</TableCell>

<TableCell className="text-center">
<ViewFeedbackButton event={event} />
</TableCell>

<TableCell className="text-center">
<EventDetailsButton
event={{ ...event, hackathonName: undefined }}
Expand All @@ -264,7 +288,7 @@ export function EventsTable() {
<TableCell className="text-right">
{sortedEvents.reduce((sum, event) => sum + event.numAttended, 0)}
</TableCell>
<TableCell colSpan={3} />
<TableCell colSpan={5} />
</TableRow>
</TableFooter>
</Table>
Expand Down
Loading
Loading