PageSourceSearch

https://komekaigi.dev/2025/js/sponsors.js

js komekaigi.dev collected 2026-09-26 04:37:32 UTC 13,558 bytes, 392 lines download raw bytes

1/**
2 * スポンサー表示システム
3 * APIからスポンサー情報を取得し、動的に表示
4 */
5
6(function(window) {
7    'use strict';
8
9    const CONFIG = {
10        // API設定
11        API: {
12            URL: 'https://fortee.jp/komekaigi-2025/api/sponsors',
13            TIMEOUT: 10000,       // APIタイムアウト(10秒)
14            RETRY_COUNT: 3,       // リトライ回数
15            RETRY_DELAY: 1000     // リトライ間隔(ミリ秒)
16        },
17        
18        // コンテンツ設定
19        CONTENT: {
20            MIN_PR_LENGTH: 20    // PR文の最小文字数
21        },
22        
23        // スポンサータイプ
24        SPONSOR_TYPES: {
25            GOLD: { key: 'gold', label: 'GOLD', labelJp: 'ゴールドスポンサー' },
26            SILVER: { key: 'silver', label: 'SILVER', labelJp: 'シルバースポンサー' },
27            VENUE: { key: 'venue', label: 'VENUE', labelJp: '会場提供' },
28            ACCOUNTING: { key: 'accounting', label: 'ACCOUNTING', labelJp: '会計協力' }
29        }
30    };
31
32    let sponsorsData = [];
33
34    /**
35     * スポンサー情報を取得して表示(リトライ機能付き)
36     * @param {number} retryCount - リトライ回数
37     */
38    async function loadSponsors(retryCount = 0) {
39        const sponsorSection = document.getElementById('sponsors-content');
40        let shouldRetry = false;
41        
42        try {
43            // タイムアウト設定付きfetch
44            const controller = new AbortController();
45            const timeout = setTimeout(() => controller.abort(), CONFIG.API.TIMEOUT);
46            
47            const response = await fetch(CONFIG.API.URL, { 
48                signal: controller.signal 
49            });
50            clearTimeout(timeout);
51            
52            if (!response.ok) {
53                shouldRetry = true;
54            } else {
55                const data = await response.json();
56                
57                if (!data || !data.sponsor_plans) {
58                    shouldRetry = true;
59                } else {
60                    // 正常にデータを取得できた場合
61                    const sponsorsByType = categorizeSponsors(data.sponsor_plans);
62                    renderSponsors(sponsorsByType);
63                    return; // 成功したので終了
64                }
65            }
66            
67        } catch (error) {
68            // ネットワークエラーやタイムアウトなど
69            shouldRetry = true;
70        }
71        
72        // エラー処理
73        if (shouldRetry) {
74            // リトライ処理
75            if (retryCount < CONFIG.API.RETRY_COUNT - 1) {
76                setTimeout(() => loadSponsors(retryCount + 1), CONFIG.API.RETRY_DELAY);
77            } else {
78                // エラー時は何も表示しない(セクション自体を非表示)
79                hideSponsorsSection(sponsorSection);
80            }
81        }
82    }
83
84    /**
85     * スポンサーセクションを非表示にする
86     * @param {HTMLElement} contentElement - コンテンツ要素
87     */
88    function hideSponsorsSection(contentElement) {
89        if (contentElement) {
90            contentElement.innerHTML = '';
91        }
92        const sponsorsSection = document.getElementById('sponsors');
93        if (sponsorsSection) {
94            sponsorsSection.style.display = 'none';
95        }
96    }
97
98/**
99 * スポンサーをタイプ別に分類
100 * @param {Array} sponsorPlans - APIから取得したスポンサープラン
101 * @returns {Object} タイプ別に分類されたスポンサー
102 */
103function categorizeSponsors(sponsorPlans) {
104    const sponsorsByType = {};
105    Object.values(CONFIG.SPONSOR_TYPES).forEach(config => {
106        sponsorsByType[config.key] = [];
107    });
108
109    sponsorPlans.forEach(plan => {
110        const type_en = plan.name_en;
111
112        if (type_en && sponsorsByType[type_en]) {
113            filterValidSponsors(plan.sponsors).forEach(sponsor => {
114                sponsorsByType[type_en].push(sponsor);
115                sponsorsData.push(sponsor);
116            });
117        }
118    });
119
120    return sponsorsByType;
121}
122
123/**
124 * 有効なスポンサーのみをフィルタリング
125 *  - avatarが存在するスポンサー
126 * @param {Array} sponsors - スポンサーリスト
127 * @returns {Array} フィルタリング済みスポンサーリスト
128 */
129function filterValidSponsors(sponsors) {
130    return sponsors.filter(sponsor => sponsor.avatar);
131}
132
133/**
134 * スポンサーセクションをレンダリング
135 * @param {Object} sponsorsByType - タイプ別スポンサー
136 */
137function renderSponsors(sponsorsByType) {
138    const sponsorSection = document.getElementById('sponsors-content');
139    if (!sponsorSection) return;
140
141    let html = '';
142
143    // 各タイプのスポンサーをレンダリング
144    Object.values(CONFIG.SPONSOR_TYPES).forEach(config => {
145        const type = config.key;
146        const sponsors = sponsorsByType[type];
147        
148        if (sponsors && sponsors.length > 0) {
149            html += renderSponsorSection(type, config, sponsors);
150        }
151    });
152
153    sponsorSection.innerHTML = html;
154}
155
156/**
157 * スポンサーセクションのHTMLを生成
158 * @param {string} type - スポンサータイプ
159 * @param {Object} config - タイプ設定
160 * @param {Array} sponsors - スポンサーリスト
161 * @returns {string} HTML文字列
162 */
163function renderSponsorSection(type, config, sponsors) {
164    const cards = sponsors
165        .map(sponsor => createSponsorCard(sponsor, type))
166        .map(card => `<div class="sponsor-card-wrapper sponsor-card-${type}">${card}</div>`);
167    
168    if (cards.length === 0) return '';
169    
170    return `
171        <div class="sponsor-section-${type}">
172            <div class="container mx-auto px-4${type !== 'silver' ? ' max-w-6xl' : ''}">
173                <div class="text-center mb-8">
174                    <h4 class="text-2xl font-semibold text-primary">
175                        ${config.labelJp}
176                    </h4>
177                </div>
178                <div class="sponsor-cards-container sponsor-cards-${type}">
179                    ${cards.join('')}
180                </div>
181            </div>
182        </div>
183    `;
184}
185
186/**
187 * スポンサーカードを作成
188 * @param {Object} sponsor - スポンサー情報
189 * @param {string} type - スポンサータイプ
190 * @returns {string} カードHTML
191 */
192function createSponsorCard(sponsor, type) {
193    const name = sponsor.name || '';
194    const logoUrl = sponsor.avatar;
195    const sponsorId = sponsor.id;
196    
197    return `
198        <div class="sponsor-card sponsor-card-${type}" data-sponsor-id="${sponsorId}">
199            <div class="sponsor-logo-wrapper overflow-container">
200                <img src="${logoUrl}" alt="${name}" loading="lazy" 
201                     class="image-border-fix sponsor-logo-img"
202                     data-sponsor-name="${name.replace(/"/g, '&quot;')}"
203                     data-sponsor-type="${type}">
204            </div>
205        </div>
206    `;
207}
208
209// ==========================================
210// モーダル関連
211// ==========================================
212
213/**
214 * スポンサーモーダルを開く
215 * @param {string} sponsorId - スポンサーID
216 */
217function openSponsorModal(sponsorId) {
218    const sponsor = sponsorsData.find(s => s.id === sponsorId);
219    if (!sponsor) return;
220    
221    const modal = document.getElementById('sponsorModal');
222    const modalLogo = document.getElementById('modalLogo');
223    const modalInfo = document.getElementById('modalInfo');
224    
225    // ロゴの表示
226    renderModalLogo(modalLogo, sponsor);
227    
228    // 情報の表示
229    renderModalInfo(modalInfo, sponsor);
230    
231    // モーダルを表示
232    modal.classList.add('active');
233    document.body.style.overflow = 'hidden';
234}
235
236/**
237 * モーダルのロゴをレンダリング
238 * @param {HTMLElement} element - ロゴ表示要素
239 * @param {Object} sponsor - スポンサー情報
240 */
241function renderModalLogo(element, sponsor) {
242    element.innerHTML = `
243        <div class="modal-logo-inner overflow-container">
244            <img src="${sponsor.avatar}" alt="${sponsor.name}" class="image-border-fix">
245        </div>
246    `;
247}
248
249/**
250 * モーダルの情報をレンダリング
251 * @param {HTMLElement} element - 情報表示要素
252 * @param {Object} sponsor - スポンサー情報
253 */
254function renderModalInfo(element, sponsor) {
255    const links = getSponsorLinks(sponsor);
256    
257    element.innerHTML = `
258        <div style="display: flex; flex-direction: column; height: 100%; min-height: 300px;">
259            <div style="flex: 1;">
260                <h4 style="margin-bottom: 1.5rem;">
261                    ${sponsor.name}
262                </h4>
263                ${sponsor.pr && sponsor.pr.length >= CONFIG.CONTENT.MIN_PR_LENGTH ? `
264                    <div>
265                        <p class="text-gray-700 leading-relaxed">${sponsor.pr}</p>
266                    </div>
267                ` : ''}
268            </div>
269            ${links ? `
270                <div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid #e5e7eb;">
271                    <div class="flex gap-3 justify-start">
272                        ${links}
273                    </div>
274                </div>
275            ` : ''}
276        </div>
277    `;
278}
279
280/**
281 * スポンサーリンクを生成
282 * @param {Object} sponsor - スポンサー情報
283 * @returns {string} リンクHTML
284 */
285function getSponsorLinks(sponsor) {
286    const links = [];
287    
288    if (sponsor.url) {
289        links.push(`
290            <a href="${sponsor.url}" target="_blank" rel="noopener noreferrer" 
291               class="inline-flex items-center px-4 py-2 bg-[#fabe00] text-white font-medium rounded-lg hover:bg-amber-600 transition-colors">
292                <svg class="w-5 h-5 mr-2 fill-none stroke-current" viewBox="0 0 24 24">
293                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
294                          d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
295                </svg>
296                スポンサーページへ
297            </a>
298        `);
299    }
300    
301    if (sponsor.twitter) {
302        const twitterUrl = sponsor.twitter.startsWith('http') 
303            ? sponsor.twitter 
304            : `https://x.com/${sponsor.twitter}`;
305        links.push(`
306            <a href="${twitterUrl}" target="_blank" rel="noopener noreferrer" 
307               class="inline-flex items-center px-4 py-2 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors">
308                <svg class="w-5 h-5 fill-current" viewBox="0 0 24 24">
309                    <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
310                </svg>
311            </a>
312        `);
313    }
314    
315    return links.join('');
316}
317
318/**
319 * スポンサーモーダルを閉じる
320 */
321function closeSponsorModal() {
322    const modal = document.getElementById('sponsorModal');
323    modal.classList.remove('active');
324    document.body.style.overflow = '';
325}
326
327// ==========================================
328// イベントリスナー
329// ==========================================
330
331// モーダルの外側をクリックしたら閉じる
332window.addEventListener('click', function(event) {
333    const modal = document.getElementById('sponsorModal');
334    if (event.target === modal) {
335        closeSponsorModal();
336    }
337});
338
339// ESCキーでモーダルを閉じる
340window.addEventListener('keydown', function(event) {
341    if (event.key === 'Escape') {
342        closeSponsorModal();
343    }
344});
345
346    // ==========================================
347    // 画像エラーハンドリング
348    // ==========================================
349    
350    /**
351     * 画像読み込みエラー時のハンドリング
352     */
353    function handleImageErrors() {
354        document.addEventListener('error', function(e) {
355            if (e.target.classList && e.target.classList.contains('sponsor-logo-img')) {
356                const img = e.target;
357                const wrapper = img.parentElement;
358                const name = img.getAttribute('data-sponsor-name');
359                const type = img.getAttribute('data-sponsor-type');
360                
361                // 画像を非表示にしてフォールバック表示
362                img.style.display = 'none';
363                wrapper.innerHTML = `<div class="sponsor-card-fallback sponsor-card-fallback-${type}">${name}</div>`;
364            }
365        }, true); // キャプチャリングフェーズで処理
366    }
367    
368    // ==========================================
369    // 初期化処理
370    // ==========================================
371    
372    // ページ読み込み時の初期化
373    document.addEventListener('DOMContentLoaded', function() {
374        loadSponsors();
375        handleImageErrors();
376        
377        // スポンサーカードのクリックイベント(イベント委譲)
378        document.addEventListener('click', function(e) {
379            const card = e.target.closest('.sponsor-card');
380            if (card && card.dataset.sponsorId) {
381                openSponsorModal(card.dataset.sponsorId);
382            }
383        });
384        
385        // モーダルの閉じるボタンのイベントリスナー
386        const modalCloseButton = document.getElementById('modalCloseButton');
387        if (modalCloseButton) {
388            modalCloseButton.addEventListener('click', closeSponsorModal);
389        }
390    });
391    
392})(window);

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.