PageSourceSearch

https://komekaigi.dev/js/speakers.js

js komekaigi.dev collected 2026-09-26 04:37:29 UTC 3,646 bytes, 102 lines download raw bytes

1/**
2 * KomeKaigi 2026 スピーカー表示
3 * data/speakers.json を読み込み #speakers セクションに描画する。
4 * データが無い・取得に失敗した場合はセクションとメニュー項目を非表示のままにする。
5 */
6(function () {
7    'use strict';
8
9    const DATA_URL = 'data/speakers.json';
10
11    const section = document.getElementById('speakers');
12    const list = section && section.querySelector('.speaker-list');
13    const menuItem = document.querySelector('[data-speakers-menu]');
14
15    if (!section || !list) {
16        return;
17    }
18
19    const el = (tag, className, text) => {
20        const node = document.createElement(tag);
21        if (className) node.className = className;
22        if (text != null) node.textContent = text;
23        return node;
24    };
25
26    // BudouX の <budoux-ja> で包み、文節の途中で改行されないようにする。
27    // BudouX が読み込めなかった場合も通常のテキストとして表示される。
28    const phrase = (text) => el('budoux-ja', null, text);
29
30    const createSpeaker = (speaker, session) => {
31        const article = el('article', 'speaker');
32
33        const photo = el('div', 'speaker-photo');
34        const img = el('img');
35        if (speaker.image) img.src = speaker.image;
36        img.alt = speaker.name || '';
37        img.width = 180;
38        img.height = 180;
39        img.loading = 'lazy';
40        photo.appendChild(img);
41
42        const body = el('div', 'speaker-body');
43
44        const type = el('p', 'speaker-type');
45        if (session.typeEn) type.appendChild(el('span', 'speaker-type-en', session.typeEn));
46        if (session.type) type.appendChild(el('span', 'speaker-type-ja', session.type));
47        body.appendChild(type);
48
49        // nameSub(読み仮名やハンドルネーム)は氏名の横に小さく添え、折り返すときはまとめて次の行へ送る
50        const name = el('h3', 'speaker-name');
51        name.appendChild(el('span', 'speaker-name-main', speaker.name));
52        if (speaker.nameSub) name.appendChild(el('span', 'speaker-name-sub', speaker.nameSub));
53        body.appendChild(name);
54
55        if (speaker.role) {
56            const role = el('p', 'speaker-role');
57            role.appendChild(phrase(speaker.role));
58            body.appendChild(role);
59        }
60
61        // description は段落ごとの配列(文字列1つでも可)
62        const paragraphs = [].concat(speaker.description || []).filter(Boolean);
63        if (paragraphs.length > 0) {
64            const profile = el('div', 'speaker-profile');
65            paragraphs.forEach((text) => {
66                const p = el('p');
67                p.appendChild(phrase(text));
68                profile.appendChild(p);
69            });
70            body.appendChild(profile);
71        }
72
73        article.append(photo, body);
74        return article;
75    };
76
77    const render = (sessions) => {
78        const items = sessions.flatMap((session) =>
79            (session.speakers || []).map((speaker) => createSpeaker(speaker, session))
80        );
81
82        if (items.length === 0) {
83            return;
84        }
85
86        list.replaceChildren(...items);
87        section.hidden = false;
88        if (menuItem) menuItem.hidden = false;
89    };
90
91    fetch(DATA_URL)
92        .then((response) => {
93            if (!response.ok) {
94                throw new Error(`HTTP error! status: ${response.status}`);
95            }
96            return response.json();
97        })
98        .then((data) => render(data.sessions || []))
99        .catch((error) => {
100            console.error('スピーカーデータの取得エラー:', error);
101        });
102})();

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.