Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/HNU-himematsu/HNU-TimeLetter/llms.txt

Use this file to discover all available pages before exploring further.

All domain types for HNU-TimeLetter are defined in a single file — src/lib/types.ts — which serves as the project’s Single Source of Truth (SSOT) for data shapes. This page describes every entity, its Feishu field mapping, the JSON artifact it ends up in, and the Zustand store slice that coordinates UI state at runtime. Reading this document alongside the Sync System architecture gives a complete picture of the journey data takes from Feishu spreadsheet to rendered page.

Data Flow Overview

Feishu Bitable (CMS)

    │  npm run sync  /  POST /api/admin/sync/jobs

src/lib/sync/ (orchestrator + per-table modules)
    │  ├─ OSS upload pipeline (attachment → MD5 → OSS → back-fill URL)
    │  └─ JSON writer (*.tmp → validate → rename)

src/data/*.json  +  src/config/locations.json  (build-time artifacts)

    │  Runtime API routes (no-store)

Frontend pages  (/, /map, /creation)

    │  src/components/creation/utils.ts

CreationCard aggregation (client-side, by CardID)
src/data/*.json and src/config/locations.json are committed to the repository. They act as build-time fallback data for Vercel Preview deployments where no sync process runs, and as hot-swappable runtime data on the self-hosted production server.

Entity Reference

1. Character

A Character represents a single Galgame-style avatar character. It corresponds to one row in the Feishu characters table. During sync, if 头像OSS_URL is empty the pipeline automatically downloads the 角色头像 attachment, uploads it to Aliyun OSS, and back-fills the resulting URL.
interface Character {
  id: string;        // Feishu field: 角色ID
  name: string;      // Feishu field: 角色名称
  avatarUrl: string; // Feishu field: 头像OSS_URL (back-filled by sync)
}
Output artifact: src/data/characters.json — array of Character.

2. Story

A Story is one exhibition record on the /map route. It pairs a high-resolution scene image with a character avatar and narrative text, pinned to a campus location. Character metadata (characterName, avatarUrl) is injected by the sync layer from the characters table rather than stored redundantly in Feishu; locationName is resolved at runtime from the LocationPoint list.
interface Story {
  id: string;           // Feishu record_id
  characterId: string;  // Feishu field: 角色ID
  characterName: string; // Injected from characters table at sync time
  avatarUrl: string;    // Injected from characters table at sync time
  mainImageUrl: string; // Feishu field: 大图OSS_URL (preferred) or 大图 attachment
  content: string;      // Feishu field: 故事内容
  author: string;       // Feishu field: 投稿人
  locationId: string;   // Feishu field: 地点ID
}
Output artifact: src/data/content.json — a LocationPoint[] where every point embeds its matched Story[].

3. LocationPoint

LocationPoint is a frontend aggregation struct, not a flat Feishu record. It combines the coordinate data from src/config/locations.json with all stories whose locationId matches, producing the pin objects rendered on the SVG campus map.
interface LocationPoint {
  id: string;       // e.g. "lib-001"
  name: string;     // e.g. "图书馆"
  x: number;        // SVG horizontal position, 0–100 (percentage)
  y: number;        // SVG vertical position, 0–100 (percentage)
  stories: Story[]; // All stories whose locationId === this id
}
Output artifact: src/data/content.jsonLocationPoint[] (stories nested inside each point).

4. CreationIdea

CreationIdea is the raw sync-layer record for one submission in the Feishu “揭示板” (creation board) table, view “收集结果”. It preserves the original field semantics exactly, including the submitter identity field that is stripped out before the data reaches the frontend.
interface CreationIdea {
  id: string;        // Feishu record_id
  cardId: string;    // Feishu: CardID → fallback: 自动编号 → fallback: record_id
  author: string;    // Feishu: 你的昵称 → fallback: 提交人
  submitter: string; // Feishu: 提交人 (raw Feishu identity — NOT exposed to frontend)
  tags: string;      // Feishu: 请选择你要添加的内容(…)
  content: string;   // Feishu: 文本 (also holds back-filled OSS URL in image scenarios)
  images: string[];  // Feishu: 请上传你的图片 (downloaded → uploaded to OSS)
  createdAt: string; // Feishu: 提交时间 (ISO 8601 string)
}
Multiple CreationIdea records may share the same cardId — this is intentional. The table is designed for repeated submission by different users contributing to the same card. Do not deduplicate by cardId at the sync stage.
Output artifact: src/data/creation-board.jsonCreationIdea[].

5. CreationEntry

CreationEntry is a frontend projection of CreationIdea. It is produced at page-render time by src/components/creation/utils.ts and is never written to disk. The only structural difference from CreationIdea is the omission of submitter — no Feishu identity is exposed client-side.
interface CreationEntry {
  id: string;        // CreationIdea.id
  cardId: string;    // CreationIdea.cardId
  author: string;    // CreationIdea.author (display alias)
  tags: string;      // CreationIdea.tags
  content: string;   // CreationIdea.content
  images: string[];  // CreationIdea.images (OSS URLs)
  createdAt: string; // CreationIdea.createdAt
}
Not written to disk. Derived purely from creation-board.json at render time.

6. CreationCard

CreationCard is the top-level unit rendered in the /creation waterfall. It does not exist in Feishu as a single record; it is assembled by grouping all CreationIdea entries sharing the same cardId, projecting each to a CreationEntry, and attaching the addIdeaUrl deep-link.
interface CreationCard {
  id: string;              // The shared CardID value
  cardId: string;          // Same as id — kept explicit for clarity
  addIdeaUrl: string;      // "Add Idea" button deep-link (Feishu form URL)
  entries: CreationEntry[]; // All projected entries for this card, sorted by createdAt
}
Not written to disk as CreationCard. Assembled from creation-board.json and creation-board-headers.json.

7. Contributor

Contributor maps one row in the Feishu credits / contributors table. Field extraction uses broad Feishu field-name compatibility so the table can be renamed without breaking the sync.
interface Contributor {
  id: string;      // Feishu record_id
  name: string;    // Feishu: 姓名
  role?: string;   // Feishu: 职位 or 角色 (optional)
  message?: string; // Feishu: 留言 / 寄语 / 感言 (optional)
}
Output artifact: src/data/contributors.jsonContributor[]. Consumed by the Credits section on the home page.

Artifact Summary

Artifact pathTypeScript typeUpdated by
src/config/locations.jsonLocationCoords (coordinate map)locations sync module
src/data/characters.jsonCharacter[]characters sync module
src/data/content.jsonLocationPoint[]stories sync module
src/data/creation-board-headers.jsonCardHeaderInfo[]creation_headers sync module
src/data/creation-board.jsonCreationIdea[]creation_board sync module
src/data/contributors.jsonContributor[]contributors sync module

Zustand Store State

Global UI state lives in src/lib/store.ts. All four slices are flat — no nested objects — to keep cross-slice transitions atomic.
// src/lib/store.ts
interface AppState {
  // ── Envelope ceremony ──────────────────────────────────────
  isEnvelopeOpened: boolean;
  setEnvelopeOpened: (v: boolean) => void;

  // ── Full-screen transition overlay ─────────────────────────
  isTransitioning: boolean;
  setTransitioning: (v: boolean) => void;

  // ── Map experience — location pin selection ─────────────────
  selectedLocationId: string | null;
  setSelectedLocationId: (id: string | null) => void;

  // ── Story reader — current card in the stack ────────────────
  currentStoryIndex: number;
  setCurrentStoryIndex: (i: number) => void;
}
KeyTypeDescription
isEnvelopeOpenedbooleantrue once the user completes the envelope-open gesture. Controls Lenis activation and scroll-section visibility.
isTransitioningbooleantrue during the full-screen wax-seal overlay that bridges the home route to /map. Pauses all scroll and interaction.
selectedLocationIdstring | nullID of the active map pin. null means no pin is selected; the story panel is hidden.
currentStoryIndexnumberZero-based index of the story currently shown within the selected location’s card stack. Resets to 0 when selectedLocationId changes.

Build docs developers (and LLMs) love