Skip to content
Closed
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
8 changes: 7 additions & 1 deletion apps/blade/src/app/_components/bad-perms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { ShieldX } from "lucide-react";
import type { PermissionKey } from "@forge/consts";
import { PERMISSION_DATA } from "@forge/consts";

/**
* Renders an "Access Denied" alert that lists human-readable names for the given permission keys.
*
* @param perms - Array of permission keys to display; unknown or missing keys are ignored
* @returns A JSX element containing an access-denied alert with the permission names joined by ", "
*/
export function BadPerms({ perms }: { perms: PermissionKey[] }) {
const permNames: string[] = [];
perms.forEach((v) => {
Expand All @@ -22,4 +28,4 @@ export function BadPerms({ perms }: { perms: PermissionKey[] }) {
<div className="font-semibold">{permNames.join(", ")}</div>
</div>
);
}
}
10 changes: 9 additions & 1 deletion apps/blade/src/app/_components/discord-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import {
DialogTitle,
} from "@forge/ui/dialog";

/**
* Render a dialog prompting the user to join the Knight Hacks Discord.
*
* The dialog's open state is initialized from `initialState`. The primary action opens the permanent Discord invite in a new tab; the secondary action closes the dialog.
*
* @param initialState - Whether the dialog is open on initial render
* @returns The rendered dialog element
*/
export function TacoTuesday({ initialState }: { initialState: boolean }) {
const [open, setOpen] = useState<boolean>(initialState);
return (
Expand Down Expand Up @@ -73,4 +81,4 @@ export function TacoTuesday({ initialState }: { initialState: boolean }) {
</DialogContent>
</Dialog>
);
}
}
8 changes: 7 additions & 1 deletion apps/blade/src/app/admin/_components/GenderPie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ interface Person {
gender?: (typeof FORMS.GENDERS)[number];
}

/**
* Render an interactive donut chart of people grouped by gender with a select control to choose the active slice.
*
* @param people - Array of person objects whose `gender` values will be aggregated for the chart
* @returns A Card element containing a responsive donut chart, tooltip, central count label for the active gender, and a dropdown to change the active slice
*/
export default function GenderPie({ people }: { people: Person[] }) {
const id = "pie-interactive";

Expand Down Expand Up @@ -206,4 +212,4 @@ export default function GenderPie({ people }: { people: Person[] }) {
</CardContent>
</Card>
);
}
}
13 changes: 12 additions & 1 deletion apps/blade/src/app/admin/_components/MajorBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ interface Person {
major?: (typeof FORMS.MAJORS)[number];
}

/**
* Render a vertical bar chart showing the top 10 majors by student count.
*
* The component tallies majors from `people`, sorts majors by count descending,
* truncates major names longer than 35 characters for display, and renders a
* vertical bar chart with tooltips and inline count labels. If no major data
* is present, a centered fallback message is shown.
*
* @param people - Array of Person objects to aggregate by `major`
* @returns The rendered Card element containing the majors bar chart or a fallback message
*/
export default function MajorBarChart({ people }: { people: Person[] }) {
const majorCounts: Record<string, number> = {};

Expand Down Expand Up @@ -121,4 +132,4 @@ export default function MajorBarChart({ people }: { people: Person[] }) {
</CardContent>
</Card>
);
}
}
8 changes: 7 additions & 1 deletion apps/blade/src/app/admin/_components/RaceOrEthnicityPie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ const shortenRaceOrEthnicity = (raceOrEthnicity: string): string => {
return replacements[raceOrEthnicity] ?? raceOrEthnicity;
};

/**
* Render a pie chart showing the distribution of people by race or ethnicity with a selectable active segment.
*
* @param people - Array of Person objects whose `raceOrEthnicity` values will be counted and displayed
* @returns The React element that renders an interactive pie chart and a select control for choosing the active segment
*/
export default function RaceOrEthnicityPie({ people }: { people: Person[] }) {
const id = "pie-interactive";

Expand Down Expand Up @@ -227,4 +233,4 @@ export default function RaceOrEthnicityPie({ people }: { people: Person[] }) {
</CardContent>
</Card>
);
}
}
10 changes: 9 additions & 1 deletion apps/blade/src/app/admin/_components/SchoolBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ interface Person {
school?: (typeof FORMS.SCHOOLS)[number];
}

/**
* Render a vertical bar chart of the top 10 schools by number of people.
*
* Truncates school names longer than 35 characters for axis labels while preserving full names in tooltips, and displays a centered message when no school data is available.
*
* @param people - Array of Person objects used to compute counts per school.
* @returns The rendered Schools bar chart element.
*/
export default function SchoolBarChart({ people }: { people: Person[] }) {
const schoolCounts: Record<string, number> = {};

Expand Down Expand Up @@ -120,4 +128,4 @@ export default function SchoolBarChart({ people }: { people: Person[] }) {
</CardContent>
</Card>
);
}
}
11 changes: 10 additions & 1 deletion apps/blade/src/app/admin/_components/SchoolYearPie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ const shortenLevelOfStudy = (levelOfStudy: string): string => {
return replacements[levelOfStudy] ?? levelOfStudy;
};

/**
* Render an interactive pie chart that visualizes counts of people grouped by level of study.
*
* The chart includes a selector to choose the active level, highlights the active slice, and
* shows the selected level's count in the chart center.
*
* @param people - Array of Person objects used to compute counts per level of study.
* @returns A card element containing the interactive pie chart and level selector.
*/
export default function SchoolYearPie({ people }: { people: Person[] }) {
const id = "pie-interactive";

Expand Down Expand Up @@ -225,4 +234,4 @@ export default function SchoolYearPie({ people }: { people: Person[] }) {
</CardContent>
</Card>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import TypePie from "./event-data/TypePie";
import { WeekdayPopularityRadar } from "./event-data/WeekdayPopularityRadar";
import { FORMS } from '@forge/consts';

/**
* Render interactive event demographics and visualizations with semester filtering and an optional hackathon inclusion toggle.
*
* The component fetches events, derives selectable semester ranges from event dates (plus an "All Semesters" option), and filters events by the selected semester and hackathon inclusion setting. It displays a semester Select control, a checkbox to include/exclude hackathon events, multiple responsive visualizations for the filtered events, and a centered message when no events match the selection.
*
* @returns A React element containing the semester selector, hackathon toggle, and data visualizations (or a "No events found" message when the filtered set is empty).
*/
export default function EventDemographics() {
const { data: events } = api.event.getEvents.useQuery();
const semestersArr: FORMS.Semester[] = [
Expand Down Expand Up @@ -172,4 +179,4 @@ export default function EventDemographics() {
)}
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ import {
} from "@forge/ui/chart";
import { FORMS } from '@forge/consts';

/**
* Render a vertical bar chart showing average attendances grouped by event tag.
*
* Averages are computed per tag from events where (numAttended + numHackerAttended) is greater than or equal to 5; each tag's average is rounded to the nearest integer. If no tags qualify, a "No attendance data found" message is rendered instead.
*
* @param events - Array of events used to compute average attendances; each event's total attendees is `numAttended + numHackerAttended` and contributes to the tag's average only when that total is >= 5
* @param className - Optional CSS class applied to the outer Card component
* @returns A Card containing a vertical bar chart of average attendees by event type, or a centered message when no data is available
*/
export default function AttendancesBarChart({
events,
className,
Expand Down Expand Up @@ -132,4 +141,4 @@ export default function AttendancesBarChart({
</CardContent>
</Card>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import { Button } from "@forge/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card";
import { FORMS } from '@forge/consts';

/**
* Render a card titled "Most Popular Events" showing events ranked by total attendance.
*
* Displays up to 10 events that have at least one attendee, showing the top 3 by default.
* If more than 3 events are available, a "Show more" / "Show less" toggle reveals or hides the full list.
*
* @param events - Array of events to rank; each event is expected to include `id`, `name`, `tag`, `numAttended`, and `numHackerAttended`
* @returns A React element containing an ordered list of ranked events with attendance counts and an optional toggle button
*/
export default function PopularityRanking({
events,
}: {
Expand Down Expand Up @@ -64,4 +73,4 @@ export default function PopularityRanking({
</CardContent>
</Card>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ import {
} from "@forge/ui/select";
import { FORMS } from '@forge/consts';

/**
* Render an interactive pie chart that visualizes the distribution of events grouped by tag.
*
* Aggregates the provided events by their `tag` field, computes counts and percentages for each
* tag, and renders a selectable card containing a pie chart with a center label showing the
* active slice's percentage.
*
* @param events - Array of events whose `tag` values will be counted and displayed
* @returns The JSX element rendering a selectable pie chart summary of event types by tag
*/
export default function TypePie({ events }: { events: ReturnEvent[] }) {
const id = "pie-interactive";

Expand Down Expand Up @@ -195,4 +205,4 @@ export default function TypePie({ events }: { events: ReturnEvent[] }) {
</CardContent>
</Card>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ import {
} from "@forge/ui/chart";
import { FORMS } from '@forge/consts';

/**
* Render a card containing a radar chart of average attendance grouped by weekday.
*
* Only events whose combined attendees (numAttended + numHackerAttended) are 5 or more are included; Saturday and Sunday are combined under "Sat/Sun".
*
* @param events - Array of events to aggregate and visualize.
* @returns A React element: a Card containing a radar chart that displays the average attendees per weekday (averages rounded to the nearest integer).
*/
export function WeekdayPopularityRadar({ events }: { events: ReturnEvent[] }) {
const chartConfig = {
attendees: {
Expand Down Expand Up @@ -159,4 +167,4 @@ export function WeekdayPopularityRadar({ events }: { events: ReturnEvent[] }) {
</CardContent>
</Card>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ import {
SelectValue,
} from "@forge/ui/select";

/**
* Render an interactive donut pie chart that visualizes member counts grouped by gender.
*
* The component aggregates the provided members by their `gender`, assigns a consistent color
* palette per gender, and displays a selectable list of genders. Selecting a gender highlights
* its slice and updates the center label to show that gender's member count. If the active
* gender is removed from the data, the component selects the first available gender.
*
* @param members - Array of members to aggregate and visualize by gender
* @returns A Card containing a configurable pie chart, a gender select control, and a center label showing the selected gender's member count
*/
export default function SchoolYearPie({
members,
}: {
Expand Down Expand Up @@ -207,4 +218,4 @@ export default function SchoolYearPie({
</CardContent>
</Card>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ interface Member {
school?: (typeof FORMS.SCHOOLS)[number];
}

/**
* Render a horizontal bar chart showing the top 10 schools by member count.
*
* The component counts `school` values from the provided `members`, displays the highest-count
* schools (up to 10) as bars, truncates long school names for on-chart labels, and exposes the
* full school name in the tooltip.
*
* @param members - Array of Member objects; entries without a `school` value are ignored.
* @returns A JSX element containing the bar chart or a centered message when no school data exists.
*/
export default function SchoolBarChart({ members }: { members: Member[] }) {
const schoolCounts: Record<string, number> = {};

Expand Down Expand Up @@ -120,4 +130,4 @@ export default function SchoolBarChart({ members }: { members: Member[] }) {
</CardContent>
</Card>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ interface Member {
shirtSize?: (typeof FORMS.SHIRT_SIZES)[number];
}

/**
* Renders a selectable donut chart summarizing member shirt sizes and highlights the selected segment.
*
* Aggregates `members` by their `shirtSize`, displays each size as a colored slice, lets the user pick an active size via a dropdown, and shows the active size's member count in the chart center.
*
* @param members - Array of members to aggregate by their `shirtSize` property.
* @returns The Shirt Sizes pie chart component.
*/
export default function ShirtSizePie({ members }: { members: Member[] }) {
const id = "pie-interactive-shirt";

Expand Down Expand Up @@ -208,4 +216,4 @@ export default function ShirtSizePie({ members }: { members: Member[] }) {
</CardContent>
</Card>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ interface YearOfStudyPieProps {
members: Member[];
}

/**
* Render an interactive pie chart of members grouped by academic year category.
*
* Calculates each member's category (e.g., "Freshman", "Sophomore", "Junior", "Senior",
* "High School", "Graduate", "Alumni", or "Unknown") from their graduation date and
* level of study, aggregates counts per category, and displays a selectable pie chart
* with the active segment's count shown in the center.
*
* @param members - Array of member records whose `gradDate` and `levelOfStudy` are used to determine categories
* @returns A React element containing a Card with a selectable, colored pie chart and a centered count for the active segment
*/
export default function YearOfStudyPie({ members }: YearOfStudyPieProps) {
const id = "pie-interactive";

Expand Down Expand Up @@ -270,4 +281,4 @@ export default function YearOfStudyPie({ members }: YearOfStudyPieProps) {
</CardContent>
</Card>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ const minutes = Array.from({ length: 12 }, (_, i) =>

const amPmOptions = ["AM", "PM"] as const;

/**
* Renders a "Create Event" button and dialog containing a form to create a new event.
*
* The component manages dialog and submission state, validates and composes start/end datetimes
* from separate date and 12-hour time inputs, and calls the event creation mutation.
*
* @returns A React element that provides the trigger button and the create-event dialog form.
*/
export function CreateEventButton() {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -618,4 +626,4 @@ export function CreateEventButton() {
</DialogContent>
</Dialog>
);
}
}
Loading