Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f0119ef
fix(events): make event page H1 title bold
tiagov8 Aug 11, 2026
d515530
feat(events): rebuild event hero card and relocate breadcrumb
tiagov8 Aug 11, 2026
b12bbb8
feat(events): show hero speakers as individual avatars with roles
tiagov8 Aug 11, 2026
53d41ba
feat(events): dynamic CTA, past-event state, and restyled About section
tiagov8 Aug 11, 2026
b6e6d18
feat(events): card-grid About the Speakers section with hover
tiagov8 Aug 11, 2026
50f4e28
fix(events): tidy speaker cards — ring hugs image, drop office line
tiagov8 Aug 11, 2026
c2e698f
fix(events): resolve lint errors and flush hero speaker rings
tiagov8 Aug 11, 2026
0fbda44
style(events): refine speaker cards
tiagov8 Aug 11, 2026
99de729
style(events): use neutral gray ring on hero speaker avatars
tiagov8 Aug 11, 2026
29813d2
build(tina): regenerate tina-lock.json for new event fields
tiagov8 Aug 11, 2026
ff359d0
feat(events): add event-details sidebar card
tiagov8 Aug 11, 2026
863108a
feat(events): sticky sidebar, smaller mobile banner, populate AHD Mel…
tiagov8 Aug 11, 2026
2e87ae7
refactor(events): remove dead headerLayout, tidy schema and hero loca…
tiagov8 Aug 11, 2026
1231033
Merge branch 'main' into fix/event-title-bold
tiagov8 Aug 12, 2026
39ae4c9
Merge branch 'main' into fix/event-title-bold
0xharkirat Aug 12, 2026
4d0886e
Update app/(events)/events/[...filename]/events-preview.tsx
tiagov8 Aug 12, 2026
3b69491
Merge branch 'main' into fix/event-title-bold
tiagov8 Aug 12, 2026
8f86ef0
fix(events): address PR review
tiagov8 Aug 12, 2026
154650b
fix(events): don't flash an empty date chip before hydration
tiagov8 Aug 12, 2026
2456b9f
Merge remote-tracking branch 'origin/main' into fix/event-title-bold
Copilot Aug 13, 2026
eb2cd88
style(events): address Ken's design feedback
tiagov8 Aug 13, 2026
85ced11
style(events): reduce hero title size
tiagov8 Aug 13, 2026
569200c
style(events): move About the Speakers into the left column
tiagov8 Aug 13, 2026
37c6cdf
fix(events): correct multi-day dates and tidy the sidebar
tiagov8 Aug 13, 2026
950a0e2
fix(breadcrumb): use a chevron separator to match the design system
tiagov8 Aug 13, 2026
acb6cb4
Revert "fix(breadcrumb): use a chevron separator to match the design …
tiagov8 Aug 13, 2026
485408d
feat(events): hide the year crumb on event pages
tiagov8 Aug 13, 2026
4bbe709
build(tina): regenerate tina-lock.json after the main merge
tiagov8 Aug 13, 2026
af81dfe
fix(events): button link styling, lone speaker width, banner and hero…
tiagov8 Aug 14, 2026
d2097e0
Merge branch 'main' into fix/event-title-bold
tiagov8 Aug 14, 2026
6a2e4fe
fix(events): address second review round
tiagov8 Aug 14, 2026
23be67f
fix(events): never claim a price we don't have, drop the dead CTA
tiagov8 Aug 14, 2026
b473cce
fix(events): stop repeating the date beside the calendar squares
tiagov8 Aug 14, 2026
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
1,005 changes: 693 additions & 312 deletions app/(events)/events/[...filename]/events-preview.tsx

Large diffs are not rendered by default.

44 changes: 34 additions & 10 deletions app/components/breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,24 @@ import { tinaField } from "tinacms/dist/react";

interface BreadcrumbsProps {
additionalReplacements?: { from: string; to: string }[];
/**
* URL segments to leave out of the trail, for parts of a path that aren't
* pages in their own right (e.g. the year in /events/2026/my-event). Links
* for the remaining crumbs still point at the real URL.
*/
excludeSegments?: string[];
path: string;
title: string;
seoSchema?: {
title?: string;
};
}

// Module-level so the default props keep a stable identity between renders —
// a fresh [] each render would re-run the memo below every time.
const NO_SEGMENTS: string[] = [];
const NO_REPLACEMENTS: { from: string; to: string }[] = [];

const defaultReplacements = [
{ from: "consulting", to: "Services" },
{ from: "products", to: "Products" },
Expand All @@ -41,17 +52,24 @@ const defaultReplacements = [
];

export const Breadcrumbs: FC<BreadcrumbsProps> = ({
additionalReplacements = [],
additionalReplacements = NO_REPLACEMENTS,
excludeSegments = NO_SEGMENTS,
Comment thread
tiagov8 marked this conversation as resolved.
path,
title,
seoSchema,
}) => {
const pathname = usePathname();

const { breadcrumbItems, mobileParent } = useMemo(() => {
const pathSegments = pathname
.split("/")
.filter((segment) => segment !== "");
// Resolve each href against the full path first, so hiding a segment
// doesn't change where the remaining crumbs point.
const allSegments = pathname.split("/").filter((segment) => segment !== "");
const pathSegments = allSegments
.map((segment, index) => ({
segment,
href: "/" + allSegments.slice(0, index + 1).join("/"),
}))
.filter(({ segment }) => !excludeSegments.includes(segment));

const allReplacements = [
...defaultReplacements,
Expand Down Expand Up @@ -80,9 +98,8 @@ export const Breadcrumbs: FC<BreadcrumbsProps> = ({
);

// Add intermediate segments as links
pathSegments.forEach((segment, index) => {
pathSegments.forEach(({ segment, href }, index) => {
const isLast = index === pathSegments.length - 1;
const href = "/" + pathSegments.slice(0, index + 1).join("/");
const displayName = getDisplayName(segment);

items.push(
Expand Down Expand Up @@ -121,17 +138,24 @@ export const Breadcrumbs: FC<BreadcrumbsProps> = ({

let mobileParent: { label: string; href: string };
if (pathSegments.length >= 2) {
const parentIndex = pathSegments.length - 2;
const parent = pathSegments[pathSegments.length - 2];
mobileParent = {
label: getDisplayName(pathSegments[parentIndex]),
href: "/" + pathSegments.slice(0, parentIndex + 1).join("/"),
label: getDisplayName(parent.segment),
href: parent.href,
};
} else {
mobileParent = { label: "Home", href: "/" };
}

return { breadcrumbItems: items, mobileParent };
}, [pathname, path, title, seoSchema, additionalReplacements]);
}, [
pathname,
path,
title,
seoSchema,
additionalReplacements,
excludeSegments,
]);

return (
<>
Expand Down
6 changes: 5 additions & 1 deletion components/button/rippleButtonV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface RippleButtonProps
variant: ColorVariant;
/** When set, renders an <a> instead of a <button> for link-style CTAs. */
href?: string;
/** Anchor target; only applies when `href` is set. */
target?: string;
onClick?: (event: MouseEvent<HTMLElement>) => void;
}

Expand Down Expand Up @@ -68,8 +70,10 @@ const RippleButton = React.forwardRef<
}
}, [buttonRipples, duration]);

// `unstyled` opts out of the global `a:not(.unstyled)` rule, which would
// otherwise underline link-style buttons and turn their text red on hover.
const sharedClassName = cn(
"text-primary relative cursor-pointer items-center justify-center overflow-hidden rounded-control px-6 py-3 text-center",
"unstyled text-primary relative cursor-pointer items-center justify-center overflow-hidden rounded-control px-6 py-3 text-center no-underline",
variants[variant],
className
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"presenter": "content/presenters/adam-cogan.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2025-04-30T22:00:00.000Z",
"endDateTime": "2025-05-02T09:00:00.000Z",
"calendarType": "Conferences",
Expand Down
1 change: 0 additions & 1 deletion content/events-calendar/2026/AI-Hack-Day---Brisbane.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"presenter": "content/presenters/jernej-kavka.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2026-07-31T23:00:30.279Z",
"endDateTime": "2026-08-01T05:30:42.937Z",
"calendarType": "Hack Days",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"presenter": "content/presenters/jernej-kavka.mdx"
}
],
"headerLayout": "multi-torso",
"startDateTime": "2027-01-08T23:00:36.207Z",
"endDateTime": "2027-01-09T07:00:03.666Z",
"calendarType": "Hack Days",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
"presenter": "content/presenters/luke-cook.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2026-10-09T23:00:36.207Z",
"endDateTime": "2026-10-10T07:00:03.666Z",
"calendarType": "Hack Days",
"entryCost": "Free entry",
"venue": "SSW Chapel",
"city": "Melbourne",
"category": "Angular and React",
"lead": "A free, community-run hack day for developers who want to build real things with Angular — beginners and experts alike.",
"abstract": "What is Angular Hack Day?\nAngular Hack Days are community-run events for Angular Developers or people who want to learn Angular for free.\n\nThere will be something for everyone. Experienced Angular developers can share ideas with other experienced developers. If you are a beginner, then there’s plenty to learn on the day.\n\nIt’s recommended that attendees bring their own laptop on the day with Angular already set up and installed. If you’ve never played with Angular before, it would be good to do a little bit of learning before the day, but it’s not a requirement, as all are welcome.\n\nWhy Learn Angular?\nAI makes it easier to write code. Angular makes it easier to organise that code into something a team can maintain.\n\nAI makes it easier than ever to write code quickly, but speed is only useful if that code can be maintained, tested, and scaled by a real team.\n\nThat is where Angular still matters.\n\nWhile React gets a lot of attention, Angular gives developers a strong, opinionated framework with TypeScript-first development, built-in routing, forms, services, dependency injection, and clear conventions. Those guardrails are especially valuable in the AI age, because they help keep AI-generated code structured, consistent, and production-ready.\n\nFor new or AI-curious developers, Angular is worth a taste test. It shows how AI can help you move fast without creating chaos, and how a well-structured framework can make your apps easier for both humans and AI to understand, extend, and maintain.\n\n",
"description": "### What is Angular Hack Day?\n\nAngular Hack Days are community-run events for Angular Developers or people who want to learn Angular for free.\n\nThere will be something for everyone. Experienced Angular developers can share ideas with other experienced developers. If you are a beginner, then there’s plenty to learn on the day.\n\nIt’s recommended that attendees bring their own laptop on the day with Angular already set up and installed. If you’ve never played with Angular before, it would be good to do a little bit of learning before the day, but it’s not a requirement, as all are welcome.\n\n### Why Learn Angular?\n\nAI makes it easier to write code. Angular makes it easier to organise that code into something a team can maintain.\n\nAI makes it easier than ever to write code quickly, but speed is only useful if that code can be maintained, tested, and scaled by a real team.\n\nThat is where Angular still matters.\n\nWhile React gets a lot of attention, Angular gives developers a strong, opinionated framework with TypeScript-first development, built-in routing, forms, services, dependency injection, and clear conventions. Those guardrails are especially valuable in the AI age, because they help keep AI-generated code structured, consistent, and production-ready.\n\nFor new or AI-curious developers, Angular is worth a taste test. It shows how AI can help you move fast without creating chaos, and how a well-structured framework can make your apps easier for both humans and AI to understand, extend, and maintain.\n",
"liveStreamDelayMinutes": 30,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"presenter": "content/presenters/matt-wicks.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2026-11-06T23:00:36.207Z",
"endDateTime": "2026-11-07T07:00:03.666Z",
"calendarType": "Hack Days",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"presenter": "content/presenters/hark-singh.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2026-08-19T08:00:00.000Z",
"endDateTime": "2026-08-19T11:00:00.000Z",
"startShowBannerDateTime": "2026-08-18T14:00:00.000Z",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"presenter": "content/presenters/vlad-kireyev.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2026-07-15T08:00:28.796Z",
"endDateTime": "2026-07-15T10:00:41.492Z",
"startShowBannerDateTime": "2026-07-14T14:00:50.272Z",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"presenter": "content/presenters/calum-simpson.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2026-06-17T08:00:18.554Z",
"endDateTime": "2026-06-17T11:00:27.447Z",
"startShowBannerDateTime": "2026-06-16T14:00:08.971Z",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"url": "https://www.eventbrite.com/e/maui-hack-day-melbourne-tickets-1991517772267",
"thumbnail": "/images/events/mauihackday-thumb.png",
"thumbnailDescription": "MAUI Hack Day",
"headerLayout": "avatars",
"startDateTime": "2027-04-16T23:00:56.249Z",
"endDateTime": "2027-04-17T02:00:09.545Z",
"calendarType": "Hack Days",
Expand Down
1 change: 0 additions & 1 deletion content/events-calendar/2026/MAUI-Hack-Day-Sydney.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"url": "https://www.eventbrite.com/e/maui-hack-day-sydney-tickets-1991444960485",
"thumbnail": "/images/events/mauihackday-thumb.png",
"thumbnailDescription": "MAUI Hack Day",
"headerLayout": "avatars",
"startDateTime": "2027-02-12T23:00:14.392Z",
"endDateTime": "2027-02-13T07:00:31.670Z",
"calendarType": "Hack Days",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"presenter": "content/presenters/rick-su.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2026-07-01T08:00:50.616Z",
"endDateTime": "2026-07-01T11:00:02.064Z",
"calendarType": "User Groups",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"presenter": "content/presenters/eli-kent.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2026-10-21T07:00:00.000Z",
"endDateTime": "2026-10-21T09:00:00.000Z",
"startShowBannerDateTime": "2026-10-20T13:00:00.000Z",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"presenter": "content/presenters/jack-bear.mdx"
}
],
"headerLayout": "avatars",
"startDateTime": "2026-09-16T08:00:00.000Z",
"endDateTime": "2026-09-16T11:00:00.000Z",
"startShowBannerDateTime": "2026-09-15T14:00:00.000Z",
Expand Down
1 change: 0 additions & 1 deletion content/events-calendar/MAUI-Hack-Day---Brisbane.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"url": "https://www.eventbrite.com/e/maui-hack-day-brisbane-tickets-1991517845486",
"thumbnail": "/images/events/mauihackday-thumb.png",
"thumbnailDescription": "MAUI Hack Day",
"headerLayout": "avatars",
"startDateTime": "2027-08-13T23:00:48.432Z",
"endDateTime": "2027-08-14T07:00:05.676Z",
"calendarType": "Hack Days",
Expand Down
73 changes: 73 additions & 0 deletions helpers/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,79 @@ export const formatEventDate = (start: Date, end: Date) => {
return isOneDayEvent ? startDate : `${startDate} - ${endDate}`;
};

export type EventSchedule = {
isMultiDay: boolean;
/** One chip for a single-day event, two (start and end) for a multi-day one. */
chips: { month: string; day: string }[];
/**
* Weekday only — the chips already carry the month and day, so repeating
* them would say the same thing twice. "Wednesday" for a single-day event,
* "Mon - Tue" across several.
*/
weekdayLine: string;
/** Full date for the page header, e.g. "Wed 16 Sep 2026". */
dateLong: string;
/**
* "5:30 PM - 7:30 PM". Multi-day events append " daily": only the overall
* start and end are stored, so this assumes each day keeps the first day's
* hours — it can't know a day that starts or finishes at a different time.
*/
timeLine: string;
};

// Event date/time broken into the pieces the event page renders: calendar
// chips, a date line and a time line. Multi-day events get a chip per end.
export const formatEventSchedule = (start: Date, end: Date): EventSchedule => {
const empty = {
isMultiDay: false,
chips: [],
weekdayLine: "",
dateLong: "",
timeLine: "",
};
if (!start || !end) return empty;

const s = dayjs(start);
const e = dayjs(end);
const chip = (d: dayjs.Dayjs) => ({
month: d.format("MMM").toUpperCase(),
day: d.format("D"),
});
const times = `${s.format("h:mm A")} - ${e.format("h:mm A")}`;

if (s.startOf("day").isSame(e.startOf("day"))) {
return {
isMultiDay: false,
chips: [chip(s)],
weekdayLine: s.format("dddd"),
dateLong: s.format("ddd D MMM YYYY"),
timeLine: times,
};
}

// Same month: name it once at the end ("Mon 20 - Tue 21 Jul"). Across a year
// boundary both halves need their own year, or the start reads as the wrong one.
const sameYear = s.isSame(e, "year");
const dateShort = !sameYear
? `${s.format("ddd D MMM YYYY")} - ${e.format("ddd D MMM YYYY")}`
: s.isSame(e, "month")
? `${s.format("ddd D")} - ${e.format("ddd D MMM")}`
: `${s.format("ddd D MMM")} - ${e.format("ddd D MMM")}`;

// "daily" only when the hours actually repeat each day. An event that simply
// runs past midnight ends earlier in the day than it starts, and isn't daily.
const repeatsDaily = e.format("HH:mm") > s.format("HH:mm");
Comment thread
tiagov8 marked this conversation as resolved.

return {
isMultiDay: true,
chips: [chip(s), chip(e)],
weekdayLine: `${s.format("ddd")} - ${e.format("ddd")}`,
// dateShort already carries both years when they differ.
dateLong: sameYear ? `${dateShort} ${e.format("YYYY")}` : dateShort,
timeLine: repeatsDaily ? `${times} daily` : times,
};
};

// Splits the long event date into a date line and a time line so callers can
// render them on separate lines.
export const formatEventLongDateParts = (start: Date, end: Date) => {
Expand Down
68 changes: 45 additions & 23 deletions hooks/useFormatDates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,59 @@ import {
formatEventDate,
formatEventLongDate,
formatEventLongDateParts,
formatEventSchedule,
formatRelativeEventDate,
type EventSchedule,
} from "../helpers/dates";

const EMPTY_DATE_PARTS = { date: "", time: "" };

const EMPTY_SCHEDULE: EventSchedule = {
isMultiDay: false,
chips: [],
weekdayLine: "",
dateLong: "",
timeLine: "",
};

export const useFormatDates = (event: EventTrimmed, formatLong: boolean) => {
const [relativeDate, setRelativeDate] = useState<string>("");
const [formattedDate, setFormattedDate] = useState<string>("");
const [formattedDateParts, setFormattedDateParts] = useState<{
date: string;
time: string;
}>({ date: "", time: "" });
const [formattedDateParts, setFormattedDateParts] =
useState<typeof EMPTY_DATE_PARTS>(EMPTY_DATE_PARTS);
const [schedule, setSchedule] = useState<EventSchedule>(EMPTY_SCHEDULE);

// Depend on primitive timestamps, not the Date objects: callers usually build
// `new Date(...)` on every render, so using those as deps would re-run this
// effect (and re-render) forever.
const startTime = event.startDateTime?.getTime();
const endTime = event.endDateTime?.getTime();

useEffect(() => {
if (typeof window !== "undefined") {
setRelativeDate(
formatRelativeEventDate(event.startDateTime, event.endDateTime)
);

if (formatLong) {
setFormattedDate(
formatEventLongDate(event.startDateTime, event.endDateTime)
);
setFormattedDateParts(
formatEventLongDateParts(event.startDateTime, event.endDateTime)
);
} else {
const date = formatEventDate(event.startDateTime, event.endDateTime);
setFormattedDate(date);
setFormattedDateParts({ date, time: "" });
}
// Number.isFinite, not a null check: callers build `new Date(value)`, so a
// missing or malformed date arrives as an Invalid Date whose time is NaN.
if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) {
setRelativeDate("");
setFormattedDate("");
setFormattedDateParts(EMPTY_DATE_PARTS);
setSchedule(EMPTY_SCHEDULE);
return;
}
const start = new Date(startTime);
const end = new Date(endTime);

setRelativeDate(formatRelativeEventDate(start, end));
setSchedule(formatEventSchedule(start, end));

if (formatLong) {
setFormattedDate(formatEventLongDate(start, end));
setFormattedDateParts(formatEventLongDateParts(start, end));
} else {
const date = formatEventDate(start, end);
setFormattedDate(date);
setFormattedDateParts({ date, time: "" });
}
}, [event.startDateTime, event.endDateTime, formatLong]);
}, [startTime, endTime, formatLong]);

return { relativeDate, formattedDate, formattedDateParts };
return { relativeDate, formattedDate, formattedDateParts, schedule };
};
Loading
Loading