{
  "id": "blog-engine-mini",
  "name": "Mini Blog Engine",
  "category": "software-builder",
  "tags": ["blog", "markdown", "editor", "journal", "notes"],
  "description": "Simple blog with post list, markdown-flavored editor, and tag filtering.",
  "triggers": ["blog", "blog engine", "write blog", "notes app", "journal"],
  "defaultSize": { "w": 5, "h": 6 },
  "source": "const BlogEngineMini = () => {\n  const STORAGE_KEY = 'titan_blog_posts';\n  const [posts, setPosts] = React.useState([]);\n  const [view, setView] = React.useState('list');\n  const [activeTag, setActiveTag] = React.useState('All');\n  const [form, setForm] = React.useState({ title: '', body: '', tags: '' });\n  const [editingId, setEditingId] = React.useState(null);\n\n  React.useEffect(() => {\n    try {\n      const raw = localStorage.getItem(STORAGE_KEY);\n      if (raw) setPosts(JSON.parse(raw));\n      else {\n        const demo = [\n          { id: 1, title: 'Welcome to Titan Blog', body: 'This is a **markdown** blog engine.\\n\\n- Edit posts\\n- Filter by tags\\n- Persisted in localStorage', tags: ['intro', 'demo'], date: '2025-04-20' },\n          { id: 2, title: 'Building with React', body: 'React makes UI development *declarative* and composable.', tags: ['react', 'dev'], date: '2025-04-22' }\n        ];\n        setPosts(demo);\n      }\n    } catch (e) {\n      setPosts([]);\n    }\n  }, []);\n\n  React.useEffect(() => {\n    localStorage.setItem(STORAGE_KEY, JSON.stringify(posts));\n  }, [posts]);\n\n  const allTags = ['All', ...Array.from(new Set(posts.flatMap(p => p.tags)))];\n\n  const filtered = activeTag === 'All' ? posts : posts.filter(p => p.tags.includes(activeTag));\n\n  const parseMarkdown = (text) => {\n    let html = text\n      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n      .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')\n      .replace(/\\*(.+?)\\*/g, '<em>$1</em>')\n      .replace(/^- (.+)$/gm, '<li>$1</li>')\n      .replace(/\\n/g, '<br/>');\n    return html;\n  };\n\n  const handleSave = () => {\n    if (!form.title.trim() || !form.body.trim()) return;\n    const tagList = form.tags.split(',').map(t => t.trim()).filter(Boolean);\n    if (editingId !== null) {\n      setPosts(prev => prev.map(p => p.id === editingId ? { ...p, title: form.title, body: form.body, tags: tagList } : p));\n    } else {\n      setPosts(prev => [{ id: Date.now(), title: form.title, body: form.body, tags: tagList, date: new Date().toISOString().slice(0, 10) }, ...prev]);\n    }\n    setForm({ title: '', body: '', tags: '' });\n    setEditingId(null);\n    setView('list');\n  };\n\n  const handleEdit = (post) => {\n    setForm({ title: post.title, body: post.body, tags: post.tags.join(', ') });\n    setEditingId(post.id);\n    setView('editor');\n  };\n\n  const handleDelete = (id) => {\n    setPosts(prev => prev.filter(p => p.id !== id));\n  };\n\n  const styles = {\n    container: {\n      fontFamily: 'system-ui, -apple-system, sans-serif',\n      padding: 24,\n      background: '#f8fafc',\n      minHeight: '100%',\n      color: '#1e293b'\n    },\n    header: {\n      display: 'flex',\n      justifyContent: 'space-between',\n      alignItems: 'center',\n      marginBottom: 16\n    },\n    title: {\n      fontSize: 20,\n      fontWeight: 700\n    },\n    btn: {\n      padding: '8px 16px',\n      borderRadius: 8,\n      border: 'none',\n      background: '#2563eb',\n      color: '#fff',\n      fontWeight: 600,\n      cursor: 'pointer',\n      fontSize: 14\n    },\n    btnGhost: {\n      padding: '8px 16px',\n      borderRadius: 8,\n      border: '1px solid #cbd5e1',\n      background: '#fff',\n      color: '#475569',\n      fontWeight: 600,\n      cursor: 'pointer',\n      fontSize: 14\n    },\n    btnDanger: {\n      padding: '6px 12px',\n      borderRadius: 6,\n      border: 'none',\n      background: '#ef4444',\n      color: '#fff',\n      fontWeight: 600,\n      cursor: 'pointer',\n      fontSize: 12\n    },\n    btnSmall: {\n      padding: '6px 12px',\n      borderRadius: 6,\n      border: 'none',\n      background: '#2563eb',\n      color: '#fff',\n      fontWeight: 600,\n      cursor: 'pointer',\n      fontSize: 12\n    },\n    tagRow: {\n      display: 'flex',\n      gap: 8,\n      flexWrap: 'wrap',\n      marginBottom: 16\n    },\n    tag: (active) => ({\n      padding: '5px 12px',\n      borderRadius: 999,\n      fontSize: 12,\n      fontWeight: 600,\n      cursor: 'pointer',\n      border: 'none',\n      background: active ? '#2563eb' : '#e2e8f0',\n      color: active ? '#fff' : '#475569'\n    }),\n    card: {\n      background: '#fff',\n      borderRadius: 12,\n      padding: 20,\n      marginBottom: 14,\n      boxShadow: '0 1px 3px rgba(0,0,0,0.06)',\n      border: '1px solid #e2e8f0'\n    },\n    postTitle: {\n      fontSize: 16,\n      fontWeight: 700,\n      marginBottom: 6\n    },\n    postMeta: {\n      fontSize: 12,\n      color: '#94a3b8',\n      marginBottom: 10\n    },\n    postBody: {\n      fontSize: 14,\n      lineHeight: 1.6,\n      color: '#334155'\n    },\n    postTags: {\n      display: 'flex',\n      gap: 6,\n      marginTop: 12\n    },\n    postTag: {\n      fontSize: 11,\n      fontWeight: 600,\n      padding: '3px 10px',\n      borderRadius: 999,\n      background: '#dbeafe',\n      color: '#1d4ed8'\n    },\n    actions: {\n      display: 'flex',\n      gap: 8,\n      marginTop: 12\n    },\n    input: {\n      width: '100%',\n      padding: '10px 12px',\n      borderRadius: 8,\n      border: '1px solid #cbd5e1',\n      fontSize: 14,\n      outline: 'none',\n      marginBottom: 12,\n      boxSizing: 'border-box'\n    },\n    textarea: {\n      width: '100%',\n      padding: '10px 12px',\n      borderRadius: 8,\n      border: '1px solid #cbd5e1',\n      fontSize: 14,\n      outline: 'none',\n      minHeight: 140,\n      resize: 'vertical',\n      marginBottom: 12,\n      boxSizing: 'border-box',\n      fontFamily: 'inherit'\n    },\n    previewBox: {\n      background: '#f8fafc',\n      borderRadius: 8,\n      padding: 16,\n      border: '1px dashed #cbd5e1',\n      marginBottom: 12,\n      fontSize: 14,\n      lineHeight: 1.6\n    },\n    label: {\n      fontSize: 12,\n      fontWeight: 600,\n      color: '#475569',\n      marginBottom: 6,\n      display: 'block'\n    }\n  };\n\n  if (view === 'editor') {\n    return (\n      <div style={styles.container}>\n        <div style={styles.header}>\n          <div style={styles.title}>{editingId !== null ? 'Edit Post' : 'New Post'}</div>\n          <button style={styles.btnGhost} onClick={() => { setView('list'); setEditingId(null); setForm({ title: '', body: '', tags: '' }); }}>Back</button>\n        </div>\n        <div style={styles.card}>\n          <label style={styles.label}>Title</label>\n          <input\n            style={styles.input}\n            placeholder=\"Post title\"\n            value={form.title}\n            onChange={e => setForm({ ...form, title: e.target.value })}\n          />\n          <label style={styles.label}>Body (markdown supported)</label>\n          <textarea\n            style={styles.textarea}\n            placeholder=\"Write your post...\"\n            value={form.body}\n            onChange={e => setForm({ ...form, body: e.target.value })}\n          />\n          <label style={styles.label}>Tags (comma separated)</label>\n          <input\n            style={styles.input}\n            placeholder=\"tech, tutorial\"\n            value={form.tags}\n            onChange={e => setForm({ ...form, tags: e.target.value })}\n          />\n          <label style={styles.label}>Preview</label>\n          <div style={styles.previewBox} dangerouslySetInnerHTML={{ __html: parseMarkdown(form.body || '(empty)') }} />\n          <div style={{ display: 'flex', gap: 10 }}>\n            <button style={styles.btn} onClick={handleSave}>Save Post</button>\n            <button style={styles.btnGhost} onClick={() => { setForm({ title: '', body: '', tags: '' }); setEditingId(null); setView('list'); }}>Cancel</button>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div style={styles.container}>\n      <div style={styles.header}>\n        <div style={styles.title}>\uD83D\uDCD6 Mini Blog</div>\n        <button style={styles.btn} onClick={() => setView('editor')}>+ New Post</button>\n      </div>\n      <div style={styles.tagRow}>\n        {allTags.map(tag => (\n          <button key={tag} style={styles.tag(activeTag === tag)} onClick={() => setActiveTag(tag)}>\n            {tag}\n          </button>\n        ))}\n      </div>\n      {filtered.length === 0 && <div style={{ color: '#94a3b8', fontSize: 14 }}>No posts found.</div>}\n      {filtered.map(post => (\n        <div key={post.id} style={styles.card}>\n          <div style={styles.postTitle}>{post.title}</div>\n          <div style={styles.postMeta}>{post.date}</div>\n          <div style={styles.postBody} dangerouslySetInnerHTML={{ __html: parseMarkdown(post.body) }} />\n          <div style={styles.postTags}>\n            {post.tags.map(t => (\n              <span key={t} style={styles.postTag}>{t}</span>\n            ))}\n          </div>\n          <div style={styles.actions}>\n            <button style={styles.btnSmall} onClick={() => handleEdit(post)}>Edit</button>\n            <button style={styles.btnDanger} onClick={() => handleDelete(post.id)}>Delete</button>\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n};\nrender(<BlogEngineMini/>);",
  "placeholders": [{ "name": "REPLACE_WITH_BLOG_TITLE", "description": "Default blog title displayed in the header", "default": "Mini Blog" }]
}
