PageSourceSearch

https://www.kevsrobots.com/assets/js/featured-projects.js?v=1789943003

js kevsrobots.com collected 2026-10-03 20:16:40 UTC 5,385 bytes, 135 lines download raw bytes

1/* Featured projects + staff picks UI helpers (issue #115).
2 *
3 * Exposes a small namespace ``FeaturedProjects`` with:
4 *   - DEFAULT_API: the projects-api base URL the rest of the projects UI
5 *     already points at.
6 *   - ribbon(): returns the gold-ribbon HTML fragment for use inside an
7 *     image wrapper.
8 *   - badge(note): returns the inline "Staff Pick" badge for the view
9 *     page header. Encodes ``note`` for the tooltip safely.
10 *   - cardHtml(project): returns the inner card HTML for a single
11 *     featured project (no wrapper). Shared by the hub carousel and the
12 *     home-page widget (issue #153) so both stay in visual lock-step.
13 *   - mountCarousel(container, projects): renders a scrollable shelf of
14 *     featured cards into a container. Returns true when at least one
15 *     card was rendered so the caller can `classList.remove('d-none')`
16 *     conditionally — we want the section hidden entirely on a fresh
17 *     install with no featured projects.
18 *   - load(limit): fetches /api/projects/featured?limit=N as JSON.
19 *
20 * Keep this dependency-free so the hub can include it before the larger
21 * project-search bundle without a forced load order.
22 */
23(function () {
24  'use strict';
25
26  var DEFAULT_API = 'https://projects.kevsrobots.com';
27
28  function esc(text) {
29    var d = document.createElement('div');
30    d.textContent = text == null ? '' : String(text);
31    return d.innerHTML;
32  }
33
34  function ribbon(label) {
35    var text = label || 'Featured';
36    return (
37      '<span class="featured-ribbon" aria-label="' + esc(text) + ' project">' +
38        '<i class="fas fa-star" aria-hidden="true"></i>' + esc(text) +
39      '</span>'
40    );
41  }
42
43  function badge(note) {
44    // Tooltip via Bootstrap's data-bs-toggle. The title attribute is
45    // escaped so a malicious note can't break out of the HTML attribute.
46    var tooltip = note ? esc(note) : 'Hand-picked by the kevsrobots team';
47    return (
48      '<span class="staff-pick-badge" ' +
49        'data-bs-toggle="tooltip" ' +
50        'data-bs-placement="bottom" ' +
51        'title="' + tooltip + '">' +
52        '<i class="fas fa-star" aria-hidden="true"></i>' +
53        'Staff Pick' +
54      '</span>'
55    );
56  }
57
58  function cardThumbStyle(project) {
59    if (project && project.cover_image) {
60      return 'background-image:url(\'' + esc(project.cover_image) + '\');background-size:cover;background-position:center;';
61    }
62    // Cheap deterministic gradient based on the project id so cards
63    // without a cover image still feel distinct.
64    var id = (project && project.id) || 0;
65    var hue = (id * 53) % 360;
66    return 'background:linear-gradient(135deg,hsl(' + hue + ',70%,55%),hsl(' + ((hue + 60) % 360) + ',70%,45%));';
67  }
68
69  // Inner card HTML for one featured project (no outer wrapper). Shared
70  // between the hub carousel and the home-page widget (issue #153) so the
71  // visual treatment stays consistent in both surfaces.
72  function cardHtml(project) {
73    // Issue #152: prefer canonical /projects/<owner>/<slug> URL when the
74    // FeaturedProjectResponse surfaced a slug.
75    var href = (project.slug && project.author_username)
76      ? '/projects/' + encodeURIComponent(project.author_username) + '/' + encodeURIComponent(project.slug)
77      : '/projects/view.html?id=' + encodeURIComponent(project.id);
78    var note = project.featured_note ? '<div class="featured-note">' + esc(project.featured_note) + '</div>' : '';
79    var author = project.author_username ? esc(project.author_username) : '';
80    return (
81      '<a href="' + href + '" class="text-decoration-none">' +
82        '<div class="card kr-project-card h-100 border-0 shadow-sm card-hover">' +
83          ribbon('Featured') +
84          '<div class="project-thumb" style="' + cardThumbStyle(project) + '"></div>' +
85          '<div class="card-body">' +
86            '<div class="fw-bold text-dark text-truncate" title="' + esc(project.title) + '">' + esc(project.title) + '</div>' +
87            (author ? '<div class="small text-muted">by ' + author + '</div>' : '') +
88            note +
89          '</div>' +
90        '</div>' +
91      '</a>'
92    );
93  }
94
95  function carouselCardHtml(project) {
96    return '<div class="featured-carousel-card">' + cardHtml(project) + '</div>';
97  }
98
99  function mountCarousel(container, projects) {
100    if (!container) return false;
101    if (!projects || !projects.length) {
102      container.innerHTML = '';
103      return false;
104    }
105    container.innerHTML = projects.map(carouselCardHtml).join('');
106    // Activate tooltips inside the rendered cards (best-effort — bootstrap
107    // may not be loaded yet on some pages).
108    if (window.bootstrap && window.bootstrap.Tooltip) {
109      container.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) {
110        new window.bootstrap.Tooltip(el);
111      });
112    }
113    return true;
114  }
115
116  function load(opts) {
117    opts = opts || {};
118    var api = opts.api || DEFAULT_API;
119    var limit = opts.limit || 6;
120    return fetch(api + '/api/projects/featured?limit=' + encodeURIComponent(limit))
121      .then(function (resp) {
122        if (!resp.ok) throw new Error('Featured fetch failed: ' + resp.status);
123        return resp.json();
124      });
125  }
126
127  window.FeaturedProjects = {
128    DEFAULT_API: DEFAULT_API,
129    ribbon: ribbon,
130    badge: badge,
131    cardHtml: cardHtml,
132    mountCarousel: mountCarousel,
133    load: load
134  };
135})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.