OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / lib / views / home-ui.js

109 lines · javascript ref:
/**
 * Home Page — Recent notes feed
 *
 * SSR view showing recent notes across all public notebooks.
 */

import { renderPage } from '../page-layout.js'
import { renderMarkdown } from '../markdown.js'

/**
 * Escape text for HTML
 */
function esc (s) {
  return (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
}

/**
 * Render the home page
 *
 * @param {Array} recentNotes - Recent notes with ownerName field
 * @param {Array} notebooks - Notebook list for sidebar
 * @param {Object} opts - { pathPrefix }
 * @returns {string} Full HTML page
 */
export function renderHomePage (recentNotes, notebooks, opts = {}) {
  const pathPrefix = opts.pathPrefix || ''

  let sidebar = '<div class="sidebar">'
  sidebar += '<h3>Notebooks</h3>'
  if (notebooks.length === 0) {
    sidebar += '<p class="meta">No notebooks yet</p>'
  } else {
    for (const nb of notebooks) {
      sidebar += `<a class="sidebar-item" href="${pathPrefix}/notebooks/${encodeURIComponent(nb.slug)}">`
      sidebar += `<span class="sidebar-name">${esc(nb.name || nb.slug)}</span>`
      sidebar += `<span class="sidebar-count">${nb.noteCount || 0}</span>`
      sidebar += '</a>'
    }
  }
  sidebar += '</div>'

  let feed = '<div class="feed">'
  feed += '<h1>Recent Notes</h1>'

  if (recentNotes.length === 0) {
    feed += '<p class="meta">No notes yet. Create a notebook to get started.</p>'
  } else {
    for (const note of recentNotes) {
      const href = `${pathPrefix}/notebooks/${encodeURIComponent(note.notebook)}/notes/${encodeURIComponent(note.slug || note.id)}`
      feed += `<a class="card feed-card" href="${href}">`
      feed += `<h3>${esc(note.title || 'Untitled')}</h3>`
      feed += `<div class="feed-meta">`
      feed += `<span>${esc(note.notebook)}</span>`
      feed += `<span>${esc(note.ownerName || 'Anonymous')}</span>`
      feed += `<span>${formatDate(note.updatedAt || note.createdAt)}</span>`
      feed += '</div>'
      if (note.content) {
        const snippet = note.content.substring(0, 160).replace(/\n/g, ' ')
        feed += `<div class="desc">${esc(snippet)}${note.content.length > 160 ? '...' : ''}</div>`
      }
      if (note.tags && note.tags.length > 0) {
        feed += '<div class="tags">'
        for (const t of note.tags) {
          feed += `<span class="tag">${esc(t)}</span>`
        }
        feed += '</div>'
      }
      feed += '</a>'
    }
  }
  feed += '</div>'

  const content = `<div class="home-layout">${sidebar}${feed}</div>`

  const css = `
    .home-layout { display: flex; gap: 24px; }
    .sidebar { width: 220px; flex-shrink: 0; }
    .sidebar h3 { font-size: 0.9rem; color: var(--text-muted); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
    .sidebar-item {
      display: flex; justify-content: space-between; align-items: center;
      padding: 8px 12px; margin: 2px 0; border-radius: 6px;
      text-decoration: none; color: var(--text); font-size: 0.9rem;
    }
    .sidebar-item:hover { background: var(--bg-hover); text-decoration: none; }
    .sidebar-count { font-size: 0.8rem; color: var(--text-muted); }
    .feed { flex: 1; min-width: 0; }
    .feed-card { margin-bottom: 12px; }
    .feed-meta { display: flex; gap: 12px; font-size: 0.8rem; color: var(--text-muted); margin: 4px 0; }
    @media (max-width: 768px) {
      .home-layout { flex-direction: column; }
      .sidebar { width: 100%; }
    }
  `

  return renderPage('Home', content, { pathPrefix, activePage: 'home', css, maxWidth: 1100 })
}

function formatDate (d) {
  if (!d) return ''
  const date = new Date(d)
  const now = new Date()
  const diff = now - date
  if (diff < 60000) return 'just now'
  if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'
  if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'
  if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago'
  return date.toLocaleDateString()
}