From 8d1c25444281cd1751ed0b6dd2e038fe51ca88b3 Mon Sep 17 00:00:00 2001 From: posimai Date: Mon, 16 Mar 2026 17:23:01 +0900 Subject: [PATCH] fix: pin lucide to @0.344.0 (was @latest) Co-Authored-By: Claude Sonnet 4.6 --- api/events.js | 225 ++----- index.html | 1759 +++++++++++++++++++++++++++++-------------------- manifest.json | 54 +- package.json | 6 +- sw.js | 4 +- 5 files changed, 1138 insertions(+), 910 deletions(-) diff --git a/api/events.js b/api/events.js index 5ab923c..9e66570 100644 --- a/api/events.js +++ b/api/events.js @@ -1,171 +1,92 @@ /** - * Posimai Events - /api/events + * Posimai Tech Events - /api/events * - * GET /api/events -> イベント一覧を返す(モックデータ or KVストア) - * POST /api/events -> n8n から構造化データを受け取り保存するエンドポイント + * GET /api/events?q=keyword -> Connpass APIを呼び出して整形したイベント一覧を返す。 * - * 現在はモックデータを返す。 - * n8n 連携時は EVENTS_STORE_TOKEN を環境変数に設定し、 - * POST で受け取ったデータを Vercel KV または外部DBへ書き込む実装に差し替える。 + * Connpass API: https://connpass.com/about/api/ */ -const MOCK_EVENTS = [ - { - id: '1', - title: '春の産直マルシェ', - startDate: '2026-03-03', startTime: '09:00', - endDate: '2026-03-05', endTime: '16:00', - location: '市民広場 イベントスペース', - address: '○○市中央1-1', - description: '地元農家が丹精込めて育てた旬の野菜・果物が並ぶ春の産直市。パン工房や手作りスイーツのブースも出店。家族でゆっくり楽しめます。', - category: 'マルシェ', - url: 'https://example.com/marche', - source: '市役所公式サイト' - }, - { - id: '2', - title: '朝の太極拳教室(無料体験)', - startDate: '2026-03-03', startTime: '07:00', - endDate: '2026-03-03', endTime: '08:30', - location: '中央公園 芝生広場', - address: '○○市中央公園', - description: '毎週火・木・土曜開催の太極拳サークルが無料体験会を開催。初心者・シニアの方歓迎。動きやすい服装でお越しください。', - category: '体験・スポーツ', - url: 'https://example.com/taichi', - source: '地域掲示板' - }, - { - id: '3', - title: '防災訓練・地域説明会', - startDate: '2026-03-05', startTime: '10:00', - endDate: '2026-03-05', endTime: '12:00', - location: '○○公民館 大ホール', - address: '○○市西町2-5', - description: '年1回の地区防災訓練と、今年度の避難計画変更に関する説明会を同日開催します。参加無料。', - category: '地域・行政', - url: 'https://example.com/bousai', - source: '町内会回覧板' - }, - { - id: '4', - title: '伝統工芸・陶芸ワークショップ', - startDate: '2026-03-06', startTime: '13:00', - endDate: '2026-03-06', endTime: '17:00', - location: '○○文化センター 工芸室', - address: '○○市文化通り3-8', - description: '地元陶芸家による手びねり体験。土から形を作り、釉薬を選んで焼き上げ(後日お渡し)。定員12名・要事前申込。参加費 2,500円。', - category: 'ワークショップ', - url: 'https://example.com/ceramics', - source: '文化センターHP' - }, - { - id: '5', - title: '春のクラフトフェア 2026', - startDate: '2026-03-07', startTime: '10:00', - endDate: '2026-03-08', endTime: '17:00', - location: '○○公園 野外広場', - address: '○○市北公園', - description: '全国から集まるクラフト作家80組が出展。アクセサリー、革工芸、テキスタイル、木工など多彩なジャンル。フードトラックも10台出店。入場無料。', - category: 'マーケット', - url: 'https://example.com/craft', - source: '実行委員会HP' - }, - { - id: '6', - title: '地域清掃ボランティア', - startDate: '2026-03-08', startTime: '09:00', - endDate: '2026-03-08', endTime: '11:00', - location: '○○川 河川敷', - address: '○○市河川敷公園', - description: '春の清掃活動。参加自由・事前申込不要。軍手・ゴミ袋は主催者が用意。終了後、軽食の提供あり。', - category: '地域・ボランティア', - url: 'https://example.com/cleanup', - source: '市環境課HP' - }, - { - id: '7', - title: '春のクラシックコンサート', - startDate: '2026-03-14', startTime: '15:00', - endDate: '2026-03-14', endTime: '17:30', - location: '○○市民ホール 小ホール', - address: '○○市文化町1-1', - description: '地元弦楽四重奏団による春のコンサート。ハイドン・シューベルトを中心に演奏。全席自由・入場料 1,000円(高校生以下無料)。', - category: '音楽・アート', - url: 'https://example.com/concert', - source: '市民ホールHP' - }, - { - id: '8', - title: 'まちなかマルシェ(4月)', - startDate: '2026-04-04', startTime: '10:00', - endDate: '2026-04-05', endTime: '16:00', - location: '商店街アーケード', - address: '○○市本町通り', - description: '毎月第1土日開催の定期マルシェ。4月は春のテーマで出店者募集中(出店費無料)。', - category: 'マルシェ', - url: 'https://example.com/monthly', - source: '商店街組合HP' - }, - { - id: '9', - title: '子ども映画祭(無料上映)', - startDate: '2026-02-28', startTime: '10:00', - endDate: '2026-03-02', endTime: '17:00', - location: '中央図書館 多目的ホール', - address: '○○市図書館通り', - description: '国内外の子ども向けアニメ・映画の無料上映会。3日間で10作品上映。入場無料・予約不要。', - category: '映画・文化', - url: 'https://example.com/film', - source: '図書館HP' - }, - { - id: '10', - title: '早春の野鳥観察会', - startDate: '2026-03-01', startTime: '07:00', - endDate: '2026-03-01', endTime: '09:30', - location: '○○自然公園 入口', - address: '○○市郊外 自然公園', - description: '自然観察指導員が案内するバードウォッチング入門ツアー。双眼鏡の貸し出しあり。申込不要。', - category: '自然・体験', - url: 'https://example.com/birds', - source: '環境教育センターHP' - } -]; - -export default function handler(req, res) { +export default async function handler(req, res) { res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { return res.status(200).end(); } - if (req.method === 'GET') { - return res.status(200).json({ - events: MOCK_EVENTS, - updatedAt: new Date().toISOString(), - source: 'mock' - }); + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); } - // POST: n8n からのデータ受信エンドポイント(プロトタイプでは受け入れのみ) - if (req.method === 'POST') { - const token = process.env.EVENTS_STORE_TOKEN; - const authHeader = req.headers['authorization']; + try { + const { q } = req.query; + // Search either specific query from UI or broad default queries for tech events + const fetchKeyword = q && q.trim().length > 0 ? q.trim() : 'IT,エンジニア,デザイン,Web,AI,アプリ'; + const urlObj = new URL('https://connpass.com/api/v1/event/'); + // ConnpassのOR検索仕様に合わせて keyword_or を使用する + urlObj.searchParams.append('keyword_or', fetchKeyword); + urlObj.searchParams.append('order', '2'); // 開催降順(新着イベント) + urlObj.searchParams.append('count', '50'); - if (token && authHeader !== `Bearer ${token}`) { - return res.status(401).json({ error: 'Unauthorized' }); + // Connpass APIはUser-Agentが必須 + const response = await fetch(urlObj.toString(), { + headers: { + 'User-Agent': 'PosimaiTechEvents/1.0 (https://posimai-tech-events.vercel.app)' + } + }); + + if (!response.ok) { + console.error('Connpass API Error', await response.text()); + res.status(502).json({ error: 'Failed to fetch events from Connpass' }); + return; } - const body = req.body; - // TODO: Vercel KV または外部DBへの書き込みをここに実装する - // const { kv } = require('@vercel/kv'); - // await kv.set(`event:${body.id}`, JSON.stringify(body)); + const data = await response.json(); - console.log('Received event from n8n:', JSON.stringify(body)); - return res.status(200).json({ ok: true, received: true }); + // Map to Posimai Events structure + const events = (data.events || []).map(ev => { + const startStr = ev.started_at || ''; + const endStr = ev.ended_at || ''; + + const startDate = startStr.slice(0, 10); + const startTime = startStr.slice(11, 16); + const endDate = endStr.slice(0, 10); + const endTime = endStr.slice(11, 16); + + let location = ev.place || 'オンライン'; + if (!ev.address && !ev.place) { + if (ev.event_url && ev.event_type === 'online') location = 'オンライン開催'; + else location = '会場未定 / オンライン'; + } + + return { + id: String(ev.event_id), + title: ev.title, + startDate: startDate, + startTime: startTime, + endDate: endDate, + endTime: endTime, + location: location, + address: ev.address || '', + description: ev.catch || '詳細説明はリンク先をご覧ください。', + category: ev.series ? ev.series.title : 'IT/テック', + url: ev.event_url, + source: 'Connpass', + // To be enhanced: derive tags based on title/description + interestTags: [], + audienceTags: [] + }; + }); + + return res.status(200).json({ + events: events, + updatedAt: new Date().toISOString(), + source: 'connpass' + }); + + } catch (e) { + console.error('API /events error:', e); + return res.status(500).json({ error: 'Internal Server Error' }); } - - return res.status(405).json({ error: 'Method not allowed' }); } diff --git a/index.html b/index.html index 4dd13f0..5a9352c 100644 --- a/index.html +++ b/index.html @@ -1,42 +1,53 @@ + + - - Posimai Events + + Posimai Tech Events - + + - + - + +
-
@@ -392,7 +878,9 @@
-
+
+
+
@@ -464,321 +952,121 @@ Brainに保存しました - + // ---- Init ---- + applyTheme(localStorage.getItem('events-theme') || 'dark'); + updateHeaderDate(); + lucide.createIcons(); + loadEvents(currentFilter); + - + + \ No newline at end of file diff --git a/manifest.json b/manifest.json index 36682cd..b5db35f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,26 +1,30 @@ { - "name": "Posimai Events", - "short_name": "Events", - "description": "AIがノイズを削ぎ落とした地域イベント情報", - "start_url": "/", - "display": "standalone", - "background_color": "#0a0a0a", - "theme_color": "#6EE7B7", - "orientation": "portrait-primary", - "lang": "ja", - "icons": [ - { - "src": "/logo.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/logo.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any maskable" - } - ], - "categories": ["lifestyle", "local", "news"] -} + "name": "Posimai Tech", + "short_name": "Tech Events", + "description": "ITイベント・Connpass検索アプリ", + "start_url": "/", + "display": "standalone", + "background_color": "#0D0D0D", + "theme_color": "#0D0D0D", + "orientation": "portrait-primary", + "lang": "ja", + "icons": [ + { + "src": "/logo.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/logo.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "categories": [ + "technology", + "education", + "event" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 033cfba..2dfbbdf 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "posimai-events", + "name": "posimai-tech-events", "version": "0.1.0", - "description": "Posimai Events - AIがキュレーションする地域イベント情報 PWA", + "description": "Posimai Tech Events - Connpass連携のITイベントPWA", "private": true, "engines": { "node": ">=18" } -} +} \ No newline at end of file diff --git a/sw.js b/sw.js index cdfc241..7fcce37 100644 --- a/sw.js +++ b/sw.js @@ -1,5 +1,5 @@ -const CACHE_NAME = 'posimai-events-v1'; -const STATIC_ASSETS = ['/', '/index.html', '/manifest.json', '/logo.png']; +const CACHE_NAME = 'posimai-events-v3'; +const STATIC_ASSETS = ['/?v=3', '/index.html?v=3', '/manifest.json', '/logo.png']; self.addEventListener('install', event => { event.waitUntil(