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
25 changes: 15 additions & 10 deletions src/components/footer/bed-scene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,13 @@ const wakeUp: Keyframe[] = [
* with the arrow keys or by swiping, and Nugget can be tucked in, and woken
* up again.
*
* Every offset is a percentage of the surrounding night scene: the base values
* come from the phone frame, the xl ones from the desktop frame. The overlay
* with the phone, the z's and the two click areas sits in the same box as the
* bed, on top of the stars.
* Every offset is a percentage of the surrounding night scene. The xl values
* come from the desktop frame. On the phone the bed is the desktop bed scaled
* by 1.13157 and moved (BED_BOX), and the headboard and the stars get the same
* scale and move, so the pieces keep their places on the bed: phone = -6.476 +
* 1.13157 x desktop across, -2.16 + 1.13157 x desktop down. The overlay with
* the phone, the z's and the two click areas sits in the same box as the bed,
* on top of the stars.
*/
const BedScene = () => {
const bed = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -106,7 +109,7 @@ const BedScene = () => {
alt=""
width={1026}
height={450}
className="absolute left-[21.9%] top-[24%] h-auto w-[60.8%] max-w-none xl:left-[14.75%] xl:top-[29.5%] xl:w-[67%]"
className="absolute left-[10.21%] top-[31.22%] h-auto w-[75.82%] max-w-none xl:left-[14.75%] xl:top-[29.5%] xl:w-[67%]"
/>
<div
ref={bed}
Expand All @@ -123,22 +126,24 @@ const BedScene = () => {
<BedArt className="block h-auto w-full" />
</div>
<CloudBorder />
{/* On the phone the bed is wider than the gap between the cloud
pillars, so the stars don't follow its scale: the group is sized to
sit on the part of the blanket that shows, 15% to 85% across, with
its top star where the bed's scale puts it. */}
<Image
src="/assets/footer/star-particles.svg"
alt=""
width={1166}
height={578}
className="absolute left-[16%] top-[42.9%] h-auto w-[71.8%] max-w-none xl:left-[8.67%] xl:top-[47.03%] xl:w-[78.82%]"
className="absolute left-[13.95%] top-[51.33%] h-auto w-[72.3%] max-w-none xl:left-[8.67%] xl:top-[47.03%] xl:w-[78.82%]"
/>
{/* The big star is kept apart from the rest: on the phone frame its
place in the group lands on the bear's phone, so there it moves
down onto the blanket. On desktop it sits where the group has it. */}
{/* The big star is its own image, kept where the group has it. */}
<Image
src="/assets/footer/star-big.svg"
alt=""
width={151}
height={164}
className="absolute left-[27.1%] top-[59.83%] h-auto w-[9.31%] max-w-none xl:left-[24.55%] xl:top-[54.32%] xl:w-[10.22%]"
className="absolute left-[28.52%] top-[58.02%] h-auto w-[9.38%] max-w-none xl:left-[24.55%] xl:top-[54.32%] xl:w-[10.22%]"
/>
<div className={cn(BED_BOX, "aspect-[1531/1504] @container")}>
<AnimatePresence>
Expand Down
9 changes: 5 additions & 4 deletions src/components/footer/cloud-border.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import Image from "next/image";

/**
* The cloud frame behind the footer: two pillars up the sides and a band across
* the bottom, all one piece of artwork. It hangs off both edges by the same
* amount it does in the design, which differs between the phone and desktop
* frames.
* the bottom, all one piece of artwork. On desktop it hangs off both edges by
* the same amount it does in the design. On the phone it is centred so both
* pillars reach about 11% of the width in; the phone frame's own placement
* left the left pillar reaching nearly twice as far in as the right.
*/
const CloudBorder = () => {
return (
Expand All @@ -13,7 +14,7 @@ const CloudBorder = () => {
alt=""
width={1927}
height={2072}
className="absolute bottom-0 left-[-4.8%] h-auto w-[114.5%] max-w-none xl:left-[-14.17%] xl:w-[125.84%]"
className="absolute bottom-0 left-[-7.8%] h-auto w-[114.5%] max-w-none xl:left-[-14.17%] xl:w-[125.84%]"
/>
);
};
Expand Down
6 changes: 5 additions & 1 deletion src/components/stats/mobile-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ export const MobileStats = () => (

{/* Stars zig-zag down the page: left, right, left, each tucked under the last */}
<div className="relative -mt-[8%] -mb-[10%]">
{/* Laid out at three times the width rather than scaled up, so Safari
rasterizes the stars at the size they're shown instead of blowing
up a bitmap; the offsets put its centre where a full-width strip
at top-[20%] would have it. */}
<Image
src="/assets/stats/desktop-small-stars.svg"
alt=""
width={1172}
height={389}
className="pointer-events-none absolute top-[20%] left-0 z-0 w-full h-auto scale-300 rotate-90"
className="pointer-events-none absolute top-[20%] left-1/2 z-0 w-[300%] max-w-none h-auto -translate-x-1/2 -translate-y-1/3 rotate-90"
/>
<div className="relative z-10 flex flex-col">
<div className="w-[118%] -ml-[4%]">
Expand Down
41 changes: 31 additions & 10 deletions src/lib/shelves.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,32 @@ describe("buildShelves", () => {
mk("inkind-co", "inkind"),
mk("platinum-co", "platinum"),
]);
const names =
shelves[0].kind === "frames"
? shelves[0].sponsors.map((s) => s.name)
: [];
expect(names).toEqual(["platinum-co", "gold-co", "bronze-co"]);
expect(
sponsorShelves(shelves).flatMap((s) =>
s.kind === "frames" ? s.sponsors.map((sponsor) => sponsor.name) : []
)
).toEqual(["platinum-co", "gold-co", "bronze-co", "inkind-co"]);
});

it("starts every tier on a fresh shelf instead of topping up the last one", () => {
// HackCamp 2026's frames: five silver, two bronze, three in-kind.
const shelves = buildShelves([
...["n8n", "elevenlabs", "render", "featherless", "bcit"].map((n) =>
mk(n, "silver")
),
...["formation", "spin", "poppi"].map((n) => mk(n, "inkind")),
...["investly", "rbc-gam"].map((n) => mk(n, "bronze")),
]);
expect(
sponsorShelves(shelves).map((s) =>
s.kind === "frames" ? s.sponsors.map((sponsor) => sponsor.tier) : []
)
).toEqual([
["silver", "silver", "silver"],
["silver", "silver"],
["bronze", "bronze"],
["inkind", "inkind", "inkind"],
]);
});

it("treats a whitespace-only blurb as no blurb", () => {
Expand Down Expand Up @@ -115,11 +136,11 @@ describe("buildShelves", () => {
it("sorts an unknown tier after every known tier", () => {
const odd = { ...mk("odd", "gold"), tier: "mystery" as SponsorDoc["tier"] };
const shelves = buildShelves([odd, mk("bronze-co", "bronze")]);
const names =
shelves[0].kind === "frames"
? shelves[0].sponsors.map((s) => s.name)
: [];
expect(names).toEqual(["bronze-co", "odd"]);
expect(
sponsorShelves(shelves).map((s) =>
s.kind === "frames" ? s.sponsors.map((sponsor) => sponsor.name) : []
)
).toEqual([["bronze-co"], ["odd"]]);
});

it("treats a missing blurb as no blurb", () => {
Expand Down
32 changes: 22 additions & 10 deletions src/lib/shelves.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ const hasBlurb = (sponsor: SponsorDoc) =>
* Turns the sponsor list into shelf rows following the design rule:
* each blurb sponsor gets its own chalkboard shelf with books one side and the
* sheep the other, then the remaining sponsors are sorted by tier and packed
* into shelves of `framesPerShelf` picture frames with the plant one side and
* books the other. Both pairs swap sides on every other row.
* into shelves of `framesPerShelf` picture frames, one tier per shelf, with
* the plant one side and books the other. Both pairs swap sides on every
* other row.
*
* The decoration-only shelf between the two groups is always included: the
* narrow layout shows it and hides the shelf-end decorations, the wide layout
Expand All @@ -76,15 +77,26 @@ export function buildShelves(
right: i % 2 === 0 ? "sheep" : "books-left",
}));

// Each tier starts on a fresh shelf, so a tier that doesn't fill its last
// shelf never shares it with the next tier down.
const tiers = new Map<string, SponsorDoc[]>();
for (const sponsor of frames) {
const tier = tiers.get(sponsor.tier);
if (tier) tier.push(sponsor);
else tiers.set(sponsor.tier, [sponsor]);
}

const frameShelves: ShelfSpec[] = [];
for (let i = 0; i < frames.length; i += framesPerShelf) {
const row = frameShelves.length;
frameShelves.push({
kind: "frames",
sponsors: frames.slice(i, i + framesPerShelf),
left: row % 2 === 0 ? "plant" : "books-right",
right: row % 2 === 0 ? "books-right" : "plant",
});
for (const tier of tiers.values()) {
for (let i = 0; i < tier.length; i += framesPerShelf) {
const row = frameShelves.length;
frameShelves.push({
kind: "frames",
sponsors: tier.slice(i, i + framesPerShelf),
left: row % 2 === 0 ? "plant" : "books-right",
right: row % 2 === 0 ? "books-right" : "plant",
});
}
}

if (cardShelves.length + frameShelves.length === 0) return [];
Expand Down
2 changes: 1 addition & 1 deletion src/sections/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Footer = () => {
the bed and the stars never drift apart from one another. The scene
takes no clicks except on the two mascots.
*/}
<div className="pointer-events-none absolute bottom-[10vw] left-0 aspect-[1531/2065] w-full xl:bottom-0">
<div className="pointer-events-none absolute bottom-[13vw] left-0 aspect-[1531/2065] w-full xl:bottom-0">
<CloudBorder />
<BedScene />
<StarField />
Expand Down
Loading