Skip to content

Commit b1d5b62

Browse files
h4rklclaude
andauthored
feat(epoch1000): add Epoch 1000 campaign experience (#1670)
* feat(epoch1000): introduce Countdown, ThousandGrid, and WalletChecker components for epoch tracking and wallet verification - Added Countdown component to display the countdown to epoch 1000, fetching epoch data from the API. - Implemented ThousandGrid component to visually represent the progress of epochs. - Created WalletChecker component for users to check their wallet's transaction history and generate survivor cards. - Established campaign configuration for epoch 1000 with defined start and end dates. - Developed Epoch1000Hero component to serve as the homepage hero during the campaign, showcasing live epoch data. - Added CSS animations for the epoch hero components to enhance visual appeal. - Introduced utility functions for handling Solana addresses and tier classifications based on epochs survived. * fix(epoch1000): render gradient epoch number, add campaign window test - Drop backdrop-blur on the hero live panel: Chrome skips background-clip:text paint inside backdrop-filter layers, hiding the gradient epoch number - Add unit tests for the campaign hero activation window - Locale-aware number/date formatting in the live panel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(epoch1000): implement odometer component and related functionality - Add Epoch1000Experience component to display live odometer and timeline. - Create Odometer component for visual representation of epoch values. - Implement odometer-math utility functions for calculating wheel targets. - Add moments data for significant events in the epoch timeline. - Introduce useEpochData hook for fetching and managing live epoch data. - Style odometer with CSS for a mechanical appearance and celebration effects. - Write unit tests for odometer math functions to ensure correctness. * feat(epoch1000): enhance campaign page with new styles, components, and validation logic * Refactor code structure for improved readability and maintainability * feat(epoch1000): update translations and metadata for Epoch 1000 campaign * feat(epoch1000): integrate GitHub contributions color palette and enhance grid functionality * feat(epoch1000): implement rate limiting for wallet checks and enhance caching mechanisms * feat(epoch1000): update eyebrow text for epoch 1000 campaign page --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 1b5b5cc commit b1d5b62

44 files changed

Lines changed: 3207 additions & 45 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ DATABRICKS_TOKEN=
3434

3535
# RWA.xyz API
3636
RWA_XYZ_API_TOKEN=
37+
38+
HELIUS_API_KEY=
39+
SOLANA_RPC_URL=
83.3 KB
Binary file not shown.
83.8 KB
Binary file not shown.
74.4 KB
Binary file not shown.

apps/web/next.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ const nextConfig: NextConfig = {
7979
trailingSlash: false,
8080
transpilePackages: ["gsap"],
8181
serverExternalPackages: ["@databricks/sql", "lz4"],
82+
// The epoch1000 OG card reads font files from disk at request time
83+
outputFileTracingIncludes: {
84+
"/api/epoch1000/og": ["./assets/fonts/epoch1000/*.woff"],
85+
},
8286

8387
async rewrites() {
8488
const baseRewrites = rewritesAndRedirectsJson.rewrites as {
38 KB
Loading

apps/web/public/social/solana.jpg

448 KB
Loading
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
CAMPAIGNS,
4+
getActiveCampaignId,
5+
} from "@/components/index/campaigns/config";
6+
7+
describe("homepage campaign config", () => {
8+
const epoch1000 = CAMPAIGNS.find((c) => c.id === "epoch1000")!;
9+
10+
it("activates epoch1000 inside its window", () => {
11+
expect(getActiveCampaignId(new Date(`${epoch1000.start}T12:00:00Z`))).toBe(
12+
"epoch1000",
13+
);
14+
expect(getActiveCampaignId(new Date(`${epoch1000.end}T12:00:00Z`))).toBe(
15+
"epoch1000",
16+
);
17+
});
18+
19+
it("falls back to the default hero before the window opens", () => {
20+
const dayBefore = new Date(`${epoch1000.start}T00:00:00Z`);
21+
dayBefore.setUTCDate(dayBefore.getUTCDate() - 1);
22+
expect(getActiveCampaignId(dayBefore)).toBeNull();
23+
});
24+
25+
it("falls back to the default hero after the window closes", () => {
26+
const dayAfter = new Date(`${epoch1000.end}T00:00:00Z`);
27+
dayAfter.setUTCDate(dayAfter.getUTCDate() + 1);
28+
expect(getActiveCampaignId(dayAfter)).toBeNull();
29+
});
30+
31+
it("keeps windows as valid ISO dates", () => {
32+
for (const c of CAMPAIGNS) {
33+
expect(new Date(`${c.start}T00:00:00Z`).getTime()).not.toBeNaN();
34+
expect(new Date(`${c.end}T00:00:00Z`).getTime()).not.toBeNaN();
35+
expect(c.start <= c.end).toBe(true);
36+
}
37+
});
38+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
nextWheelTargets,
4+
normalizeTargets,
5+
} from "@/components/epoch1000/odometer-math";
6+
7+
/** Strip offsets mod 10 are the digits actually shown on the wheels. */
8+
function shownDigits(targets: number[]): number[] {
9+
return targets.map((t) => t % 10);
10+
}
11+
12+
describe("epoch odometer wheel math", () => {
13+
it("first settle spins two revolutions and lands on the value's digits", () => {
14+
const targets = nextWheelTargets(null, 998, 4);
15+
expect(targets).toEqual([20, 29, 29, 28]);
16+
expect(shownDigits(targets)).toEqual([0, 9, 9, 8]);
17+
});
18+
19+
it("an epoch tick rolls only the wheels that change, always forward", () => {
20+
const at998 = nextWheelTargets(null, 998, 4);
21+
const at999 = nextWheelTargets(at998, 999, 4);
22+
expect(shownDigits(at999)).toEqual([0, 9, 9, 9]);
23+
// every wheel moved forward or stayed
24+
at999.forEach((t, i) => expect(t).toBeGreaterThanOrEqual(at998[i]));
25+
});
26+
27+
it("rolls 999 → 1000 forward on every wheel — the thousands wheel turns", () => {
28+
const at999 = nextWheelTargets(null, 999, 4);
29+
const at1000 = nextWheelTargets(at999, 1000, 4);
30+
expect(shownDigits(at1000)).toEqual([1, 0, 0, 0]);
31+
at1000.forEach((t, i) => expect(t).toBeGreaterThan(at999[i]));
32+
});
33+
34+
it("normalize folds an offset back one revolution without changing digits", () => {
35+
const targets = [21, 31, 35, 39];
36+
const folded = normalizeTargets(targets);
37+
expect(folded).toEqual([21, 21, 25, 29]);
38+
expect(shownDigits(folded)).toEqual(shownDigits(targets));
39+
});
40+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, it } from "vitest";
2+
import { isValidSolanaAddress } from "@/lib/epoch1000/public-key";
3+
4+
describe("epoch1000 Solana public key validation", () => {
5+
it("accepts base58 values that decode to 32 bytes", () => {
6+
expect(isValidSolanaAddress("11111111111111111111111111111111")).toBe(true);
7+
expect(
8+
isValidSolanaAddress("So11111111111111111111111111111111111111112"),
9+
).toBe(true);
10+
expect(
11+
isValidSolanaAddress("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),
12+
).toBe(true);
13+
});
14+
15+
it("rejects short, oversized, and non-base58 values", () => {
16+
expect(isValidSolanaAddress("a")).toBe(false);
17+
expect(isValidSolanaAddress("")).toBe(false);
18+
expect(
19+
isValidSolanaAddress(
20+
"5HueCGU8rMjxEXxiPuD5BDuRa5Ww7C12QQAQH9Wzj3JDPZQQwXP",
21+
),
22+
).toBe(false);
23+
expect(
24+
isValidSolanaAddress("So11111111111111111111111111111111111111110"),
25+
).toBe(false);
26+
});
27+
28+
it("trims whitespace before validating", () => {
29+
expect(isValidSolanaAddress(" 11111111111111111111111111111111 ")).toBe(
30+
true,
31+
);
32+
});
33+
});

0 commit comments

Comments
 (0)