SCALE — Build Lab
UI部品 · REACT COMPONENT

アップデート配信フィード

CATEGORYUI部品 TYPEReact Component EFFORT180〜360分 DIFFICULTY
PRIMARY CODE
tsx
type UpdateEntry = {
  version: string;
  date: string;            // 'YYYY-MM-DD'
  tag: '新機能' | '改善' | '修正' | '重要';
  title: string;
  description: string;
  screenshot?: string;
};

const TAG_STYLE: Record<UpdateEntry['tag'], string> = {
  '新機能': 'background:#34d399; color:#08080a;',
  '改善':   'background:#a5b4fc; color:#08080a;',
  '修正':   'background:#fbbf24; color:#08080a;',
  '重要':   'background:#fb7185; color:#fff;',
};

export function UpdateFeed({ entries, lastReadAt }: {
  entries: UpdateEntry[];
  lastReadAt?: string;
}) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
      {entries.map((e, i) => {
        const isNew = lastReadAt && e.date > lastReadAt;
        return (
          <article key={i} style={{
            padding: '1.5rem',
            border: '1px solid rgba(255,255,255,.07)',
            background: isNew ? 'rgba(165,180,252,.04)' : 'transparent',
            position: 'relative',
          }}>
            {isNew && <span style={{
              position: 'absolute', top: '1rem', right: '1rem',
              background: '#a5b4fc', color: '#08080a',
              padding: '2px 8px', fontSize: '.6rem', letterSpacing: '.15em',
            }}>NEW</span>}
            <div style={{ display: 'flex', gap: '.65rem', alignItems: 'center', marginBottom: '.85rem' }}>
              <span style={{ fontFamily: 'monospace', fontSize: '.7rem', color: '#71717a' }}>v{e.version}</span>
              <span style={{ fontSize: '.7rem', color: '#71717a' }}>{e.date}</span>
              <span style={{ ...({} as any), padding: '2px 8px', fontSize: '.6rem', letterSpacing: '.1em' }} dangerouslySetInnerHTML={{ __html: `<span style="${TAG_STYLE[e.tag]}; padding:2px 8px; font-size:.6rem;">${e.tag}</span>` }} />
            </div>
            <h3 style={{ fontSize: '1.05rem', marginBottom: '.5rem', color: '#fff' }}>{e.title}</h3>
            <p style={{ fontSize: '.85rem', color: 'rgba(255,255,255,.55)', lineHeight: 1.85 }}>{e.description}</p>
            {e.screenshot && <img src={e.screenshot} alt="" style={{ marginTop: '1rem', borderRadius: 4, width: '100%' }} />}
          </article>
        );
      })}
    </div>
  );
}
前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • SaaS の What's New
  • プロダクトアップデート公開

アップデート配信フィード

:LiTarget: 用途

システムの変更履歴(changelog)をユーザー向けに公開するフィード。バージョン・タグ・スクショ付き。

:LiSparkle: 特徴

  • 日付別表示
  • タグ(新機能/改善/修正)
  • スクショ添付
  • 未読バッジ
  • リアクション

:LiCode: コード(コピペ用)

type UpdateEntry = {
  version: string;
  date: string;            // 'YYYY-MM-DD'
  tag: '新機能' | '改善' | '修正' | '重要';
  title: string;
  description: string;
  screenshot?: string;
};

const TAG_STYLE: Record<UpdateEntry['tag'], string> = {
  '新機能': 'background:#34d399; color:#08080a;',
  '改善':   'background:#a5b4fc; color:#08080a;',
  '修正':   'background:#fbbf24; color:#08080a;',
  '重要':   'background:#fb7185; color:#fff;',
};

export function UpdateFeed({ entries, lastReadAt }: {
  entries: UpdateEntry[];
  lastReadAt?: string;
}) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
      {entries.map((e, i) => {
        const isNew = lastReadAt && e.date > lastReadAt;
        return (
          <article key={i} style={{
            padding: '1.5rem',
            border: '1px solid rgba(255,255,255,.07)',
            background: isNew ? 'rgba(165,180,252,.04)' : 'transparent',
            position: 'relative',
          }}>
            {isNew && <span style={{
              position: 'absolute', top: '1rem', right: '1rem',
              background: '#a5b4fc', color: '#08080a',
              padding: '2px 8px', fontSize: '.6rem', letterSpacing: '.15em',
            }}>NEW</span>}
            <div style={{ display: 'flex', gap: '.65rem', alignItems: 'center', marginBottom: '.85rem' }}>
              <span style={{ fontFamily: 'monospace', fontSize: '.7rem', color: '#71717a' }}>v{e.version}</span>
              <span style={{ fontSize: '.7rem', color: '#71717a' }}>{e.date}</span>
              <span style={{ ...({} as any), padding: '2px 8px', fontSize: '.6rem', letterSpacing: '.1em' }} dangerouslySetInnerHTML={{ __html: `<span style="${TAG_STYLE[e.tag]}; padding:2px 8px; font-size:.6rem;">${e.tag}</span>` }} />
            </div>
            <h3 style={{ fontSize: '1.05rem', marginBottom: '.5rem', color: '#fff' }}>{e.title}</h3>
            <p style={{ fontSize: '.85rem', color: 'rgba(255,255,255,.55)', lineHeight: 1.85 }}>{e.description}</p>
            {e.screenshot && <img src={e.screenshot} alt="" style={{ marginTop: '1rem', borderRadius: 4, width: '100%' }} />}
          </article>
        );
      })}
    </div>
  );
}

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加