diff --git a/app/api/push/send/route.ts b/app/api/push/send/route.ts index 32dbbad..bb671eb 100644 --- a/app/api/push/send/route.ts +++ b/app/api/push/send/route.ts @@ -2,6 +2,16 @@ import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase'; import { sendPushNotificationBatch, type PushNotificationPayload } from '@/lib/web-push'; +interface SubscriptionRow { + endpoint: string; + p256dh_key: string; + auth_key: string; + user_id: string; + users: { + role?: string; + } | null; +} + /** * POST /api/push/send * Send push notifications to users @@ -28,17 +38,11 @@ export async function POST(request: NextRequest) { // Validate payload if (!payload || typeof payload !== 'object') { - return NextResponse.json( - { error: 'Payload is required' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Payload is required' }, { status: 400 }); } if (!payload.title || !payload.body) { - return NextResponse.json( - { error: 'Payload must include title and body' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Payload must include title and body' }, { status: 400 }); } // Create Supabase client @@ -51,10 +55,7 @@ export async function POST(request: NextRequest) { if (userIds !== 'all') { if (!Array.isArray(userIds) || userIds.length === 0) { - return NextResponse.json( - { error: 'userIds must be an array or "all"' }, - { status: 400 } - ); + return NextResponse.json({ error: 'userIds must be an array or "all"' }, { status: 400 }); } query = query.in('user_id', userIds); } @@ -63,10 +64,7 @@ export async function POST(request: NextRequest) { if (fetchError) { console.error('Error fetching subscriptions:', fetchError); - return NextResponse.json( - { error: 'Failed to fetch subscriptions' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to fetch subscriptions' }, { status: 500 }); } if (!subscriptions || subscriptions.length === 0) { @@ -78,10 +76,10 @@ export async function POST(request: NextRequest) { } // Filter by roles if specified - let filteredSubscriptions = subscriptions; + let filteredSubscriptions: SubscriptionRow[] = subscriptions as SubscriptionRow[]; if (roles && Array.isArray(roles) && roles.length > 0) { - filteredSubscriptions = subscriptions.filter((sub) => { - const userRole = (sub.users as { role?: string })?.role; + filteredSubscriptions = filteredSubscriptions.filter((sub: SubscriptionRow) => { + const userRole = sub.users?.role; return userRole && roles.includes(userRole); }); } @@ -95,7 +93,7 @@ export async function POST(request: NextRequest) { } // Convert to push subscription format - const pushSubscriptions = filteredSubscriptions.map((sub) => ({ + const pushSubscriptions = filteredSubscriptions.map((sub: SubscriptionRow) => ({ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh_key, @@ -139,9 +137,6 @@ export async function POST(request: NextRequest) { }); } catch (error) { console.error('Error in send route:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } } diff --git a/app/api/push/subscribe/route.ts b/app/api/push/subscribe/route.ts index 6d4df64..b66d64a 100644 --- a/app/api/push/subscribe/route.ts +++ b/app/api/push/subscribe/route.ts @@ -13,17 +13,11 @@ export async function POST(request: NextRequest) { // Validate input if (!userId || typeof userId !== 'string') { - return NextResponse.json( - { error: 'User ID is required' }, - { status: 400 } - ); + return NextResponse.json({ error: 'User ID is required' }, { status: 400 }); } if (!validatePushSubscription(subscription)) { - return NextResponse.json( - { error: 'Invalid subscription object' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Invalid subscription object' }, { status: 400 }); } // Create Supabase client @@ -37,24 +31,23 @@ export async function POST(request: NextRequest) { .eq('endpoint', subscription.endpoint) .single(); - if (existingSubscription) { + if (existingSubscription && 'id' in existingSubscription) { // Update existing subscription + const updatePayload: Record = { + p256dh_key: subscription.keys.p256dh, + auth_key: subscription.keys.auth, + user_agent: request.headers.get('user-agent'), + updated_at: new Date().toISOString(), + }; + const { error: updateError } = await supabase .from('push_subscriptions') - .update({ - p256dh_key: subscription.keys.p256dh, - auth_key: subscription.keys.auth, - user_agent: request.headers.get('user-agent'), - updated_at: new Date().toISOString(), - }) - .eq('id', existingSubscription.id); + .update(updatePayload as never) + .eq('id', (existingSubscription as Record).id as string); if (updateError) { console.error('Error updating subscription:', updateError); - return NextResponse.json( - { error: 'Failed to update subscription' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to update subscription' }, { status: 500 }); } return NextResponse.json({ @@ -64,22 +57,21 @@ export async function POST(request: NextRequest) { } // Insert new subscription + const insertPayload: Record = { + user_id: userId, + endpoint: subscription.endpoint, + p256dh_key: subscription.keys.p256dh, + auth_key: subscription.keys.auth, + user_agent: request.headers.get('user-agent'), + }; + const { error: insertError } = await supabase .from('push_subscriptions') - .insert({ - user_id: userId, - endpoint: subscription.endpoint, - p256dh_key: subscription.keys.p256dh, - auth_key: subscription.keys.auth, - user_agent: request.headers.get('user-agent'), - }); + .insert(insertPayload as never); if (insertError) { console.error('Error inserting subscription:', insertError); - return NextResponse.json( - { error: 'Failed to save subscription' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to save subscription' }, { status: 500 }); } return NextResponse.json({ @@ -88,9 +80,6 @@ export async function POST(request: NextRequest) { }); } catch (error) { console.error('Error in subscribe route:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } } diff --git a/app/api/push/unsubscribe/route.ts b/app/api/push/unsubscribe/route.ts index fadacf4..8cf13ae 100644 --- a/app/api/push/unsubscribe/route.ts +++ b/app/api/push/unsubscribe/route.ts @@ -12,17 +12,11 @@ export async function POST(request: NextRequest) { // Validate input if (!userId || typeof userId !== 'string') { - return NextResponse.json( - { error: 'User ID is required' }, - { status: 400 } - ); + return NextResponse.json({ error: 'User ID is required' }, { status: 400 }); } if (!endpoint || typeof endpoint !== 'string') { - return NextResponse.json( - { error: 'Endpoint is required' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Endpoint is required' }, { status: 400 }); } // Create Supabase client @@ -37,10 +31,7 @@ export async function POST(request: NextRequest) { if (error) { console.error('Error deleting subscription:', error); - return NextResponse.json( - { error: 'Failed to delete subscription' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to delete subscription' }, { status: 500 }); } return NextResponse.json({ @@ -49,9 +40,6 @@ export async function POST(request: NextRequest) { }); } catch (error) { console.error('Error in unsubscribe route:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } } diff --git a/app/api/push/vapid/route.ts b/app/api/push/vapid/route.ts index 17c60a2..9bfb566 100644 --- a/app/api/push/vapid/route.ts +++ b/app/api/push/vapid/route.ts @@ -8,10 +8,7 @@ export async function GET() { const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY; if (!vapidPublicKey) { - return NextResponse.json( - { error: 'VAPID public key not configured' }, - { status: 500 } - ); + return NextResponse.json({ error: 'VAPID public key not configured' }, { status: 500 }); } return NextResponse.json({ publicKey: vapidPublicKey }); diff --git a/app/chaser/page.tsx b/app/chaser/page.tsx index d66a29a..9367e98 100644 --- a/app/chaser/page.tsx +++ b/app/chaser/page.tsx @@ -167,7 +167,7 @@ export default function ChaserPage() { {nearbyRunners.length === 0 ? (

レーダー範囲内に逃走者なし

) : ( - nearbyRunners.map((runner) => ( + nearbyRunners.map((runner: User) => (
p.role === 'runner'); - const chasers = allPlayers.filter((p) => p.role === 'chaser'); - const activePlayers = allPlayers.filter((p) => p.status === 'active'); - const capturedPlayers = allPlayers.filter((p) => p.status === 'captured'); + const runners = allPlayers.filter((p: User) => p.role === 'runner'); + const chasers = allPlayers.filter((p: User) => p.role === 'chaser'); + const activePlayers = allPlayers.filter((p: User) => p.status === 'active'); + const capturedPlayers = allPlayers.filter((p: User) => p.status === 'captured'); - const playerWithLocation = allPlayers.find((p) => p.location); + const playerWithLocation = allPlayers.find((p: User) => p.location); const mapCenter: [number, number] = playerWithLocation?.location ? [playerWithLocation.location.lat, playerWithLocation.location.lng] : [35.5522, 139.7797]; @@ -150,14 +150,15 @@ export default function GamemasterPage() {

🏃 逃走者

合計 {runners.length}人

- 逃走中 {runners.filter((r) => r.status === 'active').length}人 + 逃走中 {runners.filter((r: User) => r.status === 'active').length}人

👹 鬼

合計 {chasers.length}人

- 捕獲数 {chasers.reduce((sum, c) => sum + (c.captureCount || 0), 0)}人 + 捕獲数 {chasers.reduce((sum: number, c: User) => sum + (c.captureCount || 0), 0)} + 人

diff --git a/docs/REFACTORING.md b/docs/REFACTORING.md index 38712f5..ea26db0 100644 --- a/docs/REFACTORING.md +++ b/docs/REFACTORING.md @@ -21,6 +21,7 @@ **問題**: 5つのファイルで同じHaversine公式による距離計算が重複実装されていた **影響箇所**: + - `hooks/useLocation.ts` - `hooks/useMissions.tsx` - `hooks/useLocationHistory.tsx` @@ -28,6 +29,7 @@ - `components/GameStats.tsx` **対応**: + - `lib/geometry.ts` を新規作成 - 以下の共通関数を実装: - `calculateDistance()` - 2点間の距離計算(メートル) @@ -36,6 +38,7 @@ - 全ファイルで共通関数を使用するように変更 **効果**: + - 約200行のコード削減 - 計算ロジックの一貫性を保証 - 単一箇所での保守が可能 @@ -45,11 +48,13 @@ **問題**: データベースのユーザーレコードをアプリケーション型に変換するロジックが3つのページで重複 **影響箇所**: + - `app/runner/page.tsx` - `app/chaser/page.tsx` - `app/gamemaster/page.tsx` **対応**: + - `lib/user-mapper.ts` を新規作成 - 以下の関数を実装: - `mapDatabaseUserToAppUser()` - 単一ユーザーの変換 @@ -57,6 +62,7 @@ - 型定義 `DatabaseUserRow` を追加 **効果**: + - 約90行のコード削減 - 型安全性の向上 - データ変換ロジックの一貫性 @@ -66,6 +72,7 @@ **問題**: ゲーム状態、ミッションタイプ、ロールなどのUIラベルがコード全体に散在 **対応**: + - `constants/ui-labels.ts` を新規作成 - `GAME_STATUS_LABELS` - ゲーム状態のラベルとスタイル - `MISSION_TYPE_LABELS` - ミッションタイプのラベル @@ -79,6 +86,7 @@ - その他ゲーム設定の定数 **効果**: + - マジックナンバーの削減 - UIの一貫性向上 - 設定変更が容易に @@ -88,14 +96,17 @@ **問題**: `as any` の使用により型安全性が損なわれていた **影響箇所**: + - `hooks/useAuth.tsx` (2箇所) **対応**: + - `mapDatabaseUserToAppUser()` を使用してユーザーデータを型安全に変換 - `Record` を使用してinsertペイロードを型定義 - `as any` を完全に除去 **効果**: + - TypeScriptの型チェックが正しく機能 - 実行時エラーのリスク低減 - IDEの補完機能が正しく動作 @@ -105,14 +116,17 @@ **問題1**: `components/Map.tsx` の `getMarkerIcon` が毎レンダリングで再生成されていた **対応**: + - `useCallback` でメモ化 **問題2**: `app/gamemaster/page.tsx` で `allPlayers.find()` が3回重複実行 **対応**: + - 一度だけ `find()` を実行し、結果を再利用 **効果**: + - 不要な再計算を削減 - レンダリングパフォーマンス向上 @@ -121,15 +135,18 @@ **問題**: エラーがログ出力のみでユーザーに通知されないサイレント失敗が存在 **影響箇所**: + - `hooks/useLocation.ts` - 位置履歴の記録失敗 - `app/api/push/send/route.ts` - 無効なサブスクリプションの削除失敗 **対応**: + - 位置情報更新失敗時に `setError()` でエラー状態を設定 - エラーが続行可能かどうかをコメントで明示 - サブスクリプション削除のエラーハンドリングを追加 **効果**: + - エラーの可視性向上 - デバッグが容易に - ユーザー体験の向上 @@ -137,6 +154,7 @@ ## コード品質メトリクス ### リファクタリング前 + - 重複コード: 約300行 - `as any` 使用箇所: 2箇所 - マジックナンバー: 多数 @@ -144,6 +162,7 @@ - 保守性: 中 ### リファクタリング後 + - 重複コード削減: 約300行 → 0行 - `as any` 使用箇所: 0箇所 - マジックナンバー: ほぼゼロ(定数化完了) @@ -153,6 +172,7 @@ ## ファイル追加/変更サマリー ### 新規作成ファイル + - `lib/geometry.ts` - 地理計算ユーティリティ - `lib/user-mapper.ts` - ユーザーデータ変換 - `constants/ui-labels.ts` - UIラベル定数 @@ -160,6 +180,7 @@ - `docs/REFACTORING.md` - このドキュメント ### 変更ファイル + - `hooks/useLocation.ts` - geometry.ts使用、エラーハンドリング改善 - `hooks/useMissions.tsx` - geometry.ts使用 - `hooks/useLocationHistory.tsx` - geometry.ts使用、calculateDistanceエクスポート削除 @@ -175,6 +196,7 @@ ## 今後の推奨改善 ### 実装推奨(低優先度) + 1. **テストカバレッジ拡大** - API ルートのテスト追加 (`app/api/push/**/*.ts`) - 統合テストの追加 diff --git "a/docs/\344\273\225\346\247\230\346\233\270.md" "b/docs/\344\273\225\346\247\230\346\233\270.md" index 0a46785..5866773 100644 --- "a/docs/\344\273\225\346\247\230\346\233\270.md" +++ "b/docs/\344\273\225\346\247\230\346\233\270.md" @@ -69,13 +69,13 @@ ## 4. システム構成 -| レイヤ | 技術 | 補足 | -| ------------ | --------------------------------------------- | --------------------------------------------- | -| フロント | Next.js + React (PWA) | Leaflet.js で地図 / Workbox でオフライン | -| バックエンド | Supabase (PostgreSQL + PostGIS + Auth) | Database Functions で判定ロジック | -| 通知 | Web Push API + Service Worker | ブラウザネイティブ Push 通知 | -| インフラ | Vercel / Cloudflare Pages | 将来 **atok.dev** に CNAME 可能 | -| ドメイン | MVP は臨時サブドメイン(例: `tag-app.vercel.app`) | 本番移行時に atok.dev へ | +| レイヤ | 技術 | 補足 | +| ------------ | -------------------------------------------------- | ---------------------------------------- | +| フロント | Next.js + React (PWA) | Leaflet.js で地図 / Workbox でオフライン | +| バックエンド | Supabase (PostgreSQL + PostGIS + Auth) | Database Functions で判定ロジック | +| 通知 | Web Push API + Service Worker | ブラウザネイティブ Push 通知 | +| インフラ | Vercel / Cloudflare Pages | 将来 **atok.dev** に CNAME 可能 | +| ドメイン | MVP は臨時サブドメイン(例: `tag-app.vercel.app`) | 本番移行時に atok.dev へ | --- diff --git a/hooks/__tests__/useLocation.test.ts b/hooks/__tests__/useLocation.test.ts index 3ac5a20..f9f356a 100644 --- a/hooks/__tests__/useLocation.test.ts +++ b/hooks/__tests__/useLocation.test.ts @@ -71,9 +71,8 @@ describe('useLocation', () => { createGame: vi.fn(), startGame: vi.fn(), pauseGame: vi.fn(), - resumeGame: vi.fn(), endGame: vi.fn(), - updateSettings: vi.fn(), + updateGameSettings: vi.fn(), }); }); diff --git a/hooks/__tests__/useLocationHistory.test.tsx b/hooks/__tests__/useLocationHistory.test.tsx index 0510975..afe71c9 100644 --- a/hooks/__tests__/useLocationHistory.test.tsx +++ b/hooks/__tests__/useLocationHistory.test.tsx @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import { useLocationHistory } from '../useLocationHistory'; import { supabase } from '@/lib/supabase'; +import { calculateDistance } from '@/lib/geometry'; // Mock Supabase vi.mock('@/lib/supabase', () => ({ @@ -64,9 +65,7 @@ describe('useLocationHistory', () => { }); it('should calculate distance between two points', () => { - const { result } = renderHook(() => useLocationHistory()); - - const distance = result.current.calculateDistance( + const distance = calculateDistance( { lat: 35.6895, lng: 139.6917, timestamp: new Date() }, { lat: 35.6896, lng: 139.6918, timestamp: new Date() } ); diff --git a/hooks/useAuth.tsx b/hooks/useAuth.tsx index 1a45d98..1f66e1f 100644 --- a/hooks/useAuth.tsx +++ b/hooks/useAuth.tsx @@ -93,9 +93,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { status: 'active', }; - const { error: insertError } = await supabase - .from('users') - .insert(insertPayload as never); + const { error: insertError } = await supabase.from('users').insert(insertPayload as never); if (insertError) throw insertError; diff --git a/hooks/useLocation.ts b/hooks/useLocation.ts index 97efb32..11d2a11 100644 --- a/hooks/useLocation.ts +++ b/hooks/useLocation.ts @@ -136,11 +136,7 @@ export function useLocation( ); setWatchId(id); - }, [ - updateInterval, - updateLocationInDatabase, - previousLocation, - ]); + }, [updateInterval, updateLocationInDatabase, previousLocation]); const stopTracking = useCallback(() => { if (watchId !== null) { diff --git a/hooks/useNotifications.tsx b/hooks/useNotifications.tsx index 0a8d657..4c82393 100644 --- a/hooks/useNotifications.tsx +++ b/hooks/useNotifications.tsx @@ -114,7 +114,7 @@ export function useNotifications(): UseNotificationsReturn { // Subscribe to push notifications const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(publicKey), + applicationServerKey: urlBase64ToUint8Array(publicKey) as BufferSource, }); // Send subscription to server @@ -148,55 +148,51 @@ export function useNotifications(): UseNotificationsReturn { ); // Unsubscribe from push notifications - const unsubscribeFromPush = useCallback( - async (userId: string): Promise => { - if (!('serviceWorker' in navigator) || !('PushManager' in window)) { - setError('Push notifications are not supported'); - return false; - } + const unsubscribeFromPush = useCallback(async (userId: string): Promise => { + if (!('serviceWorker' in navigator) || !('PushManager' in window)) { + setError('Push notifications are not supported'); + return false; + } - try { - const registration = await navigator.serviceWorker.ready; - const subscription = await registration.pushManager.getSubscription(); + try { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); - if (!subscription) { - setIsSubscribed(false); - return true; - } + if (!subscription) { + setIsSubscribed(false); + return true; + } - // Unsubscribe from push manager - const unsubscribed = await subscription.unsubscribe(); - - if (unsubscribed) { - // Remove subscription from server - await fetch('/api/push/unsubscribe', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userId, - endpoint: subscription.endpoint, - }), - }); + // Unsubscribe from push manager + const unsubscribed = await subscription.unsubscribe(); - setIsSubscribed(false); - setError(null); - console.log('Successfully unsubscribed from push notifications'); - return true; - } + if (unsubscribed) { + // Remove subscription from server + await fetch('/api/push/unsubscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + userId, + endpoint: subscription.endpoint, + }), + }); - return false; - } catch (err) { - const errorMessage = - err instanceof Error ? err.message : 'Failed to unsubscribe from push'; - setError(errorMessage); - console.error('Error unsubscribing from push notifications:', err); - return false; + setIsSubscribed(false); + setError(null); + console.log('Successfully unsubscribed from push notifications'); + return true; } - }, - [] - ); + + return false; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to unsubscribe from push'; + setError(errorMessage); + console.error('Error unsubscribing from push notifications:', err); + return false; + } + }, []); // Send a notification const sendNotification = useCallback( @@ -225,7 +221,7 @@ export function useNotifications(): UseNotificationsReturn { vibrate: options.vibrate || getDefaultVibration(type), silent: options.silent || false, requireInteraction: type === 'game_start' || type === 'game_end', - }); + } as unknown as NotificationOptions & { vibrate?: number[] }); } else { // Fallback to regular notification new Notification(options.title, { @@ -235,7 +231,7 @@ export function useNotifications(): UseNotificationsReturn { data: { ...options.data, type }, vibrate: options.vibrate || getDefaultVibration(type), silent: options.silent || false, - }); + } as unknown as NotificationOptions & { vibrate?: number[] }); } } else { // Fallback to regular notification if service worker is not available @@ -246,7 +242,7 @@ export function useNotifications(): UseNotificationsReturn { data: { ...options.data, type }, vibrate: options.vibrate || getDefaultVibration(type), silent: options.silent || false, - }); + } as unknown as NotificationOptions & { vibrate?: number[] }); } setError(null); diff --git a/lib/geometry.ts b/lib/geometry.ts index bc98f12..5f9ee9b 100644 --- a/lib/geometry.ts +++ b/lib/geometry.ts @@ -14,10 +14,7 @@ export interface GeoLocation { * @param pos2 終了位置 * @returns 距離(メートル) */ -export function calculateDistance( - pos1: GeoLocation, - pos2: GeoLocation -): number { +export function calculateDistance(pos1: GeoLocation, pos2: GeoLocation): number { const R = 6371e3; // Earth's radius in meters const φ1 = (pos1.lat * Math.PI) / 180; const φ2 = (pos2.lat * Math.PI) / 180; @@ -53,18 +50,13 @@ export function calculateSpeed( * @param current 現在の位置 * @returns 方向角(度、北を0度として時計回り) */ -export function calculateHeading( - prev: GeoLocation, - current: GeoLocation -): number { +export function calculateHeading(prev: GeoLocation, current: GeoLocation): number { const φ1 = (prev.lat * Math.PI) / 180; const φ2 = (current.lat * Math.PI) / 180; const Δλ = ((current.lng - prev.lng) * Math.PI) / 180; const y = Math.sin(Δλ) * Math.cos(φ2); - const x = - Math.cos(φ1) * Math.sin(φ2) - - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ); + const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ); const θ = Math.atan2(y, x); return ((θ * 180) / Math.PI + 360) % 360; diff --git a/lib/supabase.ts b/lib/supabase.ts index 86e93ff..f74b047 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -1,4 +1,4 @@ -import { createClient } from '@supabase/supabase-js'; +import { createClient as createSupabaseClient } from '@supabase/supabase-js'; import type { Database } from '@/types/database'; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://placeholder.supabase.co'; @@ -12,7 +12,7 @@ if ( console.warn('Warning: Supabase environment variables are not set. Using placeholder values.'); } -export const supabase = createClient(supabaseUrl, supabaseAnonKey, { +export const supabase = createSupabaseClient(supabaseUrl, supabaseAnonKey, { auth: { persistSession: true, autoRefreshToken: true, @@ -24,3 +24,13 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, { }, }, }); + +// Export a function to create new Supabase clients (for API routes) +export function createClient() { + return createSupabaseClient(supabaseUrl, supabaseAnonKey, { + auth: { + persistSession: false, + autoRefreshToken: false, + }, + }); +} diff --git a/lib/user-mapper.ts b/lib/user-mapper.ts index ead5cf4..675b9fc 100644 --- a/lib/user-mapper.ts +++ b/lib/user-mapper.ts @@ -26,12 +26,7 @@ export function mapDatabaseUserToAppUser(row: DatabaseUserRow): User { nickname: row.nickname, role: row.role as User['role'], team: row.team_id || undefined, - status: - row.status === 'captured' - ? 'captured' - : row.status === 'offline' - ? 'safe' - : 'active', + status: row.status === 'captured' ? 'captured' : row.status === 'offline' ? 'safe' : 'active', lastUpdated: new Date(row.updated_at), captureCount: row.capture_count || 0, }; diff --git a/lib/web-push.ts b/lib/web-push.ts index df781bd..e30edce 100644 --- a/lib/web-push.ts +++ b/lib/web-push.ts @@ -44,13 +44,9 @@ export async function sendPushNotification( } try { - await webpush.sendNotification( - subscription, - JSON.stringify(payload), - { - TTL: 60 * 60 * 24, // 24 hours - } - ); + await webpush.sendNotification(subscription, JSON.stringify(payload), { + TTL: 60 * 60 * 24, // 24 hours + }); } catch (error) { console.error('Error sending push notification:', error); throw error; @@ -63,7 +59,11 @@ export async function sendPushNotification( export async function sendPushNotificationBatch( subscriptions: PushSubscription[], payload: PushNotificationPayload -): Promise<{ success: number; failed: number; errors: Array<{ subscription: PushSubscription; error: unknown }> }> { +): Promise<{ + success: number; + failed: number; + errors: Array<{ subscription: PushSubscription; error: unknown }>; +}> { const results = await Promise.allSettled( subscriptions.map((subscription) => sendPushNotification(subscription, payload)) ); @@ -74,8 +74,8 @@ export async function sendPushNotificationBatch( .map((r, i) => ({ result: r, subscription: subscriptions[i] })) .filter(({ result }) => result.status === 'rejected') .map(({ result, subscription }) => ({ - subscription, - error: (result as PromiseRejectedResult).reason, + subscription: subscription!, + error: (result as PromiseRejectedResult).reason as unknown, })); return { success, failed, errors }; diff --git a/package-lock.json b/package-lock.json index c219d7d..caa6071 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/web-push": "^3.6.4", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitejs/plugin-react": "^4.3.0", @@ -4160,6 +4161,16 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/web-push": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz", + "integrity": "sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", diff --git a/package.json b/package.json index da74a0d..cb50706 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/web-push": "^3.6.4", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitejs/plugin-react": "^4.3.0", diff --git a/types/web-push.d.ts b/types/web-push.d.ts new file mode 100644 index 0000000..5f8a2a3 --- /dev/null +++ b/types/web-push.d.ts @@ -0,0 +1,28 @@ +declare module 'web-push' { + export interface PushSubscription { + endpoint: string; + keys: { + p256dh: string; + auth: string; + }; + } + + export interface WebPushError { + statusCode?: number; + headers?: Record; + body?: string; + } + + export function setVapidDetails(subject: string, publicKey: string, privateKey: string): void; + + export function sendNotification( + subscription: PushSubscription, + payload?: string, + options?: Record + ): Promise; + + export function generateVAPIDKeys(): { + publicKey: string; + privateKey: string; + }; +}