← back to Stars of Design

views/public/feed.ejs

444 lines

<%- include('../partials/head', { title: title, meta_desc_override: typeof meta_desc_override !== 'undefined' ? meta_desc_override : undefined }) %>
<%- include('../partials/header') %>

<style>
  .feed-shell {
    max-width: 520px;
    margin: 0 auto;
    padding: 16px 12px 96px;
    background: var(--bg, #0e0e0e);
  }
  .feed-head {
    text-align: center;
    padding: 18px 0 8px;
    border-bottom: 1px solid var(--line, #2a2620);
    margin-bottom: 16px;
  }
  .feed-head h1 {
    font-family: 'Cormorant Garamond', Georgia, serif;
    font-weight: 400;
    font-size: 32px;
    margin: 0 0 4px;
    color: var(--gold, #c9a14b);
    letter-spacing: -0.01em;
  }
  .feed-head p {
    margin: 0;
    color: var(--ink-faint, #7a706a);
    font-size: 12px;
    letter-spacing: 0.04em;
  }
  .post {
    background: var(--card, #1a1714);
    border: 1px solid var(--line, #2a2620);
    border-radius: 12px;
    margin: 0 0 14px;
    overflow: hidden;
  }
  .post-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px;
  }
  .post-avatar {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: #14110e no-repeat center/cover;
    border: 1px solid var(--line, #2a2620);
    flex-shrink: 0;
  }
  .post-meta {
    flex: 1;
    min-width: 0;
  }
  .post-name {
    color: var(--ink, #f0ebe3);
    font-weight: 500;
    font-size: 15px;
    margin: 0;
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    display: block;
  }
  .post-name:hover { color: var(--gold, #c9a14b); }
  .post-sub {
    color: var(--ink-faint, #7a706a);
    font-size: 12px;
    margin: 1px 0 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .post-body {
    padding: 0 12px 12px;
  }
  .post-areas {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 4px 0 0;
    padding: 0;
    list-style: none;
  }
  .post-areas li {
    padding: 3px 9px;
    background: rgba(201, 161, 75, 0.08);
    border: 1px solid var(--line, #2a2620);
    border-radius: 999px;
    color: var(--ink-soft, #c8beb2);
    font-size: 11px;
    letter-spacing: 0.04em;
  }
  .post-links {
    margin: 10px 0 0;
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
  .post-links a {
    color: var(--ink-faint, #7a706a);
    font-size: 12px;
    text-decoration: none;
  }
  .post-links a:hover { color: var(--gold, #c9a14b); }
  .post-actions {
    display: flex;
    align-items: center;
    border-top: 1px solid var(--line, #2a2620);
    padding: 4px 4px;
  }
  .post-actions button {
    background: transparent;
    border: 0;
    color: var(--ink-soft, #c8beb2);
    font: inherit;
    font-size: 13px;
    padding: 10px 14px;
    cursor: pointer;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    border-radius: 6px;
  }
  .post-actions button:hover { background: rgba(255, 255, 255, 0.04); }
  .post-actions button[aria-pressed="true"] { color: var(--gold, #c9a14b); font-weight: 500; }
  .post-actions .ico { font-size: 16px; line-height: 1; }
  .like-count { font-variant-numeric: tabular-nums; }
  .comments {
    display: none;
    padding: 6px 12px 12px;
    border-top: 1px solid var(--line, #2a2620);
  }
  .comments.open { display: block; }
  .comment {
    padding: 8px 0;
    border-bottom: 1px dotted var(--line, #2a2620);
  }
  .comment:last-child { border-bottom: 0; }
  .comment-author {
    color: var(--gold, #c9a14b);
    font-size: 12px;
    font-weight: 500;
    margin: 0 0 2px;
  }
  .comment-body {
    color: var(--ink-soft, #c8beb2);
    font-size: 13.5px;
    margin: 0;
    line-height: 1.5;
    word-wrap: break-word;
  }
  .comment-time {
    color: var(--ink-faint, #7a706a);
    font-size: 11px;
    margin-left: 8px;
  }
  .comment-form {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 12px;
  }
  .comment-form input, .comment-form textarea {
    background: rgba(0, 0, 0, 0.25);
    border: 1px solid var(--line, #2a2620);
    color: var(--ink, #f0ebe3);
    border-radius: 6px;
    padding: 7px 10px;
    font: inherit;
    font-size: 13px;
    width: 100%;
    box-sizing: border-box;
    resize: vertical;
    font-family: inherit;
  }
  .comment-form textarea { min-height: 60px; }
  .comment-form .row {
    display: flex;
    gap: 6px;
    align-items: center;
  }
  .comment-form .row input { flex: 1; }
  .comment-form button {
    background: var(--gold, #c9a14b);
    color: #1a1410;
    border: 0;
    padding: 6px 14px;
    font-size: 12px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    border-radius: 4px;
    cursor: pointer;
    font-weight: 500;
  }
  .comment-form button:disabled { opacity: 0.5; cursor: not-allowed; }
  .comment-form .err {
    color: #e07a7a;
    font-size: 12px;
    margin: 0;
  }
  .pager {
    display: flex;
    justify-content: center;
    gap: 12px;
    margin-top: 24px;
  }
  .pager a {
    color: var(--ink-soft, #c8beb2);
    text-decoration: none;
    padding: 8px 18px;
    border: 1px solid var(--line, #2a2620);
    border-radius: 6px;
    font-size: 12px;
    letter-spacing: 0.08em;
  }
  .pager a:hover { color: var(--gold, #c9a14b); border-color: var(--gold, #c9a14b); }
  .pager .disabled { opacity: 0.35; pointer-events: none; }

  @media (max-width: 520px) {
    .feed-shell { padding: 8px 8px 80px; }
    .post { border-radius: 0; border-left: 0; border-right: 0; margin-left: -8px; margin-right: -8px; }
  }
</style>

<main class="feed-shell">
  <div class="feed-head">
    <h1>DW Feed</h1>
    <p>Like, comment, follow your favorite design houses.</p>
  </div>

  <% items.forEach(function(it) { %>
    <article class="post" data-id="<%= it.id %>">
      <div class="post-head">
        <div class="post-avatar" style="background-image:url('<%= it.headshot %>')"></div>
        <div class="post-meta">
          <a class="post-name" href="/clients/<%= it.slug %>"><%= it.full_name %></a>
          <p class="post-sub">
            <% if (it.firm_name) { %><%= it.firm_name %><% if (it.role) { %> · <%= it.role %><% } %><% } else if (it.role) { %><%= it.role %><% } %>
            <% if ((it.firm_name || it.role) && (it.city || it.state)) { %> · <% } %>
            <% if (it.city || it.state) { %><%= [it.city, it.state].filter(Boolean).join(', ') %><% } %>
          </p>
        </div>
      </div>

      <% if (it.areas && it.areas.length || it.links && (it.links['firm-site'] || it.links.linkedin || it.links.instagram)) { %>
      <div class="post-body">
        <% if (it.areas && it.areas.length) { %>
          <ul class="post-areas">
            <% it.areas.slice(0,5).forEach(function(a) { %><li><%= a %></li><% }); %>
          </ul>
        <% } %>
        <% var siteHref = (it.links['firm-site']||[])[0]; var liHref = (it.links.linkedin||[])[0]; var igHref = (it.links.instagram||[])[0]; %>
        <% if (siteHref || liHref || igHref) { %>
          <div class="post-links">
            <% if (siteHref) { %><a href="<%= siteHref %>" target="_blank" rel="nofollow noopener noreferrer">↗ Site</a><% } %>
            <% if (liHref)   { %><a href="<%= liHref %>" target="_blank" rel="nofollow noopener noreferrer">↗ LinkedIn</a><% } %>
            <% if (igHref)   { %><a href="<%= igHref %>" target="_blank" rel="nofollow noopener noreferrer">↗ Instagram</a><% } %>
          </div>
        <% } %>
      </div>
      <% } %>

      <div class="post-actions">
        <button class="js-like" type="button" aria-pressed="<%= it.i_liked %>">
          <span class="ico">♥</span> <span class="like-count"><%= it.like_count %></span> <span>Like</span>
        </button>
        <button class="js-comment-toggle" type="button" aria-expanded="false">
          <span class="ico">💬</span> <span class="comment-count"><%= it.comment_count %></span> <span>Comment</span>
        </button>
        <button class="js-share" type="button" data-share-url="/clients/<%= it.slug %>">
          <span class="ico">↗</span> <span>Share</span>
        </button>
      </div>

      <section class="comments">
        <div class="comments-list"><!-- lazy-loaded --></div>
        <form class="comment-form js-comment-form">
          <div class="row">
            <input name="author_name" placeholder="Your name *" maxlength="80" required>
            <input name="author_email" placeholder="Email (optional)" maxlength="160" type="email">
          </div>
          <textarea name="body" placeholder="Add a comment…" maxlength="1500" required></textarea>
          <p class="err" hidden></p>
          <div style="display:flex;justify-content:flex-end">
            <button type="submit">Post</button>
          </div>
        </form>
      </section>
    </article>
  <% }); %>

  <div class="pager">
    <a class="<%= page <= 1 ? 'disabled' : '' %>" href="?page=<%= Math.max(1, page-1) %>">← Newer</a>
    <a class="<%= items.length < 15 ? 'disabled' : '' %>" href="?page=<%= page+1 %>">Older →</a>
  </div>
</main>

<script>
(function(){
  // Like toggle
  document.querySelectorAll('.js-like').forEach(function(btn){
    btn.addEventListener('click', async function(){
      var post = btn.closest('.post');
      var id = post.dataset.id;
      btn.disabled = true;
      try {
        var r = await fetch('/api/clients/' + id + '/like', { method: 'POST' });
        if (!r.ok) throw new Error('like-failed');
        var j = await r.json();
        btn.querySelector('.like-count').textContent = j.like_count;
        btn.setAttribute('aria-pressed', j.liked ? 'true' : 'false');
      } catch(e) {
        // silent fail
      } finally {
        btn.disabled = false;
      }
    });
  });

  // Comment toggle + lazy load
  document.querySelectorAll('.js-comment-toggle').forEach(function(btn){
    btn.addEventListener('click', async function(){
      var post = btn.closest('.post');
      var sect = post.querySelector('.comments');
      var open = !sect.classList.contains('open');
      sect.classList.toggle('open', open);
      btn.setAttribute('aria-expanded', String(open));
      if (open && !sect.dataset.loaded) {
        var list = sect.querySelector('.comments-list');
        list.textContent = 'Loading…';
        try {
          var r = await fetch('/api/clients/' + post.dataset.id + '/comments');
          if (!r.ok) throw new Error('load');
          var j = await r.json();
          list.innerHTML = '';
          (j.comments || []).forEach(function(c){
            var el = document.createElement('div');
            el.className = 'comment';
            var au = document.createElement('p');
            au.className = 'comment-author';
            au.textContent = c.author_name;
            var t = document.createElement('span');
            t.className = 'comment-time';
            t.textContent = new Date(c.created_at).toLocaleString();
            au.appendChild(t);
            var bo = document.createElement('p');
            bo.className = 'comment-body';
            bo.textContent = c.body;
            el.appendChild(au); el.appendChild(bo);
            list.appendChild(el);
          });
          if (!j.comments || !j.comments.length) {
            list.innerHTML = '<p style="color:var(--ink-faint,#7a706a);font-size:12.5px;margin:8px 0 0">Be the first to comment.</p>';
          }
          sect.dataset.loaded = '1';
        } catch (e) {
          list.textContent = 'Couldn\'t load comments.';
        }
      }
    });
  });

  // Comment submit
  document.querySelectorAll('.js-comment-form').forEach(function(form){
    form.addEventListener('submit', async function(ev){
      ev.preventDefault();
      var post = form.closest('.post');
      var btn = form.querySelector('button[type=submit]');
      var err = form.querySelector('.err');
      err.hidden = true;
      btn.disabled = true;
      try {
        var body = {
          author_name: form.author_name.value.trim(),
          author_email: form.author_email.value.trim(),
          body: form.body.value.trim()
        };
        var r = await fetch('/api/clients/' + post.dataset.id + '/comment', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body)
        });
        if (!r.ok) {
          var j = await r.json().catch(function(){ return {}; });
          throw new Error(j.error || 'failed');
        }
        // Insert into list
        var list = post.querySelector('.comments-list');
        var el = document.createElement('div');
        el.className = 'comment';
        var au = document.createElement('p');
        au.className = 'comment-author';
        au.textContent = body.author_name;
        var t = document.createElement('span');
        t.className = 'comment-time';
        t.textContent = new Date().toLocaleString();
        au.appendChild(t);
        var bo = document.createElement('p');
        bo.className = 'comment-body';
        bo.textContent = body.body;
        el.appendChild(au); el.appendChild(bo);
        list.appendChild(el);
        form.body.value = '';
        // Bump comment count
        var cc = post.querySelector('.comment-count');
        cc.textContent = String(parseInt(cc.textContent || '0', 10) + 1);
      } catch (e) {
        err.hidden = false;
        err.textContent =
          e.message === 'name-required' ? 'Name is required.' :
          e.message === 'spam-or-empty' ? 'Comment looks empty or spammy.' :
          e.message === 'rate-limit'    ? 'Too many comments. Slow down.' :
          'Could not post. Try again.';
      } finally {
        btn.disabled = false;
      }
    });
  });

  // Share — Web Share API where available, fallback to copy
  document.querySelectorAll('.js-share').forEach(function(btn){
    btn.addEventListener('click', async function(){
      var url = location.origin + btn.dataset.shareUrl;
      try {
        if (navigator.share) {
          await navigator.share({ url, title: document.title });
        } else {
          await navigator.clipboard.writeText(url);
          btn.querySelector('span:last-child').textContent = 'Copied!';
          setTimeout(function(){ btn.querySelector('span:last-child').textContent = 'Share'; }, 1500);
        }
      } catch (e) { /* user-canceled */ }
    });
  });
})();
</script>

<%- include('../partials/footer') %>