Changeset 8afbdd6 in Klonkt for src/views


Ignore:
Timestamp:
06/14/2026 06:07:51 PM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
c869272
Parents:
b94c08e
Message:

feat: viewer role + artists directory + Klonkt Hub Beta rebrand

Viewer role (replaces the separate view-mode/readonly toggle):

  • 'kijker' is now a real role (VALID_ROLES) selectable in Admin. May view EVERYTHING including Admin, but cannot modify anything.
  • isViewer(user) (= role kijker or legacy readonly flag) is the source; requireGod/requireSiteManager(BySlug) let a viewer through (viewing), the global guard 403s every write. Existing readonly accounts are migrated on each role change (readonly=0).
  • Write leaks via GET patched: /prutter/new (INSERT) blocks for viewers, /prutter/:id skips markAsRead (UPDATE); WS upgrade rejects viewers (the HTTP guard doesn't cover WebSockets).
  • Clean "Viewer mode" page (viewer-blocked) instead of raw 403 text; styled sticky banner; account page shows read-only UI instead of an upload button that silently 403s. canMutate hides write buttons.

Scalability (>50 artists):

  • Hub home shows max 24 (most active first) + "All N artists ->".
  • New searchable, paginated /artiesten directory (hub only).
  • 'user' + 'artiesten' reserved as slugs.

Rebrand PrutFolio v1 -> Klonkt Hub Beta (footer, PWA manifest, account/
admin texts, default page title, startup, README; internal package +
PWA id 'prutfolio' remain for stability).

Co-Authored-By: Claude <noreply@…>

Location:
src/views
Files:
2 added
13 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/account.ejs

    rb94c08e r8afbdd6  
    1212  <% if (error)   { %><div class="ax-flash ax-flash-err"><%= error %></div><% } %>
    1313
     14  <% if (isViewer) { %>
     15    <div class="ax-viewer-note" role="note">
     16      <span class="ax-viewer-ico" aria-hidden="true">👁️</span>
     17      <div>
     18        <strong>Kijker-modus</strong>
     19        <span>Dit is een demo-account. Je kunt alles bekijken, maar niets wijzigen — ook geen foto of bio.</span>
     20      </div>
     21    </div>
     22  <% } %>
     23
    1424  <%# ── PROFILE ────────────────────────────────────────────── %>
    1525  <section class="ax-card">
     
    1727
    1828    <div class="ax-profile-id">
    19       <form action="/account/avatar" method="post" enctype="multipart/form-data" class="ax-avatar-form">
    20         <label class="ax-avatar-wrap" title="Klik om je foto te wijzigen">
    21           <span class="ax-profile-avatar">
    22             <% if (account.avatar_url) { %>
    23               <img src="<%= account.avatar_url %>" alt="">
    24             <% } else { %>
    25               <span><%= account.username.charAt(0).toUpperCase() %></span>
    26             <% } %>
    27           </span>
    28           <span class="ax-avatar-edit" aria-hidden="true">
    29             <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
    30           </span>
    31           <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp,image/gif" onchange="this.form.submit()" hidden>
    32         </label>
    33       </form>
     29      <% if (canMutate) { %>
     30        <form action="/account/avatar" method="post" enctype="multipart/form-data" class="ax-avatar-form">
     31          <label class="ax-avatar-wrap" title="Klik om je foto te wijzigen">
     32            <span class="ax-profile-avatar">
     33              <% if (account.avatar_url) { %>
     34                <img src="<%= account.avatar_url %>" alt="">
     35              <% } else { %>
     36                <span><%= account.username.charAt(0).toUpperCase() %></span>
     37              <% } %>
     38            </span>
     39            <span class="ax-avatar-edit" aria-hidden="true">
     40              <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
     41            </span>
     42            <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp,image/gif" onchange="this.form.submit()" hidden>
     43          </label>
     44        </form>
     45      <% } else { %>
     46        <span class="ax-profile-avatar ax-profile-avatar-static">
     47          <% if (account.avatar_url) { %>
     48            <img src="<%= account.avatar_url %>" alt="">
     49          <% } else { %>
     50            <span><%= account.username.charAt(0).toUpperCase() %></span>
     51          <% } %>
     52        </span>
     53      <% } %>
    3454      <div class="ax-profile-meta">
    3555        <div class="ax-profile-name"><%= account.username %></div>
     
    3858          <div class="ax-profile-joined">Lid sinds <%= formatDate(account.created_at) %></div>
    3959        <% } %>
    40         <% if (account.avatar_url) { %>
     60        <% if (account.avatar_url && canMutate) { %>
    4161          <form action="/account/avatar/remove" method="post" class="ax-avatar-remove">
    4262            <button type="submit" class="ax-linkbtn">Foto verwijderen</button>
     
    4666    </div>
    4767
    48     <form action="/account/profile" method="post" class="ax-form ax-form-bio">
    49       <label class="ax-field">
    50         <span>Bio</span>
    51         <textarea name="bio" rows="3" maxlength="500" placeholder="Een korte regel over jezelf"><%= account.bio || '' %></textarea>
    52       </label>
    53       <div class="ax-form-actions">
    54         <button type="submit" class="ax-btn ax-btn-primary">Bio opslaan</button>
    55       </div>
    56     </form>
     68    <% if (canMutate) { %>
     69      <form action="/account/profile" method="post" class="ax-form ax-form-bio">
     70        <label class="ax-field">
     71          <span>Bio</span>
     72          <textarea name="bio" rows="3" maxlength="500" placeholder="Een korte regel over jezelf"><%= account.bio || '' %></textarea>
     73        </label>
     74        <div class="ax-form-actions">
     75          <button type="submit" class="ax-btn ax-btn-primary">Bio opslaan</button>
     76        </div>
     77      </form>
     78    <% } else { %>
     79      <div class="ax-form ax-form-bio">
     80        <label class="ax-field">
     81          <span>Bio</span>
     82          <textarea rows="3" disabled placeholder="Geen bio"><%= account.bio || '' %></textarea>
     83        </label>
     84      </div>
     85    <% } %>
    5786  </section>
    5887
    5988  <%# ── WACHTWOORD ────────────────────────────────────────── %>
     89  <%# In kijker-modus geen wachtwoord-sectie (niets te wijzigen). %>
     90  <% if (canMutate) { %>
    6091  <% if (hasPassword) { %>
    6192  <section class="ax-card">
     
    87118  </section>
    88119  <% } %>
     120  <% } %>
    89121
    90122</div>
     
    107139.ax-flash-ok  { background: rgba(40,160,90,.15); color: #2a9d5e; border: 1px solid rgba(40,160,90,.3); }
    108140.ax-flash-err { background: rgba(200,60,60,.15); color: #c33; border: 1px solid rgba(200,60,60,.3); }
     141
     142/* ─── Kijker-modus notice ──────────────────────────────────────── */
     143.ax-viewer-note {
     144  display: flex; align-items: flex-start; gap: 0.75rem;
     145  padding: 0.85rem 1rem; margin-bottom: 1rem;
     146  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--rule));
     147  border-left: 3px solid var(--accent);
     148  border-radius: 10px;
     149  background: color-mix(in srgb, var(--accent) 8%, var(--paper));
     150}
     151.ax-viewer-ico { font-size: 1.1rem; line-height: 1.4; }
     152.ax-viewer-note strong { display: block; font-size: 0.92rem; }
     153.ax-viewer-note span { color: var(--ink-muted, var(--ink-soft)); font-size: 0.85rem; }
     154.ax-profile-avatar-static { flex-shrink: 0; }
     155.ax-form-bio textarea:disabled { opacity: 0.7; cursor: default; }
    109156
    110157/* ─── Cards ────────────────────────────────────────────────────── */
  • src/views/pages/admin-settings.ejs

    rb94c08e r8afbdd6  
    2424        <span>
    2525          <strong>Hub</strong> — bedrijfssite met <code>/user/</code>.
    26           <small>Jij (admin) wijst gebruikers een eigen PrutFolio toe. Gesloten cirkel: geen open registratie.</small>
     26          <small>Jij (admin) wijst gebruikers een eigen Klonkt Hub toe. Gesloten cirkel: geen open registratie.</small>
    2727        </span>
    2828      </label>
     
    3636    <p class="set-help">
    3737      De openbare hub-pagina (<code>/</code>) is generiek — van geen enkele gebruiker.
    38       Hier stel je de naam en intro in. Jouw eigen PrutFolio blijft los bestaan onder <code>/user/</code>.
     38      Hier stel je de naam en intro in. Jouw eigen Klonkt Hub blijft los bestaan onder <code>/user/</code>.
    3939    </p>
    4040    <form method="post" action="/admin/settings" class="set-form">
  • src/views/pages/admin-users.ejs

    rb94c08e r8afbdd6  
    55    <div>
    66      <h1>Users</h1>
    7       <p class="ax-tagline">Beheer gebruikers, rollen, en verwijderingen.</p>
     7      <p class="ax-tagline">Beheer gebruikers, rollen, en verwijderingen. Rol <strong>kijker</strong> = alles bekijken (incl. Beheer), niets wijzigen — handig voor demo's.</p>
    88    </div>
    99  </header>
     
    5858              <label class="ax-role">
    5959                <span>Rol</span>
    60                 <select name="role" onchange="this.form.submit()" <% if (u.id === user.id) { %>disabled<% } %>>
     60                <select name="role" onchange="this.form.submit()" <% if (u.id === user.id || !canMutate) { %>disabled<% } %>>
     61                  <option value="kijker" <%= u.role === 'kijker' ? 'selected' : '' %>>kijker</option>
    6162                  <option value="member" <%= u.role === 'member' ? 'selected' : '' %>>member</option>
    6263                  <option value="admin"  <%= u.role === 'admin'  ? 'selected' : '' %>>admin</option>
     
    6667            </form>
    6768
    68             <form method="post" action="/admin/users/<%= u.id %>/readonly" class="ax-role-form">
    69               <label class="ax-role ax-ro" title="Alleen-lezen: kan alles bekijken, niets wijzigen">
    70                 <span>Kijk-modus</span>
    71                 <input type="checkbox" name="readonly" value="1" <%= u.readonly ? 'checked' : '' %>
    72                        onchange="this.form.submit()" <% if (u.id === user.id) { %>disabled<% } %>>
    73               </label>
    74             </form>
    75 
    76             <% if (u.id !== user.id) { %>
     69            <% if (u.id !== user.id && canMutate) { %>
    7770              <% var _warn = (u.post_count + u.site_count > 0)
    7871                   ? ('Dit verwijdert ook hun site + ' + u.post_count + ' post(s). ')
     
    8376              </form>
    8477            <% } else { %>
    85               <%# Lege plek zodat de kolommen (rol/kijk-modus) op elke rij uitlijnen. %>
     78              <%# Lege plek zodat de rol-kolom op elke rij uitlijnt. %>
    8679              <span class="ax-icon-btn-spacer" aria-hidden="true"></span>
    8780            <% } %>
  • src/views/pages/admin.ejs

    rb94c08e r8afbdd6  
    22  <h1>Beheer</h1>
    33  <% if (tenancy === 'hub') { %>
    4     <p class="admin-tagline">Hub-modus — bedrijfssite met gebruikers, elk hun eigen PrutFolio.</p>
     4    <p class="admin-tagline">Hub-modus — bedrijfssite met gebruikers, elk hun eigen Klonkt Hub.</p>
    55  <% } else { %>
    66    <p class="admin-tagline">Solo-modus — jouw site.</p>
  • src/views/pages/home.ejs

    rb94c08e r8afbdd6  
    2525      <h1>Hier is het nog stil.</h1>
    2626      <p>Nog geen posts. Spannend.</p>
    27       <% if (user && permissions.canCreatePost(user, site)) { %>
     27      <% if (user && canMutate && permissions.canCreatePost(user, site)) { %>
    2828        <p style="margin-top:2rem">
    2929          <a class="btn" href="<%= _base %>/posts/new"
  • src/views/pages/hub-home.ejs

    rb94c08e r8afbdd6  
    3333    <%# ── ARTIESTEN-ROSTER ─────────────────────────────────────────── %>
    3434    <% if (artists && artists.length) { %>
     35    <% var _total = (typeof totalArtists !== 'undefined') ? totalArtists : artists.length; %>
     36    <% var _hasMore = _total > artists.length; %>
    3537    <section class="hub-sec">
    36       <h2 class="hub-sec-title">Artiesten</h2>
     38      <h2 class="hub-sec-title">
     39        Artiesten
     40        <% if (_hasMore) { %><span class="hub-sec-count"><%= _total %></span><% } %>
     41      </h2>
    3742      <div class="roster">
    3843        <% artists.forEach(function(a){ %>
     
    4853        <% }); %>
    4954      </div>
     55      <% if (_hasMore) { %>
     56        <div class="hub-more">
     57          <a class="hub-more-link" href="/artiesten">Alle <%= _total %> artiesten <span aria-hidden="true">→</span></a>
     58        </div>
     59      <% } %>
    5060    </section>
    5161    <% } %>
     
    131141}
    132142.hub-sec-title::after { content: ''; flex: 1; height: 1px; background: var(--rule); }
     143.hub-sec-count {
     144  font-family: var(--font-ui, system-ui); font-size: 0.8rem; font-weight: 700;
     145  color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent);
     146  padding: 0.1rem 0.55rem; border-radius: 99px; order: 1;
     147}
     148.hub-more { margin-top: 1.25rem; text-align: center; }
     149.hub-more-link {
     150  display: inline-flex; align-items: center; gap: 0.4rem;
     151  padding: 0.6rem 1.3rem; border: 1px solid var(--rule); border-radius: 99px;
     152  background: var(--paper-2); color: var(--ink); text-decoration: none;
     153  font-weight: 600; font-size: 0.92rem; transition: border-color 140ms, background 140ms;
     154}
     155.hub-more-link:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, var(--paper-2)); }
    133156
    134157/* ── Roster (artiesten) ───────────────────────────────────────────── */
  • src/views/pages/my-site.ejs

    rb94c08e r8afbdd6  
    11<div class="container admin-page">
    22  <p><a href="/" class="btn">&larr; Overzicht</a></p>
    3   <h1>Mijn PrutFolio</h1>
     3  <h1>Mijn Klonkt Hub</h1>
    44  <p class="admin-tagline"><%= mySite.title %></p>
    55
  • src/views/pages/post.ejs

    rb94c08e r8afbdd6  
    103103              <div class="comment-content"><%= c.content %></div>
    104104              <div class="comment-actions">
    105                 <% if (user) { %>
     105                <% if (user && canMutate) { %>
    106106                  <button type="button" class="comment-reply-btn" data-reply-to="<%= c.id %>">Reply</button>
    107107                <% } %>
     
    114114
    115115              <!-- Inline reply form (hidden by default) -->
    116               <% if (user) { %>
     116              <% if (user && canMutate) { %>
    117117                <form method="post" action="<%= _base %>/comments" class="comment-reply-form" hidden data-reply-form-for="<%= c.id %>">
    118118                  <input type="hidden" name="post_slug" value="<%= post.slug %>">
     
    163163
    164164    <!-- Top-level comment form (only for logged-in users for now) -->
    165     <% if (user) { %>
     165    <% if (user && canMutate) { %>
    166166      <form method="post" action="<%= _base %>/comments" class="comment-form">
    167167        <input type="hidden" name="post_slug" value="<%= post.slug %>">
     
    172172        <button type="submit" class="btn btn-primary">Post comment</button>
    173173      </form>
    174     <% } else { %>
     174    <% } else if (!user) { %>
    175175      <p class="comments-login-cta">
    176176        <a href="/auth/login?next=<%= encodeURIComponent(_base + '/' + post.slug + '#comments') %>" class="btn">Log in to comment</a>
    177177      </p>
     178    <% } else { %>
     179      <p class="comments-login-cta" style="color:var(--ink-muted)">👁️ Kijker-modus — reageren is uitgeschakeld.</p>
    178180    <% } %>
    179181  </section>
  • src/views/partials/bottom-tab.ejs

    rb94c08e r8afbdd6  
    1111const _siteUrlBase = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
    1212const _path        = (typeof currentPath !== 'undefined' && currentPath) ? currentPath : '/';
    13 const _canPost     = !!(user && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site));
     13const _canPost     = !!(user && (typeof canMutate === 'undefined' || canMutate)
     14                        && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site));
    1415const _hasPrutter  = !!(user && site && site.enable_prutter && (typeof tenancy !== 'undefined' && tenancy === 'hub'));
    1516const _ownProfile  = user ? ('/users/' + user.username) : null;
  • src/views/partials/footer.ejs

    rb94c08e r8afbdd6  
    22  <div class="container site-footer-inner">
    33    <div>
    4       <span>© <%= new Date().getFullYear() %> <%= (site && site.title) || 'PrutFolio' %></span>
     4      <span>© <%= new Date().getFullYear() %> <%= (site && site.title) || 'Klonkt' %></span>
    55    </div>
    66    <div class="footer-meta">
    7       <span>PrutFolio v1</span>
     7      <span>Klonkt Hub Beta</span>
    88      <%# Mobile-only theme toggle. On desktop the topnav has its own toggle
    99          so we hide this one to avoid two visible toggles at once. The same
  • src/views/partials/profile-sheet.ejs

    rb94c08e r8afbdd6  
    1414
    1515const _isAdmin = (user.role === 'god' || user.role === 'admin');
    16 const _canPost = !!(typeof permissions !== 'undefined'
     16const _canPost = !!((typeof canMutate === 'undefined' || canMutate)
     17                    && typeof permissions !== 'undefined'
    1718                    && permissions
    1819                    && permissions.canCreatePost
  • src/views/partials/topnav.ejs

    rb94c08e r8afbdd6  
    1616const _hasSearch   = !site || site.show_search === undefined || site.show_search;
    1717const _hasPrutter  = user && site && site.enable_prutter && (typeof tenancy !== 'undefined' && tenancy === 'hub');
    18 const _canPost     = user && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site);
     18const _canPost     = user && (typeof canMutate === 'undefined' || canMutate)
     19                     && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site);
    1920%>
    2021
     
    3637           hx-push-url="<%= _siteUrlBase %>/" hx-indicator="#pcms-loading"><%= site.title %></a>
    3738      <% } else { %>
    38         <a class="site-title" href="/">PrutFolio</a>
     39        <a class="site-title" href="/">Klonkt</a>
    3940      <% } %>
    4041    </div>
  • src/views/shell.ejs

    rb94c08e r8afbdd6  
    239239<body class="<%= bodyClass || 'on-home' %> has-bottom-tab" data-feed-view="<%= _e(safeSite.feed_view_default || 'timeline') %>" data-grid-cols="3" data-site-base="<%= _e((typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '') %>">
    240240
    241 <% if (user && user.readonly) { %>
    242   <div style="position:sticky;top:0;z-index:50;background:#e8b04b;color:#1a1a1a;text-align:center;font-size:0.85rem;font-weight:600;padding:0.4rem 1rem;">
    243     👁️ Demo — alleen-lezen: je kunt alles bekijken, maar niets wijzigen.
     241<% if (typeof isViewer !== 'undefined' && isViewer) { %>
     242  <div class="viewer-banner" role="status">
     243    <span class="viewer-banner-ico" aria-hidden="true">👁️</span>
     244    <span class="viewer-banner-text"><strong>Kijker-modus</strong> — je kunt alles bekijken, maar niets wijzigen.</span>
    244245  </div>
     246  <style>
     247    .viewer-banner {
     248      position: sticky; top: 0; z-index: 60;
     249      display: flex; align-items: center; justify-content: center; gap: 0.5rem;
     250      padding: 0.5rem 1rem;
     251      background: linear-gradient(90deg,
     252        color-mix(in srgb, var(--accent) 88%, #000) 0%,
     253        var(--accent) 100%);
     254      color: #fff;
     255      font-size: 0.85rem; line-height: 1.3;
     256      box-shadow: 0 1px 6px color-mix(in srgb, var(--accent) 45%, transparent);
     257    }
     258    .viewer-banner-ico { font-size: 1rem; }
     259    .viewer-banner-text strong { font-weight: 700; }
     260  </style>
    245261<% } %>
    246262
Note: See TracChangeset for help on using the changeset viewer.