From cf72e7398507ae4ea74058342034e8221f38136a Mon Sep 17 00:00:00 2001 From: wudi Date: Fri, 3 Jul 2026 08:26:08 +0000 Subject: [PATCH] refactor(flnk): pin launchpad header as fixed card above blocks list The profile header is a singleton rendered once from config.profile, but the editor modeled it as a regular block: its enable switch did nothing, multiple copies could be added (all editing the same profile), and fresh launchpads hid the profile fields behind Add block. Replace it with a permanent pinned card at the top of the build tab that edits config.profile directly, remove 'header' from the add menu, and filter legacy header blocks out of the list on read (dropped on next save). Schema and public renderer are unchanged so existing rows still validate. --- apps/flnk/messages/en.json | 1 + apps/flnk/messages/zh.json | 1 + .../components/dashboard/launchpads/blocks.ts | 11 +- .../dashboard/launchpads/build-tab.tsx | 135 +++++++++++------- 4 files changed, 89 insertions(+), 59 deletions(-) diff --git a/apps/flnk/messages/en.json b/apps/flnk/messages/en.json index 3cf8718f..cf86ac1f 100644 --- a/apps/flnk/messages/en.json +++ b/apps/flnk/messages/en.json @@ -372,6 +372,7 @@ }, "build": { "hint": "Add, reorder, and toggle the blocks shown on your page.", + "pinned": "Pinned", "addBlock": "Add block", "empty": "No blocks yet. Add your first block to get started.", "moveUp": "Move up", diff --git a/apps/flnk/messages/zh.json b/apps/flnk/messages/zh.json index cb0a09b2..cf2218a6 100644 --- a/apps/flnk/messages/zh.json +++ b/apps/flnk/messages/zh.json @@ -372,6 +372,7 @@ }, "build": { "hint": "添加、拖拽排序并启用页面上的区块。", + "pinned": "固定", "addBlock": "添加区块", "empty": "还没有区块。添加第一个区块开始构建。", "moveUp": "上移", diff --git a/apps/flnk/src/components/dashboard/launchpads/blocks.ts b/apps/flnk/src/components/dashboard/launchpads/blocks.ts index 260c3b86..a25cd9e0 100644 --- a/apps/flnk/src/components/dashboard/launchpads/blocks.ts +++ b/apps/flnk/src/components/dashboard/launchpads/blocks.ts @@ -4,9 +4,12 @@ import type { LinkRow } from '@/lib/api' export type BlockType = LaunchpadBlock['type'] +// The header is a pinned singleton edited from the fixed card at the top of +// the build tab — it is not addable as a content block. +export type AddableBlockType = Exclude + // Add-menu order — mirrors the MVP block set in the spec. -export const BLOCK_TYPES: BlockType[] = [ - 'header', +export const BLOCK_TYPES: AddableBlockType[] = [ 'socials', 'button', 'shortlink', @@ -16,11 +19,9 @@ export const BLOCK_TYPES: BlockType[] = [ ] // Fresh block of `type` with sensible empty defaults and a unique id. -export function newBlock(type: BlockType): LaunchpadBlock { +export function newBlock(type: AddableBlockType): LaunchpadBlock { const id = crypto.randomUUID() switch (type) { - case 'header': - return { id, type, enabled: true } case 'socials': return { id, type, enabled: true, items: [] } case 'button': diff --git a/apps/flnk/src/components/dashboard/launchpads/build-tab.tsx b/apps/flnk/src/components/dashboard/launchpads/build-tab.tsx index 496064e2..c79abeb7 100644 --- a/apps/flnk/src/components/dashboard/launchpads/build-tab.tsx +++ b/apps/flnk/src/components/dashboard/launchpads/build-tab.tsx @@ -1,5 +1,6 @@ 'use client' +import { Badge } from '@cdlab996/ui/components/badge' import { Button } from '@cdlab996/ui/components/button' import { Card } from '@cdlab996/ui/components/card' import { @@ -28,9 +29,12 @@ import { } from '@/components/dashboard/launchpads/link-picker' import type { LaunchpadBlock, LaunchpadConfig } from '@/database/schema' import type { LinkRow } from '@/lib/api' -import type { BlockType } from './blocks' +import type { AddableBlockType } from './blocks' import { BLOCK_TYPES, newBlock } from './blocks' +// The header is a pinned singleton (edited via HeaderCard), never a list item. +type ContentBlock = Exclude + interface BuildTabProps { config: LaunchpadConfig links: LinkRow[] @@ -39,16 +43,20 @@ interface BuildTabProps { export function BuildTab({ config, links, onChange }: BuildTabProps) { const t = useTranslations('launchpads') - const blocks = config.blocks + // Legacy 'header' blocks are ignored on read; any block edit writes back the + // filtered list, dropping them from config on the next save. + const blocks = config.blocks.filter( + (b): b is ContentBlock => b.type !== 'header', + ) const [dragIndex, setDragIndex] = useState(null) function setBlocks(next: LaunchpadBlock[]) { onChange({ ...config, blocks: next }) } - function addBlock(type: BlockType) { + function addBlock(type: AddableBlockType) { setBlocks([...blocks, newBlock(type)]) } - function updateBlock(id: string, block: LaunchpadBlock) { + function updateBlock(id: string, block: ContentBlock) { setBlocks(blocks.map((b) => (b.id === id ? block : b))) } function removeBlock(id: string) { @@ -64,6 +72,11 @@ export function BuildTab({ config, links, onChange }: BuildTabProps) { return (
+ onChange({ ...config, profile })} + /> +

{t('build.hint')}

@@ -148,12 +161,8 @@ export function BuildTab({ config, links, onChange }: BuildTabProps) {
updateBlock(block.id, b)} - onProfileChange={(profile) => - onChange({ ...config, profile }) - } />
@@ -164,63 +173,81 @@ export function BuildTab({ config, links, onChange }: BuildTabProps) { ) } -function BlockFields({ - block, - config, - links, +// Fixed profile card pinned above the blocks list — always exactly one, not +// draggable, removable, or toggleable. The public page shows the header iff +// any profile field has content, so no enabled switch here. +function HeaderCard({ + profile, onChange, - onProfileChange, }: { - block: LaunchpadBlock - config: LaunchpadConfig - links: LinkRow[] - onChange: (block: LaunchpadBlock) => void - onProfileChange: (profile: LaunchpadConfig['profile']) => void + profile: LaunchpadConfig['profile'] + onChange: (profile: LaunchpadConfig['profile']) => void }) { const t = useTranslations('launchpads') - const linkById = new Map(links.map((l) => [l.id, l])) - switch (block.type) { - case 'header': { - const profile = config.profile - return ( -
-

- {t('block.header.hint')} -

- - - onProfileChange({ ...profile, name: e.target.value }) - } - /> - - -