commit a52904b458b6b8a929257aa9da4724e8df8885a6 Author: posimai Date: Tue Mar 3 23:56:12 2026 +0900 init: Posimai Events prototype Made-with: Cursor diff --git a/api/events.js b/api/events.js new file mode 100644 index 0000000..5ab923c --- /dev/null +++ b/api/events.js @@ -0,0 +1,171 @@ +/** + * Posimai Events - /api/events + * + * GET /api/events -> イベント一覧を返す(モックデータ or KVストア) + * POST /api/events -> n8n から構造化データを受け取り保存するエンドポイント + * + * 現在はモックデータを返す。 + * n8n 連携時は EVENTS_STORE_TOKEN を環境変数に設定し、 + * POST で受け取ったデータを Vercel KV または外部DBへ書き込む実装に差し替える。 + */ + +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) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + 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' + }); + } + + // POST: n8n からのデータ受信エンドポイント(プロトタイプでは受け入れのみ) + if (req.method === 'POST') { + const token = process.env.EVENTS_STORE_TOKEN; + const authHeader = req.headers['authorization']; + + if (token && authHeader !== `Bearer ${token}`) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const body = req.body; + // TODO: Vercel KV または外部DBへの書き込みをここに実装する + // const { kv } = require('@vercel/kv'); + // await kv.set(`event:${body.id}`, JSON.stringify(body)); + + console.log('Received event from n8n:', JSON.stringify(body)); + return res.status(200).json({ ok: true, received: true }); + } + + return res.status(405).json({ error: 'Method not allowed' }); +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..5aadbf6 --- /dev/null +++ b/index.html @@ -0,0 +1,1100 @@ + + + + + + + Posimai Events + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+ + + + + +
+
+
+
+
+ + +
+ + +
+
+
+
+ +
+
+

+
+

+
+ +
+ + +
+ + Brainに保存しました +
+ + + + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..6879ba7 Binary files /dev/null and b/logo.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..36682cd --- /dev/null +++ b/manifest.json @@ -0,0 +1,26 @@ +{ + "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"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..033cfba --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "posimai-events", + "version": "0.1.0", + "description": "Posimai Events - AIがキュレーションする地域イベント情報 PWA", + "private": true, + "engines": { + "node": ">=18" + } +} diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..cdfc241 --- /dev/null +++ b/sw.js @@ -0,0 +1,52 @@ +const CACHE_NAME = 'posimai-events-v1'; +const STATIC_ASSETS = ['/', '/index.html', '/manifest.json', '/logo.png']; + +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', event => { + event.waitUntil( + caches.keys().then(keys => + Promise.all( + keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)) + ) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + + // API: network first, fallback to cache + if (url.pathname.startsWith('/api/')) { + event.respondWith( + fetch(event.request) + .then(response => { + const clone = response.clone(); + caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); + return response; + }) + .catch(() => caches.match(event.request)) + ); + return; + } + + // Static: cache first, fallback to network + event.respondWith( + caches.match(event.request).then(cached => { + if (cached) return cached; + return fetch(event.request).then(response => { + if (response.ok) { + const clone = response.clone(); + caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); + } + return response; + }); + }) + ); +}); diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..8f0b0ab --- /dev/null +++ b/vercel.json @@ -0,0 +1,14 @@ +{ + "rewrites": [ + { "source": "/api/(.*)", "destination": "/api/$1" } + ], + "headers": [ + { + "source": "/(.*)", + "headers": [ + { "key": "X-Content-Type-Options", "value": "nosniff" }, + { "key": "X-Frame-Options", "value": "DENY" } + ] + } + ] +}