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 }) - } - /> - - -