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
80 changes: 80 additions & 0 deletions .claude/skills/data-layer/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
name: data-layer
description: This skill provides patterns for working with the data-layer module. Use when creating/editing files in src/data-layer/, src/lib/data/, or adding new data sources.
---

# Data Layer

## Architecture

```
src/data-layer/ # Isolated, framework-agnostic module
├── api/ # Fetch functions (one per data source)
├── index.ts # Getter functions (pure passthrough)
└── registry.ts # Task registry (hourly/daily)

src/lib/data/ # Next.js caching adapter
└── index.ts # Cached wrappers via createCachedGetter()
```

## Rules

### 1. Getters must be pure passthrough

In `src/data-layer/index.ts`, getter functions must only call `getData<T>(TASK_ID)` with no transformations:

```typescript
// Correct
export async function getEventsData(): Promise<EventItem[] | null> {
return getData<EventItem[]>(FETCH_EVENTS_TASK_ID)
}

// Wrong - no transformations in getters
export async function getEventsData(): Promise<EventItem[] | null> {
const data = await getData<EventItem[]>(FETCH_EVENTS_TASK_ID)
return data?.map((e) => ({ ...e, computed: derive(e) })) ?? null
}
```

All transformations belong in the fetch task (`src/data-layer/api/`), not in the getter.

### 2. Expose via lib/data for caching

When a data function needs caching/revalidation, add a cached wrapper in `src/lib/data/index.ts`:

```typescript
export const getEventsData = createCachedGetter(
dataLayer.getEventsData,
["events-data"],
CACHE_REVALIDATE_DAY // or CACHE_REVALIDATE_HOUR
)
```

## Adding a New Data Source

1. Create fetch function in `src/data-layer/api/fetchNewData.ts`:
```typescript
export const FETCH_NEW_DATA_TASK_ID = "fetch-new-data"

export async function fetchNewData(): Promise<YourDataType> {
// Fetch and transform data here
}
```

2. Add getter in `src/data-layer/index.ts`:
```typescript
export async function getNewData(): Promise<YourDataType | null> {
return getData<YourDataType>(FETCH_NEW_DATA_TASK_ID)
}
```

3. Register in `src/data-layer/registry.ts` (hourlyTasks or dailyTasks)

4. Add cached wrapper in `src/lib/data/index.ts`:
```typescript
export const getNewData = createCachedGetter(
dataLayer.getNewData,
["new-data"],
CACHE_REVALIDATE_HOUR
)
```
4 changes: 2 additions & 2 deletions app/[locale]/community/events/conferences/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import { getLocaleYear } from "@/lib/utils/date"
import { getMetadata } from "@/lib/utils/metadata"
import { getRequiredNamespacesForPage } from "@/lib/utils/translations"

import { getEventsData } from "@/data-layer"

import ContinentTabs from "../_components/ContinentTabs"
import EventCard from "../_components/EventCard"
import OrganizerCTA from "../_components/OrganizerCTA"

import { getEventsData } from "@/lib/data"

const Page = async ({ params }: { params: PageParams }) => {
const { locale } = params

Expand Down
4 changes: 2 additions & 2 deletions app/[locale]/community/events/meetups/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { getLocaleYear } from "@/lib/utils/date"
import { getMetadata } from "@/lib/utils/metadata"
import { getRequiredNamespacesForPage } from "@/lib/utils/translations"

import { getEventsData } from "@/data-layer"

import OrganizerCTA from "../_components/OrganizerCTA"
import { getMeetupGroups } from "../utils"

import FilterMeetups from "./_components/FilterMeetups"

import { getEventsData } from "@/lib/data"

const Page = async ({ params }: { params: PageParams }) => {
const { locale } = params

Expand Down
2 changes: 1 addition & 1 deletion app/[locale]/community/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import { getMetadata } from "@/lib/utils/metadata"
import { getRequiredNamespacesForPage } from "@/lib/utils/translations"

import communityHubs from "@/data/community-hubs"
import { getEventsData } from "@/data-layer"

import ContinentTabs from "./_components/ContinentTabs"
import EventCard from "./_components/EventCard"
Expand All @@ -44,6 +43,7 @@ import { SECTION_IDS } from "./constants"
import EventsJsonLD from "./page-jsonld"
import { getMeetupGroups, mapEventTranslations } from "./utils"

import { getEventsData } from "@/lib/data"
import ethereumEverywhereLogo from "@/public/images/community/ethereum-everywhere-logo.png"
import geodeLabsLogo from "@/public/images/community/geode-labs-logo.png"
import heroImage from "@/public/images/enterprise-eth.png"
Expand Down
4 changes: 2 additions & 2 deletions app/[locale]/community/events/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { Section } from "@/components/ui/section"

import { getMetadata } from "@/lib/utils/metadata"

import { getEventsData } from "@/data-layer"

import EventCard from "../_components/EventCard"
import OrganizerCTA from "../_components/OrganizerCTA"
import { mapEventTranslations, sanitize } from "../utils"

import { getEventsData } from "@/lib/data"

const Page = async ({
params,
searchParams,
Expand Down
3 changes: 1 addition & 2 deletions app/[locale]/developers/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { getLocale, getTranslations } from "next-intl/server"

import type { EventItem } from "@/lib/types"

import { getEventsData } from "@/data-layer"

import type { DevelopersPath, VideoCourse } from "./types"

import { getEventsData } from "@/lib/data"
import cyfrinBasicBanner from "@/public/images/developers/cyfrin-basic-banner.webp"
import cyfrinFoundryAdvancedBanner from "@/public/images/developers/cyfrin-foundry-advanced-banner.webp"
import cyfrinFoundryFundamentalsBanner from "@/public/images/developers/cyfrin-foundry-fundamentals-banner.webp"
Expand Down
21 changes: 10 additions & 11 deletions app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,6 @@ import { getMetadata } from "@/lib/utils/metadata"
import { formatPriceUSD } from "@/lib/utils/numbers"
import { polishRSSList } from "@/lib/utils/rss"

import {
getAppsData,
getAttestantPosts,
getBeaconchainEpochData,
getEthPrice,
getEventsData,
getGrowThePieData,
getRSSData,
getTotalValueLockedData,
} from "@/data-layer"

import {
BLOGS_WITHOUT_FEED,
DEFAULT_LOCALE,
Expand All @@ -91,6 +80,16 @@ import IndexPageJsonLD from "./page-jsonld"
import { getActivity } from "./utils"

import { routing } from "@/i18n/routing"
import {
getAppsData,
getAttestantPosts,
getBeaconchainEpochData,
getEthPrice,
getEventsData,
getGrowThePieData,
getRSSData,
getTotalValueLockedData,
} from "@/lib/data"
import EventFallback from "@/public/images/events/event-placeholder.png"

const BentoCardSwiper = dynamic(
Expand Down
34 changes: 34 additions & 0 deletions src/data-layer/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,40 @@ src/lib/data/
└── index.ts # Next.js adapter - adds caching layer
```

## Key Rules

### 1. Data-layer getters must be pure passthrough

```typescript
// ✅ Correct
export async function getEventsData(): Promise<EventItem[] | null> {
return getData<EventItem[]>(FETCH_EVENTS_TASK_ID)
}

// ❌ Wrong - no transformations in getters
export async function getEventsData(): Promise<EventItem[] | null> {
const data = await getData<EventItem[]>(FETCH_EVENTS_TASK_ID)
return data?.map((e) => ({ ...e, computed: derive(e) })) ?? null
}
```

Put all transformations in the fetch task (`/api`), not in the getter.

### 2. Expose via `@/lib/data` for caching

If a data function needs caching/revalidation, expose it through `@/lib/data`:

```typescript
// src/lib/data/index.ts
export const getEventsData = createCachedGetter(
dataLayer.getEventsData,
["events-data"],
CACHE_REVALIDATE_DAY
)
```

Direct `@/data-layer` imports work but have no caching.

## Components

### 1. Public API (`src/data-layer/index.ts`)
Expand Down
10 changes: 1 addition & 9 deletions src/data-layer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { FETCH_COMMUNITY_PICKS_TASK_ID } from "./api/fetchCommunityPicks"
import { FETCH_ETHEREUM_MARKETCAP_TASK_ID } from "./api/fetchEthereumMarketcap"
import { FETCH_ETHEREUM_STABLECOINS_MCAP_TASK_ID } from "./api/fetchEthereumStablecoinsMcap"
import { FETCH_ETH_PRICE_TASK_ID } from "./api/fetchEthPrice"
import { getEventTypes } from "./api/fetchEvents"
import { FETCH_EVENTS_TASK_ID } from "./api/fetchEvents"
import { FETCH_GFIS_TASK_ID } from "./api/fetchGFIs"
import { FETCH_GIT_HISTORY_TASK_ID } from "./api/fetchGitHistory"
Expand Down Expand Up @@ -225,12 +224,5 @@ export async function getTotalValueLockedData(): Promise<MetricReturnData | null
* @returns Array of upcoming events sorted by start time, or null if not available
*/
export async function getEventsData(): Promise<EventItem[] | null> {
const data = await getData<EventItem[]>(FETCH_EVENTS_TASK_ID)
if (!data) return null

// Ensure eventTypes is present (backward compatibility with cached data)
return data.map((event) => ({
...event,
eventTypes: event.eventTypes ?? getEventTypes(event.tags),
}))
return getData<EventItem[]>(FETCH_EVENTS_TASK_ID)
}
Loading