From b58588cf3c8f48a53c2ef447cef9ac7bf125a73e Mon Sep 17 00:00:00 2001 From: RaynisDev Date: Wed, 6 May 2026 07:13:43 +0200 Subject: [PATCH] initial commit: rebreak-monorepo (RN app + standalone Nitro backend) --- .gitignore | 29 + apps/rebreak-native/.gitignore | 37 + apps/rebreak-native/.npmrc | 2 + apps/rebreak-native/README.md | 112 + apps/rebreak-native/app.config.ts | 105 + apps/rebreak-native/app/(app)/_layout.tsx | 217 + apps/rebreak-native/app/(app)/blocker.tsx | 310 + apps/rebreak-native/app/(app)/chat.tsx | 410 + apps/rebreak-native/app/(app)/coach.tsx | 24 + apps/rebreak-native/app/(app)/index.tsx | 198 + apps/rebreak-native/app/(app)/mail.tsx | 235 + .../app/(app)/notifications.tsx | 153 + apps/rebreak-native/app/(auth)/_layout.tsx | 12 + .../rebreak-native/app/(auth)/confirm-otp.tsx | 207 + apps/rebreak-native/app/(auth)/confirm.tsx | 96 + .../app/(auth)/device-limit.tsx | 44 + .../app/(auth)/forgot-password.tsx | 113 + apps/rebreak-native/app/(auth)/signin.tsx | 199 + apps/rebreak-native/app/(auth)/signup.tsx | 301 + apps/rebreak-native/app/_layout.tsx | 124 + apps/rebreak-native/app/auth/callback.tsx | 57 + apps/rebreak-native/app/dm.tsx | 365 + apps/rebreak-native/app/index.tsx | 31 + apps/rebreak-native/app/lyra.tsx | 976 ++ apps/rebreak-native/app/room.tsx | 856 ++ apps/rebreak-native/app/settings.tsx | 222 + apps/rebreak-native/app/urge.tsx | 1261 ++ .../assets/adaptive-icon-android.png | Bin 0 -> 219644 bytes apps/rebreak-native/assets/adaptive-icon.png | Bin 0 -> 222924 bytes apps/rebreak-native/assets/icon.png | Bin 0 -> 222924 bytes apps/rebreak-native/assets/lyra-avatar.riv | Bin 0 -> 264327 bytes apps/rebreak-native/assets/splash.png | Bin 0 -> 248887 bytes .../rebreak-native/assets/tabs/chatbubble.png | Bin 0 -> 437 bytes .../assets/tabs/chatbubble@2x.png | Bin 0 -> 833 bytes .../assets/tabs/chatbubble@3x.png | Bin 0 -> 1246 bytes apps/rebreak-native/assets/tabs/home.png | Bin 0 -> 517 bytes apps/rebreak-native/assets/tabs/home@2x.png | Bin 0 -> 1008 bytes apps/rebreak-native/assets/tabs/home@3x.png | Bin 0 -> 1442 bytes apps/rebreak-native/assets/tabs/mail.png | Bin 0 -> 397 bytes apps/rebreak-native/assets/tabs/mail@2x.png | Bin 0 -> 745 bytes apps/rebreak-native/assets/tabs/mail@3x.png | Bin 0 -> 997 bytes .../assets/tabs/shield-checkmark.png | Bin 0 -> 489 bytes .../assets/tabs/shield-checkmark@2x.png | Bin 0 -> 961 bytes .../assets/tabs/shield-checkmark@3x.png | Bin 0 -> 1453 bytes apps/rebreak-native/assets/tabs/sparkles.png | Bin 0 -> 517 bytes .../assets/tabs/sparkles@2x.png | Bin 0 -> 1027 bytes .../assets/tabs/sparkles@3x.png | Bin 0 -> 1503 bytes apps/rebreak-native/babel.config.js | 20 + apps/rebreak-native/clean-ios.sh | 83 + apps/rebreak-native/components/AppHeader.tsx | 238 + .../rebreak-native/components/BrandSplash.tsx | 412 + apps/rebreak-native/components/Button.tsx | 57 + apps/rebreak-native/components/Card.tsx | 19 + .../rebreak-native/components/ComposeCard.tsx | 174 + .../components/ConfirmAlert.tsx | 204 + apps/rebreak-native/components/EmptyState.tsx | 25 + .../components/HeroShieldCheck.tsx | 23 + apps/rebreak-native/components/IconButton.tsx | 31 + apps/rebreak-native/components/NativeTabs.tsx | 152 + .../components/NotificationsDropdown.tsx | 321 + apps/rebreak-native/components/PostCard.tsx | 558 + .../components/PostCardSkeleton.tsx | 18 + .../components/PostCommentsSheet.tsx | 503 + apps/rebreak-native/components/RiveAvatar.tsx | 166 + .../rebreak-native/components/StreakBadge.tsx | 31 + .../components/SuccessAlert.tsx | 173 + .../components/blocker/AddDomainSheet.tsx | 352 + .../components/blocker/CooldownBanner.tsx | 75 + .../blocker/DeactivationExplainerSheet.tsx | 227 + .../components/blocker/DomainGrid.tsx | 515 + .../components/blocker/LayerSwitchCard.tsx | 111 + .../components/blocker/ProtectionCard.tsx | 167 + .../blocker/ProtectionDetailsSheet.tsx | 703 + .../blocker/ProtectionLockedCard.tsx | 130 + .../components/chat/ChatBubble.tsx | 442 + .../components/chat/ChatInput.tsx | 332 + .../components/chat/CreateRoomSheet.tsx | 282 + .../components/chat/RoomCard.tsx | 217 + .../components/mail/ConnectMailSheet.tsx | 605 + .../components/mail/EditMailAccountSheet.tsx | 249 + .../components/mail/MailAccountCard.tsx | 391 + .../components/mail/MailActivityLog.tsx | 218 + .../components/mail/MailEmptyState.tsx | 100 + .../components/mail/MailStatsRow.tsx | 95 + .../components/urge/Breathing.tsx | 145 + .../components/urge/GamePickerDrawer.tsx | 38 + .../components/urge/InlineIndicators.tsx | 53 + .../components/urge/InlineRatingDrawer.tsx | 205 + .../components/urge/MessageRow.tsx | 90 + .../components/urge/ShareSuccessDrawer.tsx | 211 + .../components/urge/SosFeedbackModal.tsx | 141 + .../components/urge/TtsProviderToggle.tsx | 60 + .../components/urge/UrgeGames.tsx | 1056 ++ .../components/urge/UrgeStats.tsx | 367 + .../components/urge/gameSvgs.ts | 7 + apps/rebreak-native/dev-ios.sh | 67 + apps/rebreak-native/dev-iphone.sh | 34 + apps/rebreak-native/global.css | 3 + apps/rebreak-native/hooks/useBlocklistSync.ts | 54 + apps/rebreak-native/hooks/useChatRealtime.ts | 129 + .../hooks/useCommunityRealtime.ts | 153 + apps/rebreak-native/hooks/useCustomDomains.ts | 178 + .../hooks/useDomainSubmissionRealtime.ts | 96 + apps/rebreak-native/hooks/useMailConnect.ts | 94 + .../rebreak-native/hooks/useMailDisconnect.ts | 39 + apps/rebreak-native/hooks/useMailInterval.ts | 37 + apps/rebreak-native/hooks/useMailResults.ts | 52 + apps/rebreak-native/hooks/useMailStatus.ts | 137 + apps/rebreak-native/hooks/useMe.ts | 62 + .../hooks/useProtectionState.ts | 163 + apps/rebreak-native/hooks/useUserPlan.ts | 43 + apps/rebreak-native/install-android.sh | 95 + apps/rebreak-native/install-ios.sh | 89 + apps/rebreak-native/lib/api.ts | 49 + apps/rebreak-native/lib/avatars.ts | 31 + apps/rebreak-native/lib/formatTime.ts | 7 + apps/rebreak-native/lib/i18n.ts | 26 + apps/rebreak-native/lib/lyraResponse.ts | 61 + apps/rebreak-native/lib/protection.ts | 270 + apps/rebreak-native/lib/resolveAvatar.ts | 29 + apps/rebreak-native/lib/sosConstants.ts | 56 + apps/rebreak-native/lib/sosPrompts.ts | 36 + apps/rebreak-native/lib/sosStream.ts | 141 + apps/rebreak-native/lib/sosTtsQueue.ts | 209 + apps/rebreak-native/lib/supabase.ts | 28 + apps/rebreak-native/lib/tabIcons.ts | 35 + apps/rebreak-native/lib/theme.ts | 26 + apps/rebreak-native/lib/ttsProvider.ts | 54 + apps/rebreak-native/locales/de.json | 591 + apps/rebreak-native/locales/en.json | 591 + apps/rebreak-native/metro.config.js | 31 + apps/rebreak-native/metro.sh | 34 + .../expo-module.config.json | 9 + .../modules/rebreak-protection/index.ts | 17 + .../modules/rebreak-protection/package.json | 8 + .../src/RebreakProtection.types.ts | 92 + .../src/RebreakProtectionModule.ts | 92 + .../src/RebreakProtectionModule.web.ts | 69 + apps/rebreak-native/nativewind-env.d.ts | 3 + apps/rebreak-native/package.json | 71 + .../plugins/with-fmt-consteval-fix.js | 144 + .../with-rebreak-protection-android.js | 127 + .../plugins/with-rebreak-protection-ios.js | 192 + .../plugins/with-rive-asset-android.js | 50 + .../scripts/fix-embed-extension.js | 140 + apps/rebreak-native/stores/auth.ts | 157 + apps/rebreak-native/stores/coach.ts | 86 + apps/rebreak-native/stores/community.ts | 105 + apps/rebreak-native/stores/notifications.ts | 149 + apps/rebreak-native/tailwind.config.js | 50 + .../tools/gen-android-launcher.sh | 61 + apps/rebreak-native/tsconfig.json | 39 + backend/nitro.config.ts | 42 + backend/package.json | 34 + backend/prisma.config.ts | 11 + .../prisma/migrations/0_init/migration.sql | 404 + .../migration.sql | 79 + .../20260422_game_high_scores/migration.sql | 18 + .../20260422_game_ratings/migration.sql | 12 + .../migration.sql | 3 + .../migration.sql | 24 + .../migration.sql | 17 + .../20260430_add_user_devices/migration.sql | 23 + .../20260504_add_lyra_memories/migration.sql | 53 + .../20260504_sos_sessions/migration.sql | 22 + .../migrations/add_domain_submissions.sql | 38 + .../prisma/migrations/add_feedback_items.sql | 25 + .../prisma/migrations/add_game_challenges.sql | 35 + .../migrations/add_game_challenges_rls.sql | 20 + .../prisma/migrations/add_streak_events.sql | 16 + ..._user_custom_domains_unique_constraint.sql | 10 + .../remove_nickname_avatar_from_profiles.sql | 7 + backend/prisma/schema.prisma | 604 + .../domain-submissions/[id]/approve.post.ts | 112 + .../domain-submissions/[id]/reject.post.ts | 15 + .../api/admin/domain-submissions/index.get.ts | 10 + .../server/api/admin/lyra-generate.post.ts | 78 + backend/server/api/admin/lyra-post.post.ts | 125 + backend/server/api/admin/lyra-profile.get.ts | 30 + .../server/api/admin/set-lyra-avatar.post.ts | 72 + backend/server/api/admin/stats.get.ts | 54 + backend/server/api/auth/login.post.ts | 46 + backend/server/api/auth/me.get.ts | 21 + backend/server/api/auth/me.patch.ts | 17 + backend/server/api/avatar/upload.post.ts | 57 + backend/server/api/blocklist/check.get.ts | 19 + backend/server/api/blocklist/count.get.ts | 8 + backend/server/api/blocklist/download.get.ts | 39 + backend/server/api/blocklist/personal.get.ts | 76 + backend/server/api/blocklist/stats.get.ts | 132 + backend/server/api/blocklist/sync.post.ts | 33 + .../server/api/chat/dm-conversations.get.ts | 44 + backend/server/api/chat/dm.post.ts | 66 + backend/server/api/chat/dm/[userId].get.ts | 36 + backend/server/api/chat/join.post.ts | 25 + backend/server/api/chat/like.post.ts | 23 + backend/server/api/chat/message.post.ts | 19 + backend/server/api/chat/messages.get.ts | 5 + .../api/chat/rooms/[roomId]/index.get.ts | 84 + .../api/chat/rooms/[roomId]/index.patch.ts | 137 + .../api/chat/rooms/[roomId]/join.post.ts | 33 + .../api/chat/rooms/[roomId]/leave.post.ts | 23 + .../api/chat/rooms/[roomId]/messages.post.ts | 42 + backend/server/api/chat/rooms/index.get.ts | 46 + backend/server/api/chat/rooms/index.post.ts | 47 + backend/server/api/coach/history.delete.ts | 14 + backend/server/api/coach/history.get.ts | 42 + backend/server/api/coach/message.post.ts | 544 + backend/server/api/coach/sos-session.post.ts | 35 + backend/server/api/coach/sos-stream.get.ts | 302 + backend/server/api/coach/sos-stream.post.ts | 223 + backend/server/api/coach/speak-azure.post.ts | 53 + .../server/api/coach/speak-deepgram.post.ts | 44 + backend/server/api/coach/speak-gemini.post.ts | 107 + backend/server/api/coach/speak-google.post.ts | 69 + backend/server/api/coach/speak-openai.post.ts | 79 + backend/server/api/coach/speak.post.ts | 70 + backend/server/api/coach/transcribe.post.ts | 127 + .../api/community/[postId]/comments.get.ts | 35 + .../api/community/[postId]/index.get.ts | 102 + .../server/api/community/comment-like.post.ts | 37 + backend/server/api/community/comment.post.ts | 67 + .../server/api/community/domain-stats.get.ts | 25 + backend/server/api/community/like.post.ts | 73 + backend/server/api/community/post.post.ts | 81 + backend/server/api/community/posts.get.ts | 129 + backend/server/api/community/repost.post.ts | 83 + .../server/api/community/upload-image.post.ts | 55 + backend/server/api/cooldown/cancel.post.ts | 24 + backend/server/api/cooldown/request.post.ts | 59 + backend/server/api/cooldown/status.get.ts | 66 + backend/server/api/cron/lyra-post.ts | 133 + .../server/api/cron/notifications-cleanup.ts | 21 + .../server/api/custom-domains/[id].delete.ts | 19 + .../api/custom-domains/[id]/submit.post.ts | 68 + .../server/api/custom-domains/index.get.ts | 6 + .../server/api/custom-domains/index.post.ts | 52 + backend/server/api/devices/[id].delete.ts | 17 + backend/server/api/devices/index.get.ts | 29 + backend/server/api/devices/register.post.ts | 64 + backend/server/api/dns/profile.get.ts | 135 + .../api/domain-submissions/[id]/vote.post.ts | 48 + backend/server/api/feedback/[id].patch.ts | 30 + backend/server/api/feedback/index.get.ts | 21 + .../server/api/games/challenge-memory.post.ts | 62 + backend/server/api/games/challenge.post.ts | 38 + .../server/api/games/challenge/[id].get.ts | 16 + .../api/games/challenge/[id]/accept.post.ts | 35 + .../games/challenge/[id]/live-toggle.post.ts | 35 + .../games/challenge/[id]/memory-move.post.ts | 152 + .../api/games/challenge/[id]/move.post.ts | 109 + .../api/games/challenge/[id]/rematch.post.ts | 64 + backend/server/api/games/highscore.get.ts | 39 + backend/server/api/games/history.get.ts | 44 + backend/server/api/games/leaderboard.get.ts | 60 + backend/server/api/games/ranking.get.ts | 15 + backend/server/api/games/rating.post.ts | 25 + backend/server/api/games/ratings.get.ts | 68 + backend/server/api/games/score.post.ts | 37 + backend/server/api/games/share-text.post.ts | 123 + .../server/api/lyra/memories/extract.post.ts | 168 + backend/server/api/lyra/welcome-back.get.ts | 98 + backend/server/api/mail/connect.post.ts | 99 + backend/server/api/mail/disconnect.delete.ts | 19 + backend/server/api/mail/interval.patch.ts | 38 + backend/server/api/mail/proxy-account.get.ts | 27 + backend/server/api/mail/proxy-account.post.ts | 55 + backend/server/api/mail/proxy-config.get.ts | 136 + backend/server/api/mail/results.get.ts | 16 + backend/server/api/mail/scan-internal.post.ts | 205 + backend/server/api/mail/scan.post.ts | 196 + backend/server/api/mail/status.get.ts | 52 + .../server/api/notifications/[id].delete.ts | 10 + backend/server/api/notifications/index.get.ts | 10 + backend/server/api/notifications/read.post.ts | 7 + backend/server/api/protection/state.get.ts | 51 + backend/server/api/providers/index.get.ts | 284 + backend/server/api/scores/leaderboard.get.ts | 28 + backend/server/api/scores/me.get.ts | 22 + backend/server/api/social/follow.post.ts | 42 + .../server/api/social/profile/[userId].get.ts | 71 + backend/server/api/sos/session.post.ts | 42 + backend/server/api/streak/events.get.ts | 7 + backend/server/api/streak/index.get.ts | 7 + backend/server/api/streak/index.patch.ts | 27 + backend/server/api/streak/index.post.ts | 11 + backend/server/api/stripe/checkout.post.ts | 70 + backend/server/api/stripe/portal.post.ts | 35 + backend/server/api/stripe/webhook.post.ts | 103 + backend/server/api/urge/index.get.ts | 9 + backend/server/api/urge/index.post.ts | 42 + .../api/url-filter/blocklist.bin.get.ts | 109 + backend/server/api/user/delete.delete.ts | 36 + backend/server/db/chat-rooms.ts | 366 + backend/server/db/chat.ts | 139 + backend/server/db/community.ts | 434 + backend/server/db/cooldown.ts | 51 + backend/server/db/devices.ts | 146 + backend/server/db/domains.ts | 430 + backend/server/db/lyraMemory.ts | 193 + backend/server/db/mail.ts | 200 + backend/server/db/notifications.ts | 52 + backend/server/db/profile.ts | 23 + backend/server/db/scores.ts | 61 + backend/server/db/social.ts | 73 + backend/server/db/sosSession.ts | 41 + backend/server/db/streak.ts | 104 + backend/server/db/urge.ts | 43 + backend/server/db/user.ts | 11 + backend/server/middleware/cors.ts | 38 + backend/server/plugins/blocklist-cron.ts | 40 + backend/server/plugins/mail-scan-cron.ts | 37 + backend/server/utils/auth.ts | 98 + backend/server/utils/cooldownToken.ts | 83 + backend/server/utils/crypto.ts | 43 + backend/server/utils/domainHash.ts | 69 + backend/server/utils/gambling-keywords.mjs | 64 + backend/server/utils/getUsersMeta.ts | 22 + backend/server/utils/imap-providers.ts | 63 + backend/server/utils/lyraMemoryExtract.ts | 122 + backend/server/utils/plan-features.ts | 90 + backend/server/utils/prisma.ts | 23 + backend/server/utils/scoring.ts | 2 + backend/server/utils/sosSessions.ts | 49 + backend/server/utils/useSupabase.ts | 75 + backend/start-staging.sh | 31 + backend/tsconfig.json | 19 + package.json | 17 + pnpm-lock.yaml | 11232 ++++++++++++++++ pnpm-workspace.yaml | 3 + 330 files changed, 46879 insertions(+) create mode 100644 .gitignore create mode 100644 apps/rebreak-native/.gitignore create mode 100644 apps/rebreak-native/.npmrc create mode 100644 apps/rebreak-native/README.md create mode 100644 apps/rebreak-native/app.config.ts create mode 100644 apps/rebreak-native/app/(app)/_layout.tsx create mode 100644 apps/rebreak-native/app/(app)/blocker.tsx create mode 100644 apps/rebreak-native/app/(app)/chat.tsx create mode 100644 apps/rebreak-native/app/(app)/coach.tsx create mode 100644 apps/rebreak-native/app/(app)/index.tsx create mode 100644 apps/rebreak-native/app/(app)/mail.tsx create mode 100644 apps/rebreak-native/app/(app)/notifications.tsx create mode 100644 apps/rebreak-native/app/(auth)/_layout.tsx create mode 100644 apps/rebreak-native/app/(auth)/confirm-otp.tsx create mode 100644 apps/rebreak-native/app/(auth)/confirm.tsx create mode 100644 apps/rebreak-native/app/(auth)/device-limit.tsx create mode 100644 apps/rebreak-native/app/(auth)/forgot-password.tsx create mode 100644 apps/rebreak-native/app/(auth)/signin.tsx create mode 100644 apps/rebreak-native/app/(auth)/signup.tsx create mode 100644 apps/rebreak-native/app/_layout.tsx create mode 100644 apps/rebreak-native/app/auth/callback.tsx create mode 100644 apps/rebreak-native/app/dm.tsx create mode 100644 apps/rebreak-native/app/index.tsx create mode 100644 apps/rebreak-native/app/lyra.tsx create mode 100644 apps/rebreak-native/app/room.tsx create mode 100644 apps/rebreak-native/app/settings.tsx create mode 100644 apps/rebreak-native/app/urge.tsx create mode 100644 apps/rebreak-native/assets/adaptive-icon-android.png create mode 100644 apps/rebreak-native/assets/adaptive-icon.png create mode 100644 apps/rebreak-native/assets/icon.png create mode 100644 apps/rebreak-native/assets/lyra-avatar.riv create mode 100644 apps/rebreak-native/assets/splash.png create mode 100644 apps/rebreak-native/assets/tabs/chatbubble.png create mode 100644 apps/rebreak-native/assets/tabs/chatbubble@2x.png create mode 100644 apps/rebreak-native/assets/tabs/chatbubble@3x.png create mode 100644 apps/rebreak-native/assets/tabs/home.png create mode 100644 apps/rebreak-native/assets/tabs/home@2x.png create mode 100644 apps/rebreak-native/assets/tabs/home@3x.png create mode 100644 apps/rebreak-native/assets/tabs/mail.png create mode 100644 apps/rebreak-native/assets/tabs/mail@2x.png create mode 100644 apps/rebreak-native/assets/tabs/mail@3x.png create mode 100644 apps/rebreak-native/assets/tabs/shield-checkmark.png create mode 100644 apps/rebreak-native/assets/tabs/shield-checkmark@2x.png create mode 100644 apps/rebreak-native/assets/tabs/shield-checkmark@3x.png create mode 100644 apps/rebreak-native/assets/tabs/sparkles.png create mode 100644 apps/rebreak-native/assets/tabs/sparkles@2x.png create mode 100644 apps/rebreak-native/assets/tabs/sparkles@3x.png create mode 100644 apps/rebreak-native/babel.config.js create mode 100755 apps/rebreak-native/clean-ios.sh create mode 100644 apps/rebreak-native/components/AppHeader.tsx create mode 100644 apps/rebreak-native/components/BrandSplash.tsx create mode 100644 apps/rebreak-native/components/Button.tsx create mode 100644 apps/rebreak-native/components/Card.tsx create mode 100644 apps/rebreak-native/components/ComposeCard.tsx create mode 100644 apps/rebreak-native/components/ConfirmAlert.tsx create mode 100644 apps/rebreak-native/components/EmptyState.tsx create mode 100644 apps/rebreak-native/components/HeroShieldCheck.tsx create mode 100644 apps/rebreak-native/components/IconButton.tsx create mode 100644 apps/rebreak-native/components/NativeTabs.tsx create mode 100644 apps/rebreak-native/components/NotificationsDropdown.tsx create mode 100644 apps/rebreak-native/components/PostCard.tsx create mode 100644 apps/rebreak-native/components/PostCardSkeleton.tsx create mode 100644 apps/rebreak-native/components/PostCommentsSheet.tsx create mode 100644 apps/rebreak-native/components/RiveAvatar.tsx create mode 100644 apps/rebreak-native/components/StreakBadge.tsx create mode 100644 apps/rebreak-native/components/SuccessAlert.tsx create mode 100644 apps/rebreak-native/components/blocker/AddDomainSheet.tsx create mode 100644 apps/rebreak-native/components/blocker/CooldownBanner.tsx create mode 100644 apps/rebreak-native/components/blocker/DeactivationExplainerSheet.tsx create mode 100644 apps/rebreak-native/components/blocker/DomainGrid.tsx create mode 100644 apps/rebreak-native/components/blocker/LayerSwitchCard.tsx create mode 100644 apps/rebreak-native/components/blocker/ProtectionCard.tsx create mode 100644 apps/rebreak-native/components/blocker/ProtectionDetailsSheet.tsx create mode 100644 apps/rebreak-native/components/blocker/ProtectionLockedCard.tsx create mode 100644 apps/rebreak-native/components/chat/ChatBubble.tsx create mode 100644 apps/rebreak-native/components/chat/ChatInput.tsx create mode 100644 apps/rebreak-native/components/chat/CreateRoomSheet.tsx create mode 100644 apps/rebreak-native/components/chat/RoomCard.tsx create mode 100644 apps/rebreak-native/components/mail/ConnectMailSheet.tsx create mode 100644 apps/rebreak-native/components/mail/EditMailAccountSheet.tsx create mode 100644 apps/rebreak-native/components/mail/MailAccountCard.tsx create mode 100644 apps/rebreak-native/components/mail/MailActivityLog.tsx create mode 100644 apps/rebreak-native/components/mail/MailEmptyState.tsx create mode 100644 apps/rebreak-native/components/mail/MailStatsRow.tsx create mode 100644 apps/rebreak-native/components/urge/Breathing.tsx create mode 100644 apps/rebreak-native/components/urge/GamePickerDrawer.tsx create mode 100644 apps/rebreak-native/components/urge/InlineIndicators.tsx create mode 100644 apps/rebreak-native/components/urge/InlineRatingDrawer.tsx create mode 100644 apps/rebreak-native/components/urge/MessageRow.tsx create mode 100644 apps/rebreak-native/components/urge/ShareSuccessDrawer.tsx create mode 100644 apps/rebreak-native/components/urge/SosFeedbackModal.tsx create mode 100644 apps/rebreak-native/components/urge/TtsProviderToggle.tsx create mode 100644 apps/rebreak-native/components/urge/UrgeGames.tsx create mode 100644 apps/rebreak-native/components/urge/UrgeStats.tsx create mode 100644 apps/rebreak-native/components/urge/gameSvgs.ts create mode 100755 apps/rebreak-native/dev-ios.sh create mode 100755 apps/rebreak-native/dev-iphone.sh create mode 100644 apps/rebreak-native/global.css create mode 100644 apps/rebreak-native/hooks/useBlocklistSync.ts create mode 100644 apps/rebreak-native/hooks/useChatRealtime.ts create mode 100644 apps/rebreak-native/hooks/useCommunityRealtime.ts create mode 100644 apps/rebreak-native/hooks/useCustomDomains.ts create mode 100644 apps/rebreak-native/hooks/useDomainSubmissionRealtime.ts create mode 100644 apps/rebreak-native/hooks/useMailConnect.ts create mode 100644 apps/rebreak-native/hooks/useMailDisconnect.ts create mode 100644 apps/rebreak-native/hooks/useMailInterval.ts create mode 100644 apps/rebreak-native/hooks/useMailResults.ts create mode 100644 apps/rebreak-native/hooks/useMailStatus.ts create mode 100644 apps/rebreak-native/hooks/useMe.ts create mode 100644 apps/rebreak-native/hooks/useProtectionState.ts create mode 100644 apps/rebreak-native/hooks/useUserPlan.ts create mode 100755 apps/rebreak-native/install-android.sh create mode 100755 apps/rebreak-native/install-ios.sh create mode 100644 apps/rebreak-native/lib/api.ts create mode 100644 apps/rebreak-native/lib/avatars.ts create mode 100644 apps/rebreak-native/lib/formatTime.ts create mode 100644 apps/rebreak-native/lib/i18n.ts create mode 100644 apps/rebreak-native/lib/lyraResponse.ts create mode 100644 apps/rebreak-native/lib/protection.ts create mode 100644 apps/rebreak-native/lib/resolveAvatar.ts create mode 100644 apps/rebreak-native/lib/sosConstants.ts create mode 100644 apps/rebreak-native/lib/sosPrompts.ts create mode 100644 apps/rebreak-native/lib/sosStream.ts create mode 100644 apps/rebreak-native/lib/sosTtsQueue.ts create mode 100644 apps/rebreak-native/lib/supabase.ts create mode 100644 apps/rebreak-native/lib/tabIcons.ts create mode 100644 apps/rebreak-native/lib/theme.ts create mode 100644 apps/rebreak-native/lib/ttsProvider.ts create mode 100644 apps/rebreak-native/locales/de.json create mode 100644 apps/rebreak-native/locales/en.json create mode 100644 apps/rebreak-native/metro.config.js create mode 100755 apps/rebreak-native/metro.sh create mode 100644 apps/rebreak-native/modules/rebreak-protection/expo-module.config.json create mode 100644 apps/rebreak-native/modules/rebreak-protection/index.ts create mode 100644 apps/rebreak-native/modules/rebreak-protection/package.json create mode 100644 apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts create mode 100644 apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts create mode 100644 apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts create mode 100644 apps/rebreak-native/nativewind-env.d.ts create mode 100644 apps/rebreak-native/package.json create mode 100644 apps/rebreak-native/plugins/with-fmt-consteval-fix.js create mode 100644 apps/rebreak-native/plugins/with-rebreak-protection-android.js create mode 100644 apps/rebreak-native/plugins/with-rebreak-protection-ios.js create mode 100644 apps/rebreak-native/plugins/with-rive-asset-android.js create mode 100644 apps/rebreak-native/scripts/fix-embed-extension.js create mode 100644 apps/rebreak-native/stores/auth.ts create mode 100644 apps/rebreak-native/stores/coach.ts create mode 100644 apps/rebreak-native/stores/community.ts create mode 100644 apps/rebreak-native/stores/notifications.ts create mode 100644 apps/rebreak-native/tailwind.config.js create mode 100755 apps/rebreak-native/tools/gen-android-launcher.sh create mode 100644 apps/rebreak-native/tsconfig.json create mode 100644 backend/nitro.config.ts create mode 100644 backend/package.json create mode 100644 backend/prisma.config.ts create mode 100644 backend/prisma/migrations/0_init/migration.sql create mode 100644 backend/prisma/migrations/20250712_chat_rooms_and_features/migration.sql create mode 100644 backend/prisma/migrations/20260422_game_high_scores/migration.sql create mode 100644 backend/prisma/migrations/20260422_game_ratings/migration.sql create mode 100644 backend/prisma/migrations/20260426_add_actor_avatar_to_notifications/migration.sql create mode 100644 backend/prisma/migrations/20260428_add_cooldown_requests/migration.sql create mode 100644 backend/prisma/migrations/20260430_add_custom_imap_tls/migration.sql create mode 100644 backend/prisma/migrations/20260430_add_user_devices/migration.sql create mode 100644 backend/prisma/migrations/20260504_add_lyra_memories/migration.sql create mode 100644 backend/prisma/migrations/20260504_sos_sessions/migration.sql create mode 100644 backend/prisma/migrations/add_domain_submissions.sql create mode 100644 backend/prisma/migrations/add_feedback_items.sql create mode 100644 backend/prisma/migrations/add_game_challenges.sql create mode 100644 backend/prisma/migrations/add_game_challenges_rls.sql create mode 100644 backend/prisma/migrations/add_streak_events.sql create mode 100644 backend/prisma/migrations/fix_user_custom_domains_unique_constraint.sql create mode 100644 backend/prisma/migrations/remove_nickname_avatar_from_profiles.sql create mode 100644 backend/prisma/schema.prisma create mode 100644 backend/server/api/admin/domain-submissions/[id]/approve.post.ts create mode 100644 backend/server/api/admin/domain-submissions/[id]/reject.post.ts create mode 100644 backend/server/api/admin/domain-submissions/index.get.ts create mode 100644 backend/server/api/admin/lyra-generate.post.ts create mode 100644 backend/server/api/admin/lyra-post.post.ts create mode 100644 backend/server/api/admin/lyra-profile.get.ts create mode 100644 backend/server/api/admin/set-lyra-avatar.post.ts create mode 100644 backend/server/api/admin/stats.get.ts create mode 100644 backend/server/api/auth/login.post.ts create mode 100644 backend/server/api/auth/me.get.ts create mode 100644 backend/server/api/auth/me.patch.ts create mode 100644 backend/server/api/avatar/upload.post.ts create mode 100644 backend/server/api/blocklist/check.get.ts create mode 100644 backend/server/api/blocklist/count.get.ts create mode 100644 backend/server/api/blocklist/download.get.ts create mode 100644 backend/server/api/blocklist/personal.get.ts create mode 100644 backend/server/api/blocklist/stats.get.ts create mode 100644 backend/server/api/blocklist/sync.post.ts create mode 100644 backend/server/api/chat/dm-conversations.get.ts create mode 100644 backend/server/api/chat/dm.post.ts create mode 100644 backend/server/api/chat/dm/[userId].get.ts create mode 100644 backend/server/api/chat/join.post.ts create mode 100644 backend/server/api/chat/like.post.ts create mode 100644 backend/server/api/chat/message.post.ts create mode 100644 backend/server/api/chat/messages.get.ts create mode 100644 backend/server/api/chat/rooms/[roomId]/index.get.ts create mode 100644 backend/server/api/chat/rooms/[roomId]/index.patch.ts create mode 100644 backend/server/api/chat/rooms/[roomId]/join.post.ts create mode 100644 backend/server/api/chat/rooms/[roomId]/leave.post.ts create mode 100644 backend/server/api/chat/rooms/[roomId]/messages.post.ts create mode 100644 backend/server/api/chat/rooms/index.get.ts create mode 100644 backend/server/api/chat/rooms/index.post.ts create mode 100644 backend/server/api/coach/history.delete.ts create mode 100644 backend/server/api/coach/history.get.ts create mode 100644 backend/server/api/coach/message.post.ts create mode 100644 backend/server/api/coach/sos-session.post.ts create mode 100644 backend/server/api/coach/sos-stream.get.ts create mode 100644 backend/server/api/coach/sos-stream.post.ts create mode 100644 backend/server/api/coach/speak-azure.post.ts create mode 100644 backend/server/api/coach/speak-deepgram.post.ts create mode 100644 backend/server/api/coach/speak-gemini.post.ts create mode 100644 backend/server/api/coach/speak-google.post.ts create mode 100644 backend/server/api/coach/speak-openai.post.ts create mode 100644 backend/server/api/coach/speak.post.ts create mode 100644 backend/server/api/coach/transcribe.post.ts create mode 100644 backend/server/api/community/[postId]/comments.get.ts create mode 100644 backend/server/api/community/[postId]/index.get.ts create mode 100644 backend/server/api/community/comment-like.post.ts create mode 100644 backend/server/api/community/comment.post.ts create mode 100644 backend/server/api/community/domain-stats.get.ts create mode 100644 backend/server/api/community/like.post.ts create mode 100644 backend/server/api/community/post.post.ts create mode 100644 backend/server/api/community/posts.get.ts create mode 100644 backend/server/api/community/repost.post.ts create mode 100644 backend/server/api/community/upload-image.post.ts create mode 100644 backend/server/api/cooldown/cancel.post.ts create mode 100644 backend/server/api/cooldown/request.post.ts create mode 100644 backend/server/api/cooldown/status.get.ts create mode 100644 backend/server/api/cron/lyra-post.ts create mode 100644 backend/server/api/cron/notifications-cleanup.ts create mode 100644 backend/server/api/custom-domains/[id].delete.ts create mode 100644 backend/server/api/custom-domains/[id]/submit.post.ts create mode 100644 backend/server/api/custom-domains/index.get.ts create mode 100644 backend/server/api/custom-domains/index.post.ts create mode 100644 backend/server/api/devices/[id].delete.ts create mode 100644 backend/server/api/devices/index.get.ts create mode 100644 backend/server/api/devices/register.post.ts create mode 100644 backend/server/api/dns/profile.get.ts create mode 100644 backend/server/api/domain-submissions/[id]/vote.post.ts create mode 100644 backend/server/api/feedback/[id].patch.ts create mode 100644 backend/server/api/feedback/index.get.ts create mode 100644 backend/server/api/games/challenge-memory.post.ts create mode 100644 backend/server/api/games/challenge.post.ts create mode 100644 backend/server/api/games/challenge/[id].get.ts create mode 100644 backend/server/api/games/challenge/[id]/accept.post.ts create mode 100644 backend/server/api/games/challenge/[id]/live-toggle.post.ts create mode 100644 backend/server/api/games/challenge/[id]/memory-move.post.ts create mode 100644 backend/server/api/games/challenge/[id]/move.post.ts create mode 100644 backend/server/api/games/challenge/[id]/rematch.post.ts create mode 100644 backend/server/api/games/highscore.get.ts create mode 100644 backend/server/api/games/history.get.ts create mode 100644 backend/server/api/games/leaderboard.get.ts create mode 100644 backend/server/api/games/ranking.get.ts create mode 100644 backend/server/api/games/rating.post.ts create mode 100644 backend/server/api/games/ratings.get.ts create mode 100644 backend/server/api/games/score.post.ts create mode 100644 backend/server/api/games/share-text.post.ts create mode 100644 backend/server/api/lyra/memories/extract.post.ts create mode 100644 backend/server/api/lyra/welcome-back.get.ts create mode 100644 backend/server/api/mail/connect.post.ts create mode 100644 backend/server/api/mail/disconnect.delete.ts create mode 100644 backend/server/api/mail/interval.patch.ts create mode 100644 backend/server/api/mail/proxy-account.get.ts create mode 100644 backend/server/api/mail/proxy-account.post.ts create mode 100644 backend/server/api/mail/proxy-config.get.ts create mode 100644 backend/server/api/mail/results.get.ts create mode 100644 backend/server/api/mail/scan-internal.post.ts create mode 100644 backend/server/api/mail/scan.post.ts create mode 100644 backend/server/api/mail/status.get.ts create mode 100644 backend/server/api/notifications/[id].delete.ts create mode 100644 backend/server/api/notifications/index.get.ts create mode 100644 backend/server/api/notifications/read.post.ts create mode 100644 backend/server/api/protection/state.get.ts create mode 100644 backend/server/api/providers/index.get.ts create mode 100644 backend/server/api/scores/leaderboard.get.ts create mode 100644 backend/server/api/scores/me.get.ts create mode 100644 backend/server/api/social/follow.post.ts create mode 100644 backend/server/api/social/profile/[userId].get.ts create mode 100644 backend/server/api/sos/session.post.ts create mode 100644 backend/server/api/streak/events.get.ts create mode 100644 backend/server/api/streak/index.get.ts create mode 100644 backend/server/api/streak/index.patch.ts create mode 100644 backend/server/api/streak/index.post.ts create mode 100644 backend/server/api/stripe/checkout.post.ts create mode 100644 backend/server/api/stripe/portal.post.ts create mode 100644 backend/server/api/stripe/webhook.post.ts create mode 100644 backend/server/api/urge/index.get.ts create mode 100644 backend/server/api/urge/index.post.ts create mode 100644 backend/server/api/url-filter/blocklist.bin.get.ts create mode 100644 backend/server/api/user/delete.delete.ts create mode 100644 backend/server/db/chat-rooms.ts create mode 100644 backend/server/db/chat.ts create mode 100644 backend/server/db/community.ts create mode 100644 backend/server/db/cooldown.ts create mode 100644 backend/server/db/devices.ts create mode 100644 backend/server/db/domains.ts create mode 100644 backend/server/db/lyraMemory.ts create mode 100644 backend/server/db/mail.ts create mode 100644 backend/server/db/notifications.ts create mode 100644 backend/server/db/profile.ts create mode 100644 backend/server/db/scores.ts create mode 100644 backend/server/db/social.ts create mode 100644 backend/server/db/sosSession.ts create mode 100644 backend/server/db/streak.ts create mode 100644 backend/server/db/urge.ts create mode 100644 backend/server/db/user.ts create mode 100644 backend/server/middleware/cors.ts create mode 100644 backend/server/plugins/blocklist-cron.ts create mode 100644 backend/server/plugins/mail-scan-cron.ts create mode 100644 backend/server/utils/auth.ts create mode 100644 backend/server/utils/cooldownToken.ts create mode 100644 backend/server/utils/crypto.ts create mode 100644 backend/server/utils/domainHash.ts create mode 100644 backend/server/utils/gambling-keywords.mjs create mode 100644 backend/server/utils/getUsersMeta.ts create mode 100644 backend/server/utils/imap-providers.ts create mode 100644 backend/server/utils/lyraMemoryExtract.ts create mode 100644 backend/server/utils/plan-features.ts create mode 100644 backend/server/utils/prisma.ts create mode 100644 backend/server/utils/scoring.ts create mode 100644 backend/server/utils/sosSessions.ts create mode 100644 backend/server/utils/useSupabase.ts create mode 100644 backend/start-staging.sh create mode 100644 backend/tsconfig.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a550dc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +node_modules +.output +.output-staging +.nitro +.nuxt +.pnpm-store + +# RN / Expo +apps/rebreak-native/ios/Pods +apps/rebreak-native/ios/build +apps/rebreak-native/ios/DerivedData +apps/rebreak-native/android/build +apps/rebreak-native/android/app/build +apps/rebreak-native/android/.gradle +apps/rebreak-native/.expo + +# Build artefacts +*.log +*.tsbuildinfo +backend/server/generated + +# OS +.DS_Store +Thumbs.db + +# Local env +.env +.env.local +*.local diff --git a/apps/rebreak-native/.gitignore b/apps/rebreak-native/.gitignore new file mode 100644 index 0000000..1cc3d9d --- /dev/null +++ b/apps/rebreak-native/.gitignore @@ -0,0 +1,37 @@ +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +ios/ +android/ +*.jks +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# Storybook +storybook-static/ diff --git a/apps/rebreak-native/.npmrc b/apps/rebreak-native/.npmrc new file mode 100644 index 0000000..8e5a554 --- /dev/null +++ b/apps/rebreak-native/.npmrc @@ -0,0 +1,2 @@ +node-linker=hoisted +shamefully-hoist=true diff --git a/apps/rebreak-native/README.md b/apps/rebreak-native/README.md new file mode 100644 index 0000000..4d5c2da --- /dev/null +++ b/apps/rebreak-native/README.md @@ -0,0 +1,112 @@ +# Rebreak Native (React Native + Expo) + +> Mobile App für iOS + Android. Migration von Nuxt+Capacitor → React Native+Expo. +> Migration-Plan: [`apps/rebreak/docs/react-native-migration.md`](../rebreak/docs/react-native-migration.md) + +## Status + +**Phase 1 — Foundation Skeleton** (2026-05-02) + +- [x] Verzeichnisstruktur +- [x] `package.json` mit Expo SDK 53 + RN 0.76 (New Architecture) +- [x] `app.config.ts` (Expo, Bundle-ID `org.rebreak.app`) +- [x] Expo Router Skeleton +- [x] NativeWind Setup (Tailwind für RN) +- [x] Metro Config (pnpm Monorepo aware) +- [x] Supabase Client + API Wrapper +- [ ] `pnpm install` — vom User auszuführen +- [ ] `expo prebuild` — generiert `ios/` + `android/` Bare-Projekte +- [ ] Native Module Imports (NEFilter, VpnService, A11y) — Phase 5+ + +## Setup + +```bash +# vom Monorepo-Root: +pnpm install + +# Native-Projekte generieren (lokal, nicht committed) +cd apps/rebreak-native +pnpm prebuild + +# Run +pnpm ios # iOS Simulator +pnpm android # Android Emulator +``` + +## Stack + +| Bereich | Lib | +|---|---| +| Framework | React Native 0.76 (New Architecture) + Expo SDK 53 | +| Routing | Expo Router (file-based) | +| State | Zustand | +| Server State | TanStack Query (React Query) | +| Styling | NativeWind 4 (Tailwind) | +| Forms | React Hook Form + Valibot | +| i18n | react-i18next | +| Auth | @supabase/supabase-js + AsyncStorage | +| Animation | react-native-reanimated, lottie-react-native | +| Storage | react-native-mmkv (fast key-value) | + +## Verzeichnisstruktur + +``` +apps/rebreak-native/ +├── app/ # Expo Router (file-based routes) +│ ├── _layout.tsx # Root Stack +│ ├── index.tsx # Landing (auth oder app entscheiden) +│ ├── (auth)/ # Auth-Flow (signin, signup, forgot-password) +│ └── (app)/ # Authenticated App (tabs) +├── components/ # Reusable UI Components +├── hooks/ # Custom React Hooks +├── stores/ # Zustand Stores +├── lib/ # Supabase Client, API Wrapper, Utils +│ ├── supabase.ts +│ └── api.ts +├── locales/ # de.json, en.json +├── modules/ # Custom Expo Native Modules +│ ├── rebreak-ios-filter/ # NEFilter + Tunnel (Phase 5) +│ ├── rebreak-android-blocker/ # VpnService + DnsFilter (Phase 6) +│ └── rebreak-android-a11y/ # AccessibilityService (Phase 6) +├── plugins/ # Expo Config Plugins +└── assets/ # Icons, Splashscreens, Fonts +``` + +## Wichtige Konfiguration + +| Datei | Zweck | +|---|---| +| `app.config.ts` | Expo App-Config (Bundle-ID, Permissions, Plugins) | +| `metro.config.js` | Monorepo-aware Metro (Symlinks, Workspace-Folders) | +| `babel.config.js` | NativeWind Preset + Reanimated Plugin | +| `tailwind.config.js` | Tailwind Config — Brand-Colors aus apps/rebreak/ syncen | +| `global.css` | NativeWind Tailwind-Imports | + +## Native Module Strategie + +Bestehender Native-Code aus `apps/rebreak/ios/` + `apps/rebreak/android/` wird in Expo Native Modules gewrappt — **ohne neu zu schreiben**. + +### iOS — `modules/rebreak-ios-filter/` +Wrapped: +- `RebreakURLFilter` (NEFilterDataProvider) → blockt bet365 etc. +- `RebreakTunnel` (NEPacketTunnelProvider) → DNS-Filter + +### Android — `modules/rebreak-android-blocker/` +Wrapped: +- `RebreakVpnService` (Kotlin) → VpnService DNS-Filter +- `DnsFilter` + `HashList` + `DomainHasher` + +### Android — `modules/rebreak-android-a11y/` +Wrapped: +- `RebreakAccessibilityService` → Detection von Gambling-Apps + +## ⚠️ Wichtige Hinweise + +- **Schutz-Stack bleibt 1:1 erhalten** — Swift- und Kotlin-Code wandert unverändert in `modules/`. +- **Backend bleibt** in `apps/rebreak/server/` — RN ruft die gleichen Endpoints. +- **Alte Capacitor-App** bleibt deployed bis RN-App im Store ist. +- **Kein Auto-Commit** — User entscheidet wann committet wird. + +## Phasen-Tracker + +Siehe Migration-Plan für Details: [`apps/rebreak/docs/react-native-migration.md`](../rebreak/docs/react-native-migration.md) diff --git a/apps/rebreak-native/app.config.ts b/apps/rebreak-native/app.config.ts new file mode 100644 index 0000000..ee68b2c --- /dev/null +++ b/apps/rebreak-native/app.config.ts @@ -0,0 +1,105 @@ +import { ExpoConfig, ConfigContext } from "expo/config"; + +export default ({ config }: ConfigContext): ExpoConfig => ({ + ...config, + name: "ReBreak", + slug: "rebreak", + version: "0.1.0", + orientation: "portrait", + icon: "./assets/icon.png", + scheme: "rebreak", + userInterfaceStyle: "automatic", + newArchEnabled: true, + + splash: { + image: "./assets/splash.png", + resizeMode: "contain", + backgroundColor: "#0f172a", + }, + + ios: { + supportsTablet: true, + bundleIdentifier: "org.rebreak.app", + config: { + usesNonExemptEncryption: false, + }, + infoPlist: { + ITSAppUsesNonExemptEncryption: false, + NSMicrophoneUsageDescription: + "Rebreak nutzt das Mikrofon für Sprachnachrichten an Lyra.", + NSPhotoLibraryUsageDescription: + "Rebreak greift auf Fotos zu, damit du sie in deinen Posts teilen kannst.", + NSPhotoLibraryAddUsageDescription: + "Rebreak speichert Bilder in deine Foto-Mediathek.", + }, + }, + + android: { + package: "org.rebreak.app", + adaptiveIcon: { + // Chain-only foreground (ohne "ReBreak"-Schrift). Auf Android schneidet die + // Adaptive-Icon-Mask (Kreis/Squircle/etc.) den Untertitel sonst ab. + foregroundImage: "./assets/adaptive-icon-android.png", + backgroundColor: "#0a0a0a", + }, + permissions: [ + "INTERNET", + "ACCESS_NETWORK_STATE", + "BIND_VPN_SERVICE", + "FOREGROUND_SERVICE", + "POST_NOTIFICATIONS", + "BIND_ACCESSIBILITY_SERVICE", + "RECORD_AUDIO", + ], + }, + + plugins: [ + "expo-router", + "expo-localization", + [ + "expo-build-properties", + { + ios: { + deploymentTarget: "15.1", + useFrameworks: "static", + }, + android: { + minSdkVersion: 26, + compileSdkVersion: 35, + targetSdkVersion: 35, + }, + }, + ], + // Xcode 16 + RN 0.79 fmt consteval workaround + "./plugins/with-fmt-consteval-fix", + // Phase 5: NEFilter Extension + Family Controls Entitlements (iOS) + "./plugins/with-rebreak-protection-ios", + // Phase 5: VpnService + AccessibilityService (Android) + "./plugins/with-rebreak-protection-android", + // Rive-Asset (lyra-avatar.riv) als Android raw-resource bundlen + "./plugins/with-rive-asset-android", + ], + + experiments: { + typedRoutes: true, + }, + + extra: { + apiUrl: + process.env.EXPO_PUBLIC_API_URL || + process.env.API_URL || + "https://staging.rebreak.org", + // TEMP: Staging Anon-Key + URL hardcoded für lokales Dev-Testing. + // Anon-Key ist designed für Client-Ship (RLS protectiert DB). Trotzdem: + // BFF-Migration kommt in Phase 5 — dann fliegen diese 2 Zeilen wieder raus. + supabaseUrl: + process.env.EXPO_PUBLIC_SUPABASE_URL || + process.env.SUPABASE_URL || + "https://db-staging.rebreak.org", + supabaseAnonKey: + process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || + process.env.SUPABASE_KEY || + process.env.SUPABASE_ANON_KEY || + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsImF1ZCI6ImF1dGhlbnRpY2F0ZWQiLCJyb2xlIjoiYW5vbiIsImV4cCI6MjA5MTAxODk1NSwiaWF0IjoxNzc1NjU4OTU1fQ.93d2r3pft2E-alf1JezqueD0l0n1dim7dGvhBN0l1Cs", + }, +}); diff --git a/apps/rebreak-native/app/(app)/_layout.tsx b/apps/rebreak-native/app/(app)/_layout.tsx new file mode 100644 index 0000000..9f37f27 --- /dev/null +++ b/apps/rebreak-native/app/(app)/_layout.tsx @@ -0,0 +1,217 @@ +import { useEffect, useRef, useState } from 'react'; +import { View, ActivityIndicator, AppState, Platform } from 'react-native'; +import { useRouter } from 'expo-router'; +import * as Notifications from 'expo-notifications'; +import { useTranslation } from 'react-i18next'; +import { useAuthStore } from '../../stores/auth'; +import { useNotificationStore } from '../../stores/notifications'; +import { colors } from '../../lib/theme'; +import { NativeTabs } from '../../components/NativeTabs'; +import { protection } from '../../lib/protection'; +import { preloadTabIcons, getTabIcon } from '../../lib/tabIcons'; + +export default function AppLayout() { + const router = useRouter(); + const { t } = useTranslation(); + const { session, loading } = useAuthStore(); + const loadNotifications = useNotificationStore((s) => s.load); + const startRealtime = useNotificationStore((s) => s.startRealtime); + const stopRealtime = useNotificationStore((s) => s.stopRealtime); + const resetNotifications = useNotificationStore((s) => s.reset); + const rearmInFlightRef = useRef(false); + const bypassNotifiedRef = useRef(false); + + // Android-Tab-Icons müssen async aus Ionicons-Font generiert werden (kein + // SF-Symbol-Support). preloadTabIcons() läuft schon beim Modul-Import — hier + // nur den ready-State tracken damit wir re-rendern wenn der Cache fertig ist. + const [tabIconsReady, setTabIconsReady] = useState(Platform.OS !== 'android'); + useEffect(() => { + if (Platform.OS === 'android' && !tabIconsReady) { + preloadTabIcons().then(() => setTabIconsReady(true)); + } + }, [tabIconsReady]); + + useEffect(() => { + if (!loading && !session) { + router.replace('/signin'); + } + }, [session, loading]); + + useEffect(() => { + if (!session) { + resetNotifications(); + return; + } + loadNotifications(); + startRealtime(); + return () => { + stopRealtime(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session?.user?.id]); + + useEffect(() => { + if (!session || Platform.OS !== 'ios') return; + + let cancelled = false; + let pollTimer: ReturnType | null = null; + + async function notifyBypassDetected(): Promise { + const perms = await Notifications.getPermissionsAsync(); + let granted = perms.granted || perms.ios?.status === Notifications.IosAuthorizationStatus.PROVISIONAL; + if (!granted) { + const req = await Notifications.requestPermissionsAsync(); + granted = req.granted || req.ios?.status === Notifications.IosAuthorizationStatus.PROVISIONAL; + } + if (!granted) return false; + + await Notifications.scheduleNotificationAsync({ + content: { + title: 'ReBreak Schutz manipuliert', + body: 'Tippe hier, um den Schutz sofort wieder zu aktivieren.', + sound: 'default', + data: { type: 'protection_bypass_detected' }, + }, + trigger: null, + }); + return true; + } + + async function enforceProtection() { + if (cancelled || rearmInFlightRef.current) return; + try { + const state = await protection.getCombinedState(); + if (cancelled) return; + if (state.phase !== 'recoveringFromBypass') { + bypassNotifiedRef.current = false; + return; + } + if (bypassNotifiedRef.current) return; + + bypassNotifiedRef.current = true; + const notified = await notifyBypassDetected(); + if (!notified) { + // Fallback wenn Notifications nicht erlaubt sind. + rearmInFlightRef.current = true; + router.replace('/blocker'); + await protection.activateFamilyControls().catch(() => ({ enabled: false })); + } + } finally { + rearmInFlightRef.current = false; + } + } + + async function onBypassNotificationTap() { + if (rearmInFlightRef.current) return; + rearmInFlightRef.current = true; + try { + router.replace('/blocker'); + await protection.activateFamilyControls().catch(() => ({ enabled: false })); + } finally { + rearmInFlightRef.current = false; + } + } + + // Initial check + foreground re-check + periodisches Polling als Fallback. + enforceProtection(); + const notifTapSub = Notifications.addNotificationResponseReceivedListener((response) => { + const type = response.notification.request.content.data?.type; + if (type === 'protection_bypass_detected') { + void onBypassNotificationTap(); + } + }); + Notifications.getLastNotificationResponseAsync().then((response) => { + const type = response?.notification.request.content.data?.type; + if (type === 'protection_bypass_detected') { + void onBypassNotificationTap(); + } + }); + const appStateSub = AppState.addEventListener('change', (s) => { + if (s === 'active') { + enforceProtection(); + } + }); + pollTimer = setInterval(enforceProtection, 15000); + + return () => { + cancelled = true; + notifTapSub.remove(); + appStateSub.remove(); + if (pollTimer) clearInterval(pollTimer); + }; + }, [session, router]); + + if (loading || !session) { + return ( + + + + ); + } + + return ( + + + Platform.OS === 'ios' + ? { sfSymbol: 'house.fill' } + : (getTabIcon('home') as any), + }} + /> + + Platform.OS === 'ios' + ? { sfSymbol: 'bubble.left.and.bubble.right.fill' } + : (getTabIcon('chat') as any), + }} + /> + + Platform.OS === 'ios' + ? { sfSymbol: 'sparkles' } + : (getTabIcon('coach') as any), + }} + /> + + Platform.OS === 'ios' + ? { sfSymbol: 'checkmark.shield.fill' } + : (getTabIcon('blocker') as any), + }} + /> + + Platform.OS === 'ios' + ? { sfSymbol: 'envelope.fill' } + : (getTabIcon('mail') as any), + }} + /> + + + ); +} diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx new file mode 100644 index 0000000..7d267a4 --- /dev/null +++ b/apps/rebreak-native/app/(app)/blocker.tsx @@ -0,0 +1,310 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { ScrollView, View, Alert, ActivityIndicator } from 'react-native'; +import { useRouter } from 'expo-router'; +import { useBottomTabBarHeight } from 'react-native-bottom-tabs'; +import { useTranslation } from 'react-i18next'; +import { AppHeader } from '../../components/AppHeader'; +import { LayerSwitchCard } from '../../components/blocker/LayerSwitchCard'; +import { ProtectionLockedCard } from '../../components/blocker/ProtectionLockedCard'; +import { CooldownBanner } from '../../components/blocker/CooldownBanner'; +import { DomainGrid } from '../../components/blocker/DomainGrid'; +import { AddDomainSheet } from '../../components/blocker/AddDomainSheet'; +import { ProtectionDetailsSheet } from '../../components/blocker/ProtectionDetailsSheet'; +import { DeactivationExplainerSheet } from '../../components/blocker/DeactivationExplainerSheet'; +import { useProtectionState } from '../../hooks/useProtectionState'; +import { useCustomDomains } from '../../hooks/useCustomDomains'; +import { useBlocklistSync } from '../../hooks/useBlocklistSync'; +import { useDomainSubmissionRealtime } from '../../hooks/useDomainSubmissionRealtime'; +import { protection } from '../../lib/protection'; + +export default function BlockerScreen() { + const router = useRouter(); + const { t } = useTranslation(); + // react-native-bottom-tabs Tab-Bar ist iOS-nativ + translucent → unsere Content-View + // erstreckt sich UNTER den Tab-Bar. Ohne diese Höhe würden FAB + Bottom-Padding + // hinterm Tab-Bar verschwinden. + const tabBarHeight = useBottomTabBarHeight(); + const { + state, + loading, + cooldownRemainingFormatted, + refresh, + activateUrlFilter, + activateFamilyControls, + requestDeactivation, + cancelDeactivation, + } = useProtectionState(); + + const plan = state?.plan ?? 'free'; + const { + domains, + tier, + addDomain, + submitDomain, + refresh: refreshDomains, + } = useCustomDomains(plan); + const { sync: syncBlocklist } = useBlocklistSync(); + + // Realtime: Domain-Submission-Status (approved/rejected/in_review) live patchen. + // Bei domain_rejected wird die Row backend-seitig hard-deleted → refetch + // entfernt sie aus der Liste. Zusätzlich blocklist.bin neu syncen damit + // die lokale Hash-Liste nicht aus dem Tritt gerät. + const onDomainChange = useCallback(async () => { + await refreshDomains(); + if (urlFilterActiveRef.current) { + const sync = await syncBlocklist(); + console.log('[blocker] resync after domain change:', sync); + await refresh(); + } + }, [refreshDomains, syncBlocklist, refresh]); + useDomainSubmissionRealtime(onDomainChange, true); + + // Sheet-States + const [addSheetOpen, setAddSheetOpen] = useState(false); + const [detailsOpen, setDetailsOpen] = useState(false); + const [explainerOpen, setExplainerOpen] = useState(false); + + // Layer-Status (auf iOS): urlFilter + familyControls. + // AppDeletionLock=true bedeutet "locked in" → keine Switches mehr, nur Cooldown-Pfad. + const urlFilterActive = state?.layers.urlFilter === true; + const familyControlsActive = state?.layers.familyControls === true; + const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true; + const lockedIn = appDeletionLockActive; + + // Ref damit onDomainChange nicht neu rendert bei jedem urlFilter-Toggle + const urlFilterActiveRef = useRef(urlFilterActive); + useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]); + + // Auto-Sync wenn URL-Filter beim Page-Mount/-Resume schon aktiv ist und + // blocklist.bin leer/stale sein könnte. Dedupe via Ref damit wir nicht + // bei jedem Re-Render neu syncen. + const syncedOnceRef = useRef(false); + useEffect(() => { + if (!urlFilterActive) return; + if (syncedOnceRef.current) return; + syncedOnceRef.current = true; + syncBlocklist().then((res) => { + console.log('[blocker] auto-sync on mount:', res); + if (res.ok) refresh(); // Stats-Card neu rendern mit aktuellem Count + }); + }, [urlFilterActive, syncBlocklist, refresh]); + + // ─── Activate-Handler pro Layer ────────────────────────────────────── + + async function handleActivateUrlFilter() { + try { + const result = await activateUrlFilter(); + console.log('[blocker] activateUrlFilter:', result); + if (!result.enabled) { + Alert.alert( + t('blocker.activate_url_failed_title'), + result.error ?? t('blocker.activate_url_failed_msg'), + [ + { text: t('common.ok') }, + { text: t('blocker.activate_settings_btn'), onPress: () => protection.openSystemSettings() }, + ], + ); + } else { + // Filter ist aktiv aber blocklist.bin ist initial leer — sofort syncen! + // Sonst zeigt iOS "Läuft" aber blockt nichts. + const sync = await syncBlocklist(); + console.log('[blocker] post-activate sync:', sync); + if (sync.ok) { + // Stats-Card neu rendern mit dem frisch geschriebenen Count + await refresh(); + } else { + Alert.alert( + t('blocker.sync_list_failed_title'), + sync.error ?? t('blocker.sync_list_failed_msg'), + ); + } + } + return result; + } catch (e: any) { + console.error('[blocker] activateUrlFilter threw:', e); + Alert.alert(t('blocker.activation_failed_title'), e?.message ?? t('common.unknown_error')); + return { enabled: false }; + } + } + + async function handleActivateFamilyControls() { + try { + const result = await activateFamilyControls(); + console.log('[blocker] activateFamilyControls:', result); + if (!result.enabled) { + Alert.alert( + t('blocker.activate_app_lock_failed_title'), + result.error ?? t('blocker.activate_app_lock_failed_msg'), + ); + } + } catch (e: any) { + console.error('[blocker] activateFamilyControls threw:', e); + Alert.alert(t('blocker.activation_failed_title'), e?.message ?? t('common.unknown_error')); + } + return { enabled: false }; + } + + // ─── 3-Click Cooldown-Trigger ──────────────────────────────────────── + + function openDetails() { + setDetailsOpen(true); + } + + function fromDetailsToExplainer() { + setDetailsOpen(false); + setTimeout(() => setExplainerOpen(true), 250); + } + + function deflectToLyra() { + setDetailsOpen(false); + setTimeout(() => router.push('/lyra' as any), 250); + } + + function deflectToBreathe() { + setExplainerOpen(false); + setTimeout(() => router.push('/urge' as any), 250); + } + + async function handleStartCooldown(reason: string) { + await requestDeactivation(reason); + } + + async function handleCancelCooldown() { + try { + await cancelDeactivation(); + } catch (e: any) { + Alert.alert(t('common.error'), e?.message ?? t('blocker.deactivation_cancel_failed')); + } + } + + const bypassAlertShownRef = useRef(false); + useEffect(() => { + if (state?.phase !== 'recoveringFromBypass') { + bypassAlertShownRef.current = false; + return; + } + if (bypassAlertShownRef.current) return; + bypassAlertShownRef.current = true; + Alert.alert( + t('blocker.activate_app_lock_failed_title'), + t('blocker.layers_app_lock_warning'), + [{ + text: t('common.ok'), + onPress: () => { + void handleActivateFamilyControls(); + }, + }], + { cancelable: false }, + ); + }, [state?.phase, t]); + + // ─── Render ────────────────────────────────────────────────────────── + + return ( + + + + {loading && !state ? ( + + + + ) : state ? ( + <> + + {/* Locked-In Mode (FC aktiv) → NUR Schutz-Status + Cooldown-Pfad */} + {lockedIn ? ( + + ) : ( + // FC nicht aktiv → User kann pro Layer einzeln togglen + + + + + )} + + {/* CooldownBanner — nur wenn Cooldown läuft */} + {state.cooldown.active && ( + + )} + + {/* Domain Grid mit inline + Button neben SlotPill */} + + setAddSheetOpen(true)} + onSubmit={submitDomain} + onUpgradePro={() => Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))} + /> + + + + {/* Sheets */} + { + setAddSheetOpen(false); + refreshDomains(); + }} + onAdd={async (d) => { + const result = await addDomain(d); + if (result.ok) { + // Neue Custom-Domain → Filter muss aktualisierten Hash-Set kriegen + const sync = await syncBlocklist(); + if (sync.ok) refresh(); // Stats-Card mit neuem Count refreshen + } + return result; + }} + /> + + setDetailsOpen(false)} + onRequestDeactivation={fromDetailsToExplainer} + onTalkToLyra={deflectToLyra} + /> + + setExplainerOpen(false)} + onBreathe={deflectToBreathe} + onStartCooldown={handleStartCooldown} + /> + + ) : null} + + ); +} diff --git a/apps/rebreak-native/app/(app)/chat.tsx b/apps/rebreak-native/app/(app)/chat.tsx new file mode 100644 index 0000000..2d713c0 --- /dev/null +++ b/apps/rebreak-native/app/(app)/chat.tsx @@ -0,0 +1,410 @@ +import { useState, useCallback } from 'react'; +import { + View, + Text, + FlatList, + Pressable, + ActivityIndicator, + Image, + RefreshControl, + StyleSheet, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { apiFetch } from '../../lib/api'; +import { AppHeader } from '../../components/AppHeader'; +import { RoomCard, type Room } from '../../components/chat/RoomCard'; +import { CreateRoomSheet } from '../../components/chat/CreateRoomSheet'; +import { colors } from '../../lib/theme'; + +type DmConversation = { + partnerId: string; + partnerName: string; + partnerAvatar: string | null; + lastMessage: string; + lastMessageAt: string; + unreadCount: number; + isOwn: boolean; +}; + +function formatTime(ts: string, justNowLabel: string): string { + const diff = Date.now() - new Date(ts).getTime(); + if (diff < 60_000) return justNowLabel; + if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`; + if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`; + return new Date(ts).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); +} + +function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }) { + const { t } = useTranslation(); + const hasUnread = conv.unreadCount > 0; + + return ( + + {({ pressed }) => ( + + + {conv.partnerAvatar ? ( + + ) : ( + + {conv.partnerName.slice(0, 2).toUpperCase()} + + )} + + + + + {conv.partnerName} + + + {formatTime(conv.lastMessageAt, t('chat.just_now'))} + + + + + {conv.isOwn ? t('chat.you') : ''} + {conv.lastMessage} + + {hasUnread && ( + + {conv.unreadCount} + + )} + + + + )} + + ); +} + +export default function ChatScreen() { + const { t } = useTranslation(); + const router = useRouter(); + const [tab, setTab] = useState<'groups' | 'direct'>('groups'); + const [createOpen, setCreateOpen] = useState(false); + + const { + data: rooms = [], + isLoading: loadingRooms, + isRefetching: refetchingRooms, + refetch: refetchRooms, + } = useQuery({ + queryKey: ['chat-rooms'], + queryFn: () => apiFetch('/api/chat/rooms'), + staleTime: 30_000, + }); + + const { + data: convs = [], + isLoading: loadingDms, + isRefetching: refetchingDms, + refetch: refetchDms, + } = useQuery({ + queryKey: ['dm-conversations'], + queryFn: () => apiFetch('/api/chat/dm-conversations'), + staleTime: 30_000, + enabled: tab === 'direct', + }); + + const unreadDms = convs.reduce((s, c) => s + (c.unreadCount ?? 0), 0); + + const openRoom = useCallback( + (roomId: string) => { + router.push(`/room?roomId=${roomId}`); + }, + [router], + ); + + const openDm = useCallback( + (userId: string) => { + router.push(`/dm?userId=${userId}`); + }, + [router], + ); + + return ( + + + + {/* Header */} + + + {t('chat.title')} + {tab === 'groups' && ( + setCreateOpen(true)} + style={({ pressed }) => [styles.createBtn, { opacity: pressed ? 0.7 : 1 }]} + > + + + )} + + + {/* Tabs */} + + setTab('groups')} + style={[styles.tab, tab === 'groups' && styles.tabActive]} + > + + + {t('chat.groups')} + + + setTab('direct')} + style={[styles.tab, tab === 'direct' && styles.tabActive]} + > + + + {t('chat.direct')} + + {unreadDms > 0 && ( + + {unreadDms} + + )} + + + + + {tab === 'groups' ? ( + item.id} + refreshControl={ + + } + ListEmptyComponent={ + loadingRooms ? ( + + + + ) : ( + + + {t('chat.no_rooms')} + + ) + } + renderItem={({ item }) => openRoom(item.id)} />} + contentContainerStyle={{ paddingBottom: 100 }} + /> + ) : ( + item.partnerId} + refreshControl={ + + } + ListEmptyComponent={ + loadingDms ? ( + + + + ) : ( + + + {t('chat.no_chats')} + + ) + } + renderItem={({ item }) => openDm(item.partnerId)} />} + contentContainerStyle={{ paddingBottom: 100 }} + /> + )} + + setCreateOpen(false)} + onCreated={(room) => { + refetchRooms(); + openRoom(room.id); + }} + /> + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#fafafa' }, + headerSection: { + paddingHorizontal: 16, + paddingTop: 14, + paddingBottom: 10, + backgroundColor: '#fff', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#e5e5e5', + }, + titleRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + title: { + fontSize: 22, + fontFamily: 'Nunito_800ExtraBold', + color: '#171717', + }, + createBtn: { + width: 34, + height: 34, + borderRadius: 17, + backgroundColor: '#007AFF', + alignItems: 'center', + justifyContent: 'center', + }, + tabs: { + flexDirection: 'row', + marginTop: 12, + backgroundColor: '#f5f5f5', + borderRadius: 10, + padding: 3, + }, + tab: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 7, + borderRadius: 8, + }, + tabActive: { + backgroundColor: '#fff', + shadowColor: '#000', + shadowOpacity: 0.05, + shadowRadius: 2, + shadowOffset: { width: 0, height: 1 }, + }, + tabText: { + fontSize: 12, + fontFamily: 'Nunito_600SemiBold', + color: '#737373', + marginLeft: 5, + }, + tabTextActive: { + color: '#007AFF', + fontFamily: 'Nunito_700Bold', + }, + tabBadge: { + minWidth: 16, + height: 16, + borderRadius: 8, + backgroundColor: '#007AFF', + paddingHorizontal: 4, + alignItems: 'center', + justifyContent: 'center', + marginLeft: 5, + }, + tabBadgeText: { + fontSize: 9, + fontFamily: 'Nunito_700Bold', + color: '#fff', + }, + emptyBox: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 60, + paddingHorizontal: 32, + }, + emptyText: { + fontSize: 13, + fontFamily: 'Nunito_600SemiBold', + color: '#a3a3a3', + marginTop: 12, + }, + // DM row styles + dmRow: { + width: '100%', + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 14, + paddingVertical: 11, + backgroundColor: '#fff', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#f5f5f5', + }, + dmAvatar: { + width: 42, + height: 42, + borderRadius: 21, + backgroundColor: '#e5e5e5', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + marginRight: 10, + }, + dmAvatarImg: { width: 42, height: 42 }, + dmAvatarInitials: { + fontSize: 13, + fontFamily: 'Nunito_700Bold', + color: '#525252', + }, + dmInfo: { flex: 1, minWidth: 0 }, + dmHeaderRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + dmName: { + fontSize: 14, + fontFamily: 'Nunito_700Bold', + color: '#171717', + flexShrink: 1, + marginRight: 6, + }, + dmTime: { fontSize: 11, fontFamily: 'Nunito_600SemiBold' }, + dmBottomRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 2, + }, + dmLast: { fontSize: 12, flex: 1 }, + unreadBadge: { + minWidth: 20, + height: 20, + paddingHorizontal: 6, + borderRadius: 10, + backgroundColor: '#007AFF', + alignItems: 'center', + justifyContent: 'center', + marginLeft: 8, + }, + unreadBadgeText: { + fontSize: 10, + fontFamily: 'Nunito_700Bold', + color: '#fff', + }, +}); diff --git a/apps/rebreak-native/app/(app)/coach.tsx b/apps/rebreak-native/app/(app)/coach.tsx new file mode 100644 index 0000000..d2b9bdb --- /dev/null +++ b/apps/rebreak-native/app/(app)/coach.tsx @@ -0,0 +1,24 @@ +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { useRouter, useFocusEffect } from 'expo-router'; + +/** + * Placeholder-Screen für den Coach-Tab. + * Beim Fokussieren wird sofort auf den Root-Stack `/coach` umgeleitet. + * - Coach-Tab bleibt in der Tab-Bar sichtbar + * - Tap auf Coach-Tab → Root-Coach öffnet sich (über den Tabs) + * - Tab-Bar ist auf der Coach-Page automatisch versteckt + */ +export default function CoachTabRedirect() { + const router = useRouter(); + + useFocusEffect( + useCallback(() => { + // replace statt push: Back von /lyra geht damit zurück zum vorherigen Tab, + // nicht zurück auf diesen Placeholder (würde sonst infinite loop bilden) + router.replace('/lyra'); + }, [router]), + ); + + return ; +} diff --git a/apps/rebreak-native/app/(app)/index.tsx b/apps/rebreak-native/app/(app)/index.tsx new file mode 100644 index 0000000..68ba2cd --- /dev/null +++ b/apps/rebreak-native/app/(app)/index.tsx @@ -0,0 +1,198 @@ +import { useCallback, useState } from 'react'; +import { + View, + Text, + ScrollView, + FlatList, + Pressable, + RefreshControl, + ActivityIndicator, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { apiFetch } from '../../lib/api'; +import { AppHeader } from '../../components/AppHeader'; +import { ComposeCard } from '../../components/ComposeCard'; +import { PostCard } from '../../components/PostCard'; +import { PostCardSkeleton } from '../../components/PostCardSkeleton'; +import { PostCommentsSheet } from '../../components/PostCommentsSheet'; +import { useCommunityStore, type CommunityCategory, type CommunityPost } from '../../stores/community'; +import { useCommunityRealtime } from '../../hooks/useCommunityRealtime'; +import { colors } from '../../lib/theme'; + +type FilterChip = { + value: CommunityCategory; + label: string; + icon: React.ComponentProps['name']; +}; + +export default function HomeScreen() { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + // Granular selectors: subscribing to the whole store (incl. optimisticLikes) + // would re-render the screen — and thus the FlatList — on every like. + const activeCategory = useCommunityStore((s) => s.activeCategory); + const setCategory = useCommunityStore((s) => s.setCategory); + + const FILTERS: FilterChip[] = [ + { value: 'all', label: t('community.cat_all'), icon: 'grid-outline' }, + { value: 'games', label: t('community.cat_games'), icon: 'trophy-outline' }, + { value: 'domain_vote', label: t('community.cat_domain'), icon: 'shield-outline' }, + { value: 'lyra', label: t('community.cat_lyra'), icon: 'sparkles-outline' }, + { value: 'rebreak', label: t('community.cat_rebreak'), icon: 'megaphone-outline' }, + ]; + const [filterOpen, setFilterOpen] = useState(false); + const [activeCommentsPostId, setActiveCommentsPostId] = useState(null); + + const { data: posts = [], isLoading, isRefetching, refetch } = useQuery({ + queryKey: ['community-posts', activeCategory], + queryFn: () => apiFetch(`/api/community/posts?category=${activeCategory}&limit=30`), + staleTime: 60_000, + }); + + // Realtime: live updates für Posts (likes/comments/neue Posts/domain-vote-Status) + useCommunityRealtime(true); + + const toggleFilter = (value: CommunityCategory) => { + const next = activeCategory === value ? 'all' : value; + setCategory(next); + setFilterOpen(false); + }; + + // Stable callbacks — passed to memoized PostCards. Inline arrows would + // bust React.memo on every parent render (which also happens on every + // realtime patch since the posts array gets a new reference). + const openComments = useCallback((postId: string) => { + setActiveCommentsPostId(postId); + }, []); + + const closeComments = useCallback(() => setActiveCommentsPostId(null), []); + + const keyExtractor = useCallback((item: CommunityPost) => item.id, []); + + const renderItem = useCallback( + ({ item }: { item: CommunityPost }) => ( + + ), + [openComments], + ); + + return ( + + + + + } + ListHeaderComponent={ + + refetch()} /> + + {/* Filter toggle */} + + setFilterOpen((o) => !o)} + className="flex-row items-center gap-1.5 self-start" + style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} + > + + {activeCategory !== 'all' && ( + + {FILTERS.find((f) => f.value === activeCategory)?.label} + + )} + + + {filterOpen && ( + + {FILTERS.map((f) => { + const active = activeCategory === f.value; + return ( + toggleFilter(f.value)} + className={`flex-row items-center gap-1.5 h-8 px-3 rounded-full border ${ + active + ? 'bg-rebreak-500 border-rebreak-500' + : 'bg-white border-neutral-200' + }`} + style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} + > + + + {f.label} + + + ); + })} + + )} + + + {/* Skeleton */} + {isLoading && ( + + + + + + )} + + } + ListEmptyComponent={ + isLoading ? null : ( + + + + {t('community.no_posts')} + + + ) + } + renderItem={renderItem} + /> + + + + ); +} diff --git a/apps/rebreak-native/app/(app)/mail.tsx b/apps/rebreak-native/app/(app)/mail.tsx new file mode 100644 index 0000000..6b5ba24 --- /dev/null +++ b/apps/rebreak-native/app/(app)/mail.tsx @@ -0,0 +1,235 @@ +import { useState } from 'react'; +import { + ActivityIndicator, + Alert, + Pressable, + ScrollView, + Text, + View, +} from 'react-native'; +import { useBottomTabBarHeight } from 'react-native-bottom-tabs'; +import { useTranslation } from 'react-i18next'; +import { Ionicons } from '@expo/vector-icons'; +import { AppHeader } from '../../components/AppHeader'; +import { MailStatsRow } from '../../components/mail/MailStatsRow'; +import { MailAccountCard } from '../../components/mail/MailAccountCard'; +import { MailEmptyState } from '../../components/mail/MailEmptyState'; +import { MailActivityLog } from '../../components/mail/MailActivityLog'; +import { ConnectMailSheet } from '../../components/mail/ConnectMailSheet'; +import { SuccessAlert } from '../../components/SuccessAlert'; +import { useMailStatus } from '../../hooks/useMailStatus'; +import { useMailDisconnect } from '../../hooks/useMailDisconnect'; +import { useUserPlan } from '../../hooks/useUserPlan'; + +export default function MailScreen() { + const { t } = useTranslation(); + const tabBarHeight = useBottomTabBarHeight(); + + const { plan } = useUserPlan(); + + const { connected, accounts, totalBlocked, maxAccounts, loading, refresh } = + useMailStatus(plan); + const { disconnect, disconnecting } = useMailDisconnect(); + + const [sheetVisible, setSheetVisible] = useState(false); + const [successVisible, setSuccessVisible] = useState(false); + const [disconnectingId, setDisconnectingId] = useState(null); + const [expandedAccount, setExpandedAccount] = useState(null); + const [activityLogExpanded, setActivityLogExpanded] = useState(false); + + const nextScanAt = + accounts + .map((a) => a.nextScanAt) + .filter((v): v is string => v !== null) + .sort()[0] ?? null; + + const limitReached = accounts.length >= maxAccounts; + + function handleAddPress() { + if (limitReached) { + Alert.alert(t('mail.upgrade_alert_title'), t('mail.upgrade_alert_desc')); + return; + } + setSheetVisible(true); + } + + async function handleDisconnect(id: string) { + setDisconnectingId(id); + await disconnect(id); + setDisconnectingId(null); + if (expandedAccount === id) setExpandedAccount(null); + refresh(); + } + + function handleConnectSuccess() { + setSuccessVisible(true); + refresh(); + } + + function toggleAccount(id: string) { + setExpandedAccount((prev) => (prev === id ? null : id)); + } + + if (loading) { + return ( + + + + + + + ); + } + + return ( + + + + + {/* Stats card */} + {accounts.length > 0 && ( + + + + )} + + {/* Section header with prominent + button */} + + + + {t('mail.section_accounts')} + + + {maxAccounts === Infinity + ? t('mail.section_accounts_count_unlimited', { used: accounts.length }) + : t('mail.section_accounts_count', { + used: accounts.length, + max: maxAccounts, + })} + + + + + + + + {t('mail.add_account')} + + + + + + {/* Account cards or empty */} + {accounts.length === 0 ? ( + + ) : ( + + {accounts.map((account, idx) => ( + + toggleAccount(account.id)} + onDisconnect={handleDisconnect} + onIntervalChanged={refresh} + onEditSuccess={handleConnectSuccess} + disconnecting={disconnectingId === account.id && disconnecting} + /> + + ))} + + )} + + {/* Activity log */} + {accounts.length > 0 && ( + + setActivityLogExpanded((p) => !p)} + /> + + )} + + + setSheetVisible(false)} + onSuccess={handleConnectSuccess} + /> + + setSuccessVisible(false)} + /> + + ); +} diff --git a/apps/rebreak-native/app/(app)/notifications.tsx b/apps/rebreak-native/app/(app)/notifications.tsx new file mode 100644 index 0000000..0a6b153 --- /dev/null +++ b/apps/rebreak-native/app/(app)/notifications.tsx @@ -0,0 +1,153 @@ +import { useEffect } from 'react'; +import { View, Text, FlatList, Pressable, RefreshControl } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useRouter } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; +import { HeroShieldCheck } from '../../components/HeroShieldCheck'; +import { useTranslation } from 'react-i18next'; +import { EmptyState } from '../../components/EmptyState'; +import { useNotificationStore, type AppNotification } from '../../stores/notifications'; +import { colors } from '../../lib/theme'; + +export default function NotificationsScreen() { + const router = useRouter(); + const { t } = useTranslation(); + const items = useNotificationStore((s) => s.items); + const loaded = useNotificationStore((s) => s.loaded); + const load = useNotificationStore((s) => s.load); + const markRead = useNotificationStore((s) => s.markRead); + const remove = useNotificationStore((s) => s.remove); + + useEffect(() => { + load(); + const tm = setTimeout(() => { + markRead(); + }, 400); + return () => clearTimeout(tm); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + router.back()} + className="w-9 h-9 rounded-full bg-neutral-100 border border-neutral-200 items-center justify-center" + > + + + + {t('notifications.title')} + + + + {items.length === 0 ? ( + + ) : ( + n.id} + contentContainerStyle={{ paddingVertical: 8 }} + refreshControl={ + + } + renderItem={({ item }) => ( + { + if (item.postId) { + router.push(`/?postId=${item.postId}` as never); + } + }} + onDelete={() => remove(item.id)} + /> + )} + /> + )} + + ); +} + +function NotificationRow({ + notif, + onPress, + onDelete, +}: { + notif: AppNotification; + onPress: () => void; + onDelete: () => void; +}) { + const isUnread = !notif.readAt; + return ( + ({ + opacity: pressed ? 0.7 : 1, + backgroundColor: isUnread ? '#fff7ed' : '#fff', + })} + > + + {/* Pure-Icon — KEIN bg-Circle (User-Wunsch: kein extra Rand). */} + + {notif.type === 'domain_accepted' ? ( + + ) : ( + + )} + + + + {notif.actorName} + + {notif.preview && ( + + {notif.preview} + + )} + + + + + + + ); +} + +function iconForType(type: string): React.ComponentProps['name'] { + if (type.includes('like')) return 'heart'; + if (type.includes('comment')) return 'chatbubble'; + if (type.includes('follow')) return 'person-add'; + if (type.includes('domain')) return 'shield-checkmark'; + return 'notifications'; +} diff --git a/apps/rebreak-native/app/(auth)/_layout.tsx b/apps/rebreak-native/app/(auth)/_layout.tsx new file mode 100644 index 0000000..c1ca28e --- /dev/null +++ b/apps/rebreak-native/app/(auth)/_layout.tsx @@ -0,0 +1,12 @@ +import { Stack } from 'expo-router'; + +export default function AuthLayout() { + return ( + + ); +} diff --git a/apps/rebreak-native/app/(auth)/confirm-otp.tsx b/apps/rebreak-native/app/(auth)/confirm-otp.tsx new file mode 100644 index 0000000..604a6ad --- /dev/null +++ b/apps/rebreak-native/app/(auth)/confirm-otp.tsx @@ -0,0 +1,207 @@ +import { useState, useEffect, useRef } from 'react'; +import { + View, + Text, + TextInput, + Pressable, + KeyboardAvoidingView, + Platform, + ActivityIndicator, +} from 'react-native'; +import { useRouter, useLocalSearchParams } from 'expo-router'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTranslation } from 'react-i18next'; +import { useAuthStore } from '../../stores/auth'; + +const OTP_LENGTH = 6; + +const OTP_INPUT_STYLE = { + fontSize: 20, + fontFamily: 'Nunito_700Bold', + color: '#0a0a0a', + textAlign: 'center' as const, + width: 48, + height: 56, + borderRadius: 12, +}; + +export default function ConfirmOtpScreen() { + const router = useRouter(); + const { t } = useTranslation(); + const params = useLocalSearchParams<{ email: string }>(); + const email = decodeURIComponent(params.email ?? ''); + + const { verifyOtp, resendConfirmation } = useAuthStore(); + + const [digits, setDigits] = useState(Array(OTP_LENGTH).fill('')); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const [loading, setLoading] = useState(false); + const [resendCooldown, setResendCooldown] = useState(0); + + const inputs = useRef>([]); + + useEffect(() => { + if (!email) { + router.replace('/signup'); + } + }, [email]); + + useEffect(() => { + if (resendCooldown <= 0) return; + const t = setInterval(() => { + setResendCooldown((c) => { + if (c <= 1) { clearInterval(t); return 0; } + return c - 1; + }); + }, 1000); + return () => clearInterval(t); + }, [resendCooldown]); + + const otp = digits.join(''); + + const handleDigit = (value: string, index: number) => { + if (value.length === OTP_LENGTH && /^\d+$/.test(value)) { + const next = value.split(''); + setDigits(next); + inputs.current[OTP_LENGTH - 1]?.focus(); + return; + } + const digit = value.replace(/\D/g, '').slice(-1); + const next = [...digits]; + next[index] = digit; + setDigits(next); + if (digit && index < OTP_LENGTH - 1) { + inputs.current[index + 1]?.focus(); + } + }; + + const handleKeyPress = (key: string, index: number) => { + if (key === 'Backspace' && !digits[index] && index > 0) { + inputs.current[index - 1]?.focus(); + } + }; + + const verify = async () => { + if (otp.length < OTP_LENGTH || loading || success) return; + setError(null); + setLoading(true); + const res = await verifyOtp(email, otp); + setLoading(false); + if (res.error) { + setError(res.error); + setDigits(Array(OTP_LENGTH).fill('')); + inputs.current[0]?.focus(); + return; + } + setSuccess(true); + router.replace('/(app)'); + }; + + const resend = async () => { + if (resendCooldown > 0) return; + setError(null); + const res = await resendConfirmation(email); + if (res.error) { + setError(res.error); + return; + } + setResendCooldown(60); + }; + + return ( + + + + {/* Header */} + + + + + + {t('auth.confirmEmailTitle')} + + + {t('auth.confirmEmailLine1')}{'\n'} + {email} + {t('auth.confirmEmailLine2') ? `\n${t('auth.confirmEmailLine2')}` : ''} + + + + {/* OTP Input */} + + {digits.map((digit, index) => ( + { inputs.current[index] = ref; }} + style={[ + OTP_INPUT_STYLE, + { + backgroundColor: '#f5f5f5', + borderWidth: 2, + borderColor: digit ? '#f59e0b' : '#e5e5e5', + }, + ]} + value={digit} + onChangeText={(val) => handleDigit(val, index)} + onKeyPress={({ nativeEvent }) => handleKeyPress(nativeEvent.key, index)} + keyboardType="number-pad" + maxLength={OTP_LENGTH} + editable={!loading && !success} + selectTextOnFocus + /> + ))} + + + {error && ( + + {error} + + )} + + {success && ( + + {t('auth.confirmed')} + + )} + + + {loading ? ( + + ) : ( + {t('auth.confirmBtn')} + )} + + + 0 || loading} + className="py-3 items-center" + > + + {t('auth.noCode')}{' '} + 0 ? 'text-neutral-400' : 'text-rebreak-500'} style={{ fontFamily: 'Nunito_600SemiBold' }}> + {resendCooldown > 0 ? t('auth.resendCooldown', { seconds: resendCooldown }) : t('auth.resend')} + + + + + router.back()} + className="py-3 items-center mt-2" + > + {t('auth.backToSignup')} + + + + + ); +} diff --git a/apps/rebreak-native/app/(auth)/confirm.tsx b/apps/rebreak-native/app/(auth)/confirm.tsx new file mode 100644 index 0000000..a0075cf --- /dev/null +++ b/apps/rebreak-native/app/(auth)/confirm.tsx @@ -0,0 +1,96 @@ +import { useEffect, useState } from 'react'; +import { View, Text, Pressable, ActivityIndicator } from 'react-native'; +import { useRouter, useLocalSearchParams } from 'expo-router'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTranslation } from 'react-i18next'; +import { supabase } from '../../lib/supabase'; +import { useAuthStore } from '../../stores/auth'; + +/** + * Deep-link landing screen for email-confirmation OAuth callbacks. + * Invoked when the system opens rebreak://auth/confirm?access_token=...&refresh_token=... + * + * Strategy: extract tokens from URL params (Expo Router surfaces them), + * call setSession, then route to app. If params are missing, poll getSession() + * (covers the case where supabase-js processed the hash before we got here). + */ +export default function ConfirmScreen() { + const router = useRouter(); + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + access_token?: string; + refresh_token?: string; + error?: string; + error_description?: string; + }>(); + + const [statusMsg, setStatusMsg] = useState(t('auth.confirming')); + const [errorMsg, setErrorMsg] = useState(null); + + useEffect(() => { + handleConfirm(); + }, []); + + async function handleConfirm() { + if (params.error) { + setErrorMsg(params.error_description ?? params.error ?? t('auth.confirmFailed')); + return; + } + + if (params.access_token && params.refresh_token) { + const { data, error } = await supabase.auth.setSession({ + access_token: params.access_token, + refresh_token: params.refresh_token, + }); + if (error) { + setErrorMsg(error.message); + return; + } + useAuthStore.setState({ session: data.session, user: data.session?.user ?? null }); + setStatusMsg(t('auth.confirmSuccess')); + router.replace('/(app)'); + return; + } + + // Fallback: poll up to 20s for a session (supabase-js may have processed hash already) + const maxWait = 20_000; + const interval = 500; + const start = Date.now(); + + while (Date.now() - start < maxWait) { + const { data } = await supabase.auth.getSession(); + if (data.session) { + useAuthStore.setState({ session: data.session, user: data.session.user }); + setStatusMsg(t('auth.confirmSuccess')); + router.replace('/(app)'); + return; + } + await new Promise((r) => setTimeout(r, interval)); + } + + setErrorMsg(t('auth.confirmTimeout')); + } + + return ( + + + {!errorMsg ? ( + <> + + {statusMsg} + + ) : ( + <> + {errorMsg} + router.replace('/signin')} + className="bg-neutral-100 border border-neutral-200 px-6 py-3 rounded-xl" + > + {t('auth.backToLoginPlain')} + + + )} + + + ); +} diff --git a/apps/rebreak-native/app/(auth)/device-limit.tsx b/apps/rebreak-native/app/(auth)/device-limit.tsx new file mode 100644 index 0000000..7c91a5b --- /dev/null +++ b/apps/rebreak-native/app/(auth)/device-limit.tsx @@ -0,0 +1,44 @@ +import { View, Text, Pressable } from 'react-native'; +import { useRouter } from 'expo-router'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTranslation } from 'react-i18next'; + +/** + * Shown when the backend returns a device-limit error (HTTP 403 with device_limit_reached). + * Phase 2: static info + redirect to sign-in. + * Phase 4+: integrate with device-management store to show active devices + revoke option. + */ +export default function DeviceLimitScreen() { + const router = useRouter(); + const { t } = useTranslation(); + + return ( + + + 📱 + + {t('auth.deviceLimitTitle')} + + + {t('auth.deviceLimitDesc')} + + + {/* TODO Phase 4: device management — list active devices + revoke button */} + + router.replace('/signin')} + className="bg-rebreak-500 px-8 py-4 rounded-xl active:opacity-80" + > + {t('auth.toLogin')} + + + router.push('/signin')} + className="py-3 mt-2" + > + {t('auth.deviceLimitUpgrade')} + + + + ); +} diff --git a/apps/rebreak-native/app/(auth)/forgot-password.tsx b/apps/rebreak-native/app/(auth)/forgot-password.tsx new file mode 100644 index 0000000..866be24 --- /dev/null +++ b/apps/rebreak-native/app/(auth)/forgot-password.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react'; +import { + View, + Text, + TextInput, + Pressable, + KeyboardAvoidingView, + Platform, + ActivityIndicator, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTranslation } from 'react-i18next'; +import { useAuthStore } from '../../stores/auth'; + +const INPUT_STYLE = { + fontSize: 16, + lineHeight: 22, + paddingVertical: 14, + paddingHorizontal: 16, + color: '#0a0a0a', + fontFamily: 'Nunito_400Regular', +} as const; + +export default function ForgotPasswordScreen() { + const router = useRouter(); + const { t } = useTranslation(); + const { resetPasswordForEmail } = useAuthStore(); + + const [email, setEmail] = useState(''); + const [loading, setLoading] = useState(false); + const [sent, setSent] = useState(false); + const [error, setError] = useState(null); + + const onSubmit = async () => { + if (!email.trim()) return; + setError(null); + setLoading(true); + const res = await resetPasswordForEmail(email.trim()); + setLoading(false); + if (res.error) { + setError(res.error); + return; + } + setSent(true); + }; + + return ( + + + + {t('auth.resetPasswordTitle')} + + {t('auth.resetPasswordSubtitle')} + + + {!sent ? ( + <> + + + {error && ( + + {error} + + )} + + + {loading ? ( + + ) : ( + {t('auth.resetPasswordSend')} + )} + + + ) : ( + + {t('auth.resetPasswordSent')} + + {t('auth.resetPasswordSentDescPrefix')}{email}{t('auth.resetPasswordSentDescSuffix')} + + + )} + + router.back()} + className="py-4 items-center mt-2" + > + {t('auth.backToLogin')} + + + + + ); +} diff --git a/apps/rebreak-native/app/(auth)/signin.tsx b/apps/rebreak-native/app/(auth)/signin.tsx new file mode 100644 index 0000000..5534de6 --- /dev/null +++ b/apps/rebreak-native/app/(auth)/signin.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { + View, + Text, + TextInput, + Pressable, + KeyboardAvoidingView, + Platform, + ScrollView, + ActivityIndicator, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Svg, { Path } from 'react-native-svg'; +import { useTranslation } from 'react-i18next'; +import { useAuthStore } from '../../stores/auth'; + +function GoogleIcon() { + return ( + + + + + + + ); +} + +function AppleIcon() { + return ( + + + + ); +} + +type OAuthProvider = 'google' | 'apple' | null; + +const INPUT_STYLE = { + fontSize: 16, + lineHeight: 22, + paddingVertical: 14, + paddingHorizontal: 16, + color: '#0a0a0a', + fontFamily: 'Nunito_400Regular', +} as const; + +export default function SignInScreen() { + const router = useRouter(); + const { t } = useTranslation(); + const { signInWithPassword, signInWithOAuth } = useAuthStore(); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [oauthLoading, setOauthLoading] = useState(null); + + const onSubmit = async () => { + if (!email.trim() || !password) return; + setError(null); + setSubmitting(true); + const res = await signInWithPassword(email.trim(), password); + setSubmitting(false); + if (res.error) { + setError(res.error); + return; + } + router.replace('/(app)'); + }; + + const onOAuth = async (provider: 'google' | 'apple') => { + setError(null); + setOauthLoading(provider); + const res = await signInWithOAuth(provider); + setOauthLoading(null); + if (res.error) { + setError(res.error); + return; + } + router.replace('/(app)'); + }; + + const isLoading = submitting || oauthLoading !== null; + + return ( + + + + {t('auth.welcomeBack')} + + {t('auth.signinSubtitle')} + + + {/* OAuth Buttons */} + onOAuth('google')} + disabled={isLoading} + className="flex-row items-center justify-center gap-3 bg-white border border-neutral-200 rounded-xl mb-3 active:opacity-80 disabled:opacity-40" + style={{ paddingVertical: 14 }} + > + {oauthLoading === 'google' ? ( + + ) : ( + + )} + {t('auth.googleSignin')} + + + onOAuth('apple')} + disabled={isLoading} + className="flex-row items-center justify-center gap-3 bg-neutral-900 rounded-xl mb-6 active:opacity-80 disabled:opacity-40" + style={{ paddingVertical: 14 }} + > + {oauthLoading === 'apple' ? ( + + ) : ( + + )} + {t('auth.appleSignin')} + + + {/* Divider */} + + + {t('auth.orWithEmail')} + + + + {/* Email + Password */} + + + + + router.push('/forgot-password')} + className="self-end py-2 mb-4" + > + {t('auth.forgotPassword')} + + + {error && ( + {error} + )} + + + {submitting ? ( + + ) : ( + {t('auth.signin')} + )} + + + router.push('/signup')} + className="py-4 items-center mt-2" + > + + {t('auth.noAccount')}{' '} + {t('auth.signup')} + + + + + + ); +} diff --git a/apps/rebreak-native/app/(auth)/signup.tsx b/apps/rebreak-native/app/(auth)/signup.tsx new file mode 100644 index 0000000..4b80b64 --- /dev/null +++ b/apps/rebreak-native/app/(auth)/signup.tsx @@ -0,0 +1,301 @@ +import { useState } from 'react'; +import { + View, + Text, + TextInput, + Pressable, + KeyboardAvoidingView, + Platform, + ScrollView, + Image, + ActivityIndicator, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Svg, { Path } from 'react-native-svg'; +import { useTranslation } from 'react-i18next'; +import { useAuthStore } from '../../stores/auth'; +import { HERO_AVATARS, getAvatarUrl } from '../../lib/avatars'; + +function GoogleIcon() { + return ( + + + + + + + ); +} + +function AppleIcon() { + return ( + + + + ); +} + +type OAuthProvider = 'google' | 'apple' | null; + +const INPUT_STYLE = { + fontSize: 16, + lineHeight: 22, + paddingVertical: 14, + paddingHorizontal: 16, + color: '#0a0a0a', + fontFamily: 'Nunito_400Regular', +} as const; + +export default function SignUpScreen() { + const router = useRouter(); + const { t } = useTranslation(); + const { signUp, signInWithOAuth } = useAuthStore(); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [nickname, setNickname] = useState(''); + const [avatarId, setAvatarId] = useState('spider'); + const [termsAccepted, setTermsAccepted] = useState(false); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [oauthLoading, setOauthLoading] = useState(null); + + const isLoading = submitting || oauthLoading !== null; + + const onOAuth = async (provider: 'google' | 'apple') => { + setError(null); + setOauthLoading(provider); + const res = await signInWithOAuth(provider); + setOauthLoading(null); + if (res.error) { + setError(res.error); + return; + } + router.replace('/(app)'); + }; + + const onSubmit = async () => { + if (!email.trim() || !password || !nickname.trim()) { + setError(t('auth.fillRequired')); + return; + } + if (password.length < 8) { + setError(t('auth.passwordMin8')); + return; + } + if (!termsAccepted) { + setError(t('auth.pleaseAcceptTerms')); + return; + } + setError(null); + setSubmitting(true); + const res = await signUp(email.trim(), password, { + username: nickname.trim(), + firstName: firstName.trim() || undefined, + lastName: lastName.trim() || undefined, + avatarId, + avatarUrl: getAvatarUrl(avatarId), + }); + setSubmitting(false); + if (res.error) { + setError(res.error); + return; + } + router.push({ pathname: '/confirm-otp', params: { email: email.trim() } }); + }; + + return ( + + + + {t('auth.signupTitle')} + + {t('auth.signupSubtitle')} + + + {/* OAuth Buttons */} + onOAuth('google')} + disabled={isLoading} + className="flex-row items-center justify-center gap-3 bg-white border border-neutral-200 rounded-xl mb-3 active:opacity-80 disabled:opacity-40" + style={{ paddingVertical: 14 }} + > + {oauthLoading === 'google' ? ( + + ) : ( + + )} + {t('auth.googleSignup')} + + + onOAuth('apple')} + disabled={isLoading} + className="flex-row items-center justify-center gap-3 bg-neutral-900 rounded-xl mb-6 active:opacity-80 disabled:opacity-40" + style={{ paddingVertical: 14 }} + > + {oauthLoading === 'apple' ? ( + + ) : ( + + )} + {t('auth.appleSignup')} + + + {/* Divider */} + + + {t('auth.orWithEmail')} + + + + {error && ( + + {error} + + )} + + + + + + + + + + + + + {/* Avatar Picker */} + {t('auth.chooseAvatar')} + + {HERO_AVATARS.map((avatar) => { + const selected = avatar.id === avatarId; + return ( + setAvatarId(avatar.id)} + disabled={isLoading} + className={`rounded-full ${selected ? 'opacity-100' : 'opacity-40'}`} + > + + + ); + })} + + + {/* Privacy notice */} + + 🛡 + + {t('auth.privacyNotice')} + + + + {/* Terms Checkbox */} + setTermsAccepted(!termsAccepted)} + disabled={isLoading} + className="flex-row items-start gap-3 mb-6" + > + + {termsAccepted && ( + + )} + + + {t('auth.acceptTerms')}{' '} + {t('auth.termsLink')} + {t('auth.acceptTermsSuffix')} + + + + + {submitting ? ( + + ) : ( + {t('auth.signupTitle')} + )} + + + router.push('/signin')} + className="py-4 items-center mt-2" + > + + {t('auth.alreadyRegistered')}{' '} + {t('auth.signin')} + + + + + + ); +} diff --git a/apps/rebreak-native/app/_layout.tsx b/apps/rebreak-native/app/_layout.tsx new file mode 100644 index 0000000..55802a1 --- /dev/null +++ b/apps/rebreak-native/app/_layout.tsx @@ -0,0 +1,124 @@ +import { useEffect } from 'react'; +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import * as Notifications from 'expo-notifications'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import * as SplashScreen from 'expo-splash-screen'; +import { + useFonts, + Nunito_400Regular, + Nunito_600SemiBold, + Nunito_700Bold, + Nunito_800ExtraBold, +} from '@expo-google-fonts/nunito'; +import { useAuthStore } from '../stores/auth'; +import { BrandSplash } from '../components/BrandSplash'; +import '../lib/i18n'; // i18next-Init via Side-Effect +import '../global.css'; + +SplashScreen.preventAutoHideAsync(); + +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowBanner: true, + shouldShowList: true, + shouldPlaySound: true, + shouldSetBadge: false, + }), +}); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 2, + staleTime: 1000 * 60, + }, + }, +}); + +function RootLayoutInner() { + const { loading, init } = useAuthStore(); + const [fontsLoaded] = useFonts({ + Nunito_400Regular, + Nunito_600SemiBold, + Nunito_700Bold, + Nunito_800ExtraBold, + }); + + useEffect(() => { + init(); + }, []); + + useEffect(() => { + if (fontsLoaded && !loading) { + SplashScreen.hideAsync(); + } + }, [fontsLoaded, loading]); + + if (!fontsLoaded || loading) { + return ; + } + + return ( + <> + + + + + + + + + + + + ); +} + +export default function RootLayout() { + return ( + + + + + + + + ); +} diff --git a/apps/rebreak-native/app/auth/callback.tsx b/apps/rebreak-native/app/auth/callback.tsx new file mode 100644 index 0000000..5f2caf0 --- /dev/null +++ b/apps/rebreak-native/app/auth/callback.tsx @@ -0,0 +1,57 @@ +// Deep-Link Bridge für OAuth-Callback (Google/Apple). +// +// Hintergrund: nach erfolgreichem OAuth-Login redirected Supabase zu +// `rebreak://auth/callback#access_token=...`. Auf iOS schluckt +// `WebBrowser.openAuthSessionAsync` den Deep-Link bevor expo-router ihn sieht. +// Auf Android öffnet das System die App via Deep-Link → expo-router routet +// `/auth/callback` BEVOR openAuthSessionAsync's Listener feuert → 404. +// +// Diese Bridge-Page fängt das ab: zeigt einen Loader, extrahiert Tokens als +// Fallback (falls openAuthSessionAsync den Hash nicht selbst parst), und +// navigiert nach (app). signin.tsx macht zusätzlich router.replace('/(app)') +// nach openAuthSessionAsync resolve — diese Bridge ist nur für den Android- +// 404-Flash da. +import { useEffect } from 'react'; +import { View, ActivityIndicator } from 'react-native'; +import { useRouter, useLocalSearchParams } from 'expo-router'; +import { supabase } from '../../lib/supabase'; +import { colors } from '../../lib/theme'; + +export default function AuthCallback() { + const router = useRouter(); + const params = useLocalSearchParams<{ access_token?: string; refresh_token?: string }>(); + + useEffect(() => { + let cancelled = false; + (async () => { + const accessToken = typeof params.access_token === 'string' ? params.access_token : undefined; + const refreshToken = typeof params.refresh_token === 'string' ? params.refresh_token : undefined; + if (accessToken && refreshToken) { + try { + await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }); + } catch (err) { + console.warn('[auth-callback] setSession failed:', err); + } + } + // Kurzer Delay → onAuthStateChange propagiert die Session in den Store. + if (!cancelled) { + setTimeout(() => { + router.replace('/(app)' as never); + }, 60); + } + })(); + return () => { + cancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + + ); +} diff --git a/apps/rebreak-native/app/dm.tsx b/apps/rebreak-native/app/dm.tsx new file mode 100644 index 0000000..003e6a5 --- /dev/null +++ b/apps/rebreak-native/app/dm.tsx @@ -0,0 +1,365 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; +import { + View, + Text, + FlatList, + Pressable, + KeyboardAvoidingView, + Platform, + ActivityIndicator, + Image, + StyleSheet, +} from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useRouter, useLocalSearchParams } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { apiFetch } from '../lib/api'; +import { supabase } from '../lib/supabase'; +import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble'; +import { ChatInput, type SendPayload } from '../components/chat/ChatInput'; +import { useDmRealtime } from '../hooks/useChatRealtime'; +import { colors } from '../lib/theme'; + +type DmHistoryResponse = { + partner: { + id: string; + nickname: string; + username?: string; + avatar?: string | null; + }; + messages: Array<{ + id: string; + content: string; + createdAt: string; + isOwn: boolean; + readAt: string | null; + senderId?: string; + receiverId?: string; + likesCount?: number; + likedByMe?: boolean; + attachmentUrl?: string | null; + attachmentType?: string | null; + attachmentName?: string | null; + replyTo?: any; + }>; +}; + +const GROUP_GAP_MS = 5 * 60 * 1000; + +export default function DmScreen() { + const { t } = useTranslation(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const queryClient = useQueryClient(); + const flatRef = useRef(null); + const [myUserId, setMyUserId] = useState(undefined); + + const { userId } = useLocalSearchParams<{ userId: string }>(); + + const [messages, setMessages] = useState([]); + const [partner, setPartner] = useState(null); + const [replyTo, setReplyTo] = useState<{ id: string; nickname: string; content: string } | null>( + null, + ); + const [sending, setSending] = useState(false); + + // Lade meine User-ID + useEffect(() => { + supabase.auth.getSession().then(({ data }) => { + setMyUserId(data.session?.user.id); + }); + }, []); + + // Lade DM-History + const { isLoading } = useQuery({ + queryKey: ['dm-history', userId], + queryFn: async () => { + console.log('[dm] fetching history for partner', userId, 'me', myUserId); + try { + const data = await apiFetch(`/api/chat/dm/${userId}`); + console.log('[dm] partner:', data.partner?.nickname, 'msgs:', data.messages?.length); + setPartner(data.partner); + const msgs: ChatMsg[] = data.messages.map((m: any) => ({ + id: m.id, + userId: m.senderId ?? (m.isOwn ? myUserId ?? '' : userId), + nickname: m.isOwn ? 'Du' : data.partner?.nickname ?? '?', + avatar: m.isOwn ? null : data.partner?.avatar ?? null, + content: m.content, + replyTo: m.replyTo + ? { + id: m.replyTo.id, + userId: m.replyTo.senderId, + nickname: + m.replyTo.senderId === myUserId ? 'Du' : data.partner?.nickname ?? '?', + content: m.replyTo.content?.slice(0, 100) ?? '', + attachmentType: m.replyTo.attachmentType ?? null, + } + : null, + attachmentUrl: m.attachmentUrl ?? null, + attachmentType: m.attachmentType ?? null, + attachmentName: m.attachmentName ?? null, + likesCount: m.likesCount ?? 0, + likedByMe: m.likedByMe ?? false, + createdAt: m.createdAt, + isOwn: m.isOwn, + readAt: m.readAt, + })); + setMessages(msgs); + return data; + } catch (err: any) { + console.error('[dm] history fetch failed:', err?.message ?? err); + throw err; + } + }, + enabled: !!userId && !!myUserId, + }); + + // Realtime: neue DMs vom Partner + const onDmInsert = useCallback( + (row: any) => { + if (row.receiver_id !== myUserId) return; + setMessages((prev) => { + if (prev.some((m) => m.id === row.id)) return prev; + return [ + ...prev, + { + id: row.id, + userId: row.sender_id, + nickname: partner?.nickname ?? '?', + avatar: partner?.avatar ?? null, + content: row.content ?? '', + replyTo: null, + attachmentUrl: row.attachment_url ?? null, + attachmentType: row.attachment_type ?? null, + attachmentName: row.attachment_name ?? null, + likesCount: row.likes_count ?? 0, + likedByMe: false, + createdAt: row.created_at, + isOwn: false, + readAt: null, + }, + ]; + }); + }, + [myUserId, partner], + ); + useDmRealtime(userId, onDmInsert, !!myUserId); + + // Auto-Scroll bei neuen Messages + useEffect(() => { + if (messages.length > 0) { + requestAnimationFrame(() => flatRef.current?.scrollToEnd({ animated: true })); + } + }, [messages.length]); + + async function handleSend(payload: SendPayload) { + if (sending) return; + setSending(true); + try { + const newMsg = await apiFetch('/api/chat/dm', { + method: 'POST', + body: { receiverId: userId, ...payload }, + }); + setMessages((prev) => [ + ...prev, + { + id: newMsg.id, + userId: myUserId ?? '', + nickname: 'Du', + avatar: null, + content: newMsg.content, + replyTo: newMsg.replyTo + ? { + id: newMsg.replyTo.id, + userId: newMsg.replyTo.senderId, + nickname: + newMsg.replyTo.senderId === myUserId ? 'Du' : partner?.nickname ?? '?', + content: newMsg.replyTo.content?.slice(0, 100) ?? '', + attachmentType: newMsg.replyTo.attachmentType ?? null, + } + : null, + attachmentUrl: newMsg.attachmentUrl, + attachmentType: newMsg.attachmentType, + attachmentName: newMsg.attachmentName, + likesCount: newMsg.likesCount ?? 0, + likedByMe: false, + createdAt: newMsg.createdAt, + isOwn: true, + readAt: null, + }, + ]); + setReplyTo(null); + queryClient.invalidateQueries({ queryKey: ['dm-conversations'] }); + } catch (err) { + console.error('DM send failed:', err); + } finally { + setSending(false); + } + } + + async function toggleLike(msg: ChatMsg) { + try { + const { liked } = await apiFetch<{ liked: boolean }>('/api/chat/like', { + method: 'POST', + body: { messageId: msg.id, type: 'dm' }, + }); + setMessages((prev) => + prev.map((m) => + m.id === msg.id + ? { ...m, likedByMe: liked, likesCount: m.likesCount + (liked ? 1 : -1) } + : m, + ), + ); + } catch {} + } + + function startReply(msg: ChatMsg) { + setReplyTo({ + id: msg.id, + nickname: msg.nickname ?? '?', + content: msg.content?.slice(0, 100) || (msg.attachmentType === 'image' ? 'Bild' : ''), + }); + } + + function sameAuthor(a: ChatMsg | undefined, b: ChatMsg | undefined): boolean { + if (!a || !b) return false; + if (a.userId !== b.userId) return false; + return Math.abs(new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) <= GROUP_GAP_MS; + } + + return ( + + {/* Header */} + + router.back()} hitSlop={8}> + + + + + {partner?.avatar ? ( + + ) : ( + + {(partner?.nickname ?? '?').slice(0, 2).toUpperCase()} + + )} + + + {partner?.nickname ?? '…'} + + + + + + + {isLoading && messages.length === 0 ? ( + + + + ) : messages.length === 0 ? ( + + + {t('chat.no_chats')} + + ) : ( + ( + {}} + /> + )} + keyExtractor={(m) => m.id} + contentContainerStyle={{ paddingTop: 12, paddingBottom: 8 }} + showsVerticalScrollIndicator={false} + onContentSizeChange={() => flatRef.current?.scrollToEnd({ animated: false })} + /> + )} + + + setReplyTo(null)} + /> + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#fafafa' }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 12, + paddingVertical: 10, + backgroundColor: '#fff', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#e5e5e5', + }, + backBtn: { + width: 36, + height: 36, + borderRadius: 12, + backgroundColor: '#f5f5f5', + alignItems: 'center', + justifyContent: 'center', + }, + headerCenter: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginHorizontal: 8, + }, + headerAvatar: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: '#e5e5e5', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + marginRight: 8, + }, + headerAvatarImg: { width: 32, height: 32 }, + headerAvatarInitials: { + fontSize: 11, + fontFamily: 'Nunito_700Bold', + color: '#737373', + }, + headerName: { + fontSize: 15, + fontFamily: 'Nunito_700Bold', + color: '#171717', + flexShrink: 1, + }, + loadingBox: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + emptyText: { + fontSize: 13, + fontFamily: 'Nunito_600SemiBold', + color: '#a3a3a3', + marginTop: 12, + }, +}); diff --git a/apps/rebreak-native/app/index.tsx b/apps/rebreak-native/app/index.tsx new file mode 100644 index 0000000..eb3402e --- /dev/null +++ b/apps/rebreak-native/app/index.tsx @@ -0,0 +1,31 @@ +import { View, Text, Pressable } from 'react-native'; +import { useRouter } from 'expo-router'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTranslation } from 'react-i18next'; + +export default function HomeScreen() { + const router = useRouter(); + const { t } = useTranslation(); + + return ( + + + {t('landing.appName')} + + {t('landing.tagline')} + + + router.push('/signin')} + className="bg-rebreak-500 px-8 py-4 rounded-full active:opacity-80" + > + {t('landing.start')} + + + + {t('landing.version')} + + + + ); +} diff --git a/apps/rebreak-native/app/lyra.tsx b/apps/rebreak-native/app/lyra.tsx new file mode 100644 index 0000000..2af859c --- /dev/null +++ b/apps/rebreak-native/app/lyra.tsx @@ -0,0 +1,976 @@ +import { + useRef, + useState, + useEffect, + useCallback, +} from 'react'; +import { + View, + Text, + TextInput, + FlatList, + Pressable, + Platform, + Animated, + Keyboard, + KeyboardAvoidingView, + StyleSheet, + ActivityIndicator, + NativeSyntheticEvent, + NativeScrollEvent, +} from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useRouter } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; +import { Audio } from 'expo-av'; +import * as FileSystem from 'expo-file-system'; +import Constants from 'expo-constants'; +import { useTranslation } from 'react-i18next'; +import { RiveAvatar, type Emotion } from '../components/RiveAvatar'; +import { useCoachStore, type Message } from '../stores/coach'; +import { apiFetch } from '../lib/api'; +import { supabase } from '../lib/supabase'; +import { colors } from '../lib/theme'; + +const EMPATHY_RE = /schwer|rückfall|traurig|schlimm|hoffnungslos|hilf|verloren|scham|schuld|verzweifelt/i; +const HAPPY_RE = /toll|super|geschafft|stark|glückwunsch|stolz|fantastisch|weiter so|prima|gut gemacht/i; + +function detectEmotion(text: string): Emotion { + if (HAPPY_RE.test(text)) return 'happy'; + if (EMPATHY_RE.test(text)) return 'empathy'; + return 'idle'; +} + +function formatDuration(s: number): string { + const m = Math.floor(s / 60); + const sec = (s % 60).toString().padStart(2, '0'); + return `${m}:${sec}`; +} + +function formatTimestamp(date: Date): string { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +// ── Loading skeleton ────────────────────────────────────────────────────────── +// Standard-Spinner — kein zweiter Rive-Avatar (der ist bereits im topBar oben). + +function LoadingPulse() { + return ( + + + + ); +} + +// ── Thinking dots ───────────────────────────────────────────────────────────── + +function ThinkingDots() { + const anim = useRef([new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)]).current; + + useEffect(() => { + const animations = anim.map((a, i) => + Animated.loop( + Animated.sequence([ + Animated.delay(i * 160), + Animated.timing(a, { toValue: 1, duration: 300, useNativeDriver: true }), + Animated.timing(a, { toValue: 0, duration: 300, useNativeDriver: true }), + ]) + ) + ); + animations.forEach((a) => a.start()); + return () => animations.forEach((a) => a.stop()); + }, []); + + return ( + + {anim.map((a, i) => ( + + ))} + + ); +} + +// ── Voice bars ──────────────────────────────────────────────────────────────── + +function VoiceBars({ count, baseColor }: { count: number; baseColor: string }) { + const anims = useRef(Array.from({ length: count }, () => new Animated.Value(4))).current; + + useEffect(() => { + const animations = anims.map((a, i) => + Animated.loop( + Animated.sequence([ + Animated.timing(a, { toValue: 4 + Math.random() * 14, duration: 450 + (i % 5) * 80, useNativeDriver: false }), + Animated.timing(a, { toValue: 4, duration: 450 + (i % 5) * 80, useNativeDriver: false }), + ]) + ) + ); + animations.forEach((a) => a.start()); + return () => animations.forEach((a) => a.stop()); + }, []); + + return ( + + {anims.map((a, i) => ( + + ))} + + ); +} + +// ── Message row (Insta-DM style) ────────────────────────────────────────────── + +type MessageWithMeta = Message & { timestamp: Date }; + +function MessageRow({ + item, + t, +}: { + item: MessageWithMeta; + t: (key: string) => string; +}) { + const isUser = item.role === 'user'; + + return ( + + + + + {item.content} + + + + + {item.feedbackSaved && ( + <> + + {t('coach.feedback_saved')} + + )} + {formatTimestamp(item.timestamp)} + + + + ); +} + +// ── Main screen ─────────────────────────────────────────────────────────────── + +export default function CoachScreen() { + const { t, i18n } = useTranslation(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const flatRef = useRef(null); + + // Reaktive Slices — nur was UI-relevant ist (Re-Render bei diesen). + const messages = useCoachStore((s) => s.messages); + const thinking = useCoachStore((s) => s.thinking); + // Actions sind stable — keine Re-Renders bei Bezug. + const loadHistory = useCoachStore((s) => s.loadHistory); + const clearHistory = useCoachStore((s) => s.clearHistory); + const sendMessage = useCoachStore((s) => s.sendMessage); + const pushMessage = useCoachStore((s) => s.pushMessage); + const markFeedbackSaved = useCoachStore((s) => s.markFeedbackSaved); + const setThinking = useCoachStore((s) => s.setThinking); + const setWelcomeBackShown = useCoachStore((s) => s.setWelcomeBackShown); + + const [input, setInput] = useState(''); + const [emotion, setEmotion] = useState('idle'); + const [isSpeaking, setIsSpeaking] = useState(false); + const [isRecording, setIsRecording] = useState(false); + const [isTranscribing, setIsTranscribing] = useState(false); + const [recordingDuration, setRecordingDuration] = useState(0); + const [showScrollBtn, setShowScrollBtn] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [keyboardHeight, setKeyboardHeight] = useState(0); + + const recordingRef = useRef(null); + const soundRef = useRef(null); + const micHeld = useRef(false); + const recordingTimer = useRef | null>(null); + const typingTimer = useRef | null>(null); + const emotionTimer = useRef | null>(null); + const isNearBottomRef = useRef(true); + + // Load history + welcome-back. Beide Side-Effects sind store-cached: + // - historyLoaded → kein Re-Fetch + kein Spinner-Blink bei Tab-Wechsel + // - welcomeBackShownThisSession → keine doppelte Lyra-Begrüßung + useEffect(() => { + let cancelled = false; + + // Aktuelle Werte aus dem Store lesen (statt Closure-Stale beim ersten Render). + const snap = useCoachStore.getState(); + const needsHistory = !snap.historyLoaded; + const needsWelcomeBack = !snap.welcomeBackShownThisSession; + + if (!needsHistory && !needsWelcomeBack) { + // Coach war diese Session schon offen → instant rendern, kein Spinner. + requestAnimationFrame(() => flatRef.current?.scrollToEnd({ animated: false })); + return; + } + + async function init() { + // Spinner nur wenn wir wirklich History fetchen müssen (erster Coach-Open). + if (needsHistory) setIsLoading(true); + + if (needsHistory) { + try { + await loadHistory(); + } catch { + // non-fatal + } + } + + if (cancelled) return; + + if (needsWelcomeBack) { + try { + const res = await apiFetch<{ message?: string }>('/api/lyra/welcome-back'); + if (!cancelled && res?.message) { + pushMessage({ id: 'wb-' + Date.now(), role: 'assistant', content: res.message }); + } + } catch { + // no welcome-back — silent + } finally { + if (!cancelled) setWelcomeBackShown(true); + } + } + + if (!cancelled) { + if (needsHistory) setIsLoading(false); + requestAnimationFrame(() => flatRef.current?.scrollToEnd({ animated: false })); + } + } + + init(); + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + // iOS: Will*-Events feuern VOR der Keyboard-Animation → paddingBottom + // ändert sich synchron mit KeyboardAvoidingView. Sonst springt der + // Input erst hoch, dann nach unten ("Zucken"). + // Android: Will*-Events feuern unzuverlässig → Did* ist der stabile Pfad. + const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; + const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; + const show = Keyboard.addListener(showEvent, (e) => setKeyboardHeight(e.endCoordinates.height)); + const hide = Keyboard.addListener(hideEvent, () => setKeyboardHeight(0)); + return () => { show.remove(); hide.remove(); }; + }, []); + + // Enrich messages with timestamp + const enrichedMessages: MessageWithMeta[] = messages.map((msg) => ({ + ...msg, + timestamp: new Date(), + })); + + // Scroll to bottom when messages change (only when near bottom) + useEffect(() => { + if (messages.length === 0) return; + if (isNearBottomRef.current) { + requestAnimationFrame(() => flatRef.current?.scrollToEnd({ animated: true })); + } else { + setShowScrollBtn(true); + } + }, [messages.length, thinking]); + + function handleScroll(e: NativeSyntheticEvent) { + const { layoutMeasurement, contentOffset, contentSize } = e.nativeEvent; + const distFromBottom = contentSize.height - contentOffset.y - layoutMeasurement.height; + isNearBottomRef.current = distFromBottom < 80; + if (isNearBottomRef.current) setShowScrollBtn(false); + } + + function scrollToBottom() { + flatRef.current?.scrollToEnd({ animated: true }); + setShowScrollBtn(false); + } + + function handleInputChange(text: string) { + setInput(text); + if (text.length > 0) { + setEmotion((e) => (e === 'thinking' ? e : 'happy')); + if (typingTimer.current) clearTimeout(typingTimer.current); + typingTimer.current = setTimeout(() => { + setEmotion((e) => (e === 'happy' ? 'idle' : e)); + }, 2500); + } else { + if (typingTimer.current) clearTimeout(typingTimer.current); + setEmotion((e) => (e === 'happy' ? 'idle' : e)); + } + } + + function scheduleEmotionReset(delay = 4000) { + if (emotionTimer.current) clearTimeout(emotionTimer.current); + emotionTimer.current = setTimeout(() => setEmotion('idle'), delay); + } + + async function handleSend() { + const content = input.trim(); + if (!content || thinking) return; + + const userMsgId = Date.now().toString(); + pushMessage({ id: userMsgId, role: 'user', content }); + setInput(''); + setThinking(true); + setEmotion('thinking'); + + try { + const res = await sendMessage(content, i18n.language); + if (res.feedbackSaved) markFeedbackSaved(userMsgId); + pushMessage({ id: (Date.now() + 1).toString(), role: 'assistant', content: res.message }); + const e = detectEmotion(res.message); + setEmotion(e); + scheduleEmotionReset(e === 'empathy' ? 6000 : 4000); + } catch { + pushMessage({ id: (Date.now() + 1).toString(), role: 'assistant', content: t('coach.error'), isError: true }); + setEmotion('idle'); + } finally { + setThinking(false); + } + } + + async function handleVoiceSend(text: string) { + if (!text.trim() || thinking) return; + const userMsgId = Date.now().toString(); + pushMessage({ id: userMsgId, role: 'user', content: text }); + setThinking(true); + setEmotion('thinking'); + + try { + const res = await sendMessage(text, i18n.language); + if (res.feedbackSaved) markFeedbackSaved(userMsgId); + pushMessage({ id: (Date.now() + 1).toString(), role: 'assistant', content: res.message }); + const e = detectEmotion(res.message); + setEmotion(e); + + try { + const apiBase = Constants.expoConfig?.extra?.apiUrl as string; + const session = (await supabase.auth.getSession()).data.session; + const ttsRes = await fetch(`${apiBase}/api/coach/speak-openai`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(session?.access_token ? { Authorization: `Bearer ${session.access_token}` } : {}), + }, + body: JSON.stringify({ text: res.message, locale: i18n.language }), + }); + if (ttsRes.ok) { + // Backend streamt audio/mpeg direkt — als ArrayBuffer holen, base64-encoden, in tmp-File schreiben. + const buffer = await ttsRes.arrayBuffer(); + if (buffer.byteLength === 0) { + console.warn('[tts] empty audio buffer'); + } else { + const bytes = new Uint8Array(buffer); + // Chunked base64-Encoding (RN btoa kann mit großen Strings überlaufen) + let binary = ''; + const chunkSize = 0x8000; + for (let i = 0; i < bytes.length; i += chunkSize) { + binary += String.fromCharCode( + ...bytes.subarray(i, Math.min(i + chunkSize, bytes.length)) + ); + } + // eslint-disable-next-line no-undef + const base64 = global.btoa ? global.btoa(binary) : Buffer.from(binary, 'binary').toString('base64'); + const tmpPath = `${FileSystem.cacheDirectory}lyra-tts-${Date.now()}.mp3`; + await FileSystem.writeAsStringAsync(tmpPath, base64, { + encoding: FileSystem.EncodingType.Base64, + }); + + const { sound } = await Audio.Sound.createAsync({ uri: tmpPath }); + soundRef.current = sound; + setIsSpeaking(true); + sound.setOnPlaybackStatusUpdate((status) => { + if (status.isLoaded && status.didJustFinish) { + setIsSpeaking(false); + scheduleEmotionReset(0); + sound.unloadAsync(); + } + }); + await sound.playAsync(); + } + } else { + console.warn('[tts] backend error:', ttsRes.status, await ttsRes.text()); + } + } catch (err) { + console.warn('[tts] exception:', err); + scheduleEmotionReset(e === 'empathy' ? 6000 : 4000); + } + } catch { + pushMessage({ id: (Date.now() + 1).toString(), role: 'assistant', content: t('coach.error'), isError: true }); + setEmotion('idle'); + } finally { + setThinking(false); + } + } + + function stopSpeaking() { + soundRef.current?.stopAsync(); + soundRef.current?.unloadAsync(); + soundRef.current = null; + setIsSpeaking(false); + setEmotion('idle'); + } + + function startRecordingTimer() { + setRecordingDuration(0); + recordingTimer.current = setInterval(() => setRecordingDuration((d) => d + 1), 1000); + } + function stopRecordingTimer() { + if (recordingTimer.current) clearInterval(recordingTimer.current); + recordingTimer.current = null; + setRecordingDuration(0); + } + + async function onMicDown() { + if (thinking || isTranscribing || isRecording || micHeld.current) return; + if (isSpeaking) stopSpeaking(); + + const { status } = await Audio.requestPermissionsAsync(); + if (status !== 'granted') return; + + micHeld.current = true; + await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true }); + const rec = new Audio.Recording(); + try { + await rec.prepareToRecordAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY); + await rec.startAsync(); + recordingRef.current = rec; + setIsRecording(true); + startRecordingTimer(); + } catch { + micHeld.current = false; + await Audio.setAudioModeAsync({ allowsRecordingIOS: false }); + } + } + + async function cancelRecording() { + if (!isRecording) return; + micHeld.current = false; + stopRecordingTimer(); + setIsRecording(false); + try { + await recordingRef.current?.stopAndUnloadAsync(); + } catch { /* already stopped */ } + recordingRef.current = null; + await Audio.setAudioModeAsync({ allowsRecordingIOS: false }); + } + + async function onMicUp() { + if (!micHeld.current || !isRecording) return; + micHeld.current = false; + stopRecordingTimer(); + setIsRecording(false); + + const rec = recordingRef.current; + recordingRef.current = null; + if (!rec) return; + + try { + await rec.stopAndUnloadAsync(); + } catch { /* already stopped */ } + await Audio.setAudioModeAsync({ allowsRecordingIOS: false }); + + const uri = rec.getURI(); + console.log('[voice] URI after stop:', uri); + if (!uri) return; + + setIsTranscribing(true); + setEmotion('happy'); + + try { + // Backend erwartet base64-Audio in JSON-Body (NICHT FormData): + // { audio: base64String, mimeType: 'audio/m4a', language: 'de' } + const base64 = await FileSystem.readAsStringAsync(uri, { + encoding: FileSystem.EncodingType.Base64, + }); + + const session = (await supabase.auth.getSession()).data.session; + const apiUrl = Constants.expoConfig?.extra?.apiUrl as string; + console.log('[voice] POST', `${apiUrl}/api/coach/transcribe`, 'language:', i18n.language, 'base64-bytes:', base64.length); + + const res = await fetch(`${apiUrl}/api/coach/transcribe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(session?.access_token ? { Authorization: `Bearer ${session.access_token}` } : {}), + }, + body: JSON.stringify({ + audio: base64, + mimeType: 'audio/m4a', + language: i18n.language, + }), + }); + + const json = await res.json(); + console.log('[voice] transcribe response:', JSON.stringify(json).slice(0, 200)); + const text: string = json?.data?.text ?? json?.text ?? ''; + console.log('[voice] extracted text:', JSON.stringify(text)); + if (text.trim()) { + await handleVoiceSend(text); + } + } catch (err) { + console.warn('[voice] transcribe error:', err); + } finally { + setIsTranscribing(false); + setEmotion((e) => (e === 'happy' ? 'idle' : e)); + } + } + + const handleNewChat = useCallback(async () => { + await clearHistory(); + setEmotion('idle'); + }, [clearHistory]); + + const renderMessage = useCallback( + ({ item }: { item: MessageWithMeta }) => , + [t] + ); + + return ( + + {/* Backdrop hinter topBar — verhindert dass Chat-Texte hinter dem Avatar */} + {/* sichtbar werden ("kollidieren mit Lyra schriftzug"). */} + + + {/* Floating header — no bar, avatar + 2 icon buttons hover over chat */} + + router.replace('/(app)' as never)} hitSlop={12}> + + + + + + + + + {t('coach.lyra')} + {isSpeaking && ( + + + {t('coach.speaking')} + + + + + )} + + + + + + + + + {/* Content area */} + + {isLoading ? ( + + ) : ( + + m.id} + contentContainerStyle={[styles.listContent, { paddingTop: insets.top + 180 }]} + showsVerticalScrollIndicator={false} + onScroll={handleScroll} + scrollEventThrottle={100} + onContentSizeChange={() => { + if (isNearBottomRef.current) { + flatRef.current?.scrollToEnd({ animated: false }); + } + }} + ListFooterComponent={ + thinking ? ( + + + + + + ) : null + } + /> + + {/* Scroll-to-bottom button */} + {showScrollBtn && ( + + + + )} + + )} + + {/* Input bar */} + 0 ? 8 : Math.max(12, insets.bottom) }]}> + {isRecording ? ( + + + + + + {formatDuration(recordingDuration)} + + + + + ) : isTranscribing ? ( + + + {t('coach.transcribing')} + + ) : ( + + )} + + {!isTranscribing && ( + + + + )} + + {!isRecording && !isTranscribing && input.trim() !== '' && ( + + + + )} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#ffffff', + }, + topBar: { + // Floating-Overlay: schwebt über Chat, kein eigener Header-Block + // top wird inline gesetzt (insets.top + 6) damit Avatar UNTER der Notch sitzt + position: 'absolute', + left: 0, + right: 0, + zIndex: 10, + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'space-between', + paddingHorizontal: 12, + pointerEvents: 'box-none', + }, + topBarBackdrop: { + // Solider Hintergrund unter dem Floating-Avatar — Chat-Messages + // scrollen darunter durch, werden aber nicht mehr sichtbar mit Lyra + // Avatar/Name kollidieren. + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 9, + backgroundColor: '#ffffff', + }, + backBtn: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255,255,255,0.92)', + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 6, + elevation: 4, + }, + avatarCenter: { + flex: 1, + alignItems: 'center', + gap: 4, + }, + avatarMeta: { + alignItems: 'center', + gap: 2, + }, + avatarName: { + fontSize: 15, + fontFamily: 'Nunito_700Bold', + color: '#0a0a0a', + }, + statusLabel: { + fontSize: 11, + fontFamily: 'Nunito_400Regular', + color: colors.textMuted, + }, + speakingRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + speakingLabel: { + fontSize: 11, + fontFamily: 'Nunito_600SemiBold', + }, + stopBtn: { + width: 18, + height: 18, + borderRadius: 9, + backgroundColor: '#f5f5f5', + alignItems: 'center', + justifyContent: 'center', + }, + newChatBtn: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255,255,255,0.92)', + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 6, + elevation: 4, + }, + listContent: { + paddingHorizontal: 12, + // Reserve Platz für den Floating-Avatar oben (Avatar 112px + Name + Status + Gaps + Margin) + paddingTop: 210, + paddingBottom: 12, + }, + // Insta-DM row + msgRow: { + flexDirection: 'row', + marginBottom: 4, + alignItems: 'flex-end', + }, + msgRowUser: { + justifyContent: 'flex-end', + }, + msgRowAssistant: { + justifyContent: 'flex-start', + }, + assistantAvatarSlot: { + width: 28, + marginRight: 6, + alignItems: 'center', + justifyContent: 'flex-end', + }, + bubbleCol: { + maxWidth: '75%', + gap: 2, + }, + bubbleColUser: { + alignItems: 'flex-end', + }, + bubbleColAssistant: { + alignItems: 'flex-start', + }, + bubble: { + borderRadius: 20, + paddingHorizontal: 14, + paddingVertical: 9, + }, + bubbleUser: { + backgroundColor: '#007AFF', + borderBottomRightRadius: 4, + }, + bubbleAssistant: { + backgroundColor: '#f0f0f0', + borderBottomLeftRadius: 4, + }, + bubbleText: { + fontSize: 15, + lineHeight: 21, + fontFamily: 'Nunito_400Regular', + }, + bubbleTextUser: { + color: '#ffffff', + }, + bubbleTextAssistant: { + color: '#0a0a0a', + }, + metaRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + paddingHorizontal: 4, + marginBottom: 4, + }, + metaRowUser: { + justifyContent: 'flex-end', + }, + metaRowAssistant: { + justifyContent: 'flex-start', + }, + feedbackText: { + fontSize: 10, + color: '#16a34a', + fontFamily: 'Nunito_400Regular', + }, + timestampText: { + fontSize: 10, + color: '#a3a3a3', + fontFamily: 'Nunito_400Regular', + }, + thinkingRow: { + flexDirection: 'row', + gap: 4, + alignItems: 'center', + paddingVertical: 4, + }, + thinkingDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: '#d4d4d4', + }, + scrollDownBtn: { + position: 'absolute', + bottom: 12, + right: 16, + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: '#007AFF', + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 4, + }, + inputBar: { + flexDirection: 'row', + alignItems: 'flex-end', + gap: 8, + paddingHorizontal: 12, + paddingTop: 8, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: '#e5e5e5', + backgroundColor: 'rgba(255,255,255,0.97)', + }, + textInput: { + flex: 1, + backgroundColor: '#f5f5f5', + borderRadius: 22, + paddingVertical: 9, + paddingHorizontal: 16, + fontSize: 15, + fontFamily: 'Nunito_400Regular', + color: '#0a0a0a', + maxHeight: 120, + }, + micBtn: { + width: 38, + height: 38, + borderRadius: 19, + backgroundColor: '#f5f5f5', + alignItems: 'center', + justifyContent: 'center', + }, + micBtnActive: { + backgroundColor: '#dc2626', + transform: [{ scale: 1.1 }], + }, + micBtnDisabled: { + opacity: 0.4, + }, + sendBtn: { + width: 38, + height: 38, + borderRadius: 19, + backgroundColor: '#007AFF', + alignItems: 'center', + justifyContent: 'center', + }, + sendBtnDisabled: { + opacity: 0.4, + }, + recordingContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + gap: 8, + backgroundColor: 'rgba(220,38,38,0.08)', + borderWidth: 1, + borderColor: 'rgba(220,38,38,0.2)', + borderRadius: 22, + paddingHorizontal: 12, + paddingVertical: 8, + }, + cancelBtn: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: 'rgba(220,38,38,0.15)', + alignItems: 'center', + justifyContent: 'center', + }, + pulseDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: '#dc2626', + }, + recordingTimer: { + fontSize: 13, + fontFamily: 'Nunito_600SemiBold', + color: '#f87171', + fontVariant: ['tabular-nums'], + }, + transcribingRow: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + paddingVertical: 10, + }, + transcribingText: { + fontSize: 14, + color: '#737373', + fontFamily: 'Nunito_400Regular', + }, +}); diff --git a/apps/rebreak-native/app/room.tsx b/apps/rebreak-native/app/room.tsx new file mode 100644 index 0000000..612dc33 --- /dev/null +++ b/apps/rebreak-native/app/room.tsx @@ -0,0 +1,856 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { + View, + Text, + FlatList, + Pressable, + Image, + Modal, + TextInput, + ActivityIndicator, + KeyboardAvoidingView, + Platform, + Alert, + StyleSheet, + ScrollView, +} from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useRouter, useLocalSearchParams } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import * as ImagePicker from 'expo-image-picker'; +import * as FileSystem from 'expo-file-system'; +import { apiFetch } from '../lib/api'; +import { supabase } from '../lib/supabase'; +import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble'; +import { ChatInput, type SendPayload } from '../components/chat/ChatInput'; +import { useRoomRealtime } from '../hooks/useChatRealtime'; +import { colors } from '../lib/theme'; + +const GROUP_GAP_MS = 5 * 60 * 1000; + +type RoomDetail = { + room: { + id: string; + name: string; + description: string | null; + isPublic: boolean; + isDefault: boolean; + joinMode: 'approval' | 'invite_only' | 'open'; + avatarUrl: string | null; + inviteCode: string | null; + memberCount: number; + createdBy: string; + myRole: 'owner' | 'admin' | 'member' | null; + isMember: boolean; + }; + members: Array<{ userId: string; role: string; nickname: string; avatar: string | null }>; + messages: Array; + hasMore: boolean; +}; + +function decodeBase64(b64: string): Uint8Array { + const binary = (globalThis as any).atob + ? (globalThis as any).atob(b64) + : Buffer.from(b64, 'base64').toString('binary'); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes; +} + +export default function RoomScreen() { + const { t } = useTranslation(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const queryClient = useQueryClient(); + const flatRef = useRef(null); + const [myUserId, setMyUserId] = useState(); + + const { roomId } = useLocalSearchParams<{ roomId: string }>(); + + const [messages, setMessages] = useState([]); + const [replyTo, setReplyTo] = useState<{ id: string; nickname: string; content: string } | null>( + null, + ); + const [sending, setSending] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + const [joining, setJoining] = useState(false); + const [joinStatus, setJoinStatus] = useState<'joined' | 'pending' | null>(null); + + useEffect(() => { + supabase.auth.getSession().then(({ data }) => setMyUserId(data.session?.user.id)); + }, []); + + const { data, isLoading, refetch } = useQuery({ + queryKey: ['chat-room', roomId], + queryFn: async () => { + const d = await apiFetch(`/api/chat/rooms/${roomId}`); + const msgs: ChatMsg[] = d.messages.map((m: any) => ({ + id: m.id, + userId: m.userId, + nickname: m.nickname, + avatar: m.avatar, + content: m.content, + replyTo: m.replyTo + ? { + id: m.replyTo.id, + userId: m.replyTo.userId, + nickname: m.replyTo.nickname, + content: m.replyTo.content, + attachmentType: m.replyTo.attachmentType ?? null, + } + : null, + attachmentUrl: m.attachmentUrl, + attachmentType: m.attachmentType, + attachmentName: m.attachmentName, + likesCount: m.likesCount ?? 0, + likedByMe: false, + createdAt: m.createdAt, + isOwn: m.isOwn, + })); + setMessages(msgs); + return d; + }, + enabled: !!roomId, + }); + + const room = data?.room; + const members = data?.members ?? []; + const isAdmin = room?.myRole === 'owner' || room?.myRole === 'admin'; + + // Realtime: neue Messages anderer User + const onRoomInsert = useCallback( + (row: any) => { + const sender = members.find((m) => m.userId === row.user_id); + setMessages((prev) => { + if (prev.some((m) => m.id === row.id)) return prev; + return [ + ...prev, + { + id: row.id, + userId: row.user_id, + nickname: sender?.nickname ?? 'Anonym', + avatar: sender?.avatar ?? null, + content: row.content ?? '', + replyTo: null, + attachmentUrl: row.attachment_url ?? null, + attachmentType: row.attachment_type ?? null, + attachmentName: row.attachment_name ?? null, + likesCount: 0, + likedByMe: false, + createdAt: row.created_at, + isOwn: false, + }, + ]; + }); + }, + [members], + ); + useRoomRealtime(roomId, myUserId, onRoomInsert, !!myUserId && !!room?.isMember); + + useEffect(() => { + if (messages.length > 0) { + requestAnimationFrame(() => flatRef.current?.scrollToEnd({ animated: true })); + } + }, [messages.length]); + + async function handleSend(payload: SendPayload) { + if (sending || !room?.isMember) return; + setSending(true); + try { + const newMsg = await apiFetch(`/api/chat/rooms/${roomId}/messages`, { + method: 'POST', + body: payload, + }); + setMessages((prev) => [ + ...prev, + { + id: newMsg.id, + userId: myUserId ?? '', + nickname: 'Du', + avatar: null, + content: newMsg.content, + replyTo: newMsg.replyTo + ? { + id: newMsg.replyTo.id, + userId: newMsg.replyTo.userId, + nickname: newMsg.replyTo.nickname, + content: newMsg.replyTo.content, + attachmentType: newMsg.replyTo.attachmentType ?? null, + } + : null, + attachmentUrl: newMsg.attachmentUrl, + attachmentType: newMsg.attachmentType, + attachmentName: newMsg.attachmentName, + likesCount: 0, + likedByMe: false, + createdAt: newMsg.createdAt, + isOwn: true, + }, + ]); + setReplyTo(null); + } catch (err) { + console.error('Room send failed:', err); + } finally { + setSending(false); + } + } + + async function toggleLike(msg: ChatMsg) { + try { + const { liked } = await apiFetch<{ liked: boolean }>('/api/chat/like', { + method: 'POST', + body: { messageId: msg.id, type: 'chat' }, + }); + setMessages((prev) => + prev.map((m) => + m.id === msg.id + ? { ...m, likedByMe: liked, likesCount: m.likesCount + (liked ? 1 : -1) } + : m, + ), + ); + } catch {} + } + + function startReply(msg: ChatMsg) { + setReplyTo({ + id: msg.id, + nickname: msg.nickname ?? '?', + content: msg.content?.slice(0, 100) || (msg.attachmentType === 'image' ? 'Bild' : ''), + }); + } + + function sameAuthor(a: ChatMsg | undefined, b: ChatMsg | undefined): boolean { + if (!a || !b) return false; + if (a.userId !== b.userId) return false; + return Math.abs(new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) <= GROUP_GAP_MS; + } + + async function handleJoin() { + if (joining) return; + setJoining(true); + try { + const res = await apiFetch<{ status: 'joined' | 'pending' | 'already_member' }>( + `/api/chat/rooms/${roomId}/join`, + { method: 'POST' }, + ); + if (res.status === 'pending') { + setJoinStatus('pending'); + } else { + setJoinStatus('joined'); + await refetch(); + queryClient.invalidateQueries({ queryKey: ['chat-rooms'] }); + } + } catch (err: any) { + Alert.alert('Fehler', err?.message ?? 'Beitritt fehlgeschlagen'); + } finally { + setJoining(false); + } + } + + async function handleAvatarUpload() { + const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!perm.granted) return; + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ['images'], + quality: 0.8, + allowsEditing: true, + aspect: [1, 1], + }); + if (result.canceled || !result.assets[0]) return; + const asset = result.assets[0]; + try { + const ext = asset.uri.split('.').pop()?.toLowerCase() || 'jpg'; + const path = `room-avatars/${roomId}-${Date.now()}.${ext}`; + const b64 = await FileSystem.readAsStringAsync(asset.uri, { + encoding: FileSystem.EncodingType.Base64, + }); + const bytes = decodeBase64(b64); + const { error: upErr } = await supabase.storage + .from('rebreak-public') + .upload(path, bytes, { + contentType: asset.mimeType ?? `image/${ext}`, + upsert: true, + }); + if (upErr) throw upErr; + const { data: pub } = supabase.storage.from('rebreak-public').getPublicUrl(path); + await apiFetch(`/api/chat/rooms/${roomId}`, { + method: 'PATCH', + body: { avatarUrl: pub.publicUrl }, + }); + await refetch(); + queryClient.invalidateQueries({ queryKey: ['chat-rooms'] }); + } catch (err: any) { + Alert.alert('Fehler', err?.message ?? t('chat.upload_failed')); + } + } + + const initials = (room?.name ?? '?') + .split(' ') + .slice(0, 2) + .map((w) => w[0]?.toUpperCase() ?? '') + .join(''); + + return ( + + {/* Header */} + + router.back()} hitSlop={8}> + + + + + {room?.avatarUrl ? ( + + ) : ( + {initials} + )} + + + + {room?.name ?? '…'} + + {room && ( + + {t('chat.member_count', { n: room.memberCount })} + + )} + + + setSettingsOpen(true)} hitSlop={8}> + + + + + {isLoading || !room ? ( + + + + ) : !room.isMember ? ( + + + {room.name} + {room.description && {room.description}} + {t('chat.join_required')} + {joinStatus === 'pending' ? ( + + + {t('chat.join_pending')} + + ) : ( + [ + styles.joinBtn, + { opacity: pressed || joining ? 0.7 : 1 }, + ]} + > + {joining ? ( + + ) : ( + {t('chat.join')} + )} + + )} + + ) : ( + + ( + {}} + /> + )} + keyExtractor={(m) => m.id} + contentContainerStyle={{ paddingTop: 12, paddingBottom: 8 }} + showsVerticalScrollIndicator={false} + onContentSizeChange={() => flatRef.current?.scrollToEnd({ animated: false })} + /> + + setReplyTo(null)} + /> + + + )} + + {/* Settings Modal */} + setSettingsOpen(false)} + room={room} + members={members} + isAdmin={isAdmin} + onAvatarChange={handleAvatarUpload} + onRefetch={refetch} + roomId={roomId} + /> + + ); +} + +// ----- Settings Modal ----- + +function RoomSettingsModal({ + visible, + onClose, + room, + members, + isAdmin, + onAvatarChange, + onRefetch, + roomId, +}: { + visible: boolean; + onClose: () => void; + room: RoomDetail['room'] | undefined; + members: RoomDetail['members']; + isAdmin: boolean; + onAvatarChange: () => void; + onRefetch: () => void; + roomId: string; +}) { + const { t } = useTranslation(); + const [pendingRequests, setPendingRequests] = useState([]); + const [loadingReqs, setLoadingReqs] = useState(false); + + useEffect(() => { + if (!visible || !isAdmin) return; + setLoadingReqs(true); + apiFetch<{ requests: any[] }>(`/api/chat/rooms/${roomId}`, { + method: 'PATCH', + body: { action: 'list_requests' }, + }) + .then((d: any) => setPendingRequests(d?.requests ?? d ?? [])) + .catch(() => setPendingRequests([])) + .finally(() => setLoadingReqs(false)); + }, [visible, isAdmin, roomId]); + + async function handleRequest(userId: string, action: 'approve' | 'reject') { + try { + await apiFetch(`/api/chat/rooms/${roomId}`, { + method: 'PATCH', + body: { action, userId }, + }); + setPendingRequests((prev) => prev.filter((r) => r.userId !== userId)); + onRefetch(); + } catch (err: any) { + Alert.alert('Fehler', err?.message ?? 'Aktion fehlgeschlagen'); + } + } + + async function handlePromote(userId: string) { + try { + await apiFetch(`/api/chat/rooms/${roomId}`, { + method: 'PATCH', + body: { action: 'promote_admin', userId }, + }); + onRefetch(); + } catch (err: any) { + Alert.alert('Fehler', err?.message ?? 'Aktion fehlgeschlagen'); + } + } + + async function handleBan(userId: string) { + Alert.alert('Bannen?', 'User wird aus dem Raum entfernt.', [ + { text: 'Abbrechen', style: 'cancel' }, + { + text: 'Bannen', + style: 'destructive', + onPress: async () => { + try { + await apiFetch(`/api/chat/rooms/${roomId}`, { + method: 'PATCH', + body: { action: 'ban', userId }, + }); + onRefetch(); + } catch (err: any) { + Alert.alert('Fehler', err?.message ?? 'Aktion fehlgeschlagen'); + } + }, + }, + ]); + } + + async function handleLeave() { + Alert.alert(t('chat.leave_room'), '', [ + { text: 'Abbrechen', style: 'cancel' }, + { + text: t('chat.leave_room'), + style: 'destructive', + onPress: async () => { + try { + await apiFetch(`/api/chat/rooms/${roomId}/leave`, { method: 'POST' }); + onClose(); + } catch (err: any) { + Alert.alert('Fehler', err?.message ?? 'Verlassen fehlgeschlagen'); + } + }, + }, + ]); + } + + if (!room) return null; + + return ( + + + + + + + {t('chat.settings')} + + + + + {/* Avatar + Name */} + + + {room.avatarUrl ? ( + + ) : ( + + + + )} + {isAdmin && ( + + + + )} + + {room.name} + {room.description && {room.description}} + + + {/* Pending Requests */} + {isAdmin && ( + + {t('chat.pending_request')} + {loadingReqs ? ( + + ) : pendingRequests.length === 0 ? ( + + ) : ( + pendingRequests.map((req) => ( + + + {req.nickname ?? 'Anonym'} + + handleRequest(req.userId, 'approve')} + > + + {t('chat.approve')} + + + handleRequest(req.userId, 'reject')} + > + + {t('chat.reject')} + + + + )) + )} + + )} + + {/* Members */} + + + {t('chat.members')} ({members.length}) + + {members.map((m) => ( + + + {m.avatar ? ( + + ) : ( + + {m.nickname.slice(0, 2).toUpperCase()} + + )} + + + {m.nickname} + {m.role !== 'member' && ( + {m.role} + )} + + {isAdmin && m.role === 'member' && ( + <> + handlePromote(m.userId)} + > + Admin + + handleBan(m.userId)} + > + Ban + + + )} + + ))} + + + {/* Leave */} + {!room.isDefault && ( + + + {t('chat.leave_room')} + + )} + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#fafafa' }, + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 10, + backgroundColor: '#fff', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#e5e5e5', + }, + iconBtn: { + width: 36, + height: 36, + borderRadius: 12, + backgroundColor: '#f5f5f5', + alignItems: 'center', + justifyContent: 'center', + }, + headerCenter: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + marginHorizontal: 8, + }, + headerAvatar: { + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: '#e5e5e5', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + marginRight: 8, + }, + headerAvatarImg: { width: 36, height: 36 }, + headerAvatarInitials: { + fontSize: 12, + fontFamily: 'Nunito_700Bold', + color: '#737373', + }, + headerName: { + fontSize: 15, + fontFamily: 'Nunito_700Bold', + color: '#171717', + }, + headerSub: { + fontSize: 11, + fontFamily: 'Nunito_500Medium', + color: '#737373', + marginTop: 1, + }, + loadingBox: { flex: 1, alignItems: 'center', justifyContent: 'center' }, + joinBox: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 32, + }, + joinTitle: { + fontSize: 20, + fontFamily: 'Nunito_700Bold', + color: '#171717', + marginTop: 14, + }, + joinDesc: { + fontSize: 13, + fontFamily: 'Nunito_500Medium', + color: '#737373', + marginTop: 6, + textAlign: 'center', + }, + joinHint: { + fontSize: 12, + fontFamily: 'Nunito_500Medium', + color: '#a3a3a3', + marginTop: 18, + textAlign: 'center', + }, + joinBtn: { + marginTop: 16, + backgroundColor: '#007AFF', + paddingHorizontal: 32, + paddingVertical: 12, + borderRadius: 12, + minWidth: 140, + alignItems: 'center', + }, + joinBtnText: { + color: '#fff', + fontSize: 14, + fontFamily: 'Nunito_700Bold', + }, + pendingBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#fef3c7', + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 20, + marginTop: 16, + }, + pendingText: { + color: '#92400e', + fontSize: 12, + fontFamily: 'Nunito_700Bold', + marginLeft: 6, + }, +}); + +const modal = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#fafafa' }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: '#fff', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#e5e5e5', + }, + title: { fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#171717' }, + section: { + backgroundColor: '#fff', + borderRadius: 12, + padding: 14, + marginBottom: 12, + }, + sectionTitle: { + fontSize: 12, + fontFamily: 'Nunito_700Bold', + color: '#737373', + textTransform: 'uppercase', + marginBottom: 10, + letterSpacing: 0.5, + }, + avatarWrap: { alignSelf: 'center', marginBottom: 10 }, + avatar: { width: 80, height: 80, borderRadius: 40 }, + avatarPlaceholder: { + backgroundColor: '#e5e5e5', + alignItems: 'center', + justifyContent: 'center', + }, + avatarEdit: { + position: 'absolute', + right: -2, + bottom: -2, + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: '#007AFF', + borderWidth: 3, + borderColor: '#fff', + alignItems: 'center', + justifyContent: 'center', + }, + roomName: { + fontSize: 17, + fontFamily: 'Nunito_700Bold', + color: '#171717', + textAlign: 'center', + }, + roomDesc: { + fontSize: 12, + fontFamily: 'Nunito_500Medium', + color: '#737373', + textAlign: 'center', + marginTop: 4, + }, + memberRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#f5f5f5', + }, + memberAvatar: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: '#e5e5e5', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + marginRight: 10, + }, + memberAvatarImg: { width: 32, height: 32 }, + memberInitials: { + fontSize: 11, + fontFamily: 'Nunito_700Bold', + color: '#737373', + }, + memberName: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: '#171717' }, + memberRole: { fontSize: 11, color: '#a3a3a3', marginTop: 1, textTransform: 'capitalize' }, + actionBtn: { + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 6, + }, + actionText: { fontSize: 11, fontFamily: 'Nunito_700Bold' }, + emptyText: { fontSize: 12, color: '#a3a3a3' }, + leaveBtn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#fee2e2', + paddingVertical: 12, + borderRadius: 10, + marginTop: 8, + }, + leaveText: { + color: '#991b1b', + fontSize: 13, + fontFamily: 'Nunito_700Bold', + marginLeft: 6, + }, +}); diff --git a/apps/rebreak-native/app/settings.tsx b/apps/rebreak-native/app/settings.tsx new file mode 100644 index 0000000..6dd9642 --- /dev/null +++ b/apps/rebreak-native/app/settings.tsx @@ -0,0 +1,222 @@ +import { ScrollView, View, Text, Pressable, Switch } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useRouter } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAuthStore } from '../stores/auth'; +import { Card } from '../components/Card'; +import { Button } from '../components/Button'; +import { colors } from '../lib/theme'; + +type SettingRow = { + label: string; + sublabel?: string; + icon: React.ComponentProps['name']; + iconColor: string; + onPress?: () => void; + right?: React.ReactNode; +}; + +export default function SettingsScreen() { + const router = useRouter(); + const { t } = useTranslation(); + const { user, signOut } = useAuthStore(); + const [notifPush, setNotifPush] = useState(true); + const [notifStreak, setNotifStreak] = useState(true); + + const email = user?.email ?? ''; + const initials = email.slice(0, 2).toUpperCase(); + + async function handleSignOut() { + await signOut(); + router.replace('/'); + } + + const accountRows: SettingRow[] = [ + { + label: t('settings.edit_profile'), + icon: 'pencil-outline', + iconColor: '#6366f1', + onPress: () => {}, + }, + { + label: t('settings.devices'), + sublabel: t('settings.devices_desc'), + icon: 'phone-portrait-outline', + iconColor: '#16a34a', + onPress: () => {}, + }, + { + label: t('settings.subscription'), + sublabel: t('settings.plan_free'), + icon: 'star-outline', + iconColor: colors.brandOrange, + onPress: () => {}, + }, + ]; + + const prefRows: SettingRow[] = [ + { + label: t('settings.push_notifications'), + icon: 'notifications-outline', + iconColor: '#2563eb', + right: ( + + ), + }, + { + label: t('settings.streak_reminders'), + icon: 'flame-outline', + iconColor: '#f97316', + right: ( + + ), + }, + { + label: t('settings.language'), + sublabel: t('settings.language_current'), + icon: 'language-outline', + iconColor: '#a78bfa', + onPress: () => {}, + }, + ]; + + return ( + + + router.replace('/(app)' as never)} + hitSlop={8} + className="w-10 h-10 items-center justify-center" + style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} + > + + + {t('settings.title')} + + + + {/* Account Card */} + + + + {initials} + + + + {email} + + + + {t('settings.plan_free')} + + + + + + + + + {/* Account Section */} + + {t('settings.account_section')} + + + {accountRows.map((row, i) => ( + ({ opacity: pressed ? 0.7 : 1 })} + > + + + + + {row.label} + {row.sublabel ? ( + {row.sublabel} + ) : null} + + {row.right ?? ( + + )} + + ))} + + + {/* Preferences Section */} + + {t('settings.prefs_section')} + + + {prefRows.map((row, i) => ( + + + + + + {row.label} + {row.sublabel ? ( + {row.sublabel} + ) : null} + + {row.right ?? ( + + + + )} + + ))} + + + {/* Danger Zone */} + + {t('settings.danger_section')} + + + + + {t('settings.delete_desc')} + + + + + + + ); +} diff --git a/apps/rebreak-native/app/urge.tsx b/apps/rebreak-native/app/urge.tsx new file mode 100644 index 0000000..0c64f88 --- /dev/null +++ b/apps/rebreak-native/app/urge.tsx @@ -0,0 +1,1261 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { + View, Text, TextInput, FlatList, Pressable, Platform, Animated, + Keyboard, KeyboardAvoidingView, StyleSheet, NativeSyntheticEvent, + NativeScrollEvent, ActivityIndicator, AppState, +} from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useRouter } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; +import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av'; +import * as FileSystem from 'expo-file-system'; +import Constants from 'expo-constants'; +import { useTranslation } from 'react-i18next'; +import { RiveAvatar } from '../components/RiveAvatar'; +import { apiFetch } from '../lib/api'; +import { supabase } from '../lib/supabase'; +import { colors } from '../lib/theme'; +import { + type GameType, GAME_META, MemoryGame, TicTacToeGame, SnakeGame, TetrisGame, +} from '../components/urge/UrgeGames'; +import { SosFeedbackModal, type SosFeedback } from '../components/urge/SosFeedbackModal'; +import { ShareSuccessDrawer } from '../components/urge/ShareSuccessDrawer'; +import { InlineRatingDrawer } from '../components/urge/InlineRatingDrawer'; +import { BreathingDrawer } from '../components/urge/Breathing'; +import GamePickerDrawer from '../components/urge/GamePickerDrawer'; +import { VoiceBars } from '../components/urge/InlineIndicators'; +import MessageRow, { GameHeader, type SosMsg } from '../components/urge/MessageRow'; +import { SOS_BOOT } from '../lib/sosPrompts'; +import { CHIP_SETS, type ChipSet } from '../lib/sosConstants'; +import { parseLyraResponse, detectEmotion, type LyraEmotion, type ChipSpec } from '../lib/lyraResponse'; +import { streamSosLyra } from '../lib/sosStream'; +import { SosTtsQueue } from '../lib/sosTtsQueue'; +import { endpointForProvider, useTtsProvider, type TtsProvider } from '../lib/ttsProvider'; +import { TtsProviderToggle } from '../components/urge/TtsProviderToggle'; + +// ── Main Screen ─────────────────────────────────────────────────────────────── + +export default function SOSScreen() { + const { t, i18n } = useTranslation(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const flatRef = useRef(null); + + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isBreathing, setIsBreathing] = useState(false); + const [input, setInput] = useState(''); + const [thinking, setThinking] = useState(false); + const [emotion, setEmotion] = useState('idle'); + const [isSpeaking, setIsSpeaking] = useState(false); + const [isTtsLoading, setIsTtsLoading] = useState(false); + const [soundEnabled, setSoundEnabled] = useState(true); + const soundEnabledRef = useRef(true); + const [chipSet, setChipSet] = useState('start'); + const [dynamicChips, setDynamicChips] = useState([]); + const [userTurnCount, setUserTurnCount] = useState(0); + + // ——— Session-Tracking für DiGA ——— + const sessionStartRef = useRef(new Date()); + const breathingCountRef = useRef(0); + const gamesPlayedRef = useRef>([]); + const wasOvercomeRef = useRef(false); + const sessionSavedRef = useRef(false); + // Hint-Trigger: User klickt "weiter reden" nach Atmen/Spiel + const requestStatsReminderRef = useRef(false); + // Hint-Trigger: nach Atmen/Spiel → Lyra soll Check-in machen + const requestCheckInRef = useRef<'after_breathing' | 'after_game' | null>(null); + // Hint-Trigger: nach Überwinden → Lyra soll zu Share/Rate motivieren + const requestOvercomeMotivationRef = useRef(false); + // Game-Score-Tracking: PB vor dem aktuellen Spielstart (für Vergleich nach Spiel-Ende) + const pbBeforeGameRef = useRef(0); + // Hint-Trigger: nach Spiel-Ende mit neuem Personal-Best → Lyra soll feiern + const requestNewPbCelebrationRef = useRef<{ game: string; oldScore: number; newScore: number } | null>(null); + + // Exit-Feedback-Modal + const [feedbackVisible, setFeedbackVisible] = useState(false); + const exitingRef = useRef(false); + // Inline-Feedback (Drawer in der Chat-Session) — wenn gegeben, kein Exit-Modal mehr + const inlineFeedbackRef = useRef(null); + const [ratingDrawerVisible, setRatingDrawerVisible] = useState(false); + // Share-Success Drawer + const [shareDrawerVisible, setShareDrawerVisible] = useState(false); + const [shareDraft, setShareDraft] = useState(''); + const [shareGenerating, setShareGenerating] = useState(false); + const sharePostedRef = useRef(false); + const [isPickingGame, setIsPickingGame] = useState(false); + const [breathingDone, setBreathingDone] = useState(false); + const [playingGame, setPlayingGame] = useState(null); + const [keyboardHeight, setKeyboardHeight] = useState(0); + const [showScrollBtn, setShowScrollBtn] = useState(false); + + const ttsRef = useRef(null); + const ttsAbortRef = useRef(null); + // Phase B: sentence-level streaming TTS — eine Queue pro sendToLyra-Call. + const ttsQueueRef = useRef(null); + const emotionTimer = useRef | null>(null); + const isNearBottomRef = useRef(true); + + useEffect(() => { soundEnabledRef.current = soundEnabled; }, [soundEnabled]); + + // Aktueller TTS-Provider — Ref damit async-Code (sendToLyra) den frischen Wert + // sieht ohne stale-closure aus dem ursprünglichen Render. + const [ttsProvider] = useTtsProvider(); + const ttsProviderRef = useRef(ttsProvider); + useEffect(() => { ttsProviderRef.current = ttsProvider; }, [ttsProvider]); + + // Audio-Mode: bei SOS-Mount Audio-Session konfigurieren. + // - playsInSilentModeIOS: Lyra spricht auch wenn iPhone auf "stumm" + // - shouldDuckAndroid: andere Audio-Quellen (Spotify) leiser machen wenn Lyra spricht + // - staysActiveInBackground: false → OS pausiert TTS automatisch wenn App im Background + // Plus warm-up: Audio.Sound.createAsync hat auf Android ~500ms Cold-Start. + // Wir feuern einen no-op-load + unload damit ExoPlayer warm ist bevor Lyra spricht. + useEffect(() => { + Audio.setAudioModeAsync({ + allowsRecordingIOS: false, + playsInSilentModeIOS: true, + staysActiveInBackground: false, + interruptionModeIOS: InterruptionModeIOS.DoNotMix, + shouldDuckAndroid: true, + interruptionModeAndroid: InterruptionModeAndroid.DoNotMix, + playThroughEarpieceAndroid: false, + }).catch((err) => { + console.warn('[sos-audio-mode] failed:', err); + }); + }, []); + + // Cleanup-Effect: bei Component-Unmount (router.back, hardware-back, replace, + // ...) ALLE TTS-Resources freigeben — damit Lyra nicht im Background weiter + // redet wenn der User die SOS-Page verlässt. User-Bug 2026-05-04 auf A50. + useEffect(() => { + return () => { + ttsAbortRef.current?.abort(); + ttsAbortRef.current = null; + if (ttsRef.current) { + const s = ttsRef.current; + ttsRef.current = null; + s.stopAsync().catch(() => {}); + s.unloadAsync().catch(() => {}); + } + ttsQueueRef.current?.abort(); + ttsQueueRef.current = null; + if (emotionTimer.current) { + clearTimeout(emotionTimer.current); + emotionTimer.current = null; + } + }; + }, []); + + // AppState: wenn App in den Background geht (Home-Button, App-Switcher, + // Lock-Screen) → TTS stoppen. Sonst spricht Lyra durch's Lock-Screen weiter. + useEffect(() => { + const sub = AppState.addEventListener('change', (state) => { + if (state === 'background' || state === 'inactive') { + ttsAbortRef.current?.abort(); + ttsAbortRef.current = null; + if (ttsRef.current) { + const s = ttsRef.current; + ttsRef.current = null; + s.stopAsync().catch(() => {}); + s.unloadAsync().catch(() => {}); + } + ttsQueueRef.current?.abort(); + ttsQueueRef.current = null; + setIsSpeaking(false); + setIsTtsLoading(false); + } + }); + return () => sub.remove(); + }, []); + + useEffect(() => { + const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; + const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; + const show = Keyboard.addListener(showEvent, (e) => setKeyboardHeight(e.endCoordinates.height)); + const hide = Keyboard.addListener(hideEvent, () => setKeyboardHeight(0)); + return () => { show.remove(); hide.remove(); }; + }, []); + + useEffect(() => { + if (messages.length === 0) return; + if (isNearBottomRef.current) requestAnimationFrame(() => flatRef.current?.scrollToEnd({ animated: true })); + else setShowScrollBtn(true); + }, [messages.length, thinking, isBreathing]); + + function handleScroll(e: NativeSyntheticEvent) { + const { layoutMeasurement, contentOffset, contentSize } = e.nativeEvent; + const dist = contentSize.height - contentOffset.y - layoutMeasurement.height; + isNearBottomRef.current = dist < 80; + if (isNearBottomRef.current) setShowScrollBtn(false); + } + + function addMessage(msg: SosMsg) { setMessages((prev) => [...prev, msg]); } + function scheduleEmotionReset(delay = 4000) { + if (emotionTimer.current) clearTimeout(emotionTimer.current); + emotionTimer.current = setTimeout(() => setEmotion('idle'), delay); + } + + function stopSpeaking() { + ttsAbortRef.current?.abort(); + ttsAbortRef.current = null; + ttsRef.current?.stopAsync().catch(() => {}); + ttsRef.current?.unloadAsync().catch(() => {}); + ttsRef.current = null; + // Phase B: laufende Sentence-Queue auch abbrechen + ttsQueueRef.current?.abort(); + ttsQueueRef.current = null; + setIsSpeaking(false); setIsTtsLoading(false); setEmotion('idle'); + } + + // Holt Audio-MP3 von speak-openai und speichert als temporäre Datei + async function fetchTtsAudio(rawText: string): Promise<{ uri: string; controller: AbortController } | null> { + if (!soundEnabledRef.current) return null; + const text = rawText.replace(/\s+/g, ' ').trim(); + if (!text) return null; + const controller = new AbortController(); + const session = (await supabase.auth.getSession()).data.session; + if (controller.signal.aborted) return null; + const apiBase = Constants.expoConfig?.extra?.apiUrl as string; + const ttsRes = await fetch(`${apiBase}/api/coach/speak-openai`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(session?.access_token ? { Authorization: `Bearer ${session.access_token}` } : {}), + }, + body: JSON.stringify({ text, locale: i18n.language, mode: 'sos' }), + signal: controller.signal, + }); + if (!ttsRes.ok || controller.signal.aborted) return null; + const buffer = await ttsRes.arrayBuffer(); + if (controller.signal.aborted || buffer.byteLength === 0) return null; + const bytes = new Uint8Array(buffer); + const chunks: string[] = []; + const cs = 0x8000; + for (let i = 0; i < bytes.length; i += cs) + chunks.push(String.fromCharCode(...bytes.subarray(i, Math.min(i + cs, bytes.length)))); + const base64 = btoa(chunks.join('')); + const tmpPath = `${FileSystem.cacheDirectory}sos-tts-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mp3`; + await FileSystem.writeAsStringAsync(tmpPath, base64, { encoding: FileSystem.EncodingType.Base64 }); + if (controller.signal.aborted) return null; + return { uri: tmpPath, controller }; + } + + // Spielt eine fertige Audio-Datei (oder fetcht sie wenn nicht vorhanden) und wartet auf didJustFinish + async function playTtsAudio(rawText: string, prefetched: { uri: string; controller: AbortController } | null): Promise { + if (!soundEnabledRef.current) return; + // Bestehenden Audio NICHT abbrechen — wir wollen nahtlos anschließen + setIsTtsLoading(!prefetched); + let audio = prefetched; + if (!audio) { + try { audio = await fetchTtsAudio(rawText); } catch { audio = null; } + } + if (!audio) { setIsTtsLoading(false); return; } + ttsAbortRef.current = audio.controller; + try { + const { sound } = await Audio.Sound.createAsync({ uri: audio.uri }, { shouldPlay: true }); + if (audio.controller.signal.aborted) { sound.unloadAsync(); setIsTtsLoading(false); return; } + setIsTtsLoading(false); + ttsRef.current = sound; + setIsSpeaking(true); + await new Promise((resolve) => { + const aborted = () => { resolve(); }; + audio!.controller.signal.addEventListener('abort', aborted); + sound.setOnPlaybackStatusUpdate((status) => { + if (status.isLoaded && status.didJustFinish) { + setIsSpeaking(false); + scheduleEmotionReset(0); + sound.unloadAsync().catch(() => {}); + audio!.controller.signal.removeEventListener('abort', aborted); + resolve(); + } + }); + }); + } catch (err: any) { + if (err?.name === 'AbortError' || audio.controller.signal.aborted) { setIsTtsLoading(false); return; } + console.warn('[sos-tts]', err); + setIsTtsLoading(false); + setIsSpeaking(false); + } + } + + // Legacy: einmaliger TTS-Call (für nicht-Streaming Pfade z.B. opening greeting) + async function speakText(rawText: string): Promise { + if (!soundEnabledRef.current) return; + ttsAbortRef.current?.abort(); + if (ttsRef.current) { + ttsRef.current.stopAsync().catch(() => {}); + ttsRef.current.unloadAsync().catch(() => {}); + ttsRef.current = null; + setIsSpeaking(false); + } + const audio = await fetchTtsAudio(rawText).catch(() => null); + if (!audio) return; + await playTtsAudio(rawText, audio); + } + + async function sendToLyra(userText: string) { + if (thinking) return; + if (isSpeaking) stopSpeaking(); + addMessage({ id: Date.now().toString(), role: 'user', content: userText, timestamp: new Date() }); + setUserTurnCount((n) => n + 1); + setThinking(true); setEmotion('thinking'); + try { + const visibleHistory = messages.filter((m) => !m.cardType).map((m) => ({ role: m.role, content: m.content })); + + // ----- Hint-Builder ----- + const hints: string[] = []; + + // (1) Check-in nach Atmen/Spiel + const checkIn = requestCheckInRef.current; + requestCheckInRef.current = null; + if (checkIn === 'after_breathing') { + hints.push('[SYSTEM-HINT] Der User hat gerade die Atemübung beendet. Frage warm nach, wie er sich JETZT fühlt. Biete 3-4 Chips: "🫁 Nochmal atmen", "🎮 Spiel", "💭 An was anderes denken", "😄 Erzähl mir einen Witz".'); + } else if (checkIn === 'after_game') { + hints.push('[SYSTEM-HINT] Der User hat gerade das Spiel beendet. Frage warm, wie er sich JETZT fühlt. Biete 3-4 Chips: "🎮 Weiter spielen", "🫁 Atemübung", "💭 An was anderes denken", "😄 Erzähl mir einen Witz".'); + } + + // (1b) Personal-Best-Celebration — wenn User gerade neuen PB gemacht hat + if (requestNewPbCelebrationRef.current) { + const { game, oldScore, newScore } = requestNewPbCelebrationRef.current; + requestNewPbCelebrationRef.current = null; + const pbHint = oldScore > 0 + ? `[SYSTEM-HINT] NEUER REKORD! Der User hat sein bisheriges Personal-Best in ${game} (${oldScore}) auf ${newScore} verbessert. Feiere das warm in 1-2 Sätzen — nenne KONKRET die Zahlen ${oldScore} → ${newScore}. Mach klar: das ist sein eigener Sieg, ein echter Schritt. Dann der normale after_game-Check-in (siehe oben).` + : `[SYSTEM-HINT] Erster Score! Der User hat seinen ALLERSTEN Score in ${game}: ${newScore}. Feiere den Erstversuch warm — der erste Score ist immer was Besonderes. Dann der normale after_game-Check-in.`; + hints.push(pbHint); + } + + // (2) Stats-Reminder bei "weiter reden" nach Aktion + if (requestStatsReminderRef.current) { + requestStatsReminderRef.current = false; + const b = breathingCountRef.current; + const g = gamesPlayedRef.current.length; + const parts: string[] = []; + if (b > 0) parts.push(`${b}x Atemübung`); + if (g > 0) parts.push(`${g}x Spiel`); + const stats = parts.join(' + ') || 'mehrere Bewältigungs-Tools'; + hints.push(`[SYSTEM-HINT] Erinnere den User WARM und KONKRET: Wir haben in dieser Session schon ${stats} gemacht. Das senkt wissenschaftlich nachweislich den Spielimpuls (z.B. Atemübungen aktivieren den Parasympathikus). Lobe ihn: er hat die Gambling-Industrie heute schon geschlagen. Dann frage, was er jetzt braucht. Chips: "❤️ Überwunden", "🫁 Nochmal atmen", "🎮 Spiel".`); + } + + // (3) Nach 3+ Turns: Service-Angebot wenn Lyra noch nicht angeboten hat + if ((userTurnCount + 1) >= 3 && breathingCountRef.current === 0 && gamesPlayedRef.current.length === 0) { + hints.push('[SYSTEM-HINT] Ich habe jetzt schon mehrere Male geantwortet. Biete mir JETZT konkret eine Atemübung ODER ein Spiel an. Chips müssen "breathing" + "game_picker" enthalten, optional "send_text:Lass uns weiter reden".'); + } + + // (4) Nach Überwinden → Lyra motiviert zu Sharing + Bewertung + if (requestOvercomeMotivationRef.current) { + requestOvercomeMotivationRef.current = false; + hints.push( + '[SYSTEM-HINT] Der User hat den Impuls ÜBERWUNDEN. Feiere ihn warm und KURZ (2-3 Sätze max). Dann motiviere ihn zu ZWEI konkreten Aktionen: ' + + '(a) Erfolg mit der Community teilen — das stärkt andere Betroffene und macht ihn stolz. ' + + '(b) Diese Session bewerten + kurze Bemerkung geben — das hilft uns Lyra zu verbessern und die App für DiGA-Zertifizierung qualitativ besser zu machen. ' + + 'WICHTIG: Liefere GENAU diese Chips: ' + + '[{"label":"✨ Erfolg teilen","action":"share_success"}, {"label":"⭐ Session bewerten","action":"rate_session"}, {"label":"✅ Fertig","action":"close"}]' + ); + } + + const hintMsgs = hints.map((h) => ({ role: 'user' as const, content: h })); + const apiMessages = [...SOS_BOOT, ...visibleHistory, { role: 'user' as const, content: userText }, ...hintMsgs]; + + // ── SSE-Streaming via react-native-sse ── + const session = (await supabase.auth.getSession()).data.session; + const apiBase = Constants.expoConfig?.extra?.apiUrl as string; + + if (!session?.access_token) throw new Error('no token'); + + const assistantId = (Date.now() + 1).toString(); + let assistantAdded = false; + const ensureBubble = (text: string) => { + if (!assistantAdded) { + assistantAdded = true; + addMessage({ id: assistantId, role: 'assistant', content: text, timestamp: new Date() }); + } else { + setMessages((prev) => prev.map((m) => (m.id === assistantId ? { ...m, content: text } : m))); + } + }; + + let visible = ''; + let parsedChips: Array<{ label: string; action: string }> = []; + let streamError: any = null; + // Hybrid-TTS v3 (Threshold 3): warte auf 3 Sätze bevor first-chunk + // ans TTS geht. Mit prompt "max 3 Sätze" sind 99% aller Antworten + // single-shot → NULL Voice-Boundary, 100% konsistente Stimme. + // Trade-off: ~1s mehr First-Audio-Latenz im 3-Satz-Fall vs. v2. + const firstChunkSentences: string[] = []; + let firstChunkConsumed = false; + let firstChunkText = ''; + + // Hybrid-TTS-Queue: erster Satz live + Rest als ein Block. Pre-fetch in + // der Queue startet sofort beim enqueue → läuft parallel zum Playback + // des ersten Satzes → null Gap zwischen Satz-1 und Rest. + ttsQueueRef.current?.abort(); + const ttsQueue = soundEnabledRef.current + ? new SosTtsQueue({ + apiBase, + accessToken: session.access_token, + locale: i18n.language, + endpoint: endpointForProvider(ttsProviderRef.current), + onStart: () => { setIsSpeaking(true); setIsTtsLoading(false); }, + onIdle: () => { setIsSpeaking(false); setIsTtsLoading(false); scheduleEmotionReset(0); }, + onError: (err, sentence) => { + console.warn('[sos-tts-queue] segment failed:', sentence.slice(0, 50), err); + }, + }) + : null; + ttsQueueRef.current = ttsQueue; + if (ttsQueue) setIsTtsLoading(true); + + await new Promise((resolve) => { + let cancel: (() => void) | undefined; + streamSosLyra({ + apiBase, + token: session.access_token, + messages: apiMessages, + locale: i18n.language, + onTextUpdate: (full) => { + visible = full; + ensureBubble(full); + }, + onChips: (chips) => { + parsedChips = chips; + }, + onSentence: (sentence) => { + if (firstChunkConsumed) return; + firstChunkSentences.push(sentence); + // Trigger erst bei 3 vollständigen Sätzen (war v2: 2). Mit Server- + // Prompt "max 3 Sätze" landen 99% aller Antworten als single-shot + // im onDone (count<3) → NULL Boundary, 100% konsistente Stimme. + // Boundary nur bei seltenen 4+-Satz-Antworten. + if (firstChunkSentences.length >= 2) { + firstChunkConsumed = true; + firstChunkText = firstChunkSentences.join(' '); + ttsQueue?.enqueue(firstChunkText); + } + }, + onDone: (full) => { + visible = full || visible; + ensureBubble(visible); + if (ttsQueue) { + if (firstChunkConsumed) { + // Rest = full minus first-chunk. Fester Match via indexOf. + const idx = full.indexOf(firstChunkText); + const rest = + idx >= 0 + ? full.slice(idx + firstChunkText.length).trim() + : full.replace(firstChunkText, '').trim(); + if (rest.length > 0) { + // Mode 'sos-continuation' → server gibt OpenAI explizite + // "no fresh-start"-Instructions → Boundary weicher. + ttsQueue.enqueue(rest, 'sos-continuation'); + } + } else { + // <2 Sätze detektiert (kurze Antwort) — kompletten Text als + // single-shot. Voice 100% konsistent, kein Boundary überhaupt. + const cleaned = (full || visible).trim(); + if (cleaned) ttsQueue.enqueue(cleaned); + } + } + resolve(); + }, + onError: (err) => { + streamError = err; + resolve(); + }, + }) + .then((c) => { + cancel = c; + }) + .catch((err) => { + streamError = err; + resolve(); + }); + }); + + // Fallback bei Stream-Fehler: alter non-streaming Endpoint + if (streamError) { + console.warn('[sos-stream] failed, fallback', streamError); + // Queue abbrechen damit nicht halb-gefüllter Stream noch Audio spielt + ttsQueueRef.current?.abort(); + ttsQueueRef.current = null; + const res = await apiFetch<{ message: string }>('/api/coach/message', { + method: 'POST', + body: { messages: apiMessages, locale: i18n.language, sosMode: true }, + }); + const parsed = parseLyraResponse(res?.message ?? ''); + visible = parsed.message; + parsedChips = parsed.chips; + ensureBubble(visible); + if (visible) speakText(visible); + } + // Wenn Stream erfolgreich war, Queue aber leer geblieben (z.B. Stream hat + // 0 Sätze geliefert, Edge-Case bei sehr kurzer LLM-Antwort) → Loading-Flag + // selbst zurücksetzen, sonst hängt der Spinner. + if (!streamError && ttsQueue && !ttsQueue.isActive()) { + setIsTtsLoading(false); + } + + const e = detectEmotion(visible); + + // Fallback-Chips wenn Lyra keine liefert + let finalChips = parsedChips; + if (finalChips.length === 0) { + if (checkIn === 'after_breathing') { + finalChips = [ + { label: '🫁 Nochmal atmen', action: 'breathing' }, + { label: '🎮 Spiel', action: 'game_picker' }, + { label: '💭 An was anderes denken', action: 'send_text:Lenk mich bitte ab — erzähl mir was Schönes.' }, + { label: '❤️ Überwunden', action: 'overcome' }, + ]; + } else if (checkIn === 'after_game') { + finalChips = [ + { label: '🎮 Weiter spielen', action: 'game_picker' }, + { label: '🫁 Atemübung', action: 'breathing' }, + { label: '😄 Erzähl mir einen Witz', action: 'send_text:Erzähl mir bitte einen kurzen Witz.' }, + { label: '❤️ Überwunden', action: 'overcome' }, + ]; + } else if ((userTurnCount + 1) >= 3) { + finalChips = [ + { label: '🫁 Atemübung', action: 'breathing' }, + { label: '🎮 Spiel', action: 'game_picker' }, + { label: '💬 Weiter reden', action: 'send_text:Lass uns weiter reden.' }, + ]; + } else { + finalChips = [ + { label: '💬 Mehr erzählen', action: 'send_text:Ich möchte mehr erzählen.' }, + { label: '🫁 Atemübung', action: 'breathing' }, + { label: '🎮 Ablenken', action: 'game_picker' }, + ]; + } + } + setDynamicChips(finalChips); + setChipSet('none'); + setEmotion(e); scheduleEmotionReset(e === 'empathy' ? 6000 : 4000); + } catch { + addMessage({ id: (Date.now() + 1).toString(), role: 'assistant', content: t('coach.error'), timestamp: new Date() }); + setEmotion('idle'); + } finally { setThinking(false); } + } + + // Opening greeting on mount — nutzt gleichen Streaming-Pfad wie sendToLyra, + // damit Hybrid-TTS auch beim Start greift (sonst dauerte es 4-5s bis Lyra + // sprach, weil non-streaming /api/coach/message + dann separater TTS-Call). + useEffect(() => { + let cancelled = false; + async function openGreeting() { + setIsLoading(true); setEmotion('thinking'); setThinking(true); + const greetingId = 'greeting-' + Date.now(); + let assistantAdded = false; + const ensureBubble = (text: string) => { + if (!assistantAdded) { + assistantAdded = true; + addMessage({ id: greetingId, role: 'assistant', content: text, timestamp: new Date() }); + } else { + setMessages((prev) => prev.map((m) => (m.id === greetingId ? { ...m, content: text } : m))); + } + }; + try { + const session = (await supabase.auth.getSession()).data.session; + if (cancelled) return; + if (!session?.access_token) throw new Error('no token'); + const apiBase = Constants.expoConfig?.extra?.apiUrl as string; + + // Hybrid-TTS-Queue, gleiches Pattern wie sendToLyra + const ttsQueue = soundEnabledRef.current + ? new SosTtsQueue({ + apiBase, + accessToken: session.access_token, + locale: i18n.language, + endpoint: endpointForProvider(ttsProviderRef.current), + onStart: () => { setIsSpeaking(true); setIsTtsLoading(false); }, + onIdle: () => { setIsSpeaking(false); setIsTtsLoading(false); scheduleEmotionReset(0); }, + onError: (err, sentence) => { + console.warn('[sos-tts-greeting] segment failed:', sentence.slice(0, 50), err); + }, + }) + : null; + ttsQueueRef.current?.abort(); + ttsQueueRef.current = ttsQueue; + if (ttsQueue) setIsTtsLoading(true); + + const greetingChunkSentences: string[] = []; + let greetingChunkConsumed = false; + let greetingChunkText = ''; + let visible = ''; + let parsedChips: Array<{ label: string; action: string }> = []; + + await new Promise((resolve) => { + streamSosLyra({ + apiBase, + token: session.access_token, + messages: SOS_BOOT, + locale: i18n.language, + onTextUpdate: (full) => { + if (cancelled) return; + visible = full; + ensureBubble(full); + }, + onChips: (chips) => { parsedChips = chips; }, + onSentence: (s) => { + if (greetingChunkConsumed) return; + greetingChunkSentences.push(s); + if (greetingChunkSentences.length >= 2) { + greetingChunkConsumed = true; + greetingChunkText = greetingChunkSentences.join(' '); + ttsQueue?.enqueue(greetingChunkText); + } + }, + onDone: (full) => { + if (cancelled) { resolve(); return; } + visible = full || visible; + ensureBubble(visible); + if (ttsQueue) { + if (greetingChunkConsumed) { + const idx = full.indexOf(greetingChunkText); + const rest = idx >= 0 + ? full.slice(idx + greetingChunkText.length).trim() + : full.replace(greetingChunkText, '').trim(); + if (rest.length > 0) ttsQueue.enqueue(rest, 'sos-continuation'); + } else { + const cleaned = (full || visible).trim(); + if (cleaned) ttsQueue.enqueue(cleaned); + } + } + resolve(); + }, + onError: (err) => { + console.warn('[sos-greeting] stream failed:', err); + resolve(); + }, + }) + .then(() => {}) + .catch(() => resolve()); + }); + + if (cancelled) return; + const e = detectEmotion(visible); + // Fallback-Chips falls Lyra im Greeting keine liefert + const greetingChips = parsedChips.length > 0 ? parsedChips : [ + { label: '😤 Wütend', action: 'feel:Ich bin gerade sehr wütend.' }, + { label: '😰 Ängstlich', action: 'feel:Ich bin ängstlich und nervos.' }, + { label: '😔 Traurig', action: 'feel:Ich bin traurig.' }, + { label: '🤔 Etwas anderes', action: 'need_help' }, + ]; + setDynamicChips(greetingChips); + setChipSet('none'); + setEmotion(e); scheduleEmotionReset(e === 'empathy' ? 6000 : 4000); + } catch (err) { + if (cancelled) return; + console.warn('[sos-greeting] failed, fallback message', err); + addMessage({ id: 'greeting-err', role: 'assistant', content: 'Ich bin für dich da. Was ist gerade los?', timestamp: new Date() }); + setEmotion('empathy'); scheduleEmotionReset(5000); + } finally { if (!cancelled) { setIsLoading(false); setThinking(false); } } + } + openGreeting(); + return () => { cancelled = true; }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function startBreathing() { + // Laufendes Lyra-TTS stoppen — sonst spricht sie in die Atemübung rein + stopSpeaking(); + setIsBreathing(true); + setChipSet('none'); + } + + async function handleBreathingDone() { + setIsBreathing(false); + setBreathingDone(true); + breathingCountRef.current += 1; + setChipSet('after_breathing'); + requestCheckInRef.current = 'after_breathing'; + await sendToLyra('Ich habe gerade die 4-7-8 Atemübung abgeschlossen.'); + } + + async function handleChip(action: string) { + if (thinking) return; + Keyboard.dismiss(); + // Clear dynamic chips on any chip action — Lyra will provide new ones in her reply + setDynamicChips([]); + if (action.startsWith('feel:')) { + const feelingText = action.replace('feel:', ''); + setChipSet('none'); + await sendToLyra(feelingText); + } else if (action === 'need_help') { + setChipSet('none'); + await sendToLyra('Ich möchte darüber reden, was gerade los ist.'); + } else if (action === 'just_play') { + setChipSet('none'); + setIsPickingGame(true); + } else if (action === 'breathing') { + startBreathing(); + } else if (action === 'game_picker') { + setChipSet('none'); + setIsPickingGame(true); + } else if (action === 'overcome') { + await finalizeOvercome(); + } else if (action === 'show_stats') { + setChipSet('none'); + try { + const res = await apiFetch<{ urges?: number; overcomeCnt?: number; streakDays?: number }>('/api/urge/stats'); + const msg = res + ? `Du hast heute ${res.overcomeCnt ?? 1} von ${res.urges ?? 1} Impulsen widerstanden. Dein Streak: ${res.streakDays ?? 1} Tag(e). Ich bin stolz auf dich! 💪` + : 'Jeder überwundene Impuls macht dich stärker. Ich bin stolz auf dich! 💪'; + addMessage({ id: 'stats-' + Date.now(), role: 'assistant', content: msg, timestamp: new Date() }); + speakText(msg); + } catch { + addMessage({ id: 'stats-err', role: 'assistant', content: 'Jeder überwundene Impuls zählt. Du machst das großartig! 💪', timestamp: new Date() }); + } + } else if (action === 'close') { + attemptExit(); + } else if (action === 'share_success') { + setChipSet('none'); + openShareDrawer(); + } else if (action === 'rate_session') { + setChipSet('none'); + setRatingDrawerVisible(true); + } else if (action.startsWith('send_text:')) { + setChipSet('none'); + const txt = action.replace('send_text:', ''); + // Wenn der User nach Atem/Spiel "weiter reden" klickt → Stats-Reminder triggern + if ((breathingCountRef.current > 0 || gamesPlayedRef.current.length > 0) && + /weiter reden|weiterreden|reden|sprechen/i.test(txt)) { + requestStatsReminderRef.current = true; + } + await sendToLyra(txt); + } + } + + async function handleGameSelect(game: GameType) { + setIsPickingGame(false); + setPlayingGame(game); + const titles: Record = { snake: 'Snake', tetris: 'Tetris', memory: 'Memory', tictactoe: 'Tic-Tac-Toe' }; + const title = titles[game]; + + // Personal-Best fetchen (parallel zum Game-Start) — Lyra mentions PB im Pep-Talk. + // Cache pbBeforeGame für späteren Vergleich nach Spiel-Ende (Rekord-Detection). + let pbContext = ''; + pbBeforeGameRef.current = 0; + try { + const pbRes = await apiFetch<{ score: number; hasRecord: boolean }>(`/api/games/highscore?gameName=${encodeURIComponent(game)}`); + const pb = pbRes?.score ?? 0; + pbBeforeGameRef.current = pb; + if (pbRes?.hasRecord && pb > 0) { + pbContext = ` Sein bisheriger Personal-Best in ${title} ist ${pb}. Erwähne diesen PB konkret im Kommentar UND glaube an ihn dass er es heute schaffen kann.`; + } else { + pbContext = ` Es ist sein ALLERSTER ${title}-Versuch. Ermutige zum Erstversuch.`; + } + } catch { + // Silent fallback — Lyra bekommt einfach keinen PB-Hint + } + + // Fire-and-forget Lyra-Kommentar (UI launches game immediately) + (async () => { + try { + const promptMsg = `[INTERN: Der User hat das Spiel "${title}" gewählt.${pbContext} Gib EINEN warmen Kommentar (1-2 Sätze) — bei Tetris nostalgisch, Snake spielerisch, Memory ermutigend, Tic-Tac-Toe entspannt. Antworte als reines JSON: {"message":"...","chips":[]}. Kein Markdown.]`; + const visibleHistory = messages.filter((m) => !m.cardType).map((m) => ({ role: m.role, content: m.content })); + const apiMessages = [...SOS_BOOT, ...visibleHistory, { role: 'user' as const, content: promptMsg }]; + const res = await apiFetch<{ message: string }>('/api/coach/message', { + method: 'POST', + body: { messages: apiMessages, locale: i18n.language, sosMode: true }, + }); + const parsed = parseLyraResponse(res?.message ?? ''); + const visible = parsed.message || `Viel Spaß mit ${title}!`; + addMessage({ id: 'gamecomment-' + Date.now(), role: 'assistant', content: visible, timestamp: new Date() }); + setEmotion('happy'); scheduleEmotionReset(4000); speakText(visible); + } catch { + const fallback = pbBeforeGameRef.current > 0 + ? `Viel Spaß mit ${title}! Dein PB ist ${pbBeforeGameRef.current} — ich glaube du schaffst heute mehr.` + : `Viel Spaß mit ${title}! Ich bin hier, wenn du fertig bist.`; + addMessage({ id: 'gamecomment-err-' + Date.now(), role: 'assistant', content: fallback, timestamp: new Date() }); + speakText(fallback); + } + })(); + } + + async function handleGameComplete(score: number) { + const gameName = GAME_META.find((g) => g.id === playingGame)?.id ?? 'das Spiel'; + const game = playingGame ?? 'unknown'; + gamesPlayedRef.current.push({ game, score, durationSec: 0 }); + + // Score persistieren — server entscheidet ob's ein neuer PB ist (idempotent) + let isNewBest = false; + try { + const res = await apiFetch<{ ok: boolean; isNewBest: boolean }>('/api/games/score', { + method: 'POST', + body: { gameName: game, score }, + }); + isNewBest = !!res?.isNewBest; + } catch (err) { + console.warn('[urge] score submit failed', err); + } + + // Wenn Rekord: Lyra im nächsten Reply feiern lassen via Hint-Trigger + const oldPb = pbBeforeGameRef.current; + if (isNewBest && score > oldPb) { + requestNewPbCelebrationRef.current = { game: gameName, oldScore: oldPb, newScore: score }; + } + + setPlayingGame(null); setChipSet('after_game'); + requestCheckInRef.current = 'after_game'; + await sendToLyra(`Ich habe ${gameName} gespielt und ${score} Punkte erreicht.`); + } + + async function handleGameAbandon() { + const game = playingGame; + if (game) gamesPlayedRef.current.push({ game, score: 0, durationSec: 0 }); + setPlayingGame(null); setChipSet('after_game'); + requestCheckInRef.current = 'after_game'; + await sendToLyra('Ich habe das Spiel abgebrochen.'); + } + + async function finalizeOvercome() { + if (thinking) return; + setChipSet('none'); + wasOvercomeRef.current = true; + try { await apiFetch('/api/urge', { method: 'POST', body: { emotion: 'other', wasOvercome: true, breathingDone } }); } catch {} + // Lyra antwortet warm + motiviert zu Share/Rate + requestOvercomeMotivationRef.current = true; + await sendToLyra('Ich habe den Spielimpuls gerade überwunden – ich bin so erleichtert und stolz auf mich!'); + addMessage({ id: 'overcome-' + Date.now(), role: 'assistant', content: '', cardType: 'overcome', timestamp: new Date() }); + // Chips garantiert anzeigen — egal was Lyra zurückgibt (Hint kann verfehlt werden) + setDynamicChips([ + { label: '✨ Erfolg teilen', action: 'share_success' }, + { label: '⭐ Session bewerten', action: 'rate_session' }, + { label: '✅ Fertig', action: 'close' }, + ]); + setChipSet('none'); + } + + // ───── Share-Success Drawer ───── + async function generateShareDraft() { + setShareGenerating(true); + try { + const b = breathingCountRef.current; + const g = gamesPlayedRef.current.length; + const stats: string[] = []; + if (b > 0) stats.push(`${b}x Atemübung`); + if (g > 0) stats.push(`${g}x Mini-Spiel`); + const statsLine = stats.length > 0 ? stats.join(' + ') : 'Lyras Begleitung'; + + const promptMsg = + `[INTERN: Schreibe einen kurzen, ehrlichen, warmen anonymen Community-Post (max 4 Sätze, ich-Form, Deutsch) über meinen heutigen Erfolg. ` + + `Ich habe einen akuten Spielimpuls überwunden mit ${statsLine}. ` + + `Inspiriere andere, ohne zu predigen. Kein Hashtag, keine Emojis-Spam (max 1 Emoji am Ende). ` + + `Antworte als reines JSON: {"message":"","chips":[]}. Kein Markdown.]`; + const visibleHistory = messages + .filter((m) => !m.cardType && m.content) + .slice(-10) + .map((m) => ({ role: m.role, content: m.content })); + const apiMessages = [...SOS_BOOT, ...visibleHistory, { role: 'user' as const, content: promptMsg }]; + const res = await apiFetch<{ message: string }>('/api/coach/message', { + method: 'POST', + body: { messages: apiMessages, locale: i18n.language, sosMode: true }, + }); + const parsed = parseLyraResponse(res?.message ?? ''); + const draft = + parsed.message?.trim() || + 'Heute hatte ich einen heftigen Spielimpuls — und ich habe ihn überwunden. Es war hart, aber ich bin geblieben. 💪'; + setShareDraft(draft); + } catch { + setShareDraft( + 'Heute hatte ich einen Spielimpuls — und ich habe ihn überwunden. Schritt für Schritt. 💪', + ); + } finally { + setShareGenerating(false); + } + } + + async function openShareDrawer() { + setShareDrawerVisible(true); + setShareDraft(''); + await generateShareDraft(); + } + + async function submitSharePost(text: string) { + if (sharePostedRef.current) return; + sharePostedRef.current = true; + try { + await apiFetch('/api/community/post', { + method: 'POST', + body: { category: 'story', content: text }, + }); + addMessage({ + id: 'share-ack-' + Date.now(), + role: 'assistant', + content: 'Dein Erfolg ist geteilt — danke, dass du andere stärkst. 💛', + timestamp: new Date(), + }); + } catch { + sharePostedRef.current = false; + addMessage({ + id: 'share-err-' + Date.now(), + role: 'assistant', + content: 'Das Teilen hat gerade nicht geklappt. Versuch es später nochmal.', + timestamp: new Date(), + }); + } finally { + setShareDrawerVisible(false); + } + } + + // ───── Inline-Rating Drawer ───── + async function submitInlineRating(fb: SosFeedback) { + inlineFeedbackRef.current = fb; + setRatingDrawerVisible(false); + addMessage({ + id: 'rate-ack-' + Date.now(), + role: 'assistant', + content: 'Danke für dein Feedback — das hilft mir, besser zu werden. 💛', + timestamp: new Date(), + }); + } + + + async function handleSend() { + const content = input.trim(); + if (!content || thinking) return; + setInput(''); + if (chipSet === 'start') setChipSet('help'); + await sendToLyra(content); + } + + // ───── Exit + Session-Persist ───── + function hasInteracted(): boolean { + const userMsgs = messages.filter((m) => m.role === 'user').length; + return ( + userMsgs > 0 || + breathingCountRef.current > 0 || + gamesPlayedRef.current.length > 0 || + wasOvercomeRef.current + ); + } + + function attemptExit() { + if (exitingRef.current) return; + if (isSpeaking) stopSpeaking(); + // Wenn User schon inline bewertet hat → direkt speichern, kein Modal + if (inlineFeedbackRef.current) { + exitingRef.current = true; + void persistSession(inlineFeedbackRef.current).finally(() => router.back()); + return; + } + if (hasInteracted()) { + setFeedbackVisible(true); + } else { + // Nur reingeschaut → kein Modal, kein DB-Save + exitingRef.current = true; + router.back(); + } + } + + async function persistSession(feedback: SosFeedback | null) { + const endedAt = new Date(); + const durationSec = Math.max( + 1, + Math.round((endedAt.getTime() - sessionStartRef.current.getTime()) / 1000), + ); + const payload = { + startedAt: sessionStartRef.current.toISOString(), + endedAt: endedAt.toISOString(), + durationSec, + messages: messages + .filter((m) => !m.cardType && m.content) + .map((m) => ({ + role: m.role, + content: m.content, + timestamp: m.timestamp.toISOString(), + })), + gamesPlayed: gamesPlayedRef.current, + breathingCount: breathingCountRef.current, + wasOvercome: wasOvercomeRef.current, + feedbackBetter: feedback?.better ?? null, + feedbackRating: feedback?.rating ?? null, + feedbackText: feedback?.text || null, + locale: i18n.language, + }; + try { + await apiFetch('/api/sos/session', { method: 'POST', body: payload }); + } catch (err) { + console.warn('[sos-session-save]', err); + } + } + + async function handleFeedbackSubmit(feedback: SosFeedback) { + setFeedbackVisible(false); + exitingRef.current = true; + await persistSession(feedback); + router.back(); + } + + async function handleFeedbackSkip() { + setFeedbackVisible(false); + exitingRef.current = true; + await persistSession(null); + router.back(); + } + + const currentChips = dynamicChips.length > 0 ? dynamicChips : (CHIP_SETS[chipSet] ?? []); + const topBarHeight = insets.top + 160; + + const renderMessage = useCallback( + ({ item }: { item: SosMsg }) => ( + {}} + /> + ), + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + + const listData: SosMsg[] = messages; + + function renderItem({ item }: { item: SosMsg }) { + return {}} />; + } + + return ( + + + + {/* Header */} + + + + + + + + Lyra · SOS + {(thinking || isLoading) && !isSpeaking && ( + + + denkt nach … + + )} + {!thinking && !isLoading && isTtsLoading && !isSpeaking && ( + + + spricht... + + )} + {isSpeaking && ( + + + + + + + )} + + + setSoundEnabled((s) => !s)} hitSlop={12}> + + + + + + + + + {playingGame ? ( + + + + {playingGame === 'memory' && } + {playingGame === 'tictactoe' && } + {playingGame === 'snake' && } + {playingGame === 'tetris' && } + + + ) : ( + + {( + + item.id} + contentContainerStyle={[st.listContent, { paddingTop: topBarHeight + 10 }]} + showsVerticalScrollIndicator={false} + onScroll={handleScroll} + scrollEventThrottle={100} + onContentSizeChange={() => { if (isNearBottomRef.current) flatRef.current?.scrollToEnd({ animated: false }); }} + ListFooterComponent={null} + /> + {showScrollBtn && ( + { flatRef.current?.scrollToEnd({ animated: true }); setShowScrollBtn(false); }}> + + + )} + + )} + + {/* Chips above input — only after Lyra has answered. + Bei Standard-Actions (breathing/game/overcome/etc): Ionicons + (native = SF Symbols iOS, Material Android) + Label OHNE Emoji. + Bei custom-Actions ohne Mapping: Emoji aus chip.label (LLM-generiert). + Verhindert "Lung-Emoji + leaf-Icon"-Doppelung. */} + {currentChips.length > 0 && !isLoading && !thinking && ( + + {currentChips.map((chip) => { + const isOvercome = chip.action === 'overcome'; + const isStats = chip.action === 'show_stats'; + const isFeel = chip.action.startsWith('feel:'); + const isBreathing = chip.action === 'breathing'; + const isGame = chip.action === 'game_picker' || chip.action === 'just_play'; + const isHelp = chip.action === 'need_help'; + const isClose = chip.action === 'close'; + + const iconColor = + isBreathing ? '#0891b2' : + isGame ? '#9333ea' : + isOvercome ? '#16a34a' : + isStats ? '#2563eb' : + isHelp ? '#dc2626' : + isClose ? '#94a3b8' : + isFeel ? '#7c3aed' : + '#475569'; + + const iconName: any = + isBreathing ? 'leaf-outline' : + isGame ? 'game-controller-outline' : + isOvercome ? 'checkmark-circle-outline' : + isStats ? 'stats-chart-outline' : + isHelp ? 'alert-circle-outline' : + isClose ? 'close-circle-outline' : + isFeel ? 'heart-outline' : + null; + + // Wenn Ionicons-Match: Emoji aus Label strippen (kein Doppel-Icon) + const labelText = iconName + ? chip.label.replace(/^\s*[\p{Extended_Pictographic}\p{Emoji_Component}]+\s*/u, '') + : chip.label; + + return ( + handleChip(chip.action)} + disabled={thinking} + style={({ pressed }) => [ + st.chip, + pressed && st.chipPressed, + thinking && { opacity: 0.4 }, + ]} + > + + {iconName && } + {labelText} + + + ); + })} + + )} + + {/* Input bar — natürliche Höhe, außerhalb flex:1 */} + 0 ? 8 : Math.max(12, insets.bottom) }]}> + + {input.trim() !== '' && ( + + + + )} + + + )} + + {/* Breathing drawer — absolute, slides up over input */} + {isBreathing && ( + + )} + + {/* Game picker drawer — absolute, slides up over input */} + {isPickingGame && !playingGame && ( + setIsPickingGame(false)} + /> + )} + + {/* Inline-Rating Drawer (Alternative zum Exit-Modal) */} + {ratingDrawerVisible && ( + setRatingDrawerVisible(false)} + /> + )} + + {/* Share-Success Drawer */} + {shareDrawerVisible && ( + setShareDrawerVisible(false)} + onRegenerate={generateShareDraft} + /> + )} + + {/* Exit-Feedback-Modal */} + + + ); +} + +const st = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#ffffff' }, + topBar: { position: 'absolute', left: 0, right: 0, zIndex: 10, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingHorizontal: 12 }, + topBarBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9, backgroundColor: '#ffffff' }, + ttsToggleBar: { position: 'absolute', left: 0, right: 0, zIndex: 8, alignItems: 'center' }, + actionBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.92)', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 6, elevation: 4 }, + avatarCenter: { flex: 1, alignItems: 'center', gap: 4 }, + avatarMeta: { alignItems: 'center', gap: 2 }, + avatarName: { fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }, + speakingRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, + stopBtn: { width: 18, height: 18, borderRadius: 9, backgroundColor: '#f5f5f5', alignItems: 'center', justifyContent: 'center' }, + listContent: { paddingHorizontal: 12, paddingBottom: 4 }, + scrollDownBtn: { position: 'absolute', bottom: 8, right: 16, width: 36, height: 36, borderRadius: 18, backgroundColor: '#374151', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, elevation: 4 }, + chip: { + borderRadius: 14, + borderWidth: 1.5, + borderColor: '#9ca3af', // sichtbarer Ring (medium-grau gegen weiß) + backgroundColor: '#ffffff', + paddingHorizontal: 16, + paddingVertical: 11, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.12, + shadowRadius: 6, + elevation: 3, + }, + chipPressed: { + backgroundColor: '#f3f4f6', + borderColor: '#6b7280', // dunkler beim Press → spürbares Feedback + transform: [{ scale: 0.97 }], + shadowOpacity: 0.05, + }, + chipText: { fontFamily: 'Nunito_600SemiBold', fontSize: 14, color: '#334155' }, + inputBar: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: '#f3f4f6', backgroundColor: '#fff', gap: 8 }, + textInput: { flex: 1, minHeight: 40, maxHeight: 120, backgroundColor: '#f3f4f6', borderRadius: 20, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, fontFamily: 'Nunito_400Regular', color: '#111827' }, + sendBtn: { width: 38, height: 38, borderRadius: 19, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center' }, +}); diff --git a/apps/rebreak-native/assets/adaptive-icon-android.png b/apps/rebreak-native/assets/adaptive-icon-android.png new file mode 100644 index 0000000000000000000000000000000000000000..b820d5b5dfa7a94c389d1f1c65f4e0a5ffb15625 GIT binary patch literal 219644 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%0D5G$ls*< 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%0DI4k;hapdb`*B}Z`* zx48a4rrt6v%CO!39=aO|>6DZdkQ%xrMnD1SlJ4&AZYgOHX{Eb!P`bOj8|J;;dq2i-#l6w3ZpkzJV07cH%RWBEQFhAHO9V_X*XHQD` z?D2NWZ=7G+Ct%=sYK1xZ-7`EKd;&&-BihJkpy($PNbbnt!sGcA7e#|FYv7A}_#~L+ zn5w1l@@q*Y<}w3Hc<BBlu=sQU1SYVjr)9otshzsyrY`D#_DQ7L6=p*&&*zpE3S^m?a}}EF zC~!)X$o$bvPFW-{IJusxDT)@M!>CeI>T7gv=H+(tB}(&>{SR{Jp60*Ml8H`#2;7X; z`bYr**}PMUU^uqL=mpq^*ogM_3#Y6)FqZ(VHJMv@xQ00mD6b`JKkeZZ_)bUyK!CXu zAu^1v@b{INLu2#HBi;v^5lOfC5`#*!Md7+%<$idi~Tpfp#ApQm{H<@M{Z{}tDPBrEB`walEWt{dQ-(pg+ALmSHQ%C4+oG0S)P4JUY8EGJDyvt z-uhWMfA_oJlVSLu3xWgg9h~(f;zt&}(Yn%(rudp3zm+|o|1re+3GeOX4N#TNO~rG`#ENLB5=uORZgmN9uKl&zn$4`p;3@vU@R?x8RD<1aWN=DB|R^3Zu$b#>#_p#JkU@%bnZ$L37MD*>NZ(H|&L z9Ad@LQE8}62`^-N^|>+f(S8;xeV}A+GiMkaR%y0jZV|VWJ&TL(Jiv0Pu9ab6zzQe- zAg-zaAfV~s-yqTAr8I`)(q7R>QXz+Z%Mq=l^toD}2e{#JHa51fG&|1?@#P&ANnlI! ze+xajphFMfT&Zxyh%qC`A#(xAZ55#tZjS=oS7=6~v`fvh;mu#8DlJ`SJt?(SD`kIG zu{D%6dbM9R$>`W@BmNd7$1(-&KvW$L6dH!C#zsipjay{I1%Z=8W0q3*wYADDNz^H0 z=H@Zd;d5}ABFJg7a;XY6soLCoWD@kGao9X{KcY5c!cG-@z$yJX)OB3r=P%tP=ulZNBbnK!XLXVWVInjh z6M0u*$wdG)$4Zo$r~n=f_5)Zu{~a7WGw(PR%9-lw2<@l&e-8#Q>e?fm@Oap+2dZR% zGn^740|DK>dqPDb{%)b}mld6tQsOaE{N2++M!wwZn3d2#RG>Noh-`u%hD}Mb2;hp1 zh4}nu)H=d|O$4xW2sR9NEJF-(Vq&J2Mzu%iMO88rSK?JtdjrZa8Q3%sFVi!0+e%zv z^*nv$yTu%!Y%J0XF>(IwAvVq|t~7~jXRA19?0@$mSSSjoh_8w+HTAo2?N8CYx(rwk;bbYi64QcT26j1pT)fq@BO>c>Di?I-5Q zz7rOWvaJ>w`d*X52SZf_`21OueMn(%uX)Mnwyl-^im-Z4!W?n3;3EMXM2?ABTtUVK z=1~5T2@cO4$Z?vi6|8+0;Bq3z5xM%lSEZg)saf7}{tv`mJonFlpu~8 zpo$tyD@Bb+2$$u=2~ChU){xa{D^kPO>JT=;DfZ`bQF3S_C!$R&S6A^V31yhfX&sc(%A zR+M_U8(R?>1E(#{=eysI`%fdNpL{?oi+u9!$_6?!-ywH3*A>|~V`ET#Z5^EfK=yte zLv#e8`Y8eaDS<~F%NcC~X!ppz@?Nr>sW&a32onSmks=q@nH5CI#Po)f-$4svS3uc= zN!fNBCwm$j@8wyz`J$UYk#GuhagzWnGp#D%b74@ZO3DPF%;klz)_mN@y(nwKkWr?$ zwmv`RK40avG5~VH|KbFI6`TpGw7M)==-=DIXDZ_ans527HrmBa#gvTNb|@=K{&O|@ zE3C@WA(&`1)J4E+kB-0Xi1u-ygp*fbjvLb$YG6%cawInf!@o)y0oqF0tZi*|;ii~- zI3p3IGQOJmyVE|RNGT_&NI7KIeL3U_8N3&kohElS)rkGOZ+jCj7&EZiG6a{uBK5)i z*vHw|?)B8GEjJ|CKEOHVzIJCfnP$KHsM; zVP`utHz)vB%GkmJNgTKP7x|S2a#uDt;U=zx1nNAO8#6zO+|VegzNyo@)w>#@=vCHd zm0{EsTS|M_;|_(8D-)>?vWEJO0SPAZ8zyF4_qR)~bl5MffE3Z)ce!hSv$Q`Gkx|Bv zjDn)MTyF(BTixORk(c)u>Az!8w-_~M&lLtW383`ED2ZYhI$8~zh9rd*Q2$soI6Mv* zX5aD>$;q({YZWd>(NvHCBJ{kd&pti^uGaV=kb8?&9KY%M3Jwq7Kcd|<@?G!UO8OsP z_M-H)^>uW0pNAl2XUFZTTOO3wW}OD);a>i4*rE6hUbM)_k%6<`M0(3B;SS^PKIv*b zZ^64YpbzF<8V)~aK8lycV*37C6&FtvTszleL>vM?9uFAxxve0Ho;s)FGW{jXzw?xd zy<|D4fMTZCBNP$BuLcpLvawbAi#58RFY${$%=Ls={=UhCgs&pmtKcf$;uqqY&R+To z6JF2(0pV+aVU6y3$ZkIwV8~4Z@S=$T!)PKz{72(&(@GzGqqTJG>_o2fS>0cWrGkQJ zV|S!XDRQE=oA7799T6K z->zR2MFB*d$9%&xedZJe6*QXPg3^+ksQ-Pc0v35-?J17iiwNq(M2N0S27$O=iS2sw zCJdWRJp9yOQlrM6`CJ)9{->s2Q&rWWjm;Rd$Po?bkl8z>YW!W@`fOgJLa8s5pR^jJ zXdZ_IvdwD0a(6NFe8`lM3GMK4YKg4h?Wwl5?-`x$Hvj!XogymLR0*;(Q*YkB)&>R+wM{U1InqapKmVG>Xys=X3%Sy%`<*i{G>4Z3>?{6SZ&8RiWpR$wx>knaGm@V>cjmDTI=c?DdR}628p> zM@^O}&lH!$>Qj-B+-;iGoewEQ@Z%gF19&&*|M#F>#Q_FD;nlEH6aaJP@`q3(!`!7t zUc)1GY`I1B*m|bk58H+c?IGuD^*|*GRjpfk8cw2VD#ccG7r^bfPUGVIgELYaYfX;b zO@|{Q1?U?9t;)U)G&aLmh+B544}}R^>JyL5Bn-|G(7tgIKP4}jBCjTPOQM66Uamu_ zn%{c6mM)rKX%HJ$gF}+(uy3{zN-wEd{I)2=6yM_1e&x3COIL-vH-57Xr=YeJCh~*T zd@0ya2Ee1i$7o^zzM{SID?H;X3S&CSp5LCEX;k-JerGM?eotvD*F_+oB8Xvw_A_44 zC6$hrR-5znwH0vP4OR8GnEM9{Ax(fQfDcy3bx1>&nct%?5^38}408)n*wJ#`9bO%o zpi_ORS%Ol4JH~q4GCRQ^`AVlc{G9;D%c<8{8y|4212)+Rf{%^f`@(O(2t+0P%_|YV zD{?Hm+ODYhO~?Npxa{)(Z$9GnYv8hb9enzT`UDck6;nt#3|ndX^TDz`VWd{g5)30U zR9U-WbR8>sV*cG1x3wsLCKFy84r=T&TqKGNfgdiS<-zh*dHL|t`Iy72G|4%`Uk}K1 zD49AfYC>Ynw6(~XY`*31OAw^6;Rs3d8D?FnK7~9#2o(1GrsOET7v@#>bk*s@3KD~MK>+7nGm`o?2MlS@h z&t_ks6W@`J8>T2snmEwOgl_hgs+KV(;#RwWLFHrI>5%LDlJLsSV=$1T%Z?jz{1`4) zV`2E_gpH)YM-DU{G_;rX&Lb}3%5(dz&Nu1e&q&m-=%e0ovp5>^vy&l))Gu}~EeQ`7 z;SU#aa|l9j%OeuXWa_RlB*Np?^|I~l=yE*vBkNjpo~x5DI`sAQ-rqi9aI8OEll=>B z&heuTFb6nbpkpdXnMFzT6GzgtncPnh3~Rrmw%bj+Mx~}tO-gvmo%jy+%aw~ON!-17 z9T21rr;p&tjp@cc!hG!7$^6Za8*{yxJsuw*p8&qH68rNFk1QcKA_4+qrdJq<6mI|g zTYspe{>R-Q8K!FK?PI)~V4mZ%?qj!<&hDNQ#)(N&6l^5SosxvlVi%fXvTlSXGNtR) zbQ%?m*hqE@Gr=Kk2`DiX)4ovDis2-T{tRcJgRB%p3aKr49g6^T{jCv^@0KlF+SFE6 zJ?-rc4))qsz;U3`z}fe}M|~iZJ{ne$qVDe`2@+nj{ElJ9;4CS$S*B4w}7%W(?cD@9KY8AohujoOTkrGnUW$X0&`=cCqEz*fdj8Ub-U0VVG z3+dWtBAGB`WI%|fya}?u8jaYrJ4V^10L@;T5v6?+I&&Ak_DjAEdQ9zz=;oN zggv#ufelfcn$=Si25)i~CQ}bTBys{36!}wfqNmH)p3AQOKllrNhb0e#ebP1xgB*yt zgQUiKNdl6CcRhd&stJjadubrbNe2!<>0hECBlEH~_+G2~HU;Q;2Qw5ot{zOz{`up6 z`JeGTuRz;Iv^4plX;^H#O`$CD&;vEtKb)ed2$GF#cMhMK82uH3?hgf?Y^1f6HbV$a zyEm&J`gY4qOd_SlrW!={(ftcgg$%wiDkTBeAzSLTjlKR&>d&bfetQ8<9hV^-KG!n( z`m12Di{pb=<#M{Tl@pl+Iuoeg(4QnI(Lpnt>DuA@TjO7Qf-YZvKO-Bi?gBgmFKvDLimNAm5Z)4i`~=u&~7EtyumE zEo_T+$JjN6nJFJ;j36;Qt-Tvq%4V$KAd})aFuVXW73Xz1@@Sd+(Sl$&vrrLcCry;+ z_5&Ax60AD#cslpyEyLU*jFu-cNwv)SOCmfoGBR=x+jmvkU%FI6zZw65nEKjlomQWT zJNBPX{Z3;c2bWU6yc_HG&uV_DiPQifKX(3|RB&r^?M){JhhEWct{>JRnTvfLW{UvG zHjSoGg2|+?S`O(oH>LUXySl{Pf+nnCri?2n?r9nkbJ2wV6WbxzAz`y#*?@KH=2}PH z6!jtBpWme1@IiP{DiwSdQOlOG3i zzhz}TsW(+UZ5tyn=w1Ss-?;ynx5HxVcQM8@fRE|O5R{?GIUagRnN zUCtY9Rr!=x8TkGaN9z2^%>~mJIX+&LtZC9G4A$msvJ%cFZJUlr3^XbJ77oe7($>~C zy7x!=-Y=`7=Q&|H4%U}BCzCHZx58f?`db}`h-OzChttxOjxZz`VGLF;e+O-Sn;Mu4E7E_y1K*>7Jygb9 zs*wE7XZ33u>gzj-mTh56M@tudB^?*=?GGu$_D-%?A?@fgp|SwkoSyLMPgp#+L%kxJ z5HnJ|maf@lV6BQPFHYU;m=_ZKl1aE^xBNtTSEJjDF^Sz25(TD@WbA_k$S(f?Pk9X z<_TNC`F%_MpKs$HYLIG(Vfepri#UBt3O-`$uw~tp7QVx-<~T*Ay>3PWos5D&sr7z# zIvf)SBF|~|UDdf0bLMF|&d>22cF*xr<2SYIBLrB7T;K2Je%8^sH7_m(md4K+WDPsj zH8nAwZOE%Qf54oGJ3UJLUY&Wj&_vSW~59)5+S!I5p z6y@KW2_Z2t{INbVGV~45ZMyij{dot#l5Ka0ymM*2iWq;{HW$6hUah_OJsRVM$Ix?c(Lg_#($tKqetb2KFP*8L4p4(D!N4QnKyCX zti)c!-%l)SH3O7=JviVEAR_Scbaif=``|%ZEWM^NGN&y@XmR$9l? zYV1|(DWW;By9eDZr|En6oL_8@@B~Tr0Lyc+A65V(TfBTa(C|GI5h>WdWs|^J%}YZyQC=`(C5< zYQn1VT?=T0+0ev@e6*T6Sqe84Rf=Q`Plf`~WXd#IY~SUW-WnnuI*j+>aowg_&0pDS0!Mq#`UI2fp9B zc5He&WN&y5qnnx)EzFw%yxti95kmg}-;Wbkz;h1J1%(|`imz&{^6@!hf>gLg6xw=OaYTuyU*^ErhedOg6D;pCL3l3z)9H6`++bL zDOr($jiV}0Z&A8sTB7XBKkqy{0V2^*JX$=<1H|DC$}3iQd@BFK;@m#QK#zCQf>eo# zFoDb5VGyB+e9}M+w_$$xv!pcu`cHW!b! zy75vj-$jKf<1=b*(y5yVs8CA33eJ=DyX~VpijUOtWrbI6@ULM4q6Dz~hRgTD@Jb8> zfnClI*hPp770AM~(raF8h(H#0E$F{-+Oj8Nc<2LiVbMfCl3F= zw}d9}GaPOxO)##5nK1Shv%`!sZ9DXtNBnK=f?YlxT5*d$KL%2B?W}{QngSA(6u2?~ z(3e)p4HzLoQbA#|Hb+VnmMuviq^uXP<^4MXHm+x@3Z`uX!d0cF4&r$t7m)D9Syuik zvooKo5|TwR%+H)WJWFXMATrC(_{2$intu?k*-PVH8p{4w<`b*!$30s_2opT((8Z&gXl}p9>px<*)90Sf+$$r0WhEX7?Qk90cA{dRjr<{ zfTn_)`gSZ_*(OMU1u?bhmMYD6kkMLnQ3Z)<7Qr|x=mLuWNSA@wNQb}X8VLGb1s2=U zyWdGsS;i(BB0~yi@MSp{VuarXnIo=Z{IQ(EyAxGPGX820GFKie9ia4)6293(n0ESX8VF%q z4|F|s)4IC;_{!`0*Hxz)fR7*h&nugp$zcXc&x*4kF%ZyFqfePy`x7J4|9uxFkk=c| zL7JOI<$r)aIJuj+%e>6I+lB+7h(#P6eG#NuAZy-@Qadqux}kwf1ld*vh|k_u=JG7N zozQRA(ZX|~G0@e@Gm#=W^>$*iULyjnvH+Vs0U=$Wuj!HL4bSZg|Y39zHBFv|rzVLdOq4A9WD~ zZ-DU)P(q-UCFC?rL6&=GUDRMgVmQSj&Q_0$Q^|u=fzsZ1gP~agSjRX}sJaSZ^Dm(&;I39u|y~Za8T&mwqaodMY zinFAHyGpQ$riqO0l3wyaK z9ZtsiQqN+8o|A7)v8fkPEHRRr5EHbM@vqf&7 zg<3Rd^EcWN z^OsDQ;D9iruI`!$t}o5**f`+3f20G77aZQkcuk3=10|Gorx|9$;S1uj`l zl@RHDHiftyQ7G0pmAQ%MDAwTqH2GNG<;r(!K&o;~u}Gs9D71@_?y>1W_~}dDA2`qL zFtB&-2LGsVLPOu1SF-_(YD1u3?C>ztUKd9K5mo0yCq`EjT)#l#jSnKSvKl=j8t$@# zW5Xy|L(3c;j+(hoZsY54;B&7&rz|B%PNzs(WJzd%6Yg562C~pzv3Qs*X#6vW>vKs> z(eQD)o$DWurKiKm_AL?!#6MijL2i19$=OPu-I$8OXtrsddcm(%rcK8y&ICAaQ98VBGS z^|~JYQ$v?q--TUYW2sxY{93{_ETwM3O9El`l8_jyM#~=f|C$nf5dQ(SnsfN@S0e~# zO92Vh1D_hEySt8e3a<2+8?R$eJ)`g0Ug z-JUnA*YXmOeY_lJZFlV3b95vObU;fNYy;&VSQty3aQSn8Q~>&6uz#Y0nd8fy5ks%8 zCl5qUgV`-+F+tJ}9feq|EC*$rSh-IyY)*(_{mXki5@x`E4)`IxY(66o%9ZS~S2CySVC@!}cx%%GQ* zp;e(pjq3#t^m`I6`~CtB=!ydaXc|_yR_EZBVZlA9v)^~4Y!@`af{7Cuj zmb-z%621nqul1X*R=5ct+Xzw75@`KN%L;Sx;udR)J`C4dEq?fEqS6})Pdz|iC=Q-D zmEU$+!J-xlRTv6gp-#ESqsKpZwyeatGEy;x-5c0;EWoy`I_e6Sm4^Sz@UBZRj@ zMj1m@*j?>XeOz(E{)KxaE(iG{fv<~EnmivaOiL0#$AX0FOWx3 zMw<3EYoKPxA$epQ4Ae748j_3;g==5i!I z&y_;Y9J8s7*uMGGpAC0HqzsdD$Y*_9x6_tAmJN#^6$TSz3Mw1L--ODBGS2ex#LKxg8VS34|OJ=x)^6Tw2$lYz&7H5vQ%f77l06hRdsr zom$ZIQ0ol?bcn(ofhdr=irGLM$LCD z8obV>p^fk!5817!mZYNV32Jm+CkLJnZ6fbQU7f6*>B&pgwSmsw8q`)W>pa8%oERwo z(R0FL8=v@gy;3L56{U_xaGQSZPB+SHD)${j{R?+qD(v9qq7dZ{gK>u2)!t5x^m$u! zxI%GnTMvKw03B!+uk|l{>ctR&CuPs%$GDeU$a)xmgB5^Y?SJSe0zNdVtZf3~}Lm4PFzmm4Fsg*Oq|kD)Nn@ptR$Pk9<`KwxKV}cB~g?sr8rqk+-*U7n8Xg z?^F9HMI<_~{Q{O&mao@;7{?`t%@lctDKFduBy6k<^qT`DQOYJ!!0ZRVoogwhlS zCc{UAY}2a=*Azm`>H_BPrM<5+b~*XoJ~mf+sno+SEtCK<7l-6Akc5kAjBEVUpPlG24^cJXo35gUaKTXrIDlK}wG!JmA4fQneCu0_^b zMTbe8?E*RkeNW~lp5g!3`!YSN&oSr9Y!Dp<$>X%n{?^nnZ>>p`mReoY6k%Br>e&|I zIub(V`IX-X*LY2s+i^1#ZUa*alP%Rs_A-zbf-1MYd>-Drh367Fv!As;uLv)H_N!q_ zhhne8RW)0V@dny(yvRDS;`LKcHb2(5cpD`dZJJq4Ue&BoJ_~^wl^Mj`gs^h{@#g49W-?PhXGqJdF9Go}nvoJw9A4?HOBWO75O*m?_DPjg8zFtd+YaJY5Bv38F!2#Qo;)vKcxnA^M zjqprP=JG0Sa=gtFs(DPsD*Ye7%Ky1Z!{Vk`IusYOO?Dyse3GJH^P}14K0dbTJ6?~y zUAbku(|Eg_mGx+IJ0#TiM#$9gI|N@rh8=fxpAys{j=DAp%7O1JQU{3k2jH7Qgg9jt zipSg_YWLHfe&l}$_g}SvfJk6}3E0 z69oeKgXE>e)n8HauzNWJg!;Mhr=#(g$C8e-`RsxmIRzF)h5Khnct=NTtLI4{an)Le zc}`CZ8Nb>|)pu@S**dRE9V}7Y)e&j+oV1q9#L_XOuwIbSC3wOy^kpzv$nApbLyEZD zx3f5_&w|a`M=i`Q?w_68D;FNU8px5}!-Eo7g8zkh+cAMy>ffKA3^S3Ha|HuVUn);K zx1)&lcCwzW%6f^zqaO@oegp5*BF2Aw7R-W2Q1v6Fii@b3%AdBj`q}v~~?^4Z66y)X=VUIOuy4{;m%QS($5D zC8qGS?d?D4{3tRvuIb&Mn%c_t_J;B(H_ohb zb8&NGNfX$p_|)+H0%B}@>Pl1igeh~l0S*ZDAI0|g)v8(ms3qH%7T0I^-TqtONq(Qr zV#tA$l=E?X@duU0%8|UObItPS&WQ=`7RT*(qfls`UR43NK%h}guo!#*uEPzLMft!K zJY4V`1=sQLUmsIuny>hA1HYsL^$UkhsYDXI79WEr>5r z>`8$nG=viqFE|fBuXExeQormf&G*Rec~6BfG9+Vq`ga~sDjH=qTUpoCgtP!u+|*5n zo}$YYiLqH0eVymCEuddWiZkvWZ;5^jv8S(*>IVh!CYA`PU1AG-6V`1sW0|ZFdANvlV_( zaHQbJ^kSf%OlmEqy0$sAmy&g_XR2snl4je39Se#HeDRFe>SLQ4lOXS}t z&DeGBb`;?~(|@-Otu=c+(pMim`E+DY2AKqyLKTF|`ZUpj^dOLE1=@af-`C8ehV{Q@}_j6eA`iIDz^Nz~rjfH+~Yi+4}+OX}}+ z0_bB^e$!Dn$-_swK<9+dQ!b>Fk-g{5SY+|(SB?E5vKnLtKDFj5?3j7K<>qRO8u`jL zAg*5&_;5l5v>^agR`yHa83-l_enE*-!EVsS^RAq??%-Qm_1mjh6)^85<-vrqkMQQ# zeDKDSni>Q3=Ksh`|D1Gx9wFZy`-sTk86m|-6=(J3CaM15@pbJ(!=tyXE~`39Q}wE- z8Zq85v#8*-Ohh|fIp@$G4wO`YUo1Y}(7!XT*)E*3Q>^Hp02>}g+1TN6Fg3)HBjUC3 z@tG)xHviyXJ`*seiv8yqgFW)=JE4INk@aGzsSYP5&zrV12hF!5{pDXU_!a83+juP7 zi2iuV1y$N4v1K3pSRUW_@rA-~cKvby#3ZU|_CSfGpRoi+l>oFALJVk2BB+r_4^!80KH z&&qH`Mx?Fkk%J7ha`5TN@ey#|M5G8}mF#qV8CwGBMwo0+bXEqMO^^8DA1%9v2X+bG z<76-zc6K#|-T-raUzvcC;S@4@3N%3x1VeJk61|pf zF`yQ+%upwz0(wUP05|U??zl2F)jC2jrb}U7#W2MUw*p~jgY3z5wCbnd5@sFj?d|=E z*e6~3rX%%xCGhm|%o}IPxswDTI9f=AiK%~LB1@?z!E}q6Yd|qPekz0)`pZo@No4k3 zIbpS3sC<>;Y4=o;qADqiC($c@SRp2HCPi*A`YtM;$Pbw=J&Oia;w}fHVL6zP1^_Pq z3o&~yHh5!D`tEfdZaeO$<(*l8Dtai5f$c@l|MvRw@?uk@nu^0!+mOX>O40jUNCb)} zz(*C0DwKXvuE7h!bj;)ynule z*d!yE)0^!QwG~jtd11YY#rg&J3r>uA8u3gvN0n?W)rttc57!_Maxpcxw|2{yS#b`e z@PXRxvwkxE>Zaca<2dUrScrP8>;#;cbdW57VJ`sRN6XVHkow^VugOmu^XNWvNsF+^ zuPv?C;NL`}q!^53uI+D96wl3_7&FGst@pKbC#L(T*}cS1T(}u99WOHk^@OP8TzUV5 zuj;G+=v2bc%tI{sBk@~rVA^VSe`$Ww`DAon9$L{obr3!Cn=WRt;r`Qls0kiFw)hlWC3yTqDXR}|q0 zVXg#u*+hJXlte1EHgJ$3Cm>7N{$6jgxobzEHE{omi?iehTmkj$&5KLG{O{kN4*@Zd zG*;|)t2c1d0Aqpse;3@FxYf;CJjoEG@yi&Qf`3D5ol@Xthb>Od+eUyr?VS#X{ffr0IdmNX9Xe*)ByQ{Cr-;5X|{c>w;PopHJcN0 z3`d0)xGU_PTdyP6sEd>4)05dT3ijpD2{v2NBsqiVE>282X4P^HLwm;@@29-K;KxR7 z(D(4Y6WXXMIU!2>`4I#^o|~_KQ_Y=&V82yi zfKy_r9^d0)mzu88T``3&7qlg$>->7m^pep7IUeNV+PR1QsU~a+l3kU1q>w$nHA1hn zD6VgcTe;XhG0b8MeGAYxH9DrT6~=WvjIaJaLT$-!gjE%g1{nKa4j#subatV+ToHe3 zhKA1bjLQ5~0e)!aS}Iad-w%q^wyr5k`Zt4efiKFH{(`;=x5voGx4jf?aaG%|3#)Pw zADPjL5*-5@qggbo?3LFrfel@ZCzjz_4=)xbHul=pP||@&oM=9!_ged5pV2wL&hgB^ zTP2(pyF)RcVwn8>|Be<8s3a68nM)PFVWG(f(H0qbrWwPvN^;D&p_pHWoqdM!C94`& zlDGiphv7Fy{tqAL>#vU@%4*^ePQN%62X2x?r+>63CnQHnH4PvmXCcJR^Eae9xvdm) zXyK-|nxD>qJM5iZXn3?u2(@(>sZkeH3Z}1oLra1jh6_J!iS@=jR{tgE^eNR3ac$BD ze8&dTby^I`dr5>}>(gg`8H3%Z=r3EicaR5f@F@TNbnJS`HQ;^KvkT2(;b5;a^fiP7 zirMFY@~(ovfJ5gizX+l)S+Oo+11qpWL?@RwSbE>FF)9>=g{bjKH64Roqai_G&!V=( zzjbzLboz8x9mxxuc$r2JO0)XGzc$A)+cNmqjqY~$BHOD<-02jIGF)W*XdR~8Pt?Iu z6)W8p^GL0}$IU))7hce;ob^j99Y0PU(WebVXb?|{86hZ3rMZpGarmbysHh&308AG*IBw=} ztXjLgOuGVkHi{6$%@;c#k zt2|Hoop)5~)cc%l4P}IpjWP1MSvu^H)r^RU(_DCmpH#?qj!sz3iJYi%Y0Wz5M1XtV z_j8T!7gpS1H-|X8OVC#}#ch10x|V~0QgVZ$L7}B}(icT0SF*unHUa#X``tQY}WdwtSVdThz98sd&j-E{VAmP6*(!&;XDm2VvIj=i+lP z00^FG>O$yQ^SfxjJ@I`iUA@c{*Pr*DzYWT*nm(j5iWdE1p9R;$3BoOmSLIH9-o>1D zOquZ?ZOJHyZZY2=Urp&=MOPj-+aR7yTnpJKrKfjoLv{L2!$^YAUkR_ul7#w0LLtLy z2e$gx%MlqvhDg+CqJ6^;JCou}uqvabD}L6L7penR;-eQwxSk#az|+{dF@dl7X|}q( zCcmWC+}V;^bcZSNw={FJ!0nV*qa198#$G^Q0?<(eP`d)8WABr3pkB#1eK3=TZ|Q?0 zSvT>MpN@i73hLeJjzp8yICOU>5bizF_&r8Vm{~;q;Eh4&OqW#1luND{0FO?cZ4HQ( z{IhAMap-p5JCy*?wNYRcv-k8?{l`K)rTxFBE_)|N)t3T^G6C{nqw49Kr{C8x7!tu? zLc}h&dTlkL3QfC0ec)~VZHeW(wo3%daLx|s{HzXdK#~Ce{WH_|jw+m7Hzti}xhwK1VyJ0BnIwk*vNi+h~YplG}%Nft-c-P}axBvuooQ;+ipRpC4A8>-0hq zYmAd_wlza3hnC?qdU`VgY>=8I#g_!R=VBNcV2Ly! z$&q%~+N7saAjQaw3LB@_ig8A)CJ)PZIv7mNj!fs#;VmU(teWGyiewR=ojx}2aThDe z_v^*F4^Cl6O9qH5_&X7}=J|7C0vkBM_4z7G*dq`JTb&XsG6em5dn6q#sQo?@Y>*jh zNT?v)dbTl=o~@vlcBt1nI$!`1Sl9-URkLCA8NsEzhyu2R?-Tkm`hL0ZL`BbS%#}Ye zN|7|1kY$Bi!xM#{WrEu6r)GD6U<{4*VzA93Cp$qj4-<`%Mz9ibpH?R!M<6rt+wB7* z9L$}M909guuq~YxgTqP&kVAJP>yn&H%iqk1=w{kWSsFMzk8um{7?ORCvU*j0c=a(} z%n^sIi-`R)stj@}*jeu#6TrVWJY%A3ZG}j;ZTDD(sY0Z3-}OY-_Ji^rW1;x5Qv2Bl zE}kiO1#W*)Z6lr9Qu5$Gl|pS1SvWAi4Yc?$Dr+ zlb1Ifh+b^up_~l#MeTCFe;*1dyRrznT;=<)NxB>bM6Z|sFLUG_qpEQtBuR? zBiY!_>e76Lv4h8}Pe1~5VuEGC2B)2>6GswJJe(SHy@^tIJW6SujgGHUa)C7`9Wjbw z=OfMRD=E^kB`5>Feevi3juKMU zwTezCzz+Wz5u}3Jvhr0^c$}<`t$+kk6vx|cxE=#R*$@pQ$_nW76&JedBJFw$uwIA= z)wUvYzrh%~NUA&3>9L1yGyImj+@*L;Oe8(SV~=krS<#-n-lHDP|1J+~;sFC;}05T*IkcY*upag9NR!MneK zJ2X(51OR-3FL#wUNB5H!0oxXva%`O8S9^om`U-Gpd@h@oQmaovA+V$2_&mv#WXiVuS$Rjr{#MW$>OKMjE{lxa@_L}c4^P9+Rvej}|4w4S%X==`}JO$Jke zSq^Z8Ds02#`A`(Z6@bpQUp`dVW*CMf{#@O|3&^%(>;`6@=s;5-P%FtCjfs=LHh25f z4doQT>c$Pd%=u5! zpz8vBB%7$g(K?&bP7*9)^oSp2cSOy5OKx(7BDB5`HA;KQ*b*Q3KR5ezQWo6#4Ec+w zI46RpUOllC;fCTz;|Xw)WC49R)W~35;6u>W-A&s+5VkImTFZ7h$Y|piaEyOOc8L*H z2UvT8&TA6h=-84~o+2p7blH3BGb9{_iiMB82(Efv*yU&gy{F;bIyBC5a(chh!smPB zRmX{n5dBMXO40q5g8ZN)0>-HD#McMdKq;9V>Bd|Qn)lkX%35y(f&Oos!!-60?d>cc z=zWFJ-{xJI3=nr}z5c&}CR3_(>gxy&)b@C46b!q-DB?V8uhOpYS#%n2>&^AJk+!xu z&7Zd|n7_eQK$_$gGXX8Z^RV0ZfVi-ufbPRiUiW5_4``s_f%d<&>gZ^F@M3sUOn6Li z>tSaz4QdtqHT5+iMJexo{;V*zcAmqOykF~h-d>%*+p%+}0-&QBiqg%tc*`Fv5J;oi zumuoyF{RjSU0#uC(^xhzs3-16(&Hg64A6lZED+{|#14-#Uw#J&IxoK`;G)GHfyZG+ z^j?*1InJN99MK#RskJ_U<+j11Yw3lm5dJ=A6WlakiL&yHw7Bls@{*&e1R|pO=i@O9 z3$?GyO|{C(ABv1+{+6+7)8eseOCZITR!F`w3C+i2j--YQ1j&)W2k#^QSxK(?sc_0u zh2sSE04q0o@zg1~X_&dVSyKzy^~Z#J*=0EuKpQ%qk54P2BM+hQ=}7)#G6jHE1qF(2 zBDBW_hA(7P>hwgKezigtmR7{e)j}*tIBig(9XM&_;&c4a;|W6oqZ#!7DDy`?Pxg6g zn#lWC9)YWKk)CoWK@^!uX~&rRx8t{NCmExkkx;+7pmQN1rIvz}R%P1$_C9UUhT36w z)*N>OZ4jFrt*$neC6Fdz9X(gW!PDqg|qY40HObM4A!q!8#(6hg@_KR zOmKyfl?@8>0%Kt&BDk)sZWCOd#t{GI+rl=KlnpY2r$%Gq6wlX8NHQ!AkDnl&c3xlK z9eizg%|xuLG_JIs=Vbnahi9^i66|^91c?|QSHc3T!jQKH6E@cp2te;0w+0>3z9Ow} zisQa`uc3S#(`0?RY3Sg&*}&kqD4#W}Y0{9c`TuA-%dn`s@9PiUFr-5_0@5JeAV|ZA zfOLp}bR*p*B~nVah%g`x5<@CT$Iu`l-7)k$U;n@Rc{Oj~g6leS&e>=0wbo}U0$Tbg ziUFgSP@o#)fenj}Up`Jjv5>y(X^wkeZM?}V&N`lyoQ$IV@R&uo>L~XItM3qllrfj6baDQ& zoMMb2m;)>gRnDlwq`gaK*D_zaa$c>Rd9$Rl2m;B&{lyt(lG%_*W@8}2M@njTd0w%7 zTzX&6Z>Si`sV_5nOEhx5MG4N6^uGHQGm?K3HIxYhOc#tHc)%~GK#O$00pTkV)Ep3u zlCe1h?5a8r6+lK{qcj*)rfqPkLoY|+U4$sb_Fd!4ze~^cjKjm8pz_%vzs;!BuYSY-6ZK?L+leB zu%4LmNx;V->$a+IGh(;|42#r)j`y1D-a%h!f~vCumds}$<1 zWkdoDzjVE+^i;}R0YXCxl!bx?&pIwkwDN`Vt4c@Dbc2k%a*>qbhREB5hE!m05B1$jknlo9n#Or*^>( zSXC;N`1sMb5wukDnmDMm1J1(nPY$Eev$-a9@Q}N9?TJI(uIkWa$-fQDUSir_ZGKHD12y|3SE4Vg#mIWf*a zHi7*LSJVap`<8WpzQ`OVM-}}?ZgF-taCCovcRcmelL^iH5FvE>~A{$3rNej@JscdA!a?)*Nasi|D(StN?SYqO^#FO>K+?%8`U z6)=9w zlY@3r)BUO(XXyiFXl~B~g)5HRkZB6^{`5m!Ty})qX%A|K zyks?LY&j0kLStLq1tKB46F)JGENFe`%0q|Y{R(1yuy6jJIlAwJYx6AX2%%$+#QL}-ER)=Gp@AOWapYZz7|ryMVk01r3lyMgPE0C)Ea{4Y+~*te@6{g@BjiO^$70T!JgfH?_- zVZrE&j4O-HE5p+SVsHXfgJ2hh>#F6CK0Bk5BbWkf%T2|n{B?Uotnkr>8ZBQ@L0EyO z-7mjFKIWGyhx4_{$>Atvs(?KR8^R~@|SigXhcHuO(-dFe&6{JbReRk_(AP5;vhuP#S)cuFJI%?P zHx?FFXG{bCmX<`;1Dy%@lyHfdo=%J@(UxL<;a{puIPnxXy%dhp3$v6jz(Z-s;yjH%G#PFZS$b9902v--CJ8G97V`8*x`587RqFi7P66uSnO; z3L-6}nC4)J8AR%>!9Tw;ugN}i(~fa--&$a+7TV1A#|1BJzex5M;lM)Cm6i%ylBmoE zkRd)wQUhcdH(Yr8IrD=RZ>+!!tvdI2UJi11t*^>DuRl^P&lL*U z^-q*i5wjVyGXLa+X;Z<{RnIe<6d`b4TU#6Fh!Bsj7Jk_7*?MS15SR7nK7@eiIMH+) z?J2uL06O+3`g{l~|4x_iV2Z}RQ4AapHpMr3m3e>VLt~*%8Gu$0QM$8%e?)`_9?fij z9{M`##cpZw0UKJ4CO}B(as~L8PyaNSFmdV+vI9G}8`+LD6AyO!8*btj(zkJ^!;MuA ze2YE+%PNNR->P21?PQUq->p6~8X243_x5Wa*ewB4Yo#*oEp-$qp28lhK@!dS>VM{JuC@|ABaQK4emF`!^k1l?R|Dk!WOtuSkpi)ij=R30h3J8(2z(eur7 z+UJJFEk~xwgF=PT#R%L?qkSrqG9Fnw7sodw(s#UYOmC`8R2yjt7_(7TMweelJnY&1 zE#v4n30_cwH=waIVcZ1uY6*a*X@?tUSr^ogG$_VpgRUI$GLW z)}hAEpu24&zS~jYlFj>x7y7T9+E!G0&h1gg=GF*=K;{Dct7I9}?o&sfV625{Nok9) zeSD{$PzPhs)^XG!c{PSK)-(Kgz4{4#rqqC$r8gf#=c281D{m8Y)2*lFc>)lx9Q%zX zYK;}bD!z0qd#z?s0Vhe~TT0I%ldENdM*>aF+I>X}1ePZ)1c+^VyCiz`nU+;!Abp2h z{0oJdI2BF=NM9Mm_7+EdukhnOSE(5qnP=l~tR;-M`9x~gZv6bB``VaLBwpJHxdxo8 z`cblKN)|DCGf@wh!Y?q9PQm=?__3l$drcQl6Qlg$Oc1WP&+FChe3`QeSVH1C!hdK) zImE(YzVJj!H}{jgw}q2P##z~tXCK;srG zASoV7K4AARRMX1ZI&k>=z6Db8WalX0bQZ(v^fR!RamfF>h0Xl_v}7)BnYWxr5D>i2 z&uSM$j_i4ATrS=T86bSYxL9j+84h9DfFd?`$8dgIhy9|?9}Y9}f#<5c=O!26h&9Zi z$7<)sW09&0uHezP&CDMs7VIV#lT$0&b8+C2~N~-(S zoe%2FN7!y%R~_XH%l?~dn{sn=g8PMG9m)B!?)x$+-O+@#_20wZw4o)R*^6trt$hxB zA%y>T{axzJ^q!@_46=6_?YZOON6A~6WnGbcVJbBGXz#Y%y_!+AwIzM%mJYpd>TEL8 z6P08W%D3!5?_q?{tp|-Wl=0@v%xBFtTs{`uo{kUXMVJmKa2o4E znFui;zsC+hVVZIM574`$t!W12+j&RSlJAG5b|xWYOfxTY#mH@Z>gUtfp$=}4gn~5r zF&D-9T{m6~09Oqp8PNc=_1?M3#{#mVgU$=fvhO^~yp3%B6!RjE@XW)Ust_$vueKGyR8*NJP8BguFFk1KKVkWLO0Zw21F4_p=K6*di{dr zB9cvy+vDf*v#?WZck_%u4YSu#p<#FNq|rX2(w27t^d!ptY{Me7HI7U z{uViOFdx9--9ra+O61sbHoGb7gew+0UX@bIS6&=@+d0J*dR#)Cx>g#=Xs3Fn(k%z{t0t~_%(k9_c# zs?XBY*q3IUjQi#uI0xt?jDFdV57rT=vwz%*q_AX~ZHzDgTbCR60gL)~E0dr-eNdHS zKy?gvqnx@J1`^lZ`LZXDy|oC0gl<;=pax2-S5y=f-Atf$Ld?9tVD;?s@*H_nnS~Ez zLG+YF$b<*oHikYk!nLZ;U;%mMV+=-<1rq>L-n}u4d*iR(o+BuR8;Vr z$Ut=XD@t7_(OLIq^w%5r_+BaDu_pHlVWgIB=yrr*N5xA{@=<8qNr`cB?*E2-fLH-O zJKJo?m-*23{o9I|)_Oz4kH5e@>*vYx7zA{qP8tXHC%7*zk+Yt41Y6HIgFS+5_C_t* zeR}-|laBgG$wy zN4#frBGIRFxv3sy)QCO}tNo)eYAJ0WV7^SW3a+hv*>H1v`!&z7nNa1v!T*IOKMxXU z1yU%GmJBZVeS+wzq`;y;INrxBoRe-QO z@Mp*>HjbEhc-O%0E=^f-LZU~1@Q-nOx6)%GIsr7x1p^IS_a68C7x(u;K)AeuO+b0H zN-@#O8R=(c@Fzi^OelL;fa(I)^hxEOhj#*&_hJClSggg&*PB^)o|M8eZLa$9GyM%r z2ARjta7P_`T$}WK0JbCH+D64k@t~VDNso=^Z7|r;vq_PV0h0Tby`!TKIPZo%fmQpf z{C~}fb?4-rac1MWuMYyaA%6xhXpaI88Z`Qw^50P*s)x}NdU#)UEum*wl~o^d5B_-4 zzN|h!@N9Ho0V<3Npd5OpJubh-p{;DW+B%)1-@3>2!H3>3G_8;ij|muRHm-M{NiQ}F z#+0s4enBi~!-*Wc>vkB=8=9Q=sQGape6jBQ0HCvS>BNhAdYpzglvb5^{Q&C`;} z1B9sazN3vL5HV?)Lv+p=Oq~m1I$+i4Op+rRbi>~oHF2xVc{1NuX)n5$!bA~I3AEeU zSfB#%v)$ zT-5m+5VN;2Qj+sPZ<|`5BkfF2X?LoB-eDc%!ORZX;-tXG{lNi9XaSULZ2EL!ilJqkVjQ zMN$geB8n0-P*LDq2PNng(lfsQw1ZXn2e%7BmKy5&PhBBg#c!}G8*gtSlLe_ok%Ty8 ze#qLNgRj1}&Ys2ZJl;qfV$ccr4`QLMrf=iqqXwI>wdv$!x5W>SCr)=i_!n>T@WFr( z;2w9=tg_NXIo6M|)V|+zD`=%lXDA)0$Mz}=T6F$+$Lz~64^24mlp2sz6Rzf9lwpzj z+lweVxwzW6xG+>ZW9?>$EZ(C%o<~1Um%F+%dKDs@U@&iY!%{>~?@$1G_?Fm_6Yp1gSNSV$6U=_FdcrgfBpnG$h5lO7 zhi%jIee`9|*XO=T#shIh5lco+OhB#Ov#3cy_}DgX1Xz)UF}d@iE_}ajd@jpgc~vof zOH;Y2oSuQ9T`6tPtl)$*UOkleJ~6Z_g|`T4k(W6rKF1p+VJhVS6+HJfNfG+^vRwP) zQ}$vFx+ashy4GF4yF>a3Wcu5XmlKB?so}b0;83`P`X-rTXH;%3Jr7^CrGvvXcS|2* z1;vokwEuw|+7&u+g@#fR1xC*c6Yg*KDuud6fp;eZ!HwnRTfg{&t}B}*nj*W9md3;o z_fWl!qY5TKgwOpT3oTlltCYt5=|dtuJDRzSC<<-TAS!55qZcQ~cue&Ps7?f@-g=w1 z_yg2{`_*C;ArMcH4(|RkoxOdqr&?8sDSqb?Y;~<#IXyqXZ`%4s=B)jZ+vYso zXY<(Q4z#oVsC}zFKP_UOJ$AmM|Zx~4<=h=H! z698W3K<%GaVsX~o+K7?ZXX#YqiZVW9BS~28g$3*8-00Awm?>?UL;S*aR*E!m=)|ij z#lqNVT-{1UZT*k%s3==mX>ox`6Rr!i-O+~y#$Zv4RXLE~)-@j9>ZxJJiK(}x<#8;< z1pW3=_VVl^UsPBO0N@(p3vEX0Pq{*4YrG*<`@$>vo#XmzF?Q$V*lB2rAe!cvP))l; zah$w|Di(eM?0#`^u`#D6!GLva0Oydl^)5!mC|$ku>(T9sw^`GyoB;4f6nX9H^zsi* zD?O92W{TCW4nT9*WO81Cgj{ZX?^Rx2GE;x;y6+GZ6>F&*EA)Wb@70o#M@lAi4p2<& zryXmwFp7m=hK~R_>d* znK09)4uE6ecd$mu`Nq=g4tLR-I@+p-LlpstW1 ze3>XY`78;4uO8$)>cM2?=JEnw2uCfG^nDcjOQpB8tlaXbCKo+cT%5E^pUe~+YEPNu z5b9Y?sr0>gP+IBjQYUidkMv4|*~+qbT7r7++T$Xs^0}rmmPbJ>;Z_jbKt`aHH?pt+ zU{I1G6JN_sTWcEH&qgED41wuluur*h_CL+Ve7|VmD8@QAPtVv!LYh2JOY+N?-0jk} zneot2{kc5e+})}r-vT~X1$1#SAmJO#+$|6*jV9ghre03DQcd zzUyt-d88DBA4&%MID4*Np`FU|AS%>v^-2^J{kDM^o%UWTWY0C&zPw61c&n5aC(m9| z@~LUS-G`6-;o}5B?ddb}A^hB%Wa6y#3%r;Lp&~2!_klt!nTH-8zaSDmsO7|^fbCQC zxS#A~g!nn22sBN#=Y?C^fLOF`F4Qp_*p85q9N8Ano2d#{X|IPxMoq1xz#X&X5gz(N z#5&EtJ}w=X2iCuN>9WCb5K3+3EW|3<{kmev(C)yo&huY%2vb-G+zRlU7lIV2?RNlC zA2H_#OpmlUnUCKB$Tn0L>ImMHDA=d=>FhckdGKaVb<~yEdDXL@d`sHwt3@kJBTJco zjgKAHY+lpmLdu3-Z_My{SakjXFV6Y3I0#%Ibi}{FoLVIEB+T(?D0;YVC?sZ&s*s|l z;bq_HDNVnM%6nCg$01NI?{CZaC=yV<)c$BXYoDszofTiH?jt_){_Zt0nnaHIych#W z_jjeN42+)dq#^0e@GgoNUHrnsQr}f5O6qaXgit7P-wS2g!7L6-!ruax3w*C31>+vZGvJ2dZ-d-I2a$FEM8pF^C5u0fR!YeSJklgRRJ)&+&Pvup_DgMJ>0@d zIe45Kbny*td5OmSBA}W8b#XgU>HYi}pyb5r`tB6jp}c_9s}vizum7E=ejOJxntwNC zP<*dxG5$#^wpLJ6>2vh;Tg&(r*m>HDu!Mw+;os} zh>o*$-kE| zC9&r5Sd`%4==Uw5uvng-Zv=>-*UwjP+kp#2sbMA*CRB9_2@7KqCTDjhf7dyA&vhtr zrIRLFn~b#jNPxtNVM&xu2FLe>!FY zJFiLfe-ji}f~nm^)u0tr6o4~C%AzJJ`>so0>hEbq#_MvvTy3?5L}M^hrj2QsRNz9@ zbf-Ec1ieR}gC;~n^p=8DzA_xib9;PBO6(9CwL?72UA()bpg&PiWKecla%cL)Bl0nG z`W&OwWir8?6XZM^fsfT}B_Bx`3@$LPEHti^vawOhXIstMx<)y<*m^$v{25k{k!Cw^ z^R{8RS&v&1b$_h8vcdq_{*z<_KMV3RC$EOXwaB6NuYx$}snvE|j|$!7YPTvMA5a$u zvAo2!4UcI~nitT5sE{VU4K;2r9H!oKv9|xhn84{~`j~$%plfX9h_Bvw7tpmK7(@2Q zQJH(Q?b8SJ^`Esc!$_K0$Tr?a=+_GG8f_4GYm2^8GejMTM=C z(U%gR@yDQ&bGr>}3S39pJwG5EV+`>k*vtRz8skl{z?V=9bhiRb`3n6|2b%4Qv~k2j z1Qd>D2S9EVBSTr-`dLy%G`@fR!fV+W<+wN~sDZI^Gm}O1aIs_*o)!+ri-v=*7yw(Y zGz<$$wh2WOo_yiATk%-n>C<;}-Z>T`L=WY~^w*sUS1&`%utIYjY>8?}YzJTb%1tv)h!NJ$PH`{rA>qwW(j~81HB&{DN8fVv70D)CJh5){^9EX zB8S_JL0t@2S3|Q8`ID_<;lF=3H*GzhzG+Qby&%i68_# zL`9q8N#?o5^I*Gl)wE+b%znUfAL;PQz9I1UFu$dZ=Oh`6^w%`HO!dIv8e3X>=I&k=<^t;NZaHzP<|xX?jU<_oPIdM^H^M zG)?l@Rg4r{mTpMd!WC=XrdZtfkb)s(peHK}a^}3y8hm**HKJhA1QaSiF8-(Z^3&+4 zJvhq)P(uFgCxwRQZLe2yEEL-}BWOi=Cb65G=)Fjlm|iP_z;>40N+34nQ1lAT4jI;V z1W8fe%Bu+2Y{6k8@k)TqctfiuVPz4O=?Bw>Aq`WUYk4+1TfG@=R~t<`HgEp7W@NxT z-d+RGugV`^VQ$YA68|PAnpSTeAWQ~_YbP5pg(4Uxn#!2~Jrd%iY&VhMjyex(nB!PAe zvMT7zjIo-fp{MR2FIEMP>cYQNHMM5XD(BEYpiO$PG^B`rw$a3y)#T!2hw7vt;_6X% zRio~=sgU%=9tgCE*8nE4Te7gW^E(zUq6r91pxFP4$^O4ofh&*H)huFpAby{U`tILaxz+kH-4ckgo9Pg(GpC0ORzCFR}^vUiWgAY3rii?$^pko}$yzuW;7 zC7|yeKTxbaZtDxbwTQ>^-7J8;`d1w?{BIciqqJtpB^4T9}iHeOHi?iWGnD}#P<;I=1K1xMOX z(EygM(ZgrN1ex3gcz8L6mzdu<9z5K?~HJmVrT0M~Ax6CZeGJwv9rM z-!H=lJ?{H(1#IBH|G=U&qEWx1Gwz|TxCYmuZG38RLO>Hd*}(#^eh(UT@*YvVcX0rN zM14Qw%peOoojP}M;GHf32XZyeb>=+HKzOF|JPxn}SZmm>bogIb`#qmaG55Yn>?M3n zF}sIStI{7dWuUqTe3n-Xfh!TfrQ?BTW5b#yDHIp?OYo_PIFowm+K>X%gBiRrxXSX= zdmQhof1`p+MLzz)f0qzG`-0DI9)Q*Ohfr8G@EcIxqt?8 zJVg1&&&1MG$paE5;xWmpLhF*v{HYdpgGe8{Fe#yv5BJ&XbXbRrm^^%RBRtyV0xpmI#oj0p;LU6=vj>uv)+4~yfU zc6@=Cl(lC7juluwlDX@*NHI2>8JaaLDiT+X58=$`)SoLyoz(mC53y-So6pa9DW6X5 zuarp(mCUQn^H)7U+@k-jf2qS~oIJH*_WZ!O>Vr^tLI&7x_|MZpA(-{~}_Q7IX*P zP@hi1=6x-G=<>E^@KDi-ClMLA%HtQ>Ugsz&pevSbnh4_2UEF_^pE!|MU~fRF?I&L` z@J!+}#>Yl+UOff{w?!G$0p}jjJaE68B?PXgV)1~#Y`_49HXUd{YfauIq+KyMgv9-O zPB-)ax7*kSI%|gqgJk*>qc+b%Z{F|inaEvEJb9F0*Kv!^XVw-Ly;ZEg6R}Puzc|5r zQZgY2f=td8B;_G=DAQwo_fk%+crjjN zTKe6ZeG*w%)ahMAJLHc>jSsvsnZ{CWtss)CvhsA)bhset`<>B{i|9Qnd{)*To9cB! z1$72&v~DKMrshl8*dGmQTq#AG$1Cr&DI09sC{x)2fB$*CfVXVxAg}naKTazk7__&4 z4^p0zBG5JaQDYtA9`; zgrq1Ep=Age<+LI=?|?dxU@&l1hU;1abD-%4Em4W_z!E68W|UPkYos4G&T~-U)^+AD zZyw=eEf0EvYMU_9qCFU%7{??`586Q)v^x8U>9$?#2RIeTDj@mTl19IaPKpgURlgwK zy3x~;uNUuLo!DH&Fi0Y#gLboIW>=7(;n%LFKRoX182v_Vu95%xBuL8tDnv|OQ>EmkiPeDgdV5R~c4-Kw=Cers(GKk8JfZ!!7h$!0!;=6S1yVuV1u_dCRP*Mpn zDaf6xtLlT<&lOpZ!1k(6PEWU-i*n*Mmi1R-fAn*&OVkzcNH2O+-khLrxJeJ$gZE=( zn@Ug@8qnkN{tpK=5emKt9~rjn_vyS)i@*Lq-!T;#2> z(1l4BqYjJR%6X`~^GW7z2VG7-g2@y0RzTLRBlM9>V4%U(4dfjO3Q9(*SLkxE3?L&g z+N%Wm&q=oaJ+&Or)!|6#NQDB8_xh$|lzW#ik~77x9tVBbT+%irF1SFWxPNWJkut$i z1sE*HzgXD?kVA2$PiB<>a@TSxz==QspxZn53L`>-sP4wS`IVsv6tMH?F#mg!z2z*m zw=Mr`-dqbP2a{pd&Ue?3ELwh|_Gw2~(-C}yGxk>7KPlAH%>{a}%HQbvdvI;Y%X-bp$ZrIoi{vOMKU_de{ML~Cs8=X zC~WySlRypv*?GbXx`Kt=O}kP85Oi4}SLP-I@YL3}Bpw_{qK`(zx4sfU7iSf)2CFd- zJa!$V3Aj&2i!a&z^?P+JM(|#8BJ|ZF_e{OE>`htA2_+et6_`)y; z<0$Q)w593se~aeNxf&A*mh-%rUkor0IuJt5i{ z<5MN?{oYlku8I!_){0N&?KT~uo4xOM8hf~L-XiCHm-_T~2FbwDql$N#N%xavSX!9d z`x&u>m*QK%Y=;7PLRUM^H4aP2ErzfdSG9@>NwY*k?HiS11uzvpQY!8isN!M?gxa;! zrVe@8Y03j$$Dl=X*7V<>+O?aLD@=?@72^~YRMwS0X#8NIq^9Q%g~W<0QWwnG|LI?p z?l?(wg*!gg?l@}KxtGyNqO&FJL|g4IB-T$NCL|($K6*CnvoLVFAxZVfpr)e{i=C|m z7yb!t$dVL~t3GaiaP<^sG4LyuV=1P>L+M7P@X<}0{MZGj9u-J@TkA& zUSje9MC4L1eOmLQKK$asyWW_W;ys@J^9PWT-7QCl$dLeQsdCg&iry%7-8z z7&f74x%s@87^?{iiVEr3j0FD%Ye!vf0Vd=(p!0I>-5T2 z95(rR;Z{~hM6K8owFLMVg{0%4)IC9JmHRPcyR?noF&Ac9nt(VU&{1^Nfw|H74u}kI z@eWC!Q|TWCtORJPBp_(C7IRD#t9BIP!MbFa4C$cfyc3oV1l3X<(e#;Te8MRA&t5Pa zPNGX>056eN_fO6%yIz)2JUnobqzy3(20o$GG)-Q}!!sl7Cwa2ocR{qY5za2ha?m3ZXJxmQY;!}1MXv=#h2JubC%KzTe6*06#k#Vm9ZHNoL_Zvqlb9H90K z-vIeam%N-QA}kFZ8oObhnl8_l&np^ji$eZ($vT%?(Ru&6{1pE+9_wp(&U!9X&Ros%b3?YK z=d%0jMmrNU3|&Br-9L@;GwwafgPFki)YOu;5g~)GJ!}jQr}Sj!s&V5X=BnqY)|4OR zL4&?^+Q>MB#W$$dYwWpIk#rnkAQ&7-wF6%@B^XK_(D*`y#9$tAp0kt;yIxm)I zIUs&xf_~P+F%zUpw++5vf^o!nfOoWzJu9w~Uofs*8m=N2Y{^Js%!3lq2uP$} zDAEK4wL@s>6?94oM<8iwU_~}{?$UF+m+=maNzMJ7FBtmQj@TXE%d@kwv1zJU_7;|e z9=)4C>6`{}g#lGZGD-02t-IHozoVuD*J7-!edW&FoByX^ASU{6PtkWAPnQQ|SN&D; zB+!sEhKb4g>{yAnQ^FTJdkE5?}Vy{$uEZ5wuM5!G|2G4s@LbXEV?hX8;-VrpSM*n znmP)(jL>Iq54VFDK$=>FFpR6+>ACB(Xn5a7FGNK(2Jmzy-e?0MI1x;6&})aj_BXr1 zp6;-Nr861g)1&i%F=o`th5yg1gkBC-K|Fc)1j32}lUMV^ft1}O=u@;>Q+ry=FHUrR zoW-53bWS?8*VmbuG7qH2m1Q~9xx8!LL-PXk_Gc*YKv)S z5Og1;DJuo`j!mro8lQ-w5C^H?tK(yu(+0OC3LNY5Q32oYSWi10L&kvf4~FIC<+3ke zhH|dt4Ts3Bk$4XX z03@a;K4*i!e7nameN!p2r{k{hWWpFBqLIx<_F{3w$Z=)!d=Cg~kLccq%I#_9HXK3q zvblA6gb{L3{3~->4nsF5f@(*2Ezx&p?U0l5j$~AJNSG1{N3v#8j|i_wwiX2e9}|0b zmlC)h$a&782CaXnuz@6!9>rR36BCa4Sgha5pR+|LP&cZ1XJjjffZ`RnqIB&&RW-N<4;F{B*QA}ziW3YLTcLUJoVKt_|fS6=TwLj6aCYTy^|@sQ!Yr(ZJYur#I_#Q zzNl2hXSCXmC@9@H6-|HE#DaEtfw#?V*vgJKc3}8bXoJPOu}Q<2uwS`?g>%A|M-g2} zs8)oC91ZuNF?o2{`J1a#mXgxF+;3_v5t@UL;?NbWLzK)LzZ#vW!Yh@!X=C4|H4A!x zYFPhdrzIdGcr@~hzj`bt?sM|pDRcbF^gg5qQBHOJhH(qGnXEZLwSSI;#_b~+Gy?QAjqIT7Y zWA6x7b1M?*8x{5t{GI2cE8)3~eGQ{bI$0oEEK{fehJZ_$Cq_J}VsF|{hHGRSr0h04 z>ADP~EXjC%Tc#}F87s}NWNI!mPbWx?6Hb*15fHs#v~R{eGR&XjN!K>Ff)GXy_5n7D zYSHSBqui}i1M8TCd7qCGNkQqJ>t}$bssb<;yg=Bw9+z%X;*zou1`4?B7LYYN6Lhsf zOJCpmb&S7b^qt!lcmKOs`rpXPsZD$)33wGehAaIlAu@NN|wO4BOj^Vg(3d@?lFgn>hO=vw@ z0mZQ~@4pBrxZY0XKMvtQ(y$Yt`3iow`*Y){seQ&!)qiPMA?WY_J-R9XDdPIg<#=i7(%9DjSb`qE8E$s^Xr)0*G zgTh0K+slwn4K_7t@An_sRLnoMT4l73^*wF<6%#3nBDcb2$5M|%?^jh76cGpw-|Mce zx|NFc4YP)aSwV`*q20Y#;=qur_D3roKNvRxvmFYcJ+zS& zF5I;v1aF!2Ag^1rcblK?T6}1IMh?PH*bxGOnQ<|ucAo;<3~1$(%gETu5n}wgg@+OM z_w9L>IeYee7k^a-q_!%5*}EVbLW@Z!)pID~y9^y?e7WO6#E!-ekBcBzf?(p_Q2|y# zO>GDRL-&b|uz0`?1#pyF{7pc;Xxa`IrchXymvaL;kRFTZ|GUv}82BwdA2^E89JP9Q zUykzf@pjq;TeS#@btVkw(y-x1A9VY?w|q0T%l{RrqxoQ6%nfo6etJ(EYxq6xxs=Bn zLvB8dASJ4$+*!Mm_K+pN2~o?@;a}_GekD_Qa^~YuwuRC!OGc1ruoZ}<0Lo=!ZB3}8 zguzK=I#laC0tsOHO{_WTG@Eh#6eI(D&2v8jO@Ib}f`cmVZl+Kx?9f~qPxilwZOcJ_ zX&69wH7i4pb1_<_>8!V%Y$!BsTc4F|jemMM`F)iNA6GGKj#g|OVNzTE__5Ax#)VS5 zOnSqUIsyv`LE)46dsK)0fs?Gul`(aTW@r3Rj|SA=^il|KcqG zZ$Up0wPx*B)9K>2-r$=f)9?6_K+ui38#FEFeNJc3i*}M4)6=mP_`U5R8GARmGQD}fv4@xe32qu`(D4h`XLnk8Wv^Fm3T6O0tpD<8CbmuK#wA zlX)26FmT!njp0#!6b!75ZKis)9yjF|BdS}^OV8;)3KNeIfdzqb|INR?AYd#b8j&FW42s25{HOF0&?Ltvvw|q9#SstI zuTwEPpw@rO*{mwhACDr(-imfH35p2C+NhTQL#pOT2U7rJY+%SqjxIprzX3FcKdk%s zG3`nNy5tWw{5;Msm%sUVf?1C5JL_v|&Nue=O?T!mvYl+5F%;3u4!S%{f#D?ZivK{o z_!1Z|k1V~WzZZHF3}~2*d~)l<6Rc42kAQswp{FJ*wLZb>zee>DJ(!JLW9TVqD~HF@?3qX*2KrU@ii?XmGvilt z6x6~52vg?0%dHI5PHP)MITkggWjH4RhFi-NN7ooOuU`1ry_wt|Znwn%nXDt>bA2*+zaeR+f3mcT>p?$&=1^h(|DKosaMb{zE}W&D=m zK5z~0yEtIx%A=7fiAO5WJ-73aJZ1gTR#uEFh-5QIA(9#RX%<4u1uEdUcDwxTkRug< z4;%&ArO*>3J{D3qDm?q68}MfGTTOZLf1>^_Ysa9yK+fI}!>Vc7z>|rWw|$xXmOxxk zFsI%wSC#YE2_5b z;zM_XbW2KufP{37Al)F{-QC?afJjSAgLJoqv^0Vsl0$dRcYL1bUF%zmKY+E)oO56I zwfFw*3p%SuZAD9eEHI_7K>FVUfA2pla7fsRPl;{y83J%Y%5U{FdmfF;tPz|Nh0!@q zu)`Sx8y}qv49AZk5{O1JI*qBZ$afW%^73YWy=F^2Jyk}!cs~QCfSQNr?Y9;z+CN2?-)m)I<-RqLw2f`|=|*W-kQ&8P3?<{3G5sM}9pLa_W(_EmnB@La2gwAbG5dPBX@fw6}Y)SzA
  • QWG~*WUI%H?GrU@hbcMaB`JQ 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 z zS-Gg`^H)9HYaMrZ#Xl1@qR%QybnWM<-yCuMnxGJV}Dy$zV`}fE>UaV49*MYpG@LNPG#f^d2sTm{p`ComWx-SG}sxn z-o;1^J!Jmt`KwcchV$oIuD*0i?pd%3J%o-Wj9H5svupZP95jcNlH)bwkcn!7`pr?O zm!UcnLMg|RR?9Pf!>h!8uM+N_%uHZqem5X`E%K8Z3q|n3UW}cwL%QW`{aiRDkHj~jB%+zdRZbid9e$Cp`({qVL+H`kz);FJRr)~9dugOWeG-~dl|P&j}s^R(;N3FzP{cWebd~>sM#mU zip>#1jE$M6dKcb$hn5lG?1xXS^m&(1|21Fii5yB*~)9_MSqxIqCbnQf6X$^f>%DPnJBz6A;bZ5gwJT10N%yoolg;1UzcV7b}V z(cxM9DW`gRRij}Vrq!T&o9VK%Y4mLakcNTL_=OWagBjO@Sc!Z+?5s0Kzw%yCRpS`* zrf>hMW;NwDrGWE|bCi~46D8tJHI#F>)nfSv@uqs2Ft^QGIPO_b-=K}pYC6`@C-Qpe ztbus^ya2QK>#e-;!AcS!F6=i8-2%6SuO2W(k>4P?GEut+jZL8lNXnb4l($;+7VY zyZpAIuD=hXkM0~fC8Ou(kHKlqByV14(s6f9$tgPST2E$jOKs@-5TbT^7Mqd5LY*&5 z6&aqhL<-n6=Ud`tU}J7*(;S$orl}d1QQCqJf=$MN8Jh}z+h6EqsqD#PrEhwEECO_> zrmc|%(;84%hU1?093Faoo@wEdLhf5H{Ft9Nd=GGSV}KDHZaFVXjR2NsXls6Z3bgfL z)TDB5e{4T9&{~!55gB>dnS9*l7{P&B=&iI|QH3{!N$bHg#BaO@VVeHM3TA;co} zLg!j^)>g|s1^Q*L%VFyz$Ib|93+iN{x~R2?gs;XTlEpD4;a1<*#3@p@+ZJbabu||t zg$e6!np&w<1#VY>vV$W|9hNUu!kTk1`;{tsXIR=kOLk&f7|8&S@&c0oBF zkr3zglp|ul#~bE7lX^0cLEHI+R zX=2*)E1E$;stQ|`+BGTjGufj%Gc|@iG3whiCHf@1AxT1tx(X2adMnse4!^g|s-8e4 zd~$IO6rLaPrqjT!&*zVJA{(ZY>!#OzmI=tl09WBNNH0#YB5@((bYWvE)qv5~lXxLb zAfIR`b6ZIEI6a;CiXMm0EE6sAl!H&@#A*471BlJ};!H`dWVOK1)i_!&V`NP>Tncis`RTR(Q*TmTw z0m8>(%!@DI+AWO|8Dh%I-^RVn>nTnrQX|-^Q8)JHm}-MEZuf@CP$sG4eF38#$Qt_6 z98OvnYMLU<9nvUlc}b>MStuPi-$j_#Wt#?b$0fA8o;)8L8#kVtl{zvsL}-dVT8eGl z|AGoSUOE^__oxX{Bt-azg;Btfn;y8(J%A|)7e8KiU}-##TUlE{)BZ6C>gK@h;?N{_Nz` zRHB!wNJeH-HzQ=*me!seA&T7UXab^@N2z&xD1f)pzHfF#ZNuX?a_tD?=E#=M6 z;SFq?t9cQ2k-}%9Bga&Un%U?Gfwfz7m2+}d7RL0X2Qn>!Z+q;@jdzy&}QxO>zBob0MtuwkHYsO=h7a+!@?4KTBO;I}*9o z=so!>+AwpCu;A@n^jR7EAny0x_=OMamyFE}FzXgFqC(b{Wti->vX#xidHkRa#$6$| z(tJI|pm{HjiF?RCYW1KFH}{hev3egDr2C>RXzW0BIvG(}8Tp4%q@X2qlPp_6@5r2I zvOm`K^dx9_getf!b326ws+Z?EK3ZD2;K*}qagf6!7$}wy>(gde5REtke;bkNQ3>r2 zC`{{-A7rdTq=EGsyXS^hTjO8k5gwjo?);(>hzshUHUljp?PwT=w{(K9R8s@@F^r~P z7F#45({3jZcnKWH*~&>v6bGAG9wbO%C_ii;AIH%HiJI@K9tXOy?)i=xS67!D`#giE z>kjrzTh0LTACc)y!ewT%(ho!m!jGH!u~kEn)HmGl*{vP|!9dR1#3;>qu{^$AR3`s= zAcZIyE$3xw^-;*!NUOZuAvuAustK8PH{Fl9Ts;0On{16=b%?i`S5*}{{RVdaXI;8W zN<8?subact+ynu9b7;6__X`RN3a9+>MO1x`H7YP}`)AC?)dDx%JVHo1ErBH6jSYfU z!)Vf1d3jxs#=iDJx8|&4xaoyQ1r7OWB8O0dqL)qvv4CtyaX(35qM6v$LyU65AJ4qg zu4!FAV|2jNX|#tbn(Vje`tl@Hnx|D%55_PhU0_I_~TSL(N?z)AepWlsdvxGOJ zC5E(l@@#XDUzL|u*E%aDtjAsuMWm}k!aSaBiYO{J(7q=~LBDNlM;(ih`t3=f)>cS? zo}_|!xQKzKU#NnwI2^~9ICPwhwn}0$cKk!2n2toSr|Ckqu}RSEr^Xf3|Yx<`U=o(kph>Ze*6Q z-W;Isk95qup%vm#)uEK})SgWJSWs|g3h0+w<+wXRjt_==s*nqV_iIOO%uM-lLqj%w zJvZx}f`UYh;ar-2*ZYJtQM_+>;c!rAe)nybudD=?$SCeMU|ir7Gq~dV$2+{hnmKXp8cZU3K(Ce&xkCKrvpkgF7*5g+S?nFb z1coj5NJ7IMsfYL>R|4|5dpMu|BRP1dr=TB9|DFlPP5vbFNrkW5M7eHGw_G3ei%zg2*yv0`7`?_K~Z3;coDBYzO={plc|QTrUtW+ue>KHON1jZq=mCQ zu1$^WsdrN7Ou>`f&)k#KS7AFhS$Pq=7~b2@OI>G!uWUvQ93j6Ksyo^e>=pO=;kRBP z-@ZIphUEQ*a&~X;^(m91BmI9iS0n2#H{lY1Dv72PM5w5fQ?`;54DEVrYGiL17O449 z9PAl3NL_~E{ukLiY-Z;+b;^8>C##i0b~R?cg-H>-HZ}a*Ra{19R@es@Jc8(Yv2mw- z4UEh<%>FuHt9Q_T!6O)cIcZo&S$nAa3*-(a)&l@5B4dq6_;lgabGvwpa6VN}oe^7_u`v-r-KFb~82FVPR-Y6fW8)>g4^` zTx)3l&NrT_CR07Z)v_(6^Iy=g+iT|wzEd{Tp4{S{E>Ory16y`-&>M)6)Wf6C#BIOQ zTnxVy|U&cI_~E9SWvAhwnzewansb*{EVB8#X_Uh-iOau1B6o=L66L85J(?C+Hp2V zQX;6OUk7-phbct+0X8=K=UII7bQC zVi)L#x+IhGAYS4t;*JM#N75;AE463X2!ek}Z9_I}dEbNfhaij#d61=iisy3A-bMFt zxx=r!HN}=1e-zj5Gh-Gi0uRP&#i#KZeKgnzRP}kbWH8XW?LIJYQ?NC7CjzpwQ8TaY48MzuJ_DCm^E_ptVH#a} zhAJwgU16kUwbY$I(>HLQ~CNICKP1n=hG^` zip~X3Voxx7q%G~St>PplC1_(5DRru_VpZjUE%I24lM3ae(n{x@rFy|ZWT``OedE4` zC%(D?bw8yias3y$;r3kJqU~u#+-yr%gt*vXW!YIaO8iLLl`6zc{0$55u54LXeE-*j z@0~HOHa8jwK${Owz=clQOM&K;&w*lNNlkcDBhIDY#Z5{Fz12#s;eRO*o@t_RuvmU) z-uF_1i*d3UaJ=rgMw)t46L?`l@2R8q7Z%;a^9drV5zCZ7Kf&$ml#PWe!dynf^nQ-J z{ceCfkld?K^Zi_f%2UfP6m!6wFJOTcOo z<(_Hd#p?}-_sStFYo_j5OoP*8!vZp{d$u`ukElS1XaRWLJ7DSKuF&w`#C7mWP6)JLOGoNRJpfTm$^MRCoL`zs+Sb%J|uTh89F%W=S455%a zXg@6;k5kV6Tb`ok9{G&~%d|)NkQR41DsS~ABacV=Wrqz7dmca1B={G()fZW%87=YH zxe|l2`0eg?Gl*4SqSouP#NLiNzD>Vo#+-b?pWGSWpyhY{QbtnO5MFDh)lX`!@f$nV zVgk={Q-HifA~lV&?YL{`fIuw?I-olm5|RVOLR^(j{o3-r@)zvTTw4Fj0I?0-rL0G^gZe<1B;iArNwDx z)%&avxx~zP%=&ksB2SH}yd?9gBGr}dzQMX9Jd+f($niKkx0S(yGdnDZ?fL!C*#NTOjC+R!5Z>B z*Q?NjirPJvXw@TSba6k$HAg>jC^wA|=6dU#w0fg`&I@eU1P z-(ATINL*tec#|`)lN0m=&3ZYqyXDAtPq25s?dTCT9Sa)af`|xuUsiWoO1)t#D%cdMA~X(CRZmp&?6kZfR_?vL?g8Wbek4hUuTSNGw#j zUOGU&_=(09)?Vez-2CRZFUGkD^w8cgfyinQ-8rTywIQfM8gjn7)%e zKchW(PK12!W13d}xfmq#Zeyl)4iB)1pR2ocpxHo!W%#8cpL5q0ZNw-nFib?Hd75qf z3jPuv2pMQEOaWmcl_**`C51!eeL?nnvbRYkz65+&*QQ!i-#V3*pZhaw%F8Y%KR3iT zgw#8R9NerOor3`tOHaQ<)L78`gIQ{{#JY#`?#F(pd_PpJO_Y>AdZbbTd0+9{=ZgzG z5pN2N3ljlvZaqj57%}SRsRszTxPJO(Vp7@ONmDtoy@>&x6)Ku?n{ltpL76WdBV1iw z?XA^)mGH~>B8m8wfH2I~ftJ+b`rl--=JCq0k094}@?6#4i4ZBR%cmJa91##rHRQ2s z9kBqnKirkE_)`D0iJq78#7k|nVQxdQJ}u#Rt;@>GlZ_3RfhCo(e2V7tgjrZ8(re4L zzv#$y8hP}qlLp;It^!NYcWi` zI{zlRPyRp<@F?tk+2oSkC6#YU-}2~#aN@o~6gnuLTslOfM>G%Da~~O+pW0NjP^1HEy z52;_0qcet}er*^`I!0+`Hc(YTL1AGI@8bE)dpRsOcQ%F^aPrr*@Cgu`3`7;FCOb|( zHYlOn>@oRaGcz**iBe%1MOlGa167sHS)CNP#9Ga6z!&=%Yyf+qLKD1^o^IAhdfB$1 zYH~y`q;Q;IoO2DoKO0l1&ZNRjV?0RQj}`6LlH(m*IV4xt2Z}&L4t#Ax59%5tC_~-( z`LhMnQ_tvQKaqzf>#E49Ej%%K%dR3g>@7cVtDg1A7X1~ z=atGNFSVu7`}Z3->k{PszHu7*7TV16OJA6v9>;N%b~ z(|1c#(s;i@YpzycYs%a_3RoOu-))HSU4mjkI)TJ-qR{#eMLUOA*5|wZhp3Ut{-hY_RF-+ zJ|)^|ypT-t0r&Z2!VAwf*aXc+uzF zohAP0yHVa7b{}_GLV2%Mo}o<4oiGkvpWcmNaWqN+VF`M|KZP z>C+W?e+0Q7aUO8Kq6~BB)t=LT%D-)f1la9@il*n{%CT|Xq$xm><9qO0+@25Zckafv!rF!f zD%8E`_FvWG-bwnNy`XH&g>mD=xYY|8 zv#A2nb2}tgrmO2hwcqmmPLJRXRw;o{p|=nV^}ShA(hgE;csUmQ*_XCbX%jS`j;-o~ zrPr+-o&nyTy0yuukwQ)iJhnYJrbW>15*vK|OL@5uM{6i!i&v3V0ei5CchemaLh6>y zYwr=&y=MjD*L4p^H3UBp*i>{uV=0r3x^Ce!4qt!cRyO7IZV}8tA4`3?0H-W8Kk#EY zcP0KFzj`fCiGe!%^3d&{_E`l~vfhozEn#rDRwpW`8g*z9RVV|2*em3~0DhlX(=Jiz zEp6ay1*XUHipp<*uK#V4%JrDSQmUuWoR?LaG7|Cd%5tw+QjtW_l&5CHYy!|QG&VGG zZ3YH?5{)nM8*1va@;(Rp&+k7^;6G2`|ML?#O4y;FC@AylVqIXp50v%D+46ylzCK7C zI6ezHd4e6p034kF{-Btrpi}=i27#^vdk~1B@Wg*x3s3&jdJ0s?@ZZOO61Ic7{+#8S zzow=C69<216(?V3UIfDEGlD#8EhGiN`4hiA|K_dIORe+vM4*#6{k4u0yQa^Kt2+t=i|gOf8zhBf!( z1Hj1sFQX=q!qCOV&)FX&t5n(8DhyaJ|I7O5X|S`Wr3O%_m)}!wFOZD%ExFxpbMXI6 zWcBZf9;(BL|18Jq3=^?FMkLBf1IVE;#d{1Ui?!|@U!Rtc7>-|dNF{F`=7S| m_Y#zp6nxDu8~=|a|JMZoNw%zx{sNMK9%ve9RNZ_0>c0RRSM<05 literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/adaptive-icon.png b/apps/rebreak-native/assets/adaptive-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ec9b4d7130b57f40e74be8546aadb57ff59d11e2 GIT binary patch literal 222924 zcmeFZXIN8P*ESl81rSl1(h=!ZsnVPD-n&8R9f1&fQGpFg?;uTj2}th*QKZ)Zp@ks5 zgLDWXC%E_XzRz{O^Zh!1H(Xq-wPv4d%rWk9kC8-bYbp^FQWF9I0Ady8m%0D|KK2qH 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%0DS`wICch=;LpYyU~SGMx9!us*pPS%+=`U#k2a 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

    cMmLfOYQe90;c}B9nxEy_?aSV))VJ&Y3@iNg1 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 XZM5@u9bY+zv*rnT>#I)4CF%bG;U0{O literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/splash.png b/apps/rebreak-native/assets/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..2ba07d36bbcff3486caeaa615cfe3c5f161a72cd GIT binary patch literal 248887 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

    s5$JbM;fVzd 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 LoawpKmTvzK=r(|7 literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/chatbubble.png b/apps/rebreak-native/assets/tabs/chatbubble.png new file mode 100644 index 0000000000000000000000000000000000000000..8d7baa846091c12ca6b29f00ec4a6f370296989b GIT binary patch literal 437 zcmV;m0ZRUfP)^0> zwSBBbR3ah)n{?3|5CoM)`~VS6f*X<)GZT-ivpcgpyX1if2IkE9zaMkn8M>^Voa-Oq z1%^?=F{=1gWKd5qhgE#XpE_S+4b$jt2UNm5jvj!dxf+9Ws~^ibK=odU_sFDY*lh!r z_$w;SU?own+Jz-v*W|@qC=izTT^g){vkm}H@iZ)9qFFp|lzxjE#LPoCwlhZDioevu z`cUg2uqK*P|4KT7a@HEPpfX;Dz@ZMp5{-nwn=SyaL*Q{2fcF@AhIC$6#SS@F~9T!5d8Cm8jQ`MbqODTiC=^mdrv8 fU@Y%L|F!-Ch%!cM9VJuG00000NkvXXu0mjfNoKnk literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/chatbubble@2x.png b/apps/rebreak-native/assets/tabs/chatbubble@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9229ecaabaa38f915304a8dff0845293351448ae GIT binary patch literal 833 zcmV-H1HSx;P)ZeyZke#a5) z^y(rc#d`dL$I44}%0nE&+ExuZuokD)mnzB&oWSzdY%wXtpHP5jqFY``;;MX1cQ)jv`+j@;6knYQCy=J>u>A#mH}%R#kAd@I>l&@ny2Js@x<86DWSFoat4Lu*T7Efo{2v zCEWz3T0M)l+1LOR_)a*@(K93ZKT4hQeH}5vAO3#{h!myg9i=S06A&rN{wCz$=LBT3 z8N)YC$x-bnR_t7oTMn$tngpaKgkvLn^e{`AWO|4GPfLD&92am-I92$i@NlZj9UQ=6 zxiyxi*r@xJy@tKsTqqvKcH#G1_3*+i{DjXmxVzQuyy!7uD{YcLaa=emd9CF4FTe)j z_}4Pb5spia2_th`m?&RS^AEcd-2{h((O4zy*Z(Tqk^=?|Xpa8@2?SK{Afr0z00000 LNkvXXu0mjfJOGif literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/chatbubble@3x.png b/apps/rebreak-native/assets/tabs/chatbubble@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c3640826595b828dd1e9c0f304b0912cbdb878f4 GIT binary patch literal 1246 zcmV<41R?v0P)l-Mg>8H2^ESLRs=-^ogg|E+)(9#Y(nmPq|HzT-q)$4c|LDm}o*mv@-v|Z1tVmzx9c*uHE&8-|(ATv9@ z6Lzg(WN_k?Zs54*M61*QFjvJn1CxL+0!XrI?b0yM#M%Io^QDyv{$0jwWR~1_V!qUm ztQ_Q0bSi5ru&iL)7?}Z_MQ(YS6nTN|Le_5TEe6%@3S(kh_@-Xr~M;I8x zz_nN&PFZH%pHn|k=>YDtRc0C1s~<}=EVor;UGhhPdHS(L!SBEgrV5*(-NNy ziRMDvC4ZCKSfb!&Q=!e!8yyA`3yVyJHbbv>7)UG_uVR~_a~uW|3$sjxHbaBo^AiBZ zA$c-%{LC;F+6H`8a?iLQx+%TR@9!WO%+nn~B0UXaKc{_lld}%)pRx!Geof5v2^C4#+fNcsOxvI6Jj6r3$D;j61Bc=~nSLPSW z8hUTZ_U+6N@E{I5(Nts{>-!R{LL2 zhw|r-4k1tLH-JYP@p_}^Wv~j^$^VZr2<@lUy}1i45XZ(*|<#cE)+bup2MMPew4jl|R- z!GwfRHL!>#1|s5Z5JBQCB28>IG3Y`{Tf_f3?diYQZSR|;;+H(b|2hBX_x-;-=NuM^ zwnY3D9m8bA@lw7Q`)~mJvAI=H7fxXcpChJlI-Z@4aS?Y~g`EqdkJyCuco(^gxRL=M zHw#-@GJAt{*oj?Oi`Vh&dpm#?42Qu>^kFxCU>yBejb|CGDPRw7Me$Qqu?Ig(roW^{ zS%C+w0GH!l6h6dCsrx2s>|_#$qULThz$%_Z(Fm5|U>N7HGW1Byy&btvST!GPjhXwn zfo>eF1IlAiYO;jM_^v{(y}+Yy;lMg!8e={6%ggE`-M! zGw<&7m!}7#*^yjs0anEI*y&)5N2M#$jr-E2S8%Njup(}T|KVU%@}nJSBdmylGGIvt ztmZ3dgJyZo+2TBUgy&gqJ9aG8pi$ruu4cKtjR5l_?YRB}|2^P%2N<(FbKnTZ>J)!j zV6M=Z^naXISB*)1*eZR*%ld{Fe=y!=?$34tZ}GLxf;z9?KStQ@#i=uV00000NkvXX Hu0mjfplR={ literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/home@2x.png b/apps/rebreak-native/assets/tabs/home@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..784abd9700dd11225210951a9707c4e1daeb40bb GIT binary patch literal 1008 zcmVTtE@^Ql*HZ4{fO%;zN;I1+{9$id(C_wzt=Ze?tEGXR_Vg zOp=-(97yJz|D69fGjnFnf5K2h^|cB)<~jmrls>5Jc6~!#TFBy~k4JSo$3-L!rT+aXx#1~B_eM*x_ zZ^4;S`~qxl5T6_1A;Pm{ob*?m7{#2XwbO1Z&WXkyjUStg=_=r%_&Q1W6OM^u&eLAu z7{kTU_<2Gx${lb%HYUlw#oQ?7GT~vmovJv29k@2y&m3&16WbNQKp?;7-M#NrF$Y ze-yK53cylZ9PNJ}tV#Uo?PyXu7k?)^{z$n0m340y;L5f%%Ci^VOZ?k#(PZH+P+JK< zz}_W)0grVNu58Ox`o%r4GVzb$(sto4S6c~Js5~eaghsX7aAkW&=rIW$%j=1M2d-}w z?q;>c;!Vsb`HOfiN4T=REcAO1EXey{NJ_G99n>0IqCrVfW~VUY-GY zS3S6_O8B(OsWAg@^boFW@2I|1mSw~&O5qMyd*mKfxwy0Xy6hg(bzVs1abNuhupQD`d^I8DDjn ztFHdnr|2%BA>g2E2sr2(0uH){fP=0f;GnA!;5atrHIWXqNu6$<=0|Wv2Z`Hhw>oLf zMmreW(W+WIFDaKyGRGR>)mmM((({c7e-b!eSO9KC>+q7+7p+Ewe?%SGV*FO&9py^l eQW|QgLDhdASSQV$xEsp=0000+(3d5 zAM`;8f(t0dM@13W#0POSF1UkXR1}vOTrsX=)EG61i5n&^iE&3`GK&wl+o`F(_tbW( zySn>+a2|%f=hiv@R9D@)=Tw8oj;6A%nB%JFHphrEk22chbBDt7Vdj#mT%=>AIx#s7PUtQS?ScyBqjX)pf zj=w<|Hvn{n=Kew$@3PZ!$+AvN0G`I3+-JZcf$_b-N5c3QfQfff zI5gx_!aRz$w4k$*ZwxnEe5Moc?)(*IV#dKLW>E=DVbFtfm+ARNznCVqRwPIRV)2@QGu; z04H1Ky%-oyI9H(rJp=d$d!R#@az>bwkS`5!?KfbqW&ZPl-3jI?jG&8f2mUW`p2cTA zW)Cu}m{t1=yC7%c{zn&5(8bv6Kcm1d;2ew3Lf~(QPZrw+oNYPYf|$o^xS&gLU#B$A zm^3DRBV0GjLI`>l?mMY;O>>>inl=PnXgU5I;8(#fN%2n=X|1Pu*Ue%hz@?Trjz?Zq z*w)CoQ37rig3(?VYKWR<-Fqi>Xap$%cL+h`@#LaqX`alaifU%c!2QC(zb2n&YfVeD zckmeYifSf0fro{Ix^{n-3qFmqG`m~RUqMDe=$bCVmO3zI#Iy)qfM%dLoJ>jQ{t5&E3*T`g9o zAy+0Jr}knVbv8GP**aCqzypHMCg5`EU8Wx zoR(Aj7&Z;l^WJC?v~44BSSZ)1Mquj>)k4SM8!Tgw05%sO`Mm5KV5TK+SF8F0o1k65 zCxJmDz%`a}QFZoKu^$+F`y|NKjNjy0PiJ8 zj2BcHFQ_s}Y9*t&MPb;?*p^umI)HPrKiSkB_2N?o7Go*^X&3L6z}>D&qxJ#tW*97gQN9s4`wqWxSxuctMr%f-2($ zRmKadj2BcHFQ_s}1Rc&TisBPH_auUDOA$z`ur=<$B!aHOyq4W=>;_)UYw}yjr)6-M z41Jar-@INMzcE8xk#u6-(A5rlr9x~2ImFeEc@rcyX>09sfD3@5$kr?|1iS^@j`_)q wEKJ1w!o*yKn6_Zv2Q5VNgvaCYcs!~24?_zEEkrq#6aWAK07*qoM6N<$f=4Hk@Bjb+ literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/mail.png b/apps/rebreak-native/assets/tabs/mail.png new file mode 100644 index 0000000000000000000000000000000000000000..d2fedff10824dd035b2ce15de7c4f4ec7142a281 GIT binary patch literal 397 zcmV;80doF{P)I3?C`JOu3T!of>n%l zY5ixMq0%h5gQ!PQO2a1b)9pGCZSPhK6{hpF4^$@sS rgQTAr%UILhC&nqxyR_cFet2pm=q=mPf^SBp1B)PXLrH^F%o zSae#B9g9FSVa|4SpbKor65`vb14j^Ii(*ygtRt?nH)$Ph1kNB9Ab?rm05EUipOtx; zYgUe%vX)fX2s{S#E8`Vl3}{%yu%#h$tSHC&fTLLhPSbuFehKV1_)P&{6MSv#8*tW; zX9QSC@SV;hFao{+#|^&wfENzLEhBzwIunS?Gs!=l5Q+f!hd7NZ7S0Df7-`7LwnYr;<8k!4Lz1xAQRmV<^^hk%bM{Nom&Gm3Z= zvaQFtzzA`+E*s(v0dsBqo+F+&bTi7vExOr3U+m?V6{}Yi$@os5l{2R%8Hg3!W0)apv b5K#CBVMV(+CIWuN00000NkvXXu0mjfpg=&} literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/mail@3x.png b/apps/rebreak-native/assets/tabs/mail@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..bda70a06482bd66a93cdfff73f72598db2c1c0ad GIT binary patch literal 997 zcmeAS@N?(olHy`uVBq!ia0vp^At21b1|(&&1r7o!wj^(N7l!{JxM1({$qWq4vpiiK zLn`LHo#XEz>L_ubUf-mfN11CPUnG0rifVSjU-c7hTvKAZCh$2Pw<>#c>1=mt0N277 z2@-;i4-^+^Sh=`N@w#?Uhr7vW<(jt!`3L2!cU%8X`*zlDzkJQ~o94#%|J^Cve|LMn zqW}vWC9(z^{m3yc;CR7$t1(QRv+{`h1Kk343HE)rrFO8)(eoErb72NE+bU7c3oJcV z8=UtYF-<5nby{DsL3rO$kFc4?Z9VQ?w@A|3EI8w%LScNeR7Qu;b}!`;^9`Y!eH-37 z_+Me0dx7~~(MoAPH`7-w+jgH`oOeU)>I&gq7Za7f9$L1-ZEnD1ZH~Y_vx=m#{rh`e1(e#!e3226l(&uDJ!e z#T`GYr_XNu9ojR0_63I0Z}%q~xZA(VX1%Q>-Z+`FxSV-&=()+Ofp+lDQU2-j;nJa) z#665?V2k=O9E3G;Q`cj0GS*j8N1az9g}`atJq z-yGEkOdrKHWe;$l{v8s$kTK6oJ6ttTv863>^_HSYAyKakhp$VgzMizAz=`?N)G+s- z`?o}~_^Eu^7$_tfl+h5GIW^rgBw}IW9fzr=R#6@LduN?aelm0ObtmRY-rBNyUW?8+ zt=@YuXENUd!8HHWFO2>tzO|p28YQ#vwDQU=MX^Fxk4{?86Pr2J-E&Ih3V-i?lky%& zZYYnuZTFzkb#}Om-G?WFxhocb6kWBY>-Vao+%@7-mqK5&Tzd3*lD>-46(jt}YN6x0 zNRi1N?p6!qWz>T7UHe_Q)V7#!3YGVhELQsj`0t4QnxJ0ODY&Yp)BA+xr^-cVug_5Z*)Gs<(|^kK zg>g++yQ^GXRww>f5?pXE*tEAw@LKKzcVA;6L>6(>VEWI%>JY1b^lV5lFyAtGy85}S Ib4q9e0IMpz^Z)<= literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/shield-checkmark.png b/apps/rebreak-native/assets/tabs/shield-checkmark.png new file mode 100644 index 0000000000000000000000000000000000000000..098565155217b2fdc1c672496c3b26be8d182283 GIT binary patch literal 489 zcmV4n7QzJ+&kWP?|VT%IB@T9&V0|AnZw~f z(dB48*G)XfA^zYsrqS0;$T%M06OIe4iMi;TR9;d+M&Ht_~;v5uWQr$Ic! z&w>i-*pImPU@NfE15mp)pg#i7mOilR8fXR9ePu`Za*9u1EAUrM*etH%Yr^j@#<1#l zb3g8JOX$xczjN&w>{fs+#@K7vPx$TTmAu>`>=n-7f^dH+-Vjl-5y3_a*yyGWAXpRDV1e5QIyGQLBJM9g{PfOiTRSS)l})T ziNMdXGPqX~b}Sryp$70V5&R8L@Vg{zGwzwx9if@4_GM;A!o(@1hg|?Jl!XyS$x02_ zinei7MkCm+0Bho6r?5=t@u36QT1>Q-hOkruwiJE4rn@-E0UZc^*%MvDV_^&(#_zFY fL>IyT1XNom;{P=08zmeN00000NkvXXu0mjfuFu{3 literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/shield-checkmark@2x.png b/apps/rebreak-native/assets/tabs/shield-checkmark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..191f722785f58cf594d2006722f18155ae10bf60 GIT binary patch literal 961 zcmV;y13vtTP)BsD~jCm$^j`25~{b2^tsl5b>``SH&u;j5KQ+Y-=5EN&eo2zZz`2RyN{!9L7Qt|KJr|FvPq{ z(ZUw&M#qw|Zm|cq;iREtnv;##i63N}>K5N)2hM3a>3*!oLqbpV>#afKYdnMLvdI;h zWF~O2a0za~HJB`$NDn&r5+5e92mj>}7{uUNm`UO?T!~Y1rCA+E@fl{Z4`1NNtP{E! z+$2orGq?n&SDRzy9I|9~7e3EA_#(7bGmeH4ZCsc()QSvl7RK1PGJz}7U&IFMnk<@R zSqH@i$GP_wSynK_2FIeB=);NKgSi&b8u$Y*;k^6-9{a6?a9RGuRroIX{64NN zptHU-G#89QUtqpPA-rp};6>ZXRb2Vh}#f zDgJ8e+>`N6mt*Fv{12ZiR4-Lr8J`Sl4z}eKe?2;XE#8ifc`Q2S48?Zedd0Rp1~lznv=VtyE>MljNL>*_@5Eg~=k#1Rl41wr=6g3e7Dfx#s5X0u9aK z-afi7RBUd+%2ID2Ch=2&#^!Nf56uT_F!!^S1;fBRU9iy(9*MNBQ_qgwQ$@|el;X8% zN7#O!!VUPl26G1z{Zc$oLt<4$uc45O7WR&SnK<_8UP~8Zv(eh|ZVS&zDrM&g7+Z`T zRhuI$+ErE`MPsiTci_(!Hg$$&?qHHbt!xr*dzv-!qnp4(@EkS&I8L zW3q!s8}hV*JJh>;WYx8eaVw6f-rSKydnCA2wfm{R3(qM=ifMeHyt!EoECofGz_W^t z%Ql`fCeJ8mu;1Y3_6zIlSkb~gvd88jJYh_Ftrt_mWt)DT^006@mcCi-N0TqaP6@9~ juE86^JEN(zjK^$=GZuvBo#_IXmlmIV1_;P5L$GDY9J+} zdkBdSJ!C|r^w2{P6kV|Bp@&L>vddsap=R1BHWiVAMGsnOsA;A)hR&QG{$rjwv)0~w z?Xzy@v_JS6W}i7{{p&k>_GPVqFDNJ|C@3h1gi`1PQs@9~18xDTz(>G)!10htwIY*% zCBP0uWPR)gmH}skO0Jc-3|IjiVXVl;I10Q2%np)i#FT-XfzL>nsEeK2Z0}e!MfOnf#fuy(}1gi%Q-3Vv^<%%F#C}`cd+(z0J$~Qbfmj|`qNPoJ{Sb91rFDKUj=*!++?{Or6b)Q+Nwf#AoEj)>$X*aHypHe zK($dPbf1F+Tyqe(5gFrb#KoPT(veQ6@$gh&jsbTd$IeE&fJYs)Y2;`&;YV{!;$clc zO2{yzi#D&!u2V+kLrcmUPbc)_S%>YOnb9R6-|T@1WTnMQ^s^2RIBa*nfy@8c7=f^) zSPzWXhg@zhA^g8}WBiTG!m8*7zVnpF`6ePyPzL_+EG#L8k>XnwtV~)=0LjBlvyTz|~tdm{np+lkLE^D?jP{QurQ*5z` z0IOYGfJ{k*?!4_7LZ*L$usB14trm-|8ZxAW{b`br@hK53Y%>M9*9|SB+1Lx5W!X(` zt|#p6VY6`@sWyp<2MI4GZv;JVCh;8M<(S6vPE1&$jI8_>T2j_Xno*603h`I&RguA% zmRZzg0Sm!1XWJfx)Bwajfuz5V}!PTU?>S72V5(iY6a?)=m) z;8w3ah>(fM9@WeVYZ!P9_fLPc6c-}*)}}HC&_5Yc09=Pu3a5$Oj=IdeKtTtxpzDu} z75NkL^l_96SjT>`TSiVYeMqkFBEy5qC4LGD3JMAe3hMY5vLSzmdkj%|00000NkvXX Hu0mjf&Frqh literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/sparkles.png b/apps/rebreak-native/assets/tabs/sparkles.png new file mode 100644 index 0000000000000000000000000000000000000000..677a8694744c43533589717feca4a5d6332d416e GIT binary patch literal 517 zcmV+g0{Z=lP)f~apmOVij{ zX`wb=W9KW-Mgm$1+GrsNqQ*C1L;?}c#7YHSi@D|I#@XC%HkThf&Ft@)@BDe@*(sx% z#s}=jE4!czD6o#17W2$wwvO@X09fGSHep?C;6;^TTG-jM2v(qv9u^w{uMYD6;z-W0 zl7Mn?qXFYj(kf^VeljaEO5QT ze?D5&!KSbUqem3SC;5Kc5vnm;AXPe!hxkzeD`OS6aH#1{V4v`S-;aUh;tyUA^7qz( z^;92S)9B;UR3IuZZpY0pGgymcub|=00000NkvXX Hu0mjfGeqmb literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/assets/tabs/sparkles@2x.png b/apps/rebreak-native/assets/tabs/sparkles@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1afa07d22d747a817a26897b18d0348b8297375d GIT binary patch literal 1027 zcmV+e1pNDnP)a;jBHuLkJ+dpUKAA8~5 zo%4O~J2U4y-#KSMkK@2J@DM067{9Er9+(9ZNw$P5$30H*&s+lN4_A(Ryd%@#98ejO zmbE|!aKKf-Ix@|rc*a!%OT0?c(BvZEP5cdxkpE&+@6qrin_zVabBp*JOaM<21*asN zL>9Q^$-e+p*#w9JZNOXPru;YoTmxzyn2@0inDWVe!8E`zGPi=DybDAe81mwXB;P28 zOXZs+KoHhAv6P7cMZUcz0d6Y-*f0jHbmM6W)u?Mhk4caSqG0qR_BmT)Lx}ae#*L>X zR0EGSx&HvISbVURh<$EE6Zk98jr*Tu8Dh;1p#J8({DCNTyG#=mA+`dUxpfZAq@y0O z75tRswBazK> zHV$+l&a+zdBqzmCZv_#=nv*4M)#Rx{v|pnEP0ay)Kr>KmVk}+wPV{?CgGRtJ!+b@E z4y6aN&*_ZCfKFhesiB~_qY<(QY_*Kps)-pRPbZpk;I~Fh0=Q@yw-orUh}Y>ryAh5m zM5OZ*mg@u2q4>p$1DhOZH^OU)2tPJj#;%pb>2o361T_)?GCl~9W7sE7bGE`wan&ce zinE9o%?2Hw7!%~FxRes?z=db-5I4gWZVh#A&iK=Eo7>EP3cgyx5#f{2`!lTh0QHfWzdlh!g|+5#>Cd zCu4pbM;zC-xiVIf4CwM2(FGR~jryK!%te5g1z<+SIO4<-2(osl(AaBhEOqsru$>qZ zY#a3qydeB$tCl$(aZ2(wrYMz zHQP3)ct1`8*D0W8+YjdmW^Rn|{*GdW6^Pd+TZ1RV5E~TT*cTGpD~nK(k4vY}VRO2- zy*4Zev|(oMGgMS z1ueKyu`1e~8!Jc`R*DF2s#U2{%*NPKP!Ke6V^#bp1vL`1+NxDdLNp<1o{Q6wmt^M6 z{W$l|O!9tkHsQ@V|2c2o+0Z7ySOGV8z; zu(n81WundU^IKE?)(+gOpk>L?4*br-E3^)LU7)VYau?~zOzf)Sq&c$!cnUZK>;le< z8T=s46Z%R_F-q)W?m2u0ERPYm3z(sKLjML<#So*!8sJ|Rp2zJzPE7};t>k}m=ODL* zs&F6Bk@EMnfme-nz_Y+VG`~6%I|*!~8k7AmNk0emX<$2W2e2QQ&Ip=kQ^+dnZsf*a zHm;<({wFkoYye&We#;4FYrmZK+p8f`iI-`v`CA|7y)0Z#hOk-6vG(r({vuy*pKE|2 z1LvCdma9-D?!ipwtF8e|2OKRs;wn^$4=~d?io-Op4fse)sUflBHPQ{-VEEayahwL$ zfH#3j!d{u$Yl8}A90>^Bgu5RX!hYTKqXv? zOm$8=2&e;pA~*HReT8^%CNd8mAb;6xVn>1P!1+F+RsvfvPwbDtX5b8;pq^|*=9wl_ zBNv+j`hiV&6jHVs2HWf!z>_t=V`MKUL)kIld0@S*;2Qb&pA*<$nHK?jk;3&DGNrlB z;`kN7O9t9P+bnPZ%iF>J9H%>NoFOH_pHuuy0yi1@csfT@Y8leKE&JY&&e32cFAtWLuPd)Ii+327YSD zNfe#PIx>b_W0?nfkQ?y)%iMO-{)K@+(?V|>GKJ|seN5~`CJUFQv~> zpwBY)GfHuT&<&iz?PvFZqK>Z&V@~7nGq>%&^Wi~{cn>M+FzshO2pm_8t4bR%g8K+N zi$g^x1Cv5e9U09_%PqJkGqD|tdirx>xm3=g@(%t9J%(Oq+@_pZww1G}m=r=x&8su6 zfo6rCIvy*a7=zONq<6Wwpfxz1BD>+@Q)K_Bv}kTiXm-Y>6?*CfZfEqxb|P(Jnv?AV z^L(GcqiPhIPQq-Ojp4t(Pq+Z7o=pIsEBaL>*~ZnX@}$`e3R}5l=3L-mK*~_D*J{-%9j!g3Ky(sj>NN^n z9q~L$MTmX*?NQI5glKn;ly>?9aeABfDEb;e9<*K3l(GA+@|3MVG#IgZ>@|M&dyzK8 z7L=>UJ}9xS&{^PhkK;cEe$bJ!c<9FcIi)S}/dev/null || true +fi + +# 3. Prebuild: regeneriert ios/ aus app.config.ts + Config-Plugins +# Dank with-fmt-consteval-fix-Plugin wird das Podfile auto-gepatcht. +echo "→ pnpm expo prebuild --clean" +pnpm expo prebuild --clean + +# 4. Pod install +echo "→ cd ios && pod install" +(cd ios && pod install) + +echo "" +echo "✅ Clean done." +echo "" + +case "$MODE" in + --build|build) + echo "🔨 Building + running on connected device/simulator..." + pnpm ios + ;; + + --xcode|xcode) + echo "🔨 Opening Xcode-Workspace..." + osascript -e 'tell application "Xcode" to close every window whose name contains "Rebreak.xcodeproj"' 2>/dev/null || true + open -a Xcode ios/Rebreak.xcworkspace + echo "" + echo "ℹ️ In Xcode: Cmd+R für Build & Run" + ;; + + "") + echo "ℹ️ Nächste Schritte:" + echo " ./dev-ios.sh # Xcode öffnen (manueller Build)" + echo " pnpm ios # CLI-Build auf Sim/Device" + echo " ./clean-ios.sh --build # alles in einem Rutsch" + ;; + + *) + echo "Unknown mode: $MODE" + echo "Usage: ./clean-ios.sh [--build|--xcode]" + exit 1 + ;; +esac diff --git a/apps/rebreak-native/components/AppHeader.tsx b/apps/rebreak-native/components/AppHeader.tsx new file mode 100644 index 0000000..11e21f0 --- /dev/null +++ b/apps/rebreak-native/components/AppHeader.tsx @@ -0,0 +1,238 @@ +import { useState } from 'react'; +import { View, Text, Pressable, Modal, Image } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Ionicons } from '@expo/vector-icons'; +import { useRouter, type RelativePathString } from 'expo-router'; +import { useTranslation } from 'react-i18next'; +import { useAuthStore } from '../stores/auth'; +import { useNotificationStore } from '../stores/notifications'; +import { supabase } from '../lib/supabase'; +import { resolveAvatar } from '../lib/resolveAvatar'; +import { useMe } from '../hooks/useMe'; +import { NotificationsDropdown } from './NotificationsDropdown'; + +type Props = { + notifCount?: number; +}; + +type MenuItem = { + icon: React.ComponentProps['name']; + label: string; + color?: string; + action: () => void; +}; + +export function AppHeader({ notifCount }: Props = {}) { + const insets = useSafeAreaInsets(); + const router = useRouter(); + const { t } = useTranslation(); + const { user } = useAuthStore(); + const { me } = useMe(); + const storeUnread = useNotificationStore((s) => s.unread); + const badge = notifCount ?? storeUnread; + const [dropdownOpen, setDropdownOpen] = useState(false); + const [notifOpen, setNotifOpen] = useState(false); + + const firstName = (user?.user_metadata?.first_name as string | undefined) ?? ''; + const lastName = (user?.user_metadata?.last_name as string | undefined) ?? ''; + const email = user?.email ?? ''; + // Initials-Fallback: erst nickname (DB), dann firstName/email + const initials = (() => { + if (me?.nickname) return me.nickname.slice(0, 2).toUpperCase(); + return ((firstName.charAt(0) + (lastName.charAt(0) || email.charAt(0))).toUpperCase() || '?'); + })(); + + // Avatar: aus DB (`/api/auth/me` → profiles.avatar). Kann Hero-Avatar-ID + // ("spider"/"hulk"/...) ODER Custom-Photo-URL (https://... von Foto-Upload) + // sein. resolveAvatar handlet beide Fälle. + // user_metadata.avatar_id ist veraltet — wird bei Profile-Edit nicht + // aktualisiert. DB ist Single Source of Truth. + const avatarUrl = me ? resolveAvatar(me.avatar, me.nickname ?? '') : ''; + const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); + const showAvatarImage = !!avatarUrl && !avatarLoadFailed && !!me?.avatar; + + function closeAndNavigate(path: RelativePathString) { + setDropdownOpen(false); + router.push(path); + } + + async function handleSignOut() { + setDropdownOpen(false); + await supabase.auth.signOut(); + router.replace('/' as RelativePathString); + } + + const menuItems: MenuItem[] = [ + { + icon: 'person-outline', + label: t('appHeader.editProfile'), + action: () => closeAndNavigate('/settings' as RelativePathString), + }, + { + icon: 'settings-outline', + label: t('appHeader.settings'), + action: () => closeAndNavigate('/settings' as RelativePathString), + }, + ]; + + const headerHeight = insets.top + 56; + + return ( + + + + {t('appHeader.appName')} + + + + {/* Notifications dropdown trigger */} + setNotifOpen(true)} + className="w-9 h-9 rounded-full bg-white items-center justify-center" + style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} + > + + {badge > 0 && ( + + + {badge > 9 ? '9+' : String(badge)} + + + )} + + + {/* Profil-Avatar — tap → dropdown */} + setDropdownOpen(true)} + className={`w-9 h-9 rounded-full items-center justify-center overflow-hidden ${showAvatarImage ? 'bg-neutral-100' : 'bg-rebreak-500'}`} + style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} + > + {showAvatarImage ? ( + setAvatarLoadFailed(true)} + style={{ width: 36, height: 36, borderRadius: 18 }} + /> + ) : ( + {initials} + )} + + + + + {/* Dropdown modal */} + setDropdownOpen(false)} + > + setDropdownOpen(false)} + style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.18)' }} + > + true} + style={{ + position: 'absolute', + top: headerHeight + 6, + right: 12, + backgroundColor: '#ffffff', + borderRadius: 18, + shadowColor: '#000', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.18, + shadowRadius: 20, + elevation: 12, + minWidth: 260, + overflow: 'hidden', + }} + > + {/* SOS prominent oben — Pressable mit innerem Row-View */} + closeAndNavigate('/urge' as RelativePathString)}> + + + + + + + {t('appHeader.sosLabel')} + + + {t('appHeader.sosSubtitle')} + + + + + + + + + {menuItems.map((item) => ( + + + + + {item.label} + + + + ))} + + + + + + + + {t('appHeader.signOut')} + + + + + + + + setNotifOpen(false)} + topOffset={headerHeight} + /> + + ); +} diff --git a/apps/rebreak-native/components/BrandSplash.tsx b/apps/rebreak-native/components/BrandSplash.tsx new file mode 100644 index 0000000..961ddb4 --- /dev/null +++ b/apps/rebreak-native/components/BrandSplash.tsx @@ -0,0 +1,412 @@ +import { useEffect, useRef } from 'react'; +import { Animated, Dimensions, Image, Text, View } from 'react-native'; +import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg'; +import { useTranslation } from 'react-i18next'; + +// Phase-Timings (ms ab Mount) — 1:1 portiert aus apps/rebreak/app/components/AppSplash.vue +const T_GLOW = 0; +const T_NAME = 300; +const T_LOGO = 700; +const T_PULSE = 1100; +const T_TAGLINE = 1300; +const T_SUB = 1700; +const T_HOLD_END = 3200; +const T_LEAVE_DUR = 500; + +const { width: SW, height: SH } = Dimensions.get('window'); + +type ParticleConfig = { + size: number; + top?: number; + bottom?: number; + left?: number; + right?: number; + duration: number; + delay: number; +}; + +const PARTICLES: ParticleConfig[] = [ + { size: 180, top: -40, left: -60, duration: 7000, delay: 0 }, + { size: 120, bottom: SH * 0.1, right: -30, duration: 9000, delay: 1500 }, + { size: 80, top: SH * 0.35, left: SW * 0.08, duration: 11000, delay: 800 }, + { size: 60, bottom: SH * 0.2, left: SW * 0.2, duration: 8000, delay: 2200 }, + { size: 100, top: SH * 0.15, right: SW * 0.1, duration: 10000, delay: 400 }, +]; + +function Particle({ config }: { config: ParticleConfig }) { + const translateY = useRef(new Animated.Value(0)).current; + const scale = useRef(new Animated.Value(1)).current; + const opacity = useRef(new Animated.Value(0.6)).current; + + useEffect(() => { + const animate = () => { + Animated.loop( + Animated.sequence([ + Animated.parallel([ + Animated.timing(translateY, { + toValue: 18, + duration: config.duration, + useNativeDriver: true, + }), + Animated.timing(scale, { + toValue: 1.1, + duration: config.duration, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 1, + duration: config.duration, + useNativeDriver: true, + }), + ]), + Animated.parallel([ + Animated.timing(translateY, { + toValue: 0, + duration: config.duration, + useNativeDriver: true, + }), + Animated.timing(scale, { + toValue: 1, + duration: config.duration, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 0.6, + duration: config.duration, + useNativeDriver: true, + }), + ]), + ]), + ).start(); + }; + const t = setTimeout(animate, config.delay); + return () => clearTimeout(t); + }, [config, translateY, scale, opacity]); + + return ( + + ); +} + +export function BrandSplash() { + const { t } = useTranslation(); + // Phase-Opacity-Animationen + const containerOpacity = useRef(new Animated.Value(1)).current; + const glowCenterOpacity = useRef(new Animated.Value(0)).current; + const glowCenterScale = useRef(new Animated.Value(0.6)).current; + const glowTopOpacity = useRef(new Animated.Value(0.5)).current; + + const nameOpacity = useRef(new Animated.Value(0)).current; + const nameTranslateY = useRef(new Animated.Value(12)).current; + + const logoOpacity = useRef(new Animated.Value(0)).current; + const logoScale = useRef(new Animated.Value(0.82)).current; + const logoTranslateY = useRef(new Animated.Value(8)).current; + const logoPulse = useRef(new Animated.Value(1)).current; + + const taglineOpacity = useRef(new Animated.Value(0)).current; + const taglineTranslateY = useRef(new Animated.Value(8)).current; + + const subOpacity = useRef(new Animated.Value(0)).current; + const subTranslateY = useRef(new Animated.Value(6)).current; + + const footerOpacity = useRef(new Animated.Value(0)).current; + + useEffect(() => { + // Top-glow breath loop (4s alternating) — startet sofort + Animated.loop( + Animated.sequence([ + Animated.timing(glowTopOpacity, { + toValue: 0.9, + duration: 2000, + useNativeDriver: true, + }), + Animated.timing(glowTopOpacity, { + toValue: 0.5, + duration: 2000, + useNativeDriver: true, + }), + ]), + ).start(); + + const ease = (toValue: number, duration: number) => ({ + toValue, + duration, + useNativeDriver: true, + }); + + // Phase 1: glow center bloom (T=0) + Animated.parallel([ + Animated.timing(glowCenterOpacity, ease(1, 900)), + Animated.timing(glowCenterScale, ease(1, 900)), + ]).start(); + + // Phase 2: Name fade-in (T=300) + setTimeout(() => { + Animated.parallel([ + Animated.timing(nameOpacity, ease(1, 600)), + Animated.timing(nameTranslateY, ease(0, 600)), + ]).start(); + }, T_NAME); + + // Phase 3: Logo bouncy scale-in (T=700) + setTimeout(() => { + Animated.parallel([ + Animated.timing(logoOpacity, ease(1, 650)), + Animated.spring(logoScale, { + toValue: 1, + useNativeDriver: true, + friction: 6, + tension: 80, + }), + Animated.timing(logoTranslateY, ease(0, 650)), + ]).start(); + }, T_LOGO); + + // Phase 3b: Logo breathing pulse (T=1100) + setTimeout(() => { + Animated.loop( + Animated.sequence([ + Animated.timing(logoPulse, { + toValue: 1.04, + duration: 1300, + useNativeDriver: true, + }), + Animated.timing(logoPulse, { + toValue: 1, + duration: 1300, + useNativeDriver: true, + }), + ]), + ).start(); + }, T_PULSE); + + // Phase 4: Tagline (T=1300) + setTimeout(() => { + Animated.parallel([ + Animated.timing(taglineOpacity, ease(1, 550)), + Animated.timing(taglineTranslateY, ease(0, 550)), + ]).start(); + }, T_TAGLINE); + + // Phase 5: Sub-text + Footer (T=1700) + setTimeout(() => { + Animated.parallel([ + Animated.timing(subOpacity, ease(1, 500)), + Animated.timing(subTranslateY, ease(0, 500)), + Animated.timing(footerOpacity, ease(1, 600)), + ]).start(); + }, T_SUB); + + // Phase 7: whole-screen fade-out (T=3200, dauert 500ms) + const fadeOutTimer = setTimeout(() => { + Animated.timing(containerOpacity, { + toValue: 0, + duration: T_LEAVE_DUR, + useNativeDriver: true, + }).start(); + }, T_HOLD_END); + + return () => clearTimeout(fadeOutTimer); + }, [ + glowTopOpacity, + glowCenterOpacity, + glowCenterScale, + nameOpacity, + nameTranslateY, + logoOpacity, + logoScale, + logoTranslateY, + logoPulse, + taglineOpacity, + taglineTranslateY, + subOpacity, + subTranslateY, + footerOpacity, + containerOpacity, + ]); + + return ( + + {/* Top breathing radial-gradient ellipse (#1e3a8a auf transparent) */} + + + + + + + + + + + + + {/* Center indigo halo — bloomt rein wenn Logo erscheint */} + + + + + + + + + + + + + {/* Floating particles (5 Stück) */} + {PARTICLES.map((p, i) => ( + + ))} + + {/* Content-Column */} + + {/* App-Name */} + + {t('appHeader.appName')} + + + {/* Logo (mit Pulse + Bouncy Entry) */} + + + + + {/* Tagline */} + + {t('splash.tagline')} + + + {/* Sub-text */} + + {t('splash.subtitle')} + + + + {/* Footer */} + + {t('splash.madeInGermany')} + + + ); +} diff --git a/apps/rebreak-native/components/Button.tsx b/apps/rebreak-native/components/Button.tsx new file mode 100644 index 0000000..ce01a4e --- /dev/null +++ b/apps/rebreak-native/components/Button.tsx @@ -0,0 +1,57 @@ +import { ActivityIndicator, Pressable, Text } from 'react-native'; +import type { PressableProps } from 'react-native'; + +type Variant = 'primary' | 'secondary' | 'ghost' | 'danger'; + +type Props = PressableProps & { + children: React.ReactNode; + variant?: Variant; + loading?: boolean; + disabled?: boolean; + className?: string; +}; + +const variantStyles: Record = { + primary: { + container: 'bg-rebreak-500 rounded-xl px-5 py-3.5 items-center justify-center', + text: 'text-white text-base', + }, + secondary: { + container: 'bg-neutral-100 border border-neutral-200 rounded-xl px-5 py-3.5 items-center justify-center', + text: 'text-neutral-800 text-base', + }, + ghost: { + container: 'rounded-xl px-5 py-3.5 items-center justify-center', + text: 'text-neutral-600 text-base', + }, + danger: { + container: 'bg-red-50 border border-red-200 rounded-xl px-5 py-3.5 items-center justify-center', + text: 'text-red-600 text-base', + }, +}; + +export function Button({ + children, + variant = 'primary', + loading = false, + disabled = false, + className = '', + ...rest +}: Props) { + const styles = variantStyles[variant]; + const isDisabled = disabled || loading; + + return ( + + {loading ? ( + + ) : ( + {children} + )} + + ); +} diff --git a/apps/rebreak-native/components/Card.tsx b/apps/rebreak-native/components/Card.tsx new file mode 100644 index 0000000..e339494 --- /dev/null +++ b/apps/rebreak-native/components/Card.tsx @@ -0,0 +1,19 @@ +import { View } from 'react-native'; +import type { ViewProps } from 'react-native'; + +type Props = ViewProps & { + children: React.ReactNode; + className?: string; +}; + +export function Card({ children, className = '', style, ...rest }: Props) { + return ( + + {children} + + ); +} diff --git a/apps/rebreak-native/components/ComposeCard.tsx b/apps/rebreak-native/components/ComposeCard.tsx new file mode 100644 index 0000000..9522ac4 --- /dev/null +++ b/apps/rebreak-native/components/ComposeCard.tsx @@ -0,0 +1,174 @@ +import { useState, useRef } from 'react'; +import { + View, + Text, + TextInput, + Pressable, + Image, + ActivityIndicator, + Alert, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import * as FileSystem from 'expo-file-system'; +import * as ImagePicker from 'expo-image-picker'; +import { apiFetch } from '../lib/api'; +import { resolveAvatar } from '../lib/resolveAvatar'; +import { useAuthStore } from '../stores/auth'; +import { colors } from '../lib/theme'; + +type Props = { + onPosted?: () => void; +}; + +export function ComposeCard({ onPosted }: Props) { + const { t } = useTranslation(); + const { user } = useAuthStore(); + const queryClient = useQueryClient(); + const inputRef = useRef(null); + const [focused, setFocused] = useState(false); + const [content, setContent] = useState(''); + const [imageUri, setImageUri] = useState(null); + const [posting, setPosting] = useState(false); + + const avatarId = user?.user_metadata?.avatar_id as string | undefined; + const nickname = (user?.user_metadata?.username as string | undefined) ?? t('community.compose_default_user'); + const avatarUrl = resolveAvatar(avatarId ?? null, nickname); + + const cancel = () => { + setContent(''); + setImageUri(null); + setFocused(false); + inputRef.current?.blur(); + }; + + const pickImage = async () => { + const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!perm.granted) { + Alert.alert(t('community.compose_photo_perm_title'), t('community.compose_photo_perm_desc')); + return; + } + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: false, + quality: 0.8, + }); + if (!result.canceled && result.assets[0]?.uri) { + setImageUri(result.assets[0].uri); + } + }; + + const submit = async () => { + if (!content.trim() && !imageUri) return; + setPosting(true); + try { + let uploadedImageUrl: string | undefined; + + if (imageUri) { + const base64 = await FileSystem.readAsStringAsync(imageUri, { + encoding: FileSystem.EncodingType.Base64, + }); + const upload = await apiFetch<{ url: string }>('/api/community/upload-image', { + method: 'POST', + body: { + image: `data:image/jpeg;base64,${base64}`, + mimeType: 'image/jpeg', + }, + }); + uploadedImageUrl = upload?.url; + } + + await apiFetch('/api/community/post', { + method: 'POST', + body: { + category: 'story', + content: content.trim(), + ...(uploadedImageUrl ? { imageUrl: uploadedImageUrl } : {}), + }, + }); + cancel(); + queryClient.invalidateQueries({ queryKey: ['community-posts'] }); + onPosted?.(); + } catch (err: any) { + Alert.alert(t('common.error'), err?.message ?? t('community.post_failed')); + } finally { + setPosting(false); + } + }; + + const showActions = focused || content.length > 0; + + return ( + + + + + setFocused(true)} + placeholder={t('community.compose_placeholder')} + placeholderTextColor="#a3a3a3" + multiline + className="text-sm text-neutral-900 leading-5 min-h-[40px]" + style={{ textAlignVertical: 'top', fontFamily: 'Nunito_400Regular' }} + /> + {imageUri && ( + + + setImageUri(null)} + className="absolute top-2 right-2 w-7 h-7 rounded-full bg-black/50 items-center justify-center" + > + + + + )} + + + + {showActions && ( + + ({ opacity: pressed ? 0.6 : 1 })} + > + + {t('community.image')} + + + + + {t('common.cancel')} + + ({ + opacity: pressed || !content.trim() || posting ? 0.5 : 1, + })} + > + {posting ? ( + + ) : ( + {t('community.share')} + )} + + + + )} + + ); +} diff --git a/apps/rebreak-native/components/ConfirmAlert.tsx b/apps/rebreak-native/components/ConfirmAlert.tsx new file mode 100644 index 0000000..9d502dd --- /dev/null +++ b/apps/rebreak-native/components/ConfirmAlert.tsx @@ -0,0 +1,204 @@ +import { useEffect, useRef } from 'react'; +import { Modal, View, Text, Pressable, Animated, Easing } from 'react-native'; +// Wichtig (UX-Entscheidung 2026-05-05): Icon im Confirm-Modal NICHT animieren — +// User sieht zwei Modals nacheinander (Confirm → Success), beide animierte Icons +// = visuelle Doppel-Eskalation, wirkt verwirrend. Daher: Card animiert auf, +// Icon erscheint statisch (kein scale-pop). Nur das nachfolgende SuccessAlert +// behält seine Icon-Animation als "Belohnungs-Moment". +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; + +type Props = { + visible: boolean; + title: string; + message?: string; + /** Default: i18n "common.cancel" */ + cancelLabel?: string; + /** Default: i18n "common.confirm" */ + confirmLabel?: string; + /** Wenn true: Confirm-Button rot statt blau (für destructive Actions). */ + destructive?: boolean; + /** Icon im Top-Circle. Default: question-mark. */ + icon?: React.ComponentProps['name']; + /** Icon-Circle-Color. Default: #007AFF (iOS-blue). */ + iconColor?: string; + onConfirm: () => void; + onCancel: () => void; +}; + +/** + * Animiertes iOS-style Confirm-Modal — gleicher Animations-Stil wie SuccessAlert, + * aber mit zwei Buttons (Cancel + Confirm). Tap-outside cancelt. + */ +export function ConfirmAlert({ + visible, + title, + message, + cancelLabel, + confirmLabel, + destructive = false, + icon = 'help-circle', + iconColor = '#007AFF', + onConfirm, + onCancel, +}: Props) { + const { t } = useTranslation(); + const resolvedCancelLabel = cancelLabel ?? t('common.cancel'); + const resolvedConfirmLabel = confirmLabel ?? t('common.confirm'); + const cardScale = useRef(new Animated.Value(0.8)).current; + const cardOpacity = useRef(new Animated.Value(0)).current; + + useEffect(() => { + if (visible) { + cardScale.setValue(0.8); + cardOpacity.setValue(0); + + Animated.parallel([ + Animated.spring(cardScale, { + toValue: 1, + useNativeDriver: true, + friction: 7, + tension: 80, + }), + Animated.timing(cardOpacity, { + toValue: 1, + duration: 220, + useNativeDriver: true, + easing: Easing.out(Easing.cubic), + }), + ]).start(); + } + }, [visible, cardScale, cardOpacity]); + + const confirmBg = destructive ? '#FF3B30' : '#007AFF'; + + return ( + + + {}} style={{ width: '85%', maxWidth: 340 }}> + + {/* Icon-Circle — statisch (keine Pop-Animation, siehe Header-Comment). */} + + + + + + {title} + + {message && ( + + {message} + + )} + + {/* Two buttons row */} + + + + + {resolvedCancelLabel} + + + + + + + {resolvedConfirmLabel} + + + + + + + + + ); +} diff --git a/apps/rebreak-native/components/EmptyState.tsx b/apps/rebreak-native/components/EmptyState.tsx new file mode 100644 index 0000000..0abba88 --- /dev/null +++ b/apps/rebreak-native/components/EmptyState.tsx @@ -0,0 +1,25 @@ +import { View, Text } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import type { ComponentProps } from 'react'; + +type Props = { + icon: ComponentProps['name']; + title: string; + subtitle?: string; + children?: React.ReactNode; +}; + +export function EmptyState({ icon, title, subtitle, children }: Props) { + return ( + + + + + {title} + {subtitle ? ( + {subtitle} + ) : null} + {children ? {children} : null} + + ); +} diff --git a/apps/rebreak-native/components/HeroShieldCheck.tsx b/apps/rebreak-native/components/HeroShieldCheck.tsx new file mode 100644 index 0000000..eca2958 --- /dev/null +++ b/apps/rebreak-native/components/HeroShieldCheck.tsx @@ -0,0 +1,23 @@ +// HeroIcons shield-check — Replacement für Ionicons "shield-checkmark" +// (User-Entscheidung 2026-05-05: HeroIcons-Stil gefällt besser für Domain- +// Approved-Indikator). Inline-SVG via react-native-svg, kein Extra-Package. +// +// Quelle: heroicons.com/solid → shield-check (Apache 2.0) +import Svg, { Path } from 'react-native-svg'; + +type Props = { + size?: number; + color?: string; +}; + +export function HeroShieldCheck({ size = 18, color = '#22c55e' }: Props) { + return ( + + + + ); +} diff --git a/apps/rebreak-native/components/IconButton.tsx b/apps/rebreak-native/components/IconButton.tsx new file mode 100644 index 0000000..3eac807 --- /dev/null +++ b/apps/rebreak-native/components/IconButton.tsx @@ -0,0 +1,31 @@ +import { Pressable } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import type { PressableProps } from 'react-native'; +import type { ComponentProps } from 'react'; + +type Props = PressableProps & { + name: ComponentProps['name']; + size?: number; + color?: string; + className?: string; + badge?: number; +}; + +export function IconButton({ + name, + size = 22, + color = '#0a0a0a', + className = '', + badge, + ...rest +}: Props) { + return ( + + + + ); +} diff --git a/apps/rebreak-native/components/NativeTabs.tsx b/apps/rebreak-native/components/NativeTabs.tsx new file mode 100644 index 0000000..d37cf63 --- /dev/null +++ b/apps/rebreak-native/components/NativeTabs.tsx @@ -0,0 +1,152 @@ +import { + createNavigatorFactory, + TabRouter, + useNavigationBuilder, + type DefaultNavigatorOptions, + type NavigationProp, + type ParamListBase, + type TabActionHelpers, + type TabNavigationState, + type TabRouterOptions, +} from '@react-navigation/native'; +import { withLayoutContext } from 'expo-router'; +import type { ImageSourcePropType } from 'react-native'; +import TabView, { type AppleIcon } from 'react-native-bottom-tabs'; + +// Pro-Screen Optionen (kompatibel mit Expo Router's Tabs.Screen API) +export type NativeTabsScreenOptions = { + title?: string; + tabBarIcon?: (props: { focused: boolean }) => AppleIcon | ImageSourcePropType; + tabBarBadge?: string; + // Expo-Router-Konvention: href === null → Screen NICHT in TabBar zeigen + href?: string | null; +}; + +type NativeTabNavigationEventMap = { + tabPress: { data: undefined; canPreventDefault: true }; + tabLongPress: { data: undefined }; +}; + +// Native-spezifische Tab-Layer-Optionen (iOS 26 Glass-Pill, Haptik, etc.) +type NativeOnlyOptions = { + sidebarAdaptable?: boolean; + hapticFeedbackEnabled?: boolean; + disablePageAnimations?: boolean; + scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent'; + minimizeBehavior?: 'automatic' | 'onScrollDown' | 'onScrollUp' | 'never'; + tabBarActiveTintColor?: string; + tabBarInactiveTintColor?: string; + labeled?: boolean; + tabLabelStyle?: { + fontFamily?: string; + fontWeight?: string; + fontSize?: number; + }; +}; + +type Props = DefaultNavigatorOptions< + ParamListBase, + string | undefined, + TabNavigationState, + NativeTabsScreenOptions, + NativeTabNavigationEventMap, + NavigationProp +> & + TabRouterOptions & + NativeOnlyOptions; + +function NativeTabsNavigator({ + id, + initialRouteName, + children, + screenOptions, + layout, + sidebarAdaptable = true, + hapticFeedbackEnabled = true, + disablePageAnimations, + scrollEdgeAppearance, + minimizeBehavior, + tabBarActiveTintColor, + tabBarInactiveTintColor, + labeled = true, + tabLabelStyle, +}: Props) { + const { state, descriptors, navigation, NavigationContent } = + useNavigationBuilder< + TabNavigationState, + TabRouterOptions, + TabActionHelpers, + NativeTabsScreenOptions, + NativeTabNavigationEventMap + >(TabRouter, { + id, + initialRouteName, + children, + screenOptions, + layout, + }); + + return ( + + { + const options = descriptors[route.key].options; + return { + key: route.key, + title: options.title ?? route.name, + focusedIcon: options.tabBarIcon + ? options.tabBarIcon({ focused: true }) + : undefined, + unfocusedIcon: options.tabBarIcon + ? options.tabBarIcon({ focused: false }) + : undefined, + badge: options.tabBarBadge, + hidden: options.href === null, + }; + }), + }} + onIndexChange={(index) => { + const route = state.routes[index]; + const event = navigation.emit({ + type: 'tabPress', + target: route.key, + canPreventDefault: true, + }); + if (!event.defaultPrevented) { + navigation.dispatch({ + type: 'NAVIGATE', + payload: { name: route.name, merge: true }, + target: state.key, + }); + } + }} + onTabLongPress={(index) => { + const route = state.routes[index]; + navigation.emit({ + type: 'tabLongPress', + target: route.key, + }); + }} + renderScene={({ route }) => descriptors[route.key].render()} + /> + + ); +} + +export const createNativeTabNavigator = createNavigatorFactory(NativeTabsNavigator); + +const NativeTabNav = createNativeTabNavigator(); + +// withLayoutContext-wrapped Navigator für Expo-Router-Kompatibilität +export const NativeTabs = withLayoutContext(NativeTabNav.Navigator); diff --git a/apps/rebreak-native/components/NotificationsDropdown.tsx b/apps/rebreak-native/components/NotificationsDropdown.tsx new file mode 100644 index 0000000..aa79605 --- /dev/null +++ b/apps/rebreak-native/components/NotificationsDropdown.tsx @@ -0,0 +1,321 @@ +import { useEffect, useRef } from 'react'; +import { View, Text, Pressable, Modal, FlatList, Animated, Image } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useRouter, type RelativePathString } from 'expo-router'; +import { useTranslation } from 'react-i18next'; +import { useNotificationStore, type AppNotification } from '../stores/notifications'; +import { resolveAvatar } from '../lib/resolveAvatar'; +import { HeroShieldCheck } from './HeroShieldCheck'; + +type Props = { + visible: boolean; + onClose: () => void; + /** Distanz vom oberen Rand bis Dropdown anchor (Header-Höhe inkl. SafeArea) */ + topOffset: number; +}; + +export function NotificationsDropdown({ visible, onClose, topOffset }: Props) { + const { t } = useTranslation(); + const router = useRouter(); + const items = useNotificationStore((s) => s.items); + const loaded = useNotificationStore((s) => s.loaded); + const load = useNotificationStore((s) => s.load); + const markRead = useNotificationStore((s) => s.markRead); + const unread = useNotificationStore((s) => s.unread); + + const opacity = useRef(new Animated.Value(0)).current; + const translateY = useRef(new Animated.Value(-8)).current; + + useEffect(() => { + if (visible) { + if (!loaded) load(); + // Mark as read with delay so user sees the unread highlight briefly + const tm = setTimeout(() => { + if (unread > 0) markRead(); + }, 600); + Animated.parallel([ + Animated.timing(opacity, { toValue: 1, duration: 140, useNativeDriver: true }), + Animated.timing(translateY, { toValue: 0, duration: 160, useNativeDriver: true }), + ]).start(); + return () => clearTimeout(tm); + } + opacity.setValue(0); + translateY.setValue(-8); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visible]); + + function handleNavigate(n: AppNotification) { + onClose(); + if (n.type === 'domain_accepted' || n.type === 'domain_rejected') { + router.push('/blocker' as RelativePathString); + } else if (n.postId) { + router.push(`/?postId=${n.postId}` as RelativePathString); + } + } + + return ( + + + true} + style={{ + position: 'absolute', + top: topOffset + 6, + right: 12, + backgroundColor: '#ffffff', + borderRadius: 18, + shadowColor: '#000', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.18, + shadowRadius: 20, + elevation: 12, + width: 320, + maxHeight: 480, + overflow: 'hidden', + opacity, + transform: [{ translateY }], + }} + > + {/* Header */} + + + {t('notifications.title')} + + {unread > 0 && ( + markRead()} hitSlop={6}> + + {t('notifications.mark_all_read')} + + + )} + + + {items.length === 0 ? ( + + + + {t('notifications.empty_title')} + + + {t('notifications.empty_subtitle')} + + + ) : ( + n.id} + renderItem={({ item }) => ( + handleNavigate(item)} + t={t} + /> + )} + /> + )} + + + + ); +} + +function notifLabel(n: AppNotification, t: (k: string, opts?: any) => string): string { + switch (n.type) { + case 'new_like': + return `${n.actorName} ${t('notifications.liked_post')}`; + case 'new_comment': + return `${n.actorName} ${t('notifications.commented_post')}`; + case 'domain_vote': + return `${n.actorName} ${t('notifications.voted_domain')}`; + case 'domain_accepted': + return n.preview + ? `${n.preview} ${t('notifications.domain_accepted')}` + : t('notifications.domain_accepted'); + case 'domain_rejected': + return n.preview + ? `${n.preview} ${t('notifications.domain_rejected')}` + : t('notifications.domain_rejected'); + case 'new_follower': + return `${n.actorName} ${t('notifications.new_follower')}`; + default: + return `${n.actorName} ${t('notifications.generic')}`; + } +} + +function notifIcon(type: string): { + icon: React.ComponentProps['name']; + color: string; + bg: string; +} { + switch (type) { + case 'new_like': + return { icon: 'heart', color: '#dc2626', bg: '#fee2e2' }; + case 'new_comment': + return { icon: 'chatbubble-ellipses', color: '#2563eb', bg: '#dbeafe' }; + case 'domain_accepted': + return { icon: 'shield-checkmark', color: '#16a34a', bg: '#dcfce7' }; + case 'domain_rejected': + return { icon: 'close-circle', color: '#dc2626', bg: '#fee2e2' }; + case 'domain_vote': + return { icon: 'thumbs-up', color: '#d97706', bg: '#fef3c7' }; + case 'new_follower': + return { icon: 'person-add', color: '#7c3aed', bg: '#ede9fe' }; + default: + return { icon: 'notifications', color: '#737373', bg: '#f5f5f5' }; + } +} + +function timeAgo(dateStr: string, t: (k: string, opts?: any) => string): string { + const m = Math.floor((Date.now() - new Date(dateStr).getTime()) / 60000); + if (m < 1) return t('notifications.just_now'); + if (m < 60) return t('notifications.min_ago', { n: m }); + const h = Math.floor(m / 60); + if (h < 24) return t('notifications.hours_ago', { n: h }); + return t('notifications.days_ago', { n: Math.floor(h / 24) }); +} + +function NotificationRow({ + notif, + onPress, + t, +}: { + notif: AppNotification; + onPress: () => void; + t: (k: string, opts?: any) => string; +}) { + const isUnread = !notif.readAt; + const { icon, color, bg } = notifIcon(notif.type); + const isSocial = + notif.type === 'new_like' || + notif.type === 'new_comment' || + notif.type === 'new_follower' || + notif.type === 'domain_vote'; + // System-Notifications (von ReBreak selbst) bekommen das App-Icon als Avatar + const isSystem = + notif.type === 'domain_accepted' || + notif.type === 'domain_rejected' || + (notif.actorName ?? '').toLowerCase().startsWith('rebreak'); + const avatarUrl = isSocial ? resolveAvatar(notif.actorAvatar, notif.actorName) : null; + + return ( + ({ + opacity: pressed ? 0.65 : 1, + backgroundColor: isUnread ? '#fff7ed' : '#ffffff', + })} + > + + {/* Avatar-Logik: + - Social: User-Avatar mit kleinem Type-Badge + - System (ReBreak): App-Icon mit kleinem Type-Badge + - Sonst: Typed Icon */} + {avatarUrl ? ( + // Social: Avatar mit kleinem Mini-Badge — Badge ohne weißen Ring (clean). + + + + + + + ) : ( + // System (domain_accepted/rejected/etc.) + Fallback: NUR clean Icon, + // kein Avatar-Overlay mehr — vorher hatte ReBreak-App-Icon mit + // Shield-Badge-Overlay den Logo verdeckt (User-Feedback 2026-05-05). + + {notif.type === 'domain_accepted' ? ( + + ) : ( + + )} + + )} + + + {notifLabel(notif, t)} + + + {timeAgo(notif.createdAt, t)} + + + + + ); +} diff --git a/apps/rebreak-native/components/PostCard.tsx b/apps/rebreak-native/components/PostCard.tsx new file mode 100644 index 0000000..5f7cb84 --- /dev/null +++ b/apps/rebreak-native/components/PostCard.tsx @@ -0,0 +1,558 @@ +import { memo, useState, useCallback, useRef, useEffect } from 'react'; +import { View, Text, Pressable, Image, Animated } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { apiFetch } from '../lib/api'; +import { resolveAvatar } from '../lib/resolveAvatar'; +import { formatRelativeTime } from '../lib/formatTime'; +import { useCommunityStore, type CommunityPost } from '../stores/community'; +import { RiveAvatar } from './RiveAvatar'; +import { HeroShieldCheck } from './HeroShieldCheck'; + +type Props = { + post: CommunityPost; + onCommentPress: (postId: string) => void; +}; + +function PostCardImpl({ post, onCommentPress }: Props) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + // Granular selectors — subscribing to the whole store would re-render every + // PostCard whenever any user likes any post (optimisticLikes mutates). + const applyOptimisticLike = useCommunityStore((s) => s.applyOptimisticLike); + const revertOptimisticLike = useCommunityStore((s) => s.revertOptimisticLike); + const clearOptimisticLike = useCommunityStore((s) => s.clearOptimisticLike); + + const [localLike, setLocalLike] = useState<'like' | null>(post.userLike === 'like' ? 'like' : null); + const [localCount, setLocalCount] = useState(post.likesCount); + const [isLiking, setIsLiking] = useState(false); + + // Heart-Pop Animation — Insta-Style: quick scale-up + spring-bounce back + const heartScale = useRef(new Animated.Value(1)).current; + const triggerHeartPop = useCallback(() => { + heartScale.setValue(1); + Animated.sequence([ + Animated.timing(heartScale, { + toValue: 1.4, + duration: 120, + useNativeDriver: true, + }), + Animated.spring(heartScale, { + toValue: 1, + friction: 4, + tension: 80, + useNativeDriver: true, + }), + ]).start(); + }, [heartScale]); + + const displayAuthor = post.repostOf ? post.repostOf.author : post.author; + const displayContent = post.repostOf ? post.repostOf.content : post.content; + const displayImage = post.repostOf ? post.repostOf.imageUrl : post.imageUrl; + + // Image aspect-ratio: ermittelt aus onLoad event.source.{width,height}. + // Fallback während loading = 1.78 (16:9). Clamp 0.6..1.78 verhindert, + // dass hyper-portrait-Bilder (9:21) den ganzen Screen einnehmen. + const [imageAspectRatio, setImageAspectRatio] = useState(null); + // Reset bei post-change (FlatList-Recycling). + useEffect(() => { + setImageAspectRatio(null); + }, [displayImage]); + + const authorLabel = post.isAnonymous || !displayAuthor.id ? t('community.anonymous_label') : displayAuthor.nickname; + + // Lyra bot posts use the RiveAvatar (sm = 40px circle). All other bots and + // regular users use the image/initials fallback path. + const isLyraPost = post.isBot && post.botType === 'lyra'; + + // Avatar: only render Image if author has avatar id; resolveAvatar returns the URL. + // On image-load error or missing avatar id → initials fallback. + const hasAvatar = !!displayAuthor.avatar && !post.isAnonymous && !isLyraPost; + const avatarUrl = hasAvatar ? resolveAvatar(displayAuthor.avatar, displayAuthor.nickname) : ''; + const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); + // Reset error-state when post (or its avatar) changes — list-virtualization may reuse component. + useEffect(() => { + setAvatarLoadFailed(false); + }, [avatarUrl]); + const showAvatarImage = hasAvatar && !avatarLoadFailed; + const avatarInitials = ( + authorLabel.charAt(0) + (authorLabel.charAt(1) ?? '') + ).toUpperCase() || '?'; + + // domain_approved: extract domain name from Google favicon URL stored in imageUrl + const approvedDomain = (() => { + if (post.category !== 'domain_approved' || !displayImage) return null; + try { return new URL(displayImage).searchParams.get('domain'); } catch { return null; } + })(); + + // domain_vote vote action — delegate up via apiFetch, no local store mutation needed + // (realtime hook invalidates query on submission UPDATE) + const [voting, setVoting] = useState(false); + const [localVote, setLocalVote] = useState<'yes' | 'no' | null>(post.userVote ?? null); + const [localYes, setLocalYes] = useState(post.submission?.yesVotes ?? 0); + const [localNo, setLocalNo] = useState(post.submission?.noVotes ?? 0); + + useEffect(() => { + setLocalVote(post.userVote ?? null); + setLocalYes(post.submission?.yesVotes ?? 0); + setLocalNo(post.submission?.noVotes ?? 0); + }, [post.userVote, post.submission?.yesVotes, post.submission?.noVotes]); + + const handleVote = useCallback(async (vote: 'yes' | 'no') => { + if (voting || !post.submission?.id || localVote) return; + setVoting(true); + setLocalVote(vote); + if (vote === 'yes') setLocalYes((n) => n + 1); + else setLocalNo((n) => n + 1); + try { + const res = await apiFetch<{ yesVotes: number; noVotes: number; movedToReview: boolean }>( + `/api/domain-submissions/${post.submission.id}/vote`, + { method: 'POST', body: { vote } }, + ); + setLocalYes(res.yesVotes); + setLocalNo(res.noVotes); + queryClient.invalidateQueries({ queryKey: ['community-posts'] }); + } catch { + setLocalVote(null); + if (vote === 'yes') setLocalYes((n) => Math.max(0, n - 1)); + else setLocalNo((n) => Math.max(0, n - 1)); + } finally { + setVoting(false); + } + }, [voting, localVote, post.submission?.id, queryClient]); + + const authorDescription = (() => { + if (post.isBot) return post.botType === 'rebreak' ? t('community.bot_admin') : t('community.bot_ai'); + if (post.isAnonymous || !displayAuthor.id) return undefined; + const plan = displayAuthor.plan; + if (plan === 'legend') return t('community.tier_legend'); + if (plan === 'pro') return t('community.tier_pro'); + return t('community.tier_starter'); + })(); + + const handleLike = useCallback(async () => { + if (isLiking) return; + triggerHeartPop(); + const { newLike, newCount } = applyOptimisticLike(post.id, localLike, localCount); + setLocalLike(newLike); + setLocalCount(newCount); + setIsLiking(true); + try { + const res = await apiFetch<{ + likesCount: number; + dislikesCount: number; + userLike: 'like' | 'dislike' | null; + }>('/api/community/like', { + method: 'POST', + body: { postId: post.id, type: 'like' }, + }); + setLocalCount(res.likesCount); + setLocalLike(res.userLike === 'like' ? 'like' : null); + clearOptimisticLike(post.id); + // KEIN queryClient.invalidateQueries — würde die komplette Liste neu laden, + // PostCard remounted, Heart-Pop-Animation abgebrochen. Local-State reicht. + } catch { + revertOptimisticLike(post.id); + setLocalLike(post.userLike === 'like' ? 'like' : null); + setLocalCount(post.likesCount); + } finally { + setIsLiking(false); + } + }, [isLiking, localLike, localCount, post.id, post.userLike, post.likesCount, applyOptimisticLike, clearOptimisticLike, revertOptimisticLike, queryClient, triggerHeartPop]); + + return ( + + {/* Repost header */} + {post.repostOf && ( + + + + {post.author.nickname} {t('community.reposted_suffix')} + + + )} + + {/* Author + Meta */} + + + {isLyraPost ? ( + // Lyra bot posts use the animated Rive avatar at sm (40px). + // The RiveAvatar sm-variant has no border/shadow by design — fits tight in list. + + ) : showAvatarImage ? ( + setAvatarLoadFailed(true)} + className="w-10 h-10 rounded-full bg-neutral-100" + /> + ) : ( + + + {avatarInitials} + + + )} + + + {authorLabel} + + {authorDescription !== undefined && ( + {authorDescription} + )} + + + + {formatRelativeTime(post.createdAt)} + + + + {/* Content — hidden for domain_vote (replaced by poll below) */} + {!!displayContent && post.category !== 'domain_vote' && ( + + {displayContent} + + )} + + {/* domain_approved: favicon + domain name + shield badge */} + {post.category === 'domain_approved' && !!approvedDomain && ( + + + + + {approvedDomain} + + + {t('community.domain_added_to_blocklist')} + + + + + )} + + {/* domain_vote: poll card with domain banner + yes/no bars + vote buttons */} + {post.category === 'domain_vote' && !!post.submission && ( + + )} + + {/* Image — respektiert echtes Aspect-Ratio (portrait/square/landscape), + clamped 0.6..1.78 damit 9:21-Storys nicht den ganzen Screen einnehmen. */} + {!!displayImage && post.category !== 'domain_approved' && post.category !== 'domain_vote' && ( + { + const { width, height } = e.nativeEvent.source; + if (width && height) { + const ratio = width / height; + setImageAspectRatio(Math.max(0.6, Math.min(1.78, ratio))); + } + }} + className="w-full rounded-xl mt-3" + style={{ aspectRatio: imageAspectRatio ?? 1.78 }} + resizeMode="cover" + /> + )} + + {/* Actions: Like, Comment — not shown for domain_vote */} + {/* HitSlop +12pt rundum → effektiver Touch-Bereich ~44pt (HIG-Min). */} + {/* Vorher: Tap-Area = nur Icon-Größe (~21pt) → User-Feedback "reagiert nicht beim 1. Klick". */} + {post.category !== 'domain_vote' && ( + + ({ opacity: pressed ? 0.5 : 1, transform: [{ scale: pressed ? 0.94 : 1 }] })} + > + + + + {localCount > 0 && ( + {localCount} + )} + + + onCommentPress(post.id)} + hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} + android_ripple={{ color: 'rgba(0,0,0,0.08)', borderless: true, radius: 22 }} + className="flex-row items-center gap-1.5" + style={({ pressed }) => ({ opacity: pressed ? 0.5 : 1, transform: [{ scale: pressed ? 0.94 : 1 }] })} + > + + {post.commentsCount > 0 && ( + {post.commentsCount} + )} + + + )} + + ); +} + +// React.memo with a shallow compare on the fields that actually drive visible +// content. Without this, every realtime patch (likes/comments/domain-vote) +// re-mapping the posts array would re-render every visible PostCard, even +// though only one item changed. onCommentPress is expected stable from parent. +export const PostCard = memo(PostCardImpl, (prev, next) => { + if (prev.onCommentPress !== next.onCommentPress) return false; + const a = prev.post; + const b = next.post; + if (a === b) return true; + if (a.id !== b.id) return false; + if (a.likesCount !== b.likesCount) return false; + if (a.dislikesCount !== b.dislikesCount) return false; + if (a.commentsCount !== b.commentsCount) return false; + if (a.repostsCount !== b.repostsCount) return false; + if (a.userLike !== b.userLike) return false; + if (a.userVote !== b.userVote) return false; + if (a.content !== b.content) return false; + if (a.imageUrl !== b.imageUrl) return false; + if (a.category !== b.category) return false; + if (a.isAnonymous !== b.isAnonymous) return false; + if (a.challengeStatus !== b.challengeStatus) return false; + if (a.isLive !== b.isLive) return false; + // submission shallow compare on the fields the card reads + const sa = a.submission; + const sb = b.submission; + if (!!sa !== !!sb) return false; + if (sa && sb) { + if (sa.id !== sb.id) return false; + if (sa.status !== sb.status) return false; + if (sa.yesVotes !== sb.yesVotes) return false; + if (sa.noVotes !== sb.noVotes) return false; + if (sa.reviewedAt !== sb.reviewedAt) return false; + } + // author identity (anonymous toggling, avatar swap) + if (a.author.id !== b.author.id) return false; + if (a.author.avatar !== b.author.avatar) return false; + if (a.author.nickname !== b.author.nickname) return false; + if (a.author.plan !== b.author.plan) return false; + // repostOf identity + const ra = a.repostOf; + const rb = b.repostOf; + if (!!ra !== !!rb) return false; + if (ra && rb) { + if (ra.content !== rb.content) return false; + if (ra.imageUrl !== rb.imageUrl) return false; + if (ra.author.id !== rb.author.id) return false; + if (ra.author.nickname !== rb.author.nickname) return false; + if (ra.author.avatar !== rb.author.avatar) return false; + } + return true; +}); + +// ── Domain Favicon ───────────────────────────────────────────────────────── +// Google S2 favicon-API as in Nuxt PostCard — on error: letter-avatar fallback. +type DomainFaviconProps = { domain: string; size: number }; + +function DomainFavicon({ domain, size }: DomainFaviconProps) { + const [failed, setFailed] = useState(false); + const uri = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=64`; + const letter = domain.charAt(0).toUpperCase(); + + if (failed) { + return ( + + + {letter} + + + ); + } + + return ( + setFailed(true)} + /> + ); +} + +// ── Domain Vote Poll Card ─────────────────────────────────────────────────── +type Submission = NonNullable; + +type DomainVoteCardProps = { + submission: Submission; + localYes: number; + localNo: number; + localVote: 'yes' | 'no' | null; + voting: boolean; + isOwnPost: boolean; + onVote: (v: 'yes' | 'no') => void; + // TFunction from react-i18next has a complex overload signature; using + // a simple callable type avoids generics noise here. + t: (key: string) => string; +}; + +function DomainVoteCard({ + submission, + localYes, + localNo, + localVote, + voting, + isOwnPost, + onVote, + t, +}: DomainVoteCardProps) { + const total = localYes + localNo; + const yesWidth = total === 0 ? 0 : Math.round((localYes / 10) * 100); + // No-bar is relative to yes + no total to mirror Nuxt logic + const noWidth = total === 0 ? 0 : Math.round((localNo / Math.max(total, localYes)) * 100); + + const isPending = submission.status === 'pending' || submission.status === 'in_review'; + const isApproved = submission.status === 'approved'; + + const statusLabel = (() => { + if (isApproved) return submission.reviewedAt ? `Approved ${formatApprovedDate(submission.reviewedAt)}` : 'Global'; + if (submission.status === 'rejected') return t('community.vote_rejected'); + if (submission.status === 'in_review') return t('community.vote_in_review'); + return `${localYes} / 10`; + })(); + + return ( + + {/* Header: label + status badge */} + + + + + {t('community.domain_proposal_label')} + + + + + {statusLabel} + + + + + {/* Domain card */} + + + + + {submission.domain} + + + {isApproved ? t('community.domain_added') : t('community.domain_proposed')} + + + + + + {/* Yes bar */} + + + + + + {t('community.vote_yes')} + + + + {localYes} / 10 + + + + + + + + {/* No bar */} + + + + + + {t('community.vote_no')} + + + + {localNo} + + + + + + + + {/* Vote buttons — only for pending + not own post + not already voted */} + {isPending && !isOwnPost && !localVote && ( + + onVote('yes')} + disabled={voting} + className="flex-1 flex-row items-center justify-center gap-1.5 h-9 rounded-xl border border-rebreak-500" + style={({ pressed }) => ({ opacity: pressed || voting ? 0.5 : 1 })} + > + + + {t('community.vote_yes')} + + + onVote('no')} + disabled={voting} + className="flex-1 flex-row items-center justify-center gap-1.5 h-9 rounded-xl border border-neutral-300" + style={({ pressed }) => ({ opacity: pressed || voting ? 0.5 : 1 })} + > + + + {t('community.vote_no')} + + + + )} + + {/* Already voted indicator */} + {isPending && !isOwnPost && !!localVote && ( + + {t('community.voted_thanks')} + + )} + + {/* Own post indicator */} + {isPending && isOwnPost && ( + + {t('community.domain_vote_own')} + + )} + + ); +} + +function formatApprovedDate(dateStr: string): string { + try { + const d = new Date(dateStr); + return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); + } catch { return ''; } +} diff --git a/apps/rebreak-native/components/PostCardSkeleton.tsx b/apps/rebreak-native/components/PostCardSkeleton.tsx new file mode 100644 index 0000000..f5edf20 --- /dev/null +++ b/apps/rebreak-native/components/PostCardSkeleton.tsx @@ -0,0 +1,18 @@ +import { View } from 'react-native'; + +export function PostCardSkeleton() { + return ( + + + + + + + + + + + + + ); +} diff --git a/apps/rebreak-native/components/PostCommentsSheet.tsx b/apps/rebreak-native/components/PostCommentsSheet.tsx new file mode 100644 index 0000000..fb5bcb0 --- /dev/null +++ b/apps/rebreak-native/components/PostCommentsSheet.tsx @@ -0,0 +1,503 @@ +import { useState, useRef, useCallback, useEffect } from 'react'; +import { + View, + Text, + Modal, + FlatList, + TextInput, + Pressable, + Keyboard, + Platform, + ActivityIndicator, + Animated, + PanResponder, + useWindowDimensions, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Ionicons } from '@expo/vector-icons'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { apiFetch } from '../lib/api'; +import { formatRelativeTime } from '../lib/formatTime'; +import { colors } from '../lib/theme'; +import type { CommunityComment } from '../stores/community'; + +const EMOJIS = ['❤️', '🙌', '🔥', '👏', '😢', '😍', '😮', '😂']; +const SNAP_THRESHOLD = 50; + +type Props = { + postId: string | null; + visible: boolean; + onClose: () => void; +}; + +export function PostCommentsSheet({ postId, visible, onClose }: Props) { + const { t } = useTranslation(); + const insets = useSafeAreaInsets(); + const queryClient = useQueryClient(); + const inputRef = useRef(null); + const [text, setText] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [replyTarget, setReplyTarget] = useState<{ id: string; nickname: string } | null>(null); + const [keyboardHeight, setKeyboardHeight] = useState(0); + + // useWindowDimensions: live-tracking. Auf Android schrumpft `height` wenn die + // Tastatur aufgeht (windowSoftInputMode=adjustResize) — daher dynamisch statt + // `Dimensions.get` (statisch beim Modul-Load). + const { height: SCREEN_HEIGHT } = useWindowDimensions(); + const COLLAPSED_HEIGHT = SCREEN_HEIGHT * 0.65; + const EXPANDED_HEIGHT = SCREEN_HEIGHT * 0.92; + const MIN_HEIGHT = SCREEN_HEIGHT * 0.35; + + // Sheet-Höhe animiert (height-based, bottom: 0 fix → Input bleibt immer am Edge sichtbar). + // Plus separater translateY für die Dismiss-Slide-Animation (native). + const sheetHeight = useRef(new Animated.Value(COLLAPSED_HEIGHT)).current; + const dismissY = useRef(new Animated.Value(0)).current; + const currentHeight = useRef(COLLAPSED_HEIGHT); + + const handleClose = useCallback(() => { + Keyboard.dismiss(); + setText(''); + setReplyTarget(null); + sheetHeight.setValue(COLLAPSED_HEIGHT); + dismissY.setValue(0); + currentHeight.current = COLLAPSED_HEIGHT; + onClose(); + }, [onClose, sheetHeight, dismissY]); + + useEffect(() => { + if (visible) { + sheetHeight.setValue(COLLAPSED_HEIGHT); + dismissY.setValue(0); + currentHeight.current = COLLAPSED_HEIGHT; + } + }, [visible, sheetHeight, dismissY]); + + const panResponder = useRef( + PanResponder.create({ + // Claim Gesture sofort, kein Wartet-bis-5px + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderTerminationRequest: () => false, + onPanResponderMove: (_, g) => { + // Drag rauf (g.dy < 0) → height grösser. Drag runter → height kleiner. + const next = currentHeight.current - g.dy; + const clamped = Math.max(MIN_HEIGHT - 100, Math.min(EXPANDED_HEIGHT + 20, next)); + sheetHeight.setValue(clamped); + }, + onPanResponderRelease: (_, g) => { + const finalH = currentHeight.current - g.dy; + const velocity = g.vy; // Pixel pro ms (negativ = nach oben, positiv = nach unten) + + // Unter MIN_HEIGHT oder schneller Flick nach unten → dismiss + if (finalH < MIN_HEIGHT || velocity > 1.5) { + Animated.timing(dismissY, { + toValue: SCREEN_HEIGHT, + duration: 200, + useNativeDriver: true, + }).start(() => { + handleClose(); + }); + return; + } + + // Schneller Flick nach oben → auf Maximum schnappen + let target = finalH; + if (velocity < -1.5) { + target = EXPANDED_HEIGHT; + } + + // Clamp auf gültigen Bereich, sonst bleibt's wo der User losgelassen hat + const clamped = Math.max(MIN_HEIGHT, Math.min(EXPANDED_HEIGHT, target)); + + Animated.spring(sheetHeight, { + toValue: clamped, + useNativeDriver: false, + friction: 9, + tension: 70, + }).start(); + currentHeight.current = clamped; + }, + }), + ).current; + + useEffect(() => { + const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; + const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; + const showSub = Keyboard.addListener(showEvent, (e) => { + setKeyboardHeight(e.endCoordinates.height); + }); + const hideSub = Keyboard.addListener(hideEvent, () => { + setKeyboardHeight(0); + }); + return () => { + showSub.remove(); + hideSub.remove(); + }; + }, []); + + const { data: comments = [], isLoading } = useQuery({ + queryKey: ['post-comments', postId], + queryFn: () => apiFetch(`/api/community/${postId}/comments`), + enabled: !!postId && visible, + staleTime: 30_000, + }); + + const topLevel = comments.filter((c) => !c.parentCommentId); + const repliesFor = (id: string) => comments.filter((c) => c.parentCommentId === id); + + const submit = useCallback(async () => { + if (!text.trim() || !postId) return; + setSubmitting(true); + try { + await apiFetch('/api/community/comment', { + method: 'POST', + body: { + postId, + content: text.trim(), + ...(replyTarget ? { parentCommentId: replyTarget.id } : {}), + }, + }); + setText(''); + setReplyTarget(null); + queryClient.invalidateQueries({ queryKey: ['post-comments', postId] }); + queryClient.invalidateQueries({ queryKey: ['community-posts'] }); + } catch { + // ignore + } finally { + setSubmitting(false); + } + }, [text, postId, replyTarget, queryClient]); + + const likeComment = useCallback( + async (comment: CommunityComment) => { + try { + await apiFetch('/api/community/comment-like', { + method: 'POST', + body: { commentId: comment.id }, + }); + queryClient.invalidateQueries({ queryKey: ['post-comments', postId] }); + } catch { + // ignore + } + }, + [postId, queryClient], + ); + + // Bei offener Tastatur → automatisch expanded + useEffect(() => { + if (keyboardHeight > 0 && currentHeight.current !== EXPANDED_HEIGHT) { + Animated.spring(sheetHeight, { + toValue: EXPANDED_HEIGHT, + useNativeDriver: false, + friction: 9, + tension: 70, + }).start(); + currentHeight.current = EXPANDED_HEIGHT; + } + }, [keyboardHeight, sheetHeight]); + + return ( + + {/* Backdrop — sehr leichter Dim damit man Posts vom Drawer unterscheidet */} + + + {/* Outer: animated height (non-native driver) */} + + {/* Inner: animated transform (native driver) — getrennt damit kein Driver-Mix */} + + {/* Drag-Bar — drag-down dismisst via PanResponder */} + + + + + {/* Header — auch drag-area, kein X-Button */} + + + {t('community.comments_title')} + + + + {/* Comments-Liste */} + {isLoading ? ( + + + + ) : ( + item.id} + style={{ flex: 1 }} + contentContainerStyle={{ paddingVertical: 8, paddingHorizontal: 16 }} + keyboardShouldPersistTaps="handled" + ListEmptyComponent={ + + + {t('community.comments_empty')} + + + } + renderItem={({ item: comment }) => ( + + { + setReplyTarget({ id: comment.id, nickname: comment.authorNickname }); + inputRef.current?.focus(); + }} + onLike={() => likeComment(comment)} + /> + {repliesFor(comment.id).map((reply) => ( + + likeComment(reply)} /> + + ))} + + )} + /> + )} + + {/* Emoji-Bar */} + + {EMOJIS.map((e) => ( + setText((t) => t + e)}> + {e} + + ))} + + + {/* Reply-Context */} + {replyTarget && ( + + + {t('community.reply_to')}{' '} + @{replyTarget.nickname} + + setReplyTarget(null)}> + + + + )} + + {/* Input + Send-Button */} + 0 ? 8 : Math.max(12, insets.bottom), + borderTopWidth: 1, + borderTopColor: '#e5e5e5', + }} + > + + ({ + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: colors.brandOrange, + alignItems: 'center', + justifyContent: 'center', + opacity: pressed || !text.trim() || submitting ? 0.5 : 1, + })} + > + {submitting ? ( + + ) : ( + + )} + + + + + + ); +} + +type CommentRowProps = { + comment: CommunityComment; + isReply?: boolean; + onReply?: () => void; + onLike: () => void; +}; + +function CommentRow({ comment, isReply = false, onReply, onLike }: CommentRowProps) { + const { t } = useTranslation(); + const heartScale = useRef(new Animated.Value(1)).current; + const handleLikeWithPop = useCallback(() => { + heartScale.setValue(1); + Animated.sequence([ + Animated.timing(heartScale, { toValue: 1.4, duration: 120, useNativeDriver: true }), + Animated.spring(heartScale, { toValue: 1, friction: 4, tension: 80, useNativeDriver: true }), + ]).start(); + onLike(); + }, [heartScale, onLike]); + + return ( + + + + {(comment.authorNickname ?? 'AN').slice(0, 2).toUpperCase()} + + + + + + {comment.authorNickname ?? t('community.anonymous_label')} + + + {comment.content} + + + + {formatRelativeTime(comment.createdAt)} + + {!isReply && onReply && ( + + + {t('community.reply')} + + + )} + + + + + + + + {comment.likesCount > 0 && ( + + {comment.likesCount} + + )} + + + ); +} diff --git a/apps/rebreak-native/components/RiveAvatar.tsx b/apps/rebreak-native/components/RiveAvatar.tsx new file mode 100644 index 0000000..254e180 --- /dev/null +++ b/apps/rebreak-native/components/RiveAvatar.tsx @@ -0,0 +1,166 @@ +import { useEffect, useState } from 'react'; +import { View, Text, Platform } from 'react-native'; +import { Asset } from 'expo-asset'; +import Rive, { Fit, Alignment } from 'rive-react-native'; + +// Android: Rive akzeptiert NUR raw-resource oder url, kein file:// uri. +// Asset liegt als android/app/src/main/res/raw/lyra_avatar.riv (gebundelt +// via plugins/with-rive-asset-android.js). resourceName = filename ohne +// extension, lowercase + underscores (Android raw-resource convention). +const ANDROID_RIVE_RESOURCE = 'lyra_avatar'; + +// Modul-Level: nur EINMAL die Asset registrieren + URI cachen. +// In Production-Builds wird die .riv ins App-Bundle gebakt (Asset.localUri +// zeigt sofort auf das Bundle-File). In Dev-Builds wird sie beim ersten Mal +// von Metro gezogen + ins App-Sandbox-Cache geschrieben — danach offline. +const RIVE_MODULE = require('../assets/lyra-avatar.riv'); +let cachedRiveUri: string | null = null; +let preloadPromise: Promise | null = null; + +function preloadRiveAsset(): Promise { + if (cachedRiveUri) return Promise.resolve(cachedRiveUri); + if (preloadPromise) return preloadPromise; + preloadPromise = Asset.fromModule(RIVE_MODULE) + .downloadAsync() + .then((asset) => { + const uri = asset.localUri ?? asset.uri; + cachedRiveUri = uri; + return uri; + }) + .catch((err) => { + console.warn('[RiveAvatar] preload failed:', err?.message ?? err); + preloadPromise = null; + return null; + }); + return preloadPromise; +} + +// Kicke den Preload sofort beim Modul-Import an — damit der erste +// Render bereits die cached URI nutzt (außer im allerersten App-Start). +preloadRiveAsset(); + +export type Emotion = 'idle' | 'happy' | 'thinking' | 'empathy'; + +// Direkte Animation-Namen aus der .riv-Datei (1:1 mit Nuxt BenAvatar.vue). +// "happy" hat zwei Phasen: erst Übergangs-Animation, dann Loop. +const EMOTION_ANIMATIONS: Record = { + idle: 'Idle Loop', + happy: 'idle to Pose 1', + thinking: 'WALK', + empathy: '01 Wave 1', +}; + +const EMOTION_LABELS: Record = { + idle: 'bereit', + happy: 'froh für dich', + thinking: 'überlegt ...', + empathy: 'versteht dich', +}; + +const SIZE_PX: Record<'sm' | 'md' | 'lg', number> = { + sm: 40, + md: 112, + lg: 160, +}; + +type Props = { + emotion: Emotion; + size?: 'sm' | 'md' | 'lg'; + showLabel?: boolean; +}; + +export function RiveAvatar({ emotion, size = 'md', showLabel = false }: Props) { + const px = SIZE_PX[size]; + + // Aktuelle Animation als deklarativer State (kein imperatives ref.play()). + const [currentAnim, setCurrentAnim] = useState(EMOTION_ANIMATIONS.idle); + + // Lokale URI für die .riv-Datei — geht über expo-asset damit der File + // im App-Sandbox gecached wird statt jedes Mal von Metro zu streamen. + // Nach erstem Load funktioniert's auch komplett offline. + const [riveUri, setRiveUri] = useState(cachedRiveUri); + + useEffect(() => { + if (riveUri) return; // schon gecached + let active = true; + preloadRiveAsset().then((uri) => { + if (active && uri) setRiveUri(uri); + }); + return () => { + active = false; + }; + }, [riveUri]); + + useEffect(() => { + if (emotion === 'happy') { + // 2-Phasen-Flow: Übergang (~900ms) → Loop (1:1 wie Nuxt-BenAvatar) + setCurrentAnim('idle to Pose 1'); + const t = setTimeout(() => setCurrentAnim('Pose 1 loop'), 900); + return () => clearTimeout(t); + } + setCurrentAnim(EMOTION_ANIMATIONS[emotion]); + }, [emotion]); + + return ( + + + + {Platform.OS === 'android' ? ( + // Android: Bundle-Resource direkt (kein expo-asset Preload nötig) + + ) : riveUri ? ( + // iOS: file:// URI aus expo-asset Cache funktioniert + + ) : ( + + )} + + + + {showLabel && ( + + {EMOTION_LABELS[emotion]} + + )} + + ); +} diff --git a/apps/rebreak-native/components/StreakBadge.tsx b/apps/rebreak-native/components/StreakBadge.tsx new file mode 100644 index 0000000..cc03ee4 --- /dev/null +++ b/apps/rebreak-native/components/StreakBadge.tsx @@ -0,0 +1,31 @@ +import { View, Text } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { colors } from '../lib/theme'; + +type Props = { + days: number; + size?: 'sm' | 'md' | 'lg'; +}; + +const sizeMap = { + sm: { number: 'text-2xl', label: 'text-xs', icon: 16, padding: 'px-3 py-2' }, + md: { number: 'text-5xl', label: 'text-sm', icon: 20, padding: 'px-5 py-4' }, + lg: { number: 'text-7xl', label: 'text-base', icon: 24, padding: 'px-6 py-5' }, +}; + +export function StreakBadge({ days, size = 'md' }: Props) { + const { t } = useTranslation(); + const s = sizeMap[size]; + return ( + + + + {days} + + + {days === 1 ? t('streak.label_one') : t('streak.label_other')} {t('streak.label_suffix')} + + + ); +} diff --git a/apps/rebreak-native/components/SuccessAlert.tsx b/apps/rebreak-native/components/SuccessAlert.tsx new file mode 100644 index 0000000..5185b7e --- /dev/null +++ b/apps/rebreak-native/components/SuccessAlert.tsx @@ -0,0 +1,173 @@ +import { useEffect, useRef } from 'react'; +import { Modal, View, Text, Pressable, Animated, Easing } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; + +type Props = { + visible: boolean; + title: string; + message?: string; + onClose: () => void; +}; + +/** + * iOS-style success alert mit animiertem Check-Icon. + * - Card scaled mit Spring-overshoot rein + * - Check-Icon sequenced danach mit eigenem Spring + rotation-pop + * - Tap auf Backdrop schließt + * - OK-Button schließt + */ +export function SuccessAlert({ visible, title, message, onClose }: Props) { + const { t } = useTranslation(); + const cardScale = useRef(new Animated.Value(0.8)).current; + const cardOpacity = useRef(new Animated.Value(0)).current; + const checkScale = useRef(new Animated.Value(0)).current; + const checkRotate = useRef(new Animated.Value(0)).current; + + useEffect(() => { + if (visible) { + cardScale.setValue(0.8); + cardOpacity.setValue(0); + checkScale.setValue(0); + checkRotate.setValue(0); + + Animated.parallel([ + Animated.spring(cardScale, { + toValue: 1, + useNativeDriver: true, + friction: 7, + tension: 80, + }), + Animated.timing(cardOpacity, { + toValue: 1, + duration: 220, + useNativeDriver: true, + easing: Easing.out(Easing.cubic), + }), + Animated.sequence([ + Animated.delay(140), + Animated.parallel([ + Animated.spring(checkScale, { + toValue: 1, + useNativeDriver: true, + friction: 5, + tension: 180, + }), + Animated.timing(checkRotate, { + toValue: 1, + duration: 380, + useNativeDriver: true, + easing: Easing.out(Easing.back(1.7)), + }), + ]), + ]), + ]).start(); + } + }, [visible, cardScale, cardOpacity, checkScale, checkRotate]); + + const rotateInterpolate = checkRotate.interpolate({ + inputRange: [0, 1], + outputRange: ['-30deg', '0deg'], + }); + + return ( + + {/* Backdrop — Pressable damit Tap-outside schließt */} + + {/* Card — Pressable mit onPress={()=>{}} damit Tap auf Card NICHT bubbelt + * zum Backdrop und das Modal schließt. */} + {}} style={{ width: '85%', maxWidth: 320 }}> + + {/* Animated Check-Circle */} + + + + + + {title} + + {message && ( + + {message} + + )} + + + + {t('common.ok')} + + + + + + + ); +} diff --git a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx new file mode 100644 index 0000000..92f6582 --- /dev/null +++ b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx @@ -0,0 +1,352 @@ +import { useState, useEffect, useRef } from 'react'; +import { + Modal, + View, + Text, + TextInput, + Pressable, + KeyboardAvoidingView, + Platform, + Image, + ActivityIndicator, + Animated, + Dimensions, + Easing, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { + isValidDomain, + normalizeDomain, + type Tier, +} from '../../hooks/useCustomDomains'; + +const SCREEN_HEIGHT = Dimensions.get('window').height; +const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65; // wie bei PostCommentsSheet — 65% der Screen-Höhe + +type Props = { + visible: boolean; + tier: Tier; + onClose: () => void; + onAdd: (domain: string) => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>; +}; + +export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { + const { t } = useTranslation(); + const insets = useSafeAreaInsets(); + const [input, setInput] = useState(''); + const [confirmPermanent, setConfirmPermanent] = useState(false); + const [adding, setAdding] = useState(false); + const [error, setError] = useState(null); + + const valid = isValidDomain(input); + const normalized = normalizeDomain(input); + + // Slide-up Animation für die Sheet (translateY von SHEET_HEIGHT → 0) + const translateY = useRef(new Animated.Value(SHEET_HEIGHT)).current; + const backdropOpacity = useRef(new Animated.Value(0)).current; + + useEffect(() => { + if (visible) { + translateY.setValue(SHEET_HEIGHT); + backdropOpacity.setValue(0); + Animated.parallel([ + Animated.timing(translateY, { + toValue: 0, + duration: 280, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + Animated.timing(backdropOpacity, { + toValue: 1, + duration: 220, + useNativeDriver: true, + }), + ]).start(); + } + }, [visible, translateY, backdropOpacity]); + + function close() { + setInput(''); + setConfirmPermanent(false); + setError(null); + onClose(); + } + + async function handleAdd() { + if (!valid || !confirmPermanent || adding) return; + setAdding(true); + setError(null); + const result = await onAdd(input); + setAdding(false); + if (result.ok) { + close(); + return; + } + if (result.alreadyGlobal) { + setError(t('blocker.add_sheet_already_global', { domain: normalized })); + } else { + setError(result.error ?? t('blocker.add_sheet_add_failed')); + } + } + + const warningText = + tier.plan === 'free' + ? t('blocker.add_sheet_warning_free') + : t('blocker.add_sheet_warning_pro'); + + return ( + + {/* Backdrop — Tap-outside schließt */} + + + + + {/* Sheet — slide-up von unten, 65% der Screen-Höhe */} + + + {/* Drag-handle */} + + + + + {/* Header */} + + + + {t('common.cancel')} + + + + {t('blocker.add_sheet_title')} + + + + + + {/* Input */} + + + {t('blocker.add_sheet_label')} + + { + setInput(v); + setError(null); + }} + placeholder={t('blocker.add_sheet_placeholder')} + placeholderTextColor="#a3a3a3" + autoCapitalize="none" + autoCorrect={false} + autoFocus + keyboardType="url" + returnKeyType="done" + onSubmitEditing={handleAdd} + style={{ + backgroundColor: '#f5f5f5', + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 12, + fontSize: 15, + fontFamily: 'Nunito_400Regular', + color: '#0a0a0a', + }} + /> + {input && !valid && ( + + {t('blocker.add_sheet_invalid')} + + )} + + + {/* Preview */} + {valid && ( + + + + {normalized} + + + )} + + {/* Warning */} + {valid && ( + + + + {warningText} + + + )} + + {/* Confirm-Checkbox */} + {valid && ( + setConfirmPermanent((v) => !v)} + style={{ + flexDirection: 'row', + alignItems: 'flex-start', + gap: 10, + paddingVertical: 4, + }} + > + + {confirmPermanent && } + + + {t('blocker.add_sheet_confirm_permanent')} + + + )} + + {/* Error */} + {error && ( + + {error} + + )} + + + + {/* Add-Button */} + ({ + backgroundColor: !valid || !confirmPermanent ? '#d4d4d4' : '#dc2626', + borderRadius: 14, + paddingVertical: 14, + alignItems: 'center', + opacity: pressed ? 0.85 : 1, + marginBottom: insets.bottom > 0 ? 8 : 12, + })} + > + {adding ? ( + + ) : ( + + {t('blocker.add_sheet_title')} + + )} + + + + + + ); +} diff --git a/apps/rebreak-native/components/blocker/CooldownBanner.tsx b/apps/rebreak-native/components/blocker/CooldownBanner.tsx new file mode 100644 index 0000000..161cc4a --- /dev/null +++ b/apps/rebreak-native/components/blocker/CooldownBanner.tsx @@ -0,0 +1,75 @@ +import { View, Text, Pressable, ActivityIndicator } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + remainingFormatted: string; // "23:59:42" + onCancel: () => Promise; +}; + +export function CooldownBanner({ remainingFormatted, onCancel }: Props) { + const { t } = useTranslation(); + const [cancelling, setCancelling] = useState(false); + + async function handleCancel() { + setCancelling(true); + try { + await onCancel(); + } finally { + setCancelling(false); + } + } + + return ( + + + + + {t('blocker.cooldown_banner_title')} + + + {remainingFormatted} + + + ({ + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 12, + backgroundColor: '#16a34a', + opacity: pressed || cancelling ? 0.7 : 1, + })} + > + {cancelling ? ( + + ) : ( + + {t('common.cancel')} + + )} + + + ); +} diff --git a/apps/rebreak-native/components/blocker/DeactivationExplainerSheet.tsx b/apps/rebreak-native/components/blocker/DeactivationExplainerSheet.tsx new file mode 100644 index 0000000..dea1390 --- /dev/null +++ b/apps/rebreak-native/components/blocker/DeactivationExplainerSheet.tsx @@ -0,0 +1,227 @@ +import { Modal, View, Text, Pressable, ScrollView, ActionSheetIOS, Platform, Alert } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + visible: boolean; + onClose: () => void; + /** User triggert "Atmen" → Deflector zur Urge-Page (Click-2 ende, kein Cooldown). */ + onBreathe: () => void; + /** Click-3 Bestätigung — final destruktive Aktion via ActionSheet. */ + onStartCooldown: (reason: string) => Promise; +}; + +/** + * Click 2 of 3 (Cooldown-Friction). + * + * Erklärt was Cooldown bedeutet, bietet [Atmen] als primary Deflector an, + * und ein kleines [Cooldown trotzdem starten] das einen nativen ActionSheet + * zur finalen Bestätigung öffnet (Click 3). + */ +export function DeactivationExplainerSheet({ + visible, + onClose, + onBreathe, + onStartCooldown, +}: Props) { + const { t } = useTranslation(); + const [submitting, setSubmitting] = useState(false); + + function showFinalConfirm() { + if (Platform.OS === 'ios') { + ActionSheetIOS.showActionSheetWithOptions( + { + title: t('blocker.deactivation_actionsheet_title'), + message: t('blocker.deactivation_actionsheet_message'), + options: [t('common.cancel'), t('blocker.deactivation_start_cta')], + destructiveButtonIndex: 1, + cancelButtonIndex: 0, + }, + async (idx) => { + if (idx === 1) await runCooldown(); + }, + ); + } else { + // Android Fallback + Alert.alert( + t('blocker.deactivation_actionsheet_title'), + t('blocker.deactivation_actionsheet_message'), + [ + { text: t('common.cancel'), style: 'cancel' }, + { text: t('blocker.deactivation_start_cta'), style: 'destructive', onPress: runCooldown }, + ], + ); + } + } + + async function runCooldown() { + setSubmitting(true); + try { + await onStartCooldown('user_requested_deactivation'); + onClose(); + } catch (e: any) { + Alert.alert(t('common.error'), e?.message ?? t('blocker.deactivation_failed_msg')); + } finally { + setSubmitting(false); + } + } + + return ( + + + {/* Header */} + + + + {t('common.back')} + + + + {t('blocker.deactivation_heading')} + + + + + + + {t('blocker.deactivation_title')} + + + {t('blocker.deactivation_intro')} + + + {/* Was passiert */} + + + + + + + + + {/* Primary Deflector */} + ({ + backgroundColor: '#16a34a', + borderRadius: 14, + paddingVertical: 16, + paddingHorizontal: 16, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 10, + opacity: pressed ? 0.85 : 1, + })} + > + + + {t('blocker.deactivation_breathe_cta')} + + + + {/* Destructive secondary */} + ({ + alignSelf: 'center', + paddingVertical: 12, + opacity: pressed || submitting ? 0.5 : 1, + })} + > + + {submitting ? t('blocker.deactivation_starting') : t('blocker.deactivation_start_anyway')} + + + + + + ); +} + +function BulletRow({ + icon, + title, + text, +}: { + icon: React.ComponentProps['name']; + title: string; + text: string; +}) { + return ( + + + + + + + {title} + + + {text} + + + + ); +} diff --git a/apps/rebreak-native/components/blocker/DomainGrid.tsx b/apps/rebreak-native/components/blocker/DomainGrid.tsx new file mode 100644 index 0000000..7d0b05f --- /dev/null +++ b/apps/rebreak-native/components/blocker/DomainGrid.tsx @@ -0,0 +1,515 @@ +import { useState, useMemo } from 'react'; +import { + View, + Text, + Pressable, + Image, + ActivityIndicator, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { SuccessAlert } from '../SuccessAlert'; +import { ConfirmAlert } from '../ConfirmAlert'; +import type { CustomDomain, Tier } from '../../hooks/useCustomDomains'; + +// ─── Helpers ───────────────────────────────────────────────────────────── + +function timeAgo(input?: string | Date): string { + if (!input) return ''; + const date = typeof input === 'string' ? new Date(input) : input; + const diffMs = Date.now() - date.getTime(); + const minutes = Math.floor(diffMs / 60_000); + if (minutes < 1) return 'jetzt'; + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}d`; + const weeks = Math.floor(days / 7); + if (weeks < 5) return `${weeks}w`; + const months = Math.floor(days / 30); + return `${months}mo`; +} + +function timeSinceSubmit(input?: string | Date): string { + if (!input) return ''; + const date = typeof input === 'string' ? new Date(input) : input; + const diffMs = Date.now() - date.getTime(); + const hours = Math.floor(diffMs / 3_600_000); + if (hours < 1) { + const minutes = Math.max(1, Math.floor(diffMs / 60_000)); + return `${minutes} min`; + } + if (hours < 24) return `${hours} Std`; + const days = Math.floor(hours / 24); + return `${days} Tag${days === 1 ? '' : 'e'}`; +} + +type Props = { + domains: CustomDomain[]; + tier: Tier; + onAdd?: () => void; + onSubmit?: (id: string) => Promise<{ ok: boolean }>; + onRemove?: (id: string) => Promise<{ ok: boolean }>; + onUpgradePro?: () => void; +}; + +// Sort-Reihenfolge: User sieht zuerst was Aufmerksamkeit braucht. +// submitted (in Prüfung) > rejected (kann erneut) > active (settled OK) +const STATUS_PRIORITY: Record = { + submitted: 0, + rejected: 1, + active: 2, + approved: 99, +}; + +export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Props) { + const { t } = useTranslation(); + // Slot-relevante Domains (alles außer approved). Sortiert nach Status-Priority, + // innerhalb gleicher Priority dann newest-first by addedAt. + const visible = useMemo(() => { + return domains + .filter((d) => d.status !== 'approved') + .slice() + .sort((a, b) => { + const pa = STATUS_PRIORITY[a.status] ?? 99; + const pb = STATUS_PRIORITY[b.status] ?? 99; + if (pa !== pb) return pa - pb; + const ta = a.addedAt ? new Date(a.addedAt).getTime() : 0; + const tb = b.addedAt ? new Date(b.addedAt).getTime() : 0; + return tb - ta; + }); + }, [domains]); + + return ( + + {/* Header: Section-Title + Slot-Counter + Add-Button (inline, neben SlotPill) */} + + + {t('blocker.domain_section_title')} + + + + {onAdd && ( + + + + + + )} + + + + {/* Progress-Bar — 3-stufige Color-Schwelle: <60% grün, 60-90% orange, >=90% rot */} + {(() => { + const pct = (tier.usedSlots / tier.domainLimit) * 100; + const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a'; + return ( + + + + ); + })()} + + {/* Limit-Reached Upsell (nur Free) */} + {tier.atLimit && tier.plan === 'free' && ( + ({ + backgroundColor: '#eff6ff', + borderWidth: 1, + borderColor: '#bfdbfe', + borderRadius: 12, + padding: 12, + flexDirection: 'row', + alignItems: 'center', + gap: 10, + opacity: pressed ? 0.85 : 1, + })} + > + + + + {t('blocker.domain_limit_title')} + + + {t('blocker.domain_limit_desc')} + + + + )} + + {/* Empty State */} + {visible.length === 0 ? ( + + + + {t('blocker.domain_empty')} + + + ) : ( + + )} + + ); +} + +// ─── SlotPill ───────────────────────────────────────────────────────────── + +function SlotPill({ tier }: { tier: Tier }) { + const bg = tier.atLimit ? '#fee2e2' : '#f5f5f5'; + const fg = tier.atLimit ? '#dc2626' : '#525252'; + return ( + + + {tier.usedSlots}/{tier.domainLimit} + + + ); +} + +// ─── Tiles ──────────────────────────────────────────────────────────────── + +function DomainTilesGrid({ + domains, + tier, + onSubmit, +}: { + domains: CustomDomain[]; + tier: Tier; + onSubmit?: (id: string) => Promise<{ ok: boolean }>; +}) { + // 3-Spalten-Grid via flex-wrap. Parent ScrollView (in blocker.tsx) handles scroll — + // KEIN nested ScrollView hier, sonst collabiert der Layout-Pass weil ScrollView + // inner-content-view keine definierte Width für %-basierte Tile-Widths hat. + return ( + + {domains.map((d) => ( + + + + ))} + + ); +} + +function DomainTile({ + domain, + tier, + onSubmit, +}: { + domain: CustomDomain; + tier: Tier; + onSubmit?: (id: string) => Promise<{ ok: boolean }>; +}) { + const { t } = useTranslation(); + const [submitting, setSubmitting] = useState(false); + const [imgError, setImgError] = useState(false); + const [successVisible, setSuccessVisible] = useState(false); + const [successContent, setSuccessContent] = useState<{ title: string; message: string }>({ + title: '', + message: '', + }); + const [confirmVisible, setConfirmVisible] = useState(false); + const stripped = domain.domain.replace(/^www\./, ''); + + const isLegend = tier.plan === 'legend'; + + // statusColor wird auf Badge + Button angewendet. + // iOS-native: blue (active), orange (submitted), red (rejected). + const statusColor = (() => { + switch (domain.status) { + case 'submitted': + return '#f59e0b'; // orange (Voting/Prüfung) + case 'rejected': + return '#FF3B30'; // iOS-red + default: + return '#007AFF'; // iOS-blue (active, "freigeben"-CTA) + } + })(); + + // Time-Color: nur Status die Aufmerksamkeit brauchen (submitted/rejected) sind farbig. + // Active = neutral gray (settled state, kein Alarm-Indikator nötig). + const timeColor = domain.status === 'active' ? '#a3a3a3' : statusColor; + + const badgeLabel = (() => { + switch (domain.status) { + case 'submitted': + return isLegend ? t('blocker.domain_badge_pruefung') : t('blocker.domain_badge_voting'); + case 'rejected': + return t('blocker.domain_badge_rejected'); + default: + return t('blocker.domain_badge_active'); + } + })(); + + // Tier-aware Confirm-Dialog vor Freigabe — Pro geht zu Community-Voting, + // Legend direkt zum ReBreak-Team. Animiertes Modal statt nativem Alert. + const isResubmit = domain.status === 'rejected'; + const confirmTitle = isLegend + ? isResubmit + ? t('blocker.domain_confirm_legend_resubmit') + : t('blocker.domain_confirm_legend_first') + : isResubmit + ? t('blocker.domain_confirm_community_resubmit') + : t('blocker.domain_confirm_community_first'); + const confirmMessage = isLegend + ? t('blocker.domain_confirm_legend_message', { domain: stripped }) + : t('blocker.domain_confirm_community_message', { domain: stripped }); + + function openConfirm() { + if (!onSubmit) return; + setConfirmVisible(true); + } + + async function handleConfirm() { + setConfirmVisible(false); + if (!onSubmit) return; + + setSubmitting(true); + try { + const result = await onSubmit(domain.id); + if (result.ok) { + setSuccessContent({ + title: isLegend ? t('blocker.domain_success_legend_title') : t('blocker.domain_success_community_title'), + message: isLegend + ? t('blocker.domain_success_legend_message') + : t('blocker.domain_success_community_message'), + }); + setSuccessVisible(true); + } + } finally { + setSubmitting(false); + } + } + + const isFreeAndUsed = tier.plan === 'free' && domain.status !== 'active'; + const showSubmit = tier.canSubmit && domain.status === 'active'; + const showResubmit = tier.canSubmit && domain.status === 'rejected'; + const showInPruefungBtn = domain.status === 'submitted'; + + return ( + + {/* Top-Row: Zeit links · Badge rechts — beide in Status-Color (matcht Bottom-Button). */} + + + + + {timeAgo(domain.addedAt)} + + + + + {badgeLabel} + + + + + {/* Mitte: Favicon + Domain-Name (zentriert, flex-1) */} + + {!imgError ? ( + setImgError(true)} + /> + ) : ( + + + {stripped.slice(0, 2).toUpperCase()} + + + )} + + {stripped} + + + + {/* Bottom-Slot: ALWAYS rendered Container (32px), Inhalt je nach Status. + * Garantiert konsistente Tile-Höhe + sichtbaren Button. */} + + {showInPruefungBtn && ( + + + {isLegend ? t('blocker.domain_btn_rebreak_prueft') : t('blocker.domain_btn_in_abstimmung')} + + + )} + {showSubmit && ( + + {submitting ? ( + + ) : ( + + {t('blocker.domain_btn_freigeben')} + + )} + + )} + {showResubmit && ( + + {submitting ? ( + + ) : ( + + {t('blocker.domain_btn_erneut')} + + )} + + )} + + + {/* Confirm-Modal vor Submit (statt nativer Alert.alert — selber animation-Style wie SuccessAlert) */} + setConfirmVisible(false)} + /> + + {/* Success-Alert mit animiertem Check-Icon nach erfolgreichem Submit */} + setSuccessVisible(false)} + /> + + ); +} diff --git a/apps/rebreak-native/components/blocker/LayerSwitchCard.tsx b/apps/rebreak-native/components/blocker/LayerSwitchCard.tsx new file mode 100644 index 0000000..124820f --- /dev/null +++ b/apps/rebreak-native/components/blocker/LayerSwitchCard.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react'; +import { View, Text, Switch, ActivityIndicator } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; + +type Props = { + icon: React.ComponentProps['name']; + title: string; + subtitle: string; + /** Wenn true: zeigt grünen Check statt Switch (Layer ist an). */ + active: boolean; + /** Aktivierung (zeigt System-Dialog). UI hat nur read-on-flow, + * Toggle-off ist nicht hier — passiert nur über Cooldown. */ + onActivate: () => Promise<{ enabled: boolean; error?: string }>; + /** Optional: Hinweistext unter Subtitle für commit-heavy Layer. */ + warning?: string; +}; + +export function LayerSwitchCard({ icon, title, subtitle, active, onActivate, warning }: Props) { + const [busy, setBusy] = useState(false); + + async function handleSwitch(v: boolean) { + if (!v || active || busy) return; + setBusy(true); + try { + await onActivate(); + } finally { + setBusy(false); + } + } + + const iconBg = active ? '#dcfce7' : '#f5f5f5'; + const iconColor = active ? '#16a34a' : '#737373'; + const borderColor = active ? '#86efac' : '#e5e5e5'; + const cardBg = active ? '#f0fdf4' : '#ffffff'; + + return ( + + + + + + + + {title} + + + {subtitle} + + + + {busy ? ( + + ) : active ? ( + + ) : ( + + )} + + + {warning && !active && ( + + + + {warning} + + + )} + + ); +} diff --git a/apps/rebreak-native/components/blocker/ProtectionCard.tsx b/apps/rebreak-native/components/blocker/ProtectionCard.tsx new file mode 100644 index 0000000..f5a9a06 --- /dev/null +++ b/apps/rebreak-native/components/blocker/ProtectionCard.tsx @@ -0,0 +1,167 @@ +import { View, Text, Switch, Pressable, ActivityIndicator } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import type { ProtectionState } from '../../lib/protection'; +import { colors } from '../../lib/theme'; + +type Props = { + state: ProtectionState; + loading: boolean; + /** Aktiviert den Schutz. UI sollte missingLayers den User durchsteppen lassen. */ + onActivate: () => Promise; + /** Click 1 of 3 — öffnet ProtectionDetailsSheet. KEIN direktes deactivate! */ + onPressSettings: () => void; +}; + +export function ProtectionCard({ state, loading, onActivate, onPressSettings }: Props) { + const { t } = useTranslation(); + const isActive = state.phase === 'active' || state.phase === 'cooldownActive'; + const isCooldown = state.phase === 'cooldownActive'; + + const subtitle = (() => { + if (state.phase === 'inactive') return t('blocker.protection_subtitle_inactive'); + if (state.phase === 'cooldownActive') return t('blocker.protection_subtitle_cooldown'); + if (state.plan === 'free') { + return t('blocker.protection_subtitle_free', { count: state.blocklistCount }); + } + if (state.plan === 'legend') { + return t('blocker.protection_subtitle_legend'); + } + return t('blocker.protection_subtitle_pro'); + })(); + + const cardBg = isCooldown ? '#fef3c7' : isActive ? '#dcfce7' : '#ffffff'; + const cardBorder = isCooldown ? '#fcd34d' : isActive ? '#86efac' : '#e5e5e5'; + const iconBg = isCooldown ? '#fde68a' : isActive ? '#bbf7d0' : '#f5f5f5'; + const iconColor = isCooldown ? '#d97706' : isActive ? '#16a34a' : '#a3a3a3'; + + return ( + + + + + + + + {t('blocker.protection_card_title')} + + + {subtitle} + + + + {/* Loading: Spinner. Inactive: Switch zum aktivieren. Active: Settings-Icon */} + {loading ? ( + + ) : isActive ? ( + ({ + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: '#ffffff', + alignItems: 'center', + justifyContent: 'center', + opacity: pressed ? 0.6 : 1, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 2, + })} + accessibilityLabel={t('blocker.protection_settings_a11y')} + > + + + ) : ( + { + if (v) onActivate(); + }} + trackColor={{ true: '#16a34a' }} + /> + )} + + + {/* Stats-Row nur wenn aktiv und kein Cooldown */} + {state.phase === 'active' && ( + + + + + + )} + + ); +} + +function Stat({ + label, + value, + valueColor = '#0a0a0a', +}: { + label: string; + value: string; + valueColor?: string; +}) { + return ( + + + {value} + + + {label} + + + ); +} + +function formatCount(n: number): string { + if (n >= 1000) return `${Math.floor(n / 1000)}k+`; + return String(n); +} diff --git a/apps/rebreak-native/components/blocker/ProtectionDetailsSheet.tsx b/apps/rebreak-native/components/blocker/ProtectionDetailsSheet.tsx new file mode 100644 index 0000000..0390065 --- /dev/null +++ b/apps/rebreak-native/components/blocker/ProtectionDetailsSheet.tsx @@ -0,0 +1,703 @@ +import { useEffect, useRef, useState } from 'react'; +import { + Modal, + View, + Text, + Pressable, + ScrollView, + Dimensions, + Animated, + PanResponder, + ActivityIndicator, + Easing, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import Svg, { Path, Circle } from 'react-native-svg'; +import type { ProtectionState } from '../../lib/protection'; +import { apiFetch } from '../../lib/api'; + +type Props = { + visible: boolean; + state: ProtectionState; + onClose: () => void; + onRequestDeactivation: () => void; + onTalkToLyra: () => void; +}; + +type StatsResponse = { + current: number; + weeklyAdded: number; + monthlyAdded: number; + history: { label: string; count: number }[]; + submissions: { inVote: number; inReview: number }; + mySubmissions: { active: number; inVote: number; inReview: number }; + avgPerUser: number; + avgApprovalWaitDays: number; +}; + +const SCREEN_HEIGHT = Dimensions.get('window').height; +const DEFAULT_HEIGHT = SCREEN_HEIGHT * 0.85; +const EXPANDED_HEIGHT = SCREEN_HEIGHT * 0.95; +const MIN_HEIGHT = SCREEN_HEIGHT * 0.4; +const DISMISS_HEIGHT = SCREEN_HEIGHT * 0.3; + +// Brand colors +const HERO_COLOR = '#f97316'; // orange-500 (counter accent) +const SEG_ACTIVE = '#16a34a'; +const SEG_VOTE = '#3b82f6'; +const SEG_REVIEW = '#f59e0b'; + +export function ProtectionDetailsSheet({ + visible, + state, + onClose, + onRequestDeactivation, +}: Props) { + const { t, i18n } = useTranslation(); + const localeTag = i18n.language === 'de' ? 'de-DE' : 'en-US'; + + const sheetHeight = useRef(new Animated.Value(DEFAULT_HEIGHT)).current; + const dismissY = useRef(new Animated.Value(0)).current; + const currentHeight = useRef(DEFAULT_HEIGHT); + + useEffect(() => { + if (visible) { + sheetHeight.setValue(DEFAULT_HEIGHT); + dismissY.setValue(0); + currentHeight.current = DEFAULT_HEIGHT; + } + }, [visible, sheetHeight, dismissY]); + + const handleClose = () => { + sheetHeight.setValue(DEFAULT_HEIGHT); + dismissY.setValue(0); + currentHeight.current = DEFAULT_HEIGHT; + onClose(); + }; + + const panResponder = useRef( + PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onPanResponderTerminationRequest: () => false, + onPanResponderMove: (_, g) => { + const next = currentHeight.current - g.dy; + const clamped = Math.max(DISMISS_HEIGHT - 60, Math.min(EXPANDED_HEIGHT + 20, next)); + sheetHeight.setValue(clamped); + }, + onPanResponderRelease: (_, g) => { + const finalH = currentHeight.current - g.dy; + const velocity = g.vy; + + if (finalH < DISMISS_HEIGHT || velocity > 1.5) { + Animated.timing(dismissY, { + toValue: SCREEN_HEIGHT, + duration: 200, + useNativeDriver: true, + }).start(() => handleClose()); + return; + } + + let target = finalH; + if (velocity < -1.5) target = EXPANDED_HEIGHT; + const clamped = Math.max(MIN_HEIGHT, Math.min(EXPANDED_HEIGHT, target)); + + Animated.spring(sheetHeight, { + toValue: clamped, + useNativeDriver: false, + friction: 9, + tension: 70, + }).start(); + currentHeight.current = clamped; + }, + }), + ).current; + + const [stats, setStats] = useState(null); + const [loadingStats, setLoadingStats] = useState(false); + + useEffect(() => { + if (!visible) return; + let alive = true; + setLoadingStats(true); + apiFetch('/api/blocklist/stats') + .then((res) => { if (alive) setStats(res); }) + .catch(() => { /* silent */ }) + .finally(() => { if (alive) setLoadingStats(false); }); + return () => { alive = false; }; + }, [visible]); + + const globalCount = stats?.current ?? state.blocklistCount; + const weeklyAdded = stats?.weeklyAdded ?? 0; + const monthlyAdded = stats?.monthlyAdded ?? 0; + const myActive = stats?.mySubmissions?.active ?? 0; + const myInVote = stats?.mySubmissions?.inVote ?? 0; + const myInReview = stats?.mySubmissions?.inReview ?? 0; + const avgPerUser = stats?.avgPerUser ?? 0; + const avgWait = stats?.avgApprovalWaitDays ?? 0; + + return ( + + + + + + {/* Drag-Bar */} + + + + + {/* Header */} + + + + {t('blocker.details_title')} + + + + {t('blocker.details_done')} + + + + + + {loadingStats && !stats ? ( + + + + ) : null} + + {/* HERO – Globale geblockte Domains: Counter (slow, color) + 2 Delta-Badges */} + + + + {t('blocker.kpi_global_label')} + + + + + + + + + + + {/* SUBMISSIONS – Half Donut mit center-number + center-legend */} + + + + {t('blocker.kpi_submissions_title')} + + + {t('blocker.kpi_submissions_subtitle')} + + + + + + {/* Centered Legend */} + + + + + + + + {/* AVG KPIs – kleiner */} + + + + + + {/* FAQ-Banner: Heading-Row mit Help-Icon rechts (kein gestapeltes Layout) */} + + + + {t('blocker.faq_heading')} + + + + {[1, 2, 3, 4].map((n) => ( + + ))} + + + {/* MEHR INFO – outline button, Icon + Label nebeneinander (flex-row, NICHT col) */} + ({ + marginTop: 4, + paddingVertical: 14, + paddingHorizontal: 16, + borderRadius: 12, + borderWidth: 1.5, + borderColor: HERO_COLOR, + backgroundColor: pressed ? '#fed7aa' : '#fff7ed', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + })} + > + + + {t('blocker.more_info_title')} + + + + + + + ); +} + +// ─── Animated Counter ────────────────────────────────────────────────────── +function AnimatedCounter({ + value, + locale, + decimals = 0, + durationMs = 1200, + style, +}: { + value: number; + locale: string; + decimals?: number; + durationMs?: number; + style?: any; +}) { + const anim = useRef(new Animated.Value(0)).current; + const [display, setDisplay] = useState(0); + + useEffect(() => { + anim.setValue(0); + const listener = anim.addListener(({ value: v }) => { + setDisplay(v * value); + }); + Animated.timing(anim, { + toValue: 1, + duration: durationMs, + easing: Easing.out(Easing.cubic), + useNativeDriver: false, + }).start(); + return () => anim.removeListener(listener); + }, [value, anim, durationMs]); + + const formatted = display.toLocaleString(locale, { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); + + return {formatted}; +} + +// ─── Delta Badge (e.g. "+25 diese Woche ↗") ──────────────────────────────── +function DeltaBadge({ + value, + label, + locale, +}: { + value: number; + label: string; + locale: string; +}) { + const formatted = `+${value.toLocaleString(locale)}`; + return ( + + + + + + + {formatted} + + + {label} + + + + ); +} + +// ─── KPI Card (small) ────────────────────────────────────────────────────── +function KpiCard({ + icon, + label, + value, + locale, + decimals = 0, + suffix, +}: { + icon: any; + label: string; + value: number; + locale: string; + decimals?: number; + suffix?: string; +}) { + return ( + + + + + {label} + + + + + {suffix ? ( + {suffix} + ) : null} + + + ); +} + +// ─── Legend Item (compact, centered row) ─────────────────────────────────── +function LegendItem({ + color, + label, + value, +}: { + color: string; + label: string; + value: number; +}) { + return ( + + + + {value} + + {label} + + ); +} + +// ─── Half Donut (multi-segment) ──────────────────────────────────────────── +function HalfDonut({ + segments, + centerValue, + centerLabel, +}: { + segments: { value: number; color: string }[]; + centerValue: number; + centerLabel: string; +}) { + const total = Math.max(1, segments.reduce((s, x) => s + x.value, 0)); + + const W = 220; + const H = 130; + const cx = W / 2; + const cy = H - 8; + const r = 90; + const stroke = 18; + + // Compute cumulative angles in [180, 360] + let cumAngle = 180; + const arcs = segments.map((seg) => { + const startAngle = cumAngle; + const endAngle = cumAngle + 180 * (seg.value / total); + cumAngle = endAngle; + return { ...seg, startAngle, endAngle }; + }); + + const animProgress = useRef(new Animated.Value(0)).current; + const [progress, setProgress] = useState(0); + + useEffect(() => { + animProgress.setValue(0); + const l = animProgress.addListener(({ value }) => setProgress(value)); + Animated.timing(animProgress, { + toValue: 1, + duration: 1100, + easing: Easing.out(Easing.cubic), + useNativeDriver: false, + }).start(); + return () => animProgress.removeListener(l); + }, [centerValue, animProgress]); + + return ( + + + {/* Background track */} + + {arcs.map((a, i) => { + const animatedEnd = + a.startAngle + (a.endAngle - a.startAngle) * progress; + if (animatedEnd <= a.startAngle + 0.5) return null; + return ( + + ); + })} + {centerValue === 0 && ( + + )} + + + {/* Center number — exactly centered horizontally + vertically inside semicircle */} + + + {centerValue} + + + {centerLabel} + + + + ); +} + +function arcPath(cx: number, cy: number, r: number, startDeg: number, endDeg: number) { + const start = polar(cx, cy, r, startDeg); + const end = polar(cx, cy, r, endDeg); + const largeArc = endDeg - startDeg > 180 ? 1 : 0; + return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y}`; +} + +function polar(cx: number, cy: number, r: number, angleDeg: number) { + const rad = (angleDeg * Math.PI) / 180; + return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }; +} + +// ─── FAQ Item (chevron AT END of header row, on right) ───────────────────── +function FaqItem({ question, answer }: { question: string; answer: string }) { + const [open, setOpen] = useState(false); + const rotateAnim = useRef(new Animated.Value(0)).current; + + useEffect(() => { + Animated.timing(rotateAnim, { + toValue: open ? 1 : 0, + duration: 200, + useNativeDriver: true, + }).start(); + }, [open, rotateAnim]); + + const rotate = rotateAnim.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '90deg'], + }); + + return ( + + setOpen((v) => !v)} + style={({ pressed }) => ({ + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 14, + paddingVertical: 14, + backgroundColor: pressed ? '#fafafa' : '#fff', + })} + > + + {question} + + + + + + {open && ( + + + {answer} + + + )} + + ); +} diff --git a/apps/rebreak-native/components/blocker/ProtectionLockedCard.tsx b/apps/rebreak-native/components/blocker/ProtectionLockedCard.tsx new file mode 100644 index 0000000..5d1f88c --- /dev/null +++ b/apps/rebreak-native/components/blocker/ProtectionLockedCard.tsx @@ -0,0 +1,130 @@ +import { View, Text, Pressable } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import type { ProtectionState } from '../../lib/protection'; + +type Props = { + state: ProtectionState; + /** Click-1 of 3-Click-Cooldown-Trigger — öffnet ProtectionDetailsSheet. */ + onPressSettings: () => void; +}; + +/** + * Wird gezeigt sobald Family Controls aktiv ist — der Schutz ist dann + * "locked in" und kann nur über den Cooldown-Flow deaktiviert werden. + * Daher: KEINE Switches mehr, nur ein Settings-Icon das den 3-Click-Flow startet. + */ +export function ProtectionLockedCard({ state, onPressSettings }: Props) { + const { t } = useTranslation(); + const isCooldown = state.phase === 'cooldownActive'; + const cardBg = isCooldown ? '#fef3c7' : '#dcfce7'; + const cardBorder = isCooldown ? '#fcd34d' : '#86efac'; + const iconBg = isCooldown ? '#fde68a' : '#bbf7d0'; + const iconColor = isCooldown ? '#d97706' : '#16a34a'; + + const subtitle = (() => { + if (isCooldown) return t('blocker.protection_subtitle_cooldown'); + if (state.plan === 'legend') { + return t('blocker.protection_subtitle_legend'); + } + if (state.plan === 'pro') { + return t('blocker.protection_subtitle_pro'); + } + return t('blocker.protection_subtitle_free', { count: state.blocklistCount }); + })(); + + return ( + + + + + + + + {t('blocker.protection_card_locked_title')} + + + {subtitle} + + + + ({ + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: '#ffffff', + alignItems: 'center', + justifyContent: 'center', + opacity: pressed ? 0.6 : 1, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 2, + })} + accessibilityLabel={t('blocker.protection_settings_a11y')} + > + + + + + {/* Stats nur wenn aktiv und kein Cooldown */} + {!isCooldown && ( + + + + + + )} + + ); +} + +function Stat({ label, value, valueColor = '#0a0a0a' }: { label: string; value: string; valueColor?: string }) { + return ( + + {value} + {label} + + ); +} + +function formatCount(n: number): string { + if (n >= 1000) return `${Math.floor(n / 1000)}k+`; + return String(n); +} diff --git a/apps/rebreak-native/components/chat/ChatBubble.tsx b/apps/rebreak-native/components/chat/ChatBubble.tsx new file mode 100644 index 0000000..0fa07a9 --- /dev/null +++ b/apps/rebreak-native/components/chat/ChatBubble.tsx @@ -0,0 +1,442 @@ +import { useState, useRef } from 'react'; +import { + View, + Text, + Pressable, + Image, + StyleSheet, + Modal, + Alert, + Platform, +} from 'react-native'; +import * as Clipboard from 'expo-clipboard'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { resolveAvatar } from '../../lib/resolveAvatar'; + +export type ChatMsg = { + id: string; + userId: string; + nickname?: string | null; + avatar?: string | null; + content: string; + replyTo?: { + id: string; + userId: string; + nickname?: string | null; + content: string; + attachmentType?: string | null; + } | null; + attachmentUrl?: string | null; + attachmentType?: string | null; + attachmentName?: string | null; + likesCount: number; + likedByMe?: boolean; + createdAt: string; + isOwn: boolean; + readAt?: string | null; +}; + +type Props = { + msg: ChatMsg; + showName?: boolean; + isFirstInGroup?: boolean; + isLastInGroup?: boolean; + hideReadStatus?: boolean; + onReply: (msg: ChatMsg) => void; + onLike: (msg: ChatMsg) => void; + onOpenImage: (url: string) => void; +}; + +function formatTime(ts: string) { + return new Date(ts).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); +} + +export function ChatBubble({ + msg, + showName = false, + isFirstInGroup = true, + isLastInGroup = true, + hideReadStatus = false, + onReply, + onLike, + onOpenImage, +}: Props) { + const { t } = useTranslation(); + const [actionsOpen, setActionsOpen] = useState(false); + const longPressTimer = useRef | null>(null); + + const isImageOnly = + !!msg.attachmentUrl && msg.attachmentType === 'image' && !msg.content && !msg.replyTo; + const replyHasAttachment = msg.replyTo?.attachmentType === 'image'; + const avatarUrl = msg.avatar ? msg.avatar : resolveAvatar(null, msg.nickname ?? '?'); + + const cornerStyle = msg.isOwn + ? isLastInGroup + ? { borderBottomRightRadius: 6 } + : { borderTopRightRadius: 6, borderBottomRightRadius: 6 } + : isLastInGroup + ? { borderBottomLeftRadius: 6 } + : { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }; + + function copyContent() { + if (msg.content) Clipboard.setStringAsync(msg.content); + setActionsOpen(false); + } + + return ( + <> + + {/* Avatar slot left (last of group, not own) */} + {!msg.isOwn && ( + + {isLastInGroup ? ( + + ) : null} + + )} + + + {showName && !msg.isOwn && isFirstInGroup && ( + + {msg.nickname ?? '?'} + + )} + + setActionsOpen(true)} + onPress={() => { + /* tap eats - keeps long-press primary */ + }} + style={[ + styles.bubble, + msg.isOwn ? styles.bubbleOwn : styles.bubbleOther, + cornerStyle, + isImageOnly && { padding: 4 }, + ]} + > + {/* Reply preview */} + {msg.replyTo && ( + { + /* could implement scroll-to */ + }} + style={[ + styles.replyPreview, + { + backgroundColor: msg.isOwn ? 'rgba(255,255,255,0.18)' : '#e5e5e5', + borderLeftColor: msg.isOwn ? '#fff' : '#007AFF', + }, + ]} + > + + {msg.replyTo.nickname ?? '?'} + + + {replyHasAttachment && ( + + )}{' '} + {msg.replyTo.content || (replyHasAttachment ? t('chat.image_attachment') : '…')} + + + )} + + {/* Image attachment */} + {msg.attachmentUrl && msg.attachmentType === 'image' && ( + onOpenImage(msg.attachmentUrl!)} + style={[styles.imageWrap, msg.content ? { marginBottom: 4 } : null]} + > + + {isImageOnly && ( + + {msg.likesCount > 0 && ( + + + + {msg.likesCount} + + + )} + {formatTime(msg.createdAt)} + + )} + + )} + + {/* File attachment */} + {msg.attachmentUrl && msg.attachmentType !== 'image' && ( + + + + {msg.attachmentName ?? t('chat.file_attachment')} + + + )} + + {/* Content */} + {msg.content !== '' && ( + + {msg.content} + + )} + + {/* Footer */} + {!isImageOnly && ( + + {msg.likesCount > 0 && ( + + + + {msg.likesCount} + + + )} + + {formatTime(msg.createdAt)} + + {msg.isOwn && !hideReadStatus && ( + + )} + + )} + + + + + {/* Long-press action sheet */} + setActionsOpen(false)} + > + setActionsOpen(false)}> + {}}> + + { + setActionsOpen(false); + onReply(msg); + }} + > + + {t('chat.reply')} + + { + setActionsOpen(false); + onLike(msg); + }} + > + + + {msg.likedByMe ? t('chat.unlike') : t('chat.like')} + + + {msg.content !== '' && ( + + + {t('chat.copy')} + + )} + + + + + ); +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + paddingHorizontal: 8, + }, + avatarSlot: { + width: 30, + marginRight: 4, + justifyContent: 'flex-end', + }, + avatar: { + width: 26, + height: 26, + borderRadius: 13, + backgroundColor: '#e5e5e5', + }, + bubbleCol: { + maxWidth: '78%', + }, + nickname: { + fontSize: 10, + fontFamily: 'Nunito_700Bold', + color: '#007AFF', + marginBottom: 2, + marginLeft: 10, + }, + bubble: { + borderRadius: 18, + paddingHorizontal: 12, + paddingVertical: 6, + shadowColor: '#000', + shadowOpacity: 0.05, + shadowRadius: 1, + shadowOffset: { width: 0, height: 1 }, + }, + bubbleOwn: { + backgroundColor: '#007AFF', + }, + bubbleOther: { + backgroundColor: '#ffffff', + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#e5e5e5', + }, + replyPreview: { + borderLeftWidth: 3, + borderRadius: 8, + paddingHorizontal: 8, + paddingVertical: 4, + marginBottom: 4, + }, + imageWrap: { + borderRadius: 12, + overflow: 'hidden', + position: 'relative', + }, + image: { + width: 220, + height: 220, + backgroundColor: '#f5f5f5', + }, + imageTimeOverlay: { + position: 'absolute', + bottom: 6, + right: 6, + backgroundColor: 'rgba(0,0,0,0.5)', + borderRadius: 10, + paddingHorizontal: 6, + paddingVertical: 2, + flexDirection: 'row', + alignItems: 'center', + }, + content: { + fontSize: 14, + lineHeight: 20, + fontFamily: 'Nunito_400Regular', + }, + footer: { + position: 'absolute', + bottom: 4, + right: 8, + flexDirection: 'row', + alignItems: 'center', + }, + sheetBackdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'flex-end', + }, + sheet: { + backgroundColor: '#fff', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + padding: 8, + paddingBottom: Platform.OS === 'ios' ? 32 : 16, + }, + sheetGrabber: { + width: 36, + height: 4, + borderRadius: 2, + backgroundColor: '#d4d4d4', + alignSelf: 'center', + marginBottom: 10, + }, + sheetItem: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 14, + borderRadius: 12, + }, + sheetText: { + fontSize: 15, + fontFamily: 'Nunito_600SemiBold', + color: '#171717', + marginLeft: 12, + }, +}); diff --git a/apps/rebreak-native/components/chat/ChatInput.tsx b/apps/rebreak-native/components/chat/ChatInput.tsx new file mode 100644 index 0000000..48263c5 --- /dev/null +++ b/apps/rebreak-native/components/chat/ChatInput.tsx @@ -0,0 +1,332 @@ +import { useState, useRef } from 'react'; +import { + View, + Text, + TextInput, + Pressable, + Image, + StyleSheet, + ActivityIndicator, + Platform, + Alert, +} from 'react-native'; +import * as ImagePicker from 'expo-image-picker'; +import * as FileSystem from 'expo-file-system'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { supabase } from '../../lib/supabase'; + +type ReplyTo = { id: string; nickname: string; content: string }; + +export type SendPayload = { + content: string; + replyToId?: string; + attachmentUrl?: string; + attachmentType?: string; + attachmentName?: string; +}; + +type Props = { + replyTo: ReplyTo | null; + sending?: boolean; + placeholder?: string; + disabled?: boolean; + onSend: (data: SendPayload) => void; + onCancelReply: () => void; +}; + +export function ChatInput({ + replyTo, + sending, + placeholder, + disabled, + onSend, + onCancelReply, +}: Props) { + const { t } = useTranslation(); + const [text, setText] = useState(''); + const [attachment, setAttachment] = useState<{ + uri: string; + name: string; + isImage: boolean; + } | null>(null); + const [uploading, setUploading] = useState(false); + const inputRef = useRef(null); + + const hasContent = text.trim().length > 0 || attachment !== null; + + async function pickImage() { + const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!perm.granted) { + Alert.alert('Foto-Zugriff', 'Bitte Foto-Zugriff in den Einstellungen erlauben.'); + return; + } + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + quality: 0.8, + }); + if (!result.canceled && result.assets[0]?.uri) { + const a = result.assets[0]; + setAttachment({ + uri: a.uri, + name: a.fileName ?? `image-${Date.now()}.jpg`, + isImage: true, + }); + } + } + + function clearAttachment() { + setAttachment(null); + } + + async function uploadAttachment(): Promise<{ + url: string; + type: string; + name: string; + } | null> { + if (!attachment) return null; + try { + setUploading(true); + const ext = attachment.name.split('.').pop() || 'jpg'; + const path = `chat/${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`; + const base64 = await FileSystem.readAsStringAsync(attachment.uri, { + encoding: FileSystem.EncodingType.Base64, + }); + const arrayBuffer = decodeBase64(base64); + const { error } = await supabase.storage + .from('chat-attachments') + .upload(path, arrayBuffer, { + cacheControl: '3600', + upsert: false, + contentType: attachment.isImage ? 'image/jpeg' : 'application/octet-stream', + }); + if (error) throw error; + const { data } = supabase.storage.from('chat-attachments').getPublicUrl(path); + return { + url: data.publicUrl, + type: attachment.isImage ? 'image' : 'file', + name: attachment.name, + }; + } catch (err: any) { + Alert.alert(t('chat.upload_failed'), err?.message ?? ''); + return null; + } finally { + setUploading(false); + } + } + + async function handleSend() { + const content = text.trim(); + if (!content && !attachment) return; + + let attachmentMeta: { url: string; type: string; name: string } | null = null; + if (attachment) { + attachmentMeta = await uploadAttachment(); + if (!attachmentMeta) return; + } + + onSend({ + content, + replyToId: replyTo?.id, + attachmentUrl: attachmentMeta?.url, + attachmentType: attachmentMeta?.type, + attachmentName: attachmentMeta?.name, + }); + setText(''); + setAttachment(null); + } + + return ( + + {/* Reply preview */} + {replyTo && ( + + + + + {t('chat.reply_to')} {replyTo.nickname} + + + {replyTo.content || '…'} + + + + + + + )} + + {/* Attachment preview */} + {attachment && ( + + {attachment.isImage ? ( + + ) : ( + + + + )} + + {attachment.name} + + + + + + )} + + {/* Input row */} + + + + + + + + + + + {sending || uploading ? ( + + ) : ( + + )} + + + + ); +} + +// Base64 → Uint8Array (für Supabase Storage Upload) +function decodeBase64(base64: string): Uint8Array { + const binary = + typeof atob === 'function' + ? atob(base64) + : Buffer.from(base64, 'base64').toString('binary'); + const len = binary.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i); + return bytes; +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#ffffff', + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: '#e5e5e5', + }, + replyBar: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 8, + backgroundColor: '#eff6ff', + borderLeftWidth: 3, + borderLeftColor: '#007AFF', + marginHorizontal: 8, + marginTop: 6, + borderRadius: 8, + }, + replyName: { + fontSize: 11, + fontFamily: 'Nunito_700Bold', + color: '#007AFF', + }, + replyContent: { + fontSize: 11, + fontFamily: 'Nunito_400Regular', + color: '#525252', + marginTop: 1, + }, + attachBar: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 6, + backgroundColor: '#fafafa', + marginHorizontal: 8, + marginTop: 6, + borderRadius: 8, + }, + attachImg: { + width: 36, + height: 36, + borderRadius: 6, + marginRight: 8, + }, + attachFileIcon: { + width: 36, + height: 36, + borderRadius: 6, + backgroundColor: '#e5e5e5', + alignItems: 'center', + justifyContent: 'center', + marginRight: 8, + }, + attachName: { + flex: 1, + fontSize: 12, + fontFamily: 'Nunito_600SemiBold', + color: '#171717', + }, + row: { + flexDirection: 'row', + alignItems: 'flex-end', + paddingHorizontal: 8, + paddingTop: 8, + paddingBottom: 8, + }, + iconBtn: { + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + marginRight: 4, + }, + inputWrap: { + flex: 1, + backgroundColor: '#f5f5f5', + borderRadius: 22, + paddingHorizontal: 14, + minHeight: 36, + maxHeight: 120, + justifyContent: 'center', + }, + input: { + fontSize: 14, + lineHeight: 19, + fontFamily: 'Nunito_400Regular', + color: '#171717', + paddingVertical: Platform.OS === 'ios' ? 8 : 4, + }, + sendBtn: { + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + marginLeft: 6, + }, +}); diff --git a/apps/rebreak-native/components/chat/CreateRoomSheet.tsx b/apps/rebreak-native/components/chat/CreateRoomSheet.tsx new file mode 100644 index 0000000..5b9be05 --- /dev/null +++ b/apps/rebreak-native/components/chat/CreateRoomSheet.tsx @@ -0,0 +1,282 @@ +import { useState } from 'react'; +import { + Modal, + View, + Text, + TextInput, + Pressable, + StyleSheet, + ActivityIndicator, + Platform, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { apiFetch } from '../../lib/api'; + +type Props = { + visible: boolean; + onClose: () => void; + onCreated: (room: any) => void; +}; + +export function CreateRoomSheet({ visible, onClose, onCreated }: Props) { + const { t } = useTranslation(); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [isPublic, setIsPublic] = useState(true); + const [joinMode, setJoinMode] = useState<'approval' | 'invite_only'>('approval'); + const [creating, setCreating] = useState(false); + + function reset() { + setName(''); + setDescription(''); + setIsPublic(true); + setJoinMode('approval'); + } + + async function create() { + const trimmed = name.trim(); + if (!trimmed || creating) return; + setCreating(true); + try { + const room = await apiFetch('/api/chat/rooms', { + method: 'POST', + body: { + name: trimmed, + description: description.trim() || undefined, + isPublic, + joinMode: isPublic ? 'open' : joinMode, + }, + }); + onCreated(room); + reset(); + onClose(); + } catch (err: any) { + console.error('Room erstellen fehlgeschlagen:', err.message); + } finally { + setCreating(false); + } + } + + return ( + + + {}}> + + {t('chat.create_group')} + + + + + {/* Public toggle */} + setIsPublic((v) => !v)} + > + {t('chat.public_room')} + + + + + + {/* Join mode (private only) */} + {!isPublic && ( + + {t('chat.join_mode')} + + {(['approval', 'invite_only'] as const).map((mode) => ( + setJoinMode(mode)} + > + + {t(`chat.join_mode_${mode === 'approval' ? 'approval' : 'invite'}`)} + + + ))} + + + )} + + {/* Actions */} + + + {t('common.cancel')} + + + {creating ? ( + + ) : ( + {t('chat.create')} + )} + + + + + + ); +} + +const styles = StyleSheet.create({ + backdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'flex-end', + }, + sheet: { + backgroundColor: '#fff', + borderTopLeftRadius: 22, + borderTopRightRadius: 22, + padding: 18, + paddingBottom: Platform.OS === 'ios' ? 32 : 18, + }, + grabber: { + width: 36, + height: 4, + borderRadius: 2, + backgroundColor: '#d4d4d4', + alignSelf: 'center', + marginBottom: 12, + }, + title: { + fontSize: 17, + fontFamily: 'Nunito_700Bold', + color: '#171717', + marginBottom: 14, + }, + input: { + backgroundColor: '#f5f5f5', + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 12, + fontSize: 14, + fontFamily: 'Nunito_400Regular', + color: '#171717', + marginBottom: 10, + }, + toggleRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 6, + marginTop: 4, + }, + toggleLabel: { + fontSize: 14, + fontFamily: 'Nunito_600SemiBold', + color: '#171717', + }, + toggle: { + width: 46, + height: 28, + borderRadius: 14, + backgroundColor: '#e5e5e5', + padding: 2, + justifyContent: 'center', + }, + toggleOn: { + backgroundColor: '#007AFF', + }, + toggleKnob: { + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: '#fff', + shadowColor: '#000', + shadowOpacity: 0.15, + shadowRadius: 2, + shadowOffset: { width: 0, height: 1 }, + elevation: 2, + }, + toggleKnobOn: { + transform: [{ translateX: 18 }], + }, + subLabel: { + fontSize: 12, + fontFamily: 'Nunito_600SemiBold', + color: '#737373', + marginBottom: 6, + }, + modeRow: { + flexDirection: 'row', + }, + modeBtn: { + flex: 1, + paddingVertical: 8, + borderRadius: 10, + borderWidth: 1, + borderColor: '#e5e5e5', + alignItems: 'center', + marginRight: 6, + }, + modeBtnActive: { + backgroundColor: '#eff6ff', + borderColor: '#007AFF', + }, + modeBtnText: { + fontSize: 12, + fontFamily: 'Nunito_600SemiBold', + color: '#737373', + }, + modeBtnTextActive: { + color: '#007AFF', + }, + actions: { + flexDirection: 'row', + marginTop: 20, + }, + cancelBtn: { + flex: 1, + backgroundColor: '#f5f5f5', + paddingVertical: 12, + borderRadius: 12, + alignItems: 'center', + marginRight: 6, + }, + cancelText: { + fontSize: 14, + fontFamily: 'Nunito_600SemiBold', + color: '#171717', + }, + createBtn: { + flex: 1, + backgroundColor: '#007AFF', + paddingVertical: 12, + borderRadius: 12, + alignItems: 'center', + marginLeft: 6, + }, + createText: { + fontSize: 14, + fontFamily: 'Nunito_700Bold', + color: '#fff', + }, +}); diff --git a/apps/rebreak-native/components/chat/RoomCard.tsx b/apps/rebreak-native/components/chat/RoomCard.tsx new file mode 100644 index 0000000..1afbac5 --- /dev/null +++ b/apps/rebreak-native/components/chat/RoomCard.tsx @@ -0,0 +1,217 @@ +import { View, Text, Pressable, Image, StyleSheet } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; + +export type Room = { + id: string; + name: string; + description?: string | null; + isPublic: boolean; + isDefault: boolean; + memberCount: number; + isMember: boolean; + avatarUrl?: string | null; + lastMessage?: { content: string; createdAt: string; senderName: string } | null; +}; + +type Props = { + room: Room; + onPress: () => void; +}; + +function formatTime(ts: string, justNow: string) { + const diff = Date.now() - new Date(ts).getTime(); + if (diff < 60_000) return justNow; + if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`; + if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`; + return new Date(ts).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); +} + +export function RoomCard({ room, onPress }: Props) { + const { t } = useTranslation(); + const initials = room.name + .split(' ') + .slice(0, 2) + .map((w) => w[0]?.toUpperCase() ?? '') + .join(''); + + return ( + + {({ pressed }) => ( + + + {room.avatarUrl ? ( + + ) : !room.isPublic ? ( + {initials} + ) : ( + + )} + + + + + + {room.name} + + {room.isDefault && ( + + Standard + + )} + {room.lastMessage && ( + + {formatTime(room.lastMessage.createdAt, t('chat.just_now'))} + + )} + + + + {room.lastMessage ? ( + + + {room.lastMessage.senderName}:{' '} + + {room.lastMessage.content} + + ) : room.description ? ( + + {room.description} + + ) : null} + + + + {room.memberCount} + + {!room.isMember && ( + + {t('chat.join')} + + )} + + + + )} + + ); +} + +const styles = StyleSheet.create({ + row: { + width: '100%', + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 14, + paddingVertical: 11, + backgroundColor: '#fff', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#f5f5f5', + }, + avatar: { + width: 42, + height: 42, + borderRadius: 21, + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + marginRight: 10, + }, + avatarImg: { + width: 42, + height: 42, + }, + avatarInitials: { + fontSize: 13, + fontFamily: 'Nunito_700Bold', + color: '#525252', + }, + info: { + flex: 1, + minWidth: 0, + }, + headerRow: { + flexDirection: 'row', + alignItems: 'center', + }, + footerRow: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 3, + }, + footerTextWrap: { + flex: 1, + minWidth: 0, + }, + metaPill: { + flexDirection: 'row', + alignItems: 'center', + marginLeft: 8, + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 8, + backgroundColor: '#f5f5f5', + }, + name: { + fontSize: 14, + fontFamily: 'Nunito_700Bold', + color: '#171717', + flexShrink: 1, + }, + defaultBadge: { + marginLeft: 6, + paddingHorizontal: 6, + paddingVertical: 1, + backgroundColor: '#eff6ff', + borderRadius: 8, + }, + defaultBadgeText: { + fontSize: 9, + fontFamily: 'Nunito_700Bold', + color: '#007AFF', + }, + lastMessage: { + fontSize: 12, + fontFamily: 'Nunito_400Regular', + color: '#737373', + }, + description: { + fontSize: 12, + fontFamily: 'Nunito_400Regular', + color: '#a3a3a3', + }, + right: { + alignItems: 'flex-end', + marginLeft: 8, + }, + memberCount: { + fontSize: 11, + fontFamily: 'Nunito_700Bold', + color: '#737373', + marginLeft: 3, + }, + time: { + fontSize: 10, + fontFamily: 'Nunito_500Medium', + color: '#a3a3a3', + marginLeft: 'auto', + paddingLeft: 6, + }, + joinBadge: { + marginLeft: 6, + paddingHorizontal: 8, + paddingVertical: 3, + backgroundColor: '#eff6ff', + borderRadius: 10, + }, + joinBadgeText: { + fontSize: 10, + fontFamily: 'Nunito_700Bold', + color: '#007AFF', + }, +}); diff --git a/apps/rebreak-native/components/mail/ConnectMailSheet.tsx b/apps/rebreak-native/components/mail/ConnectMailSheet.tsx new file mode 100644 index 0000000..726dd8b --- /dev/null +++ b/apps/rebreak-native/components/mail/ConnectMailSheet.tsx @@ -0,0 +1,605 @@ +import { useEffect, useRef, useState } from 'react'; +import { + ActivityIndicator, + Animated, + Dimensions, + Easing, + KeyboardAvoidingView, + Linking, + Modal, + Platform, + Pressable, + ScrollView, + Text, + TextInput, + View, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { useMailConnect, detectProvider, type MailProvider } from '../../hooks/useMailConnect'; + +const SCREEN_HEIGHT = Dimensions.get('window').height; +const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65; + +type Props = { + visible: boolean; + onClose: () => void; + onSuccess: () => void; +}; + +type ProviderConfig = { + id: MailProvider; + labelKey: string; + icon: React.ComponentProps['name']; + color: string; + guideKey: string; + guideUrl: string; +}; + +const PROVIDERS: ProviderConfig[] = [ + { + id: 'gmail', + labelKey: 'mail.provider_gmail', + icon: 'mail', + color: '#EA4335', + guideKey: 'mail.app_password_guide_gmail', + guideUrl: 'https://myaccount.google.com/apppasswords', + }, + { + id: 'icloud', + labelKey: 'mail.provider_icloud', + icon: 'cloud', + color: '#007AFF', + guideKey: 'mail.app_password_guide_icloud', + guideUrl: 'https://appleid.apple.com/account/manage', + }, + { + id: 'outlook', + labelKey: 'mail.provider_outlook', + icon: 'mail-open', + color: '#0078D4', + guideKey: 'mail.app_password_guide_outlook', + guideUrl: 'https://account.microsoft.com/security', + }, + { + id: 'yahoo', + labelKey: 'mail.provider_yahoo', + icon: 'at', + color: '#7C3AED', + guideKey: 'mail.app_password_guide_yahoo', + guideUrl: 'https://login.yahoo.com/account/security', + }, + { + id: 'gmx', + labelKey: 'mail.provider_gmx', + icon: 'mail-unread', + color: '#E87A22', + guideKey: 'mail.app_password_guide_gmx', + guideUrl: 'https://www.gmx.net/mail/security', + }, + { + id: 'other', + labelKey: 'mail.provider_other', + icon: 'server', + color: '#737373', + guideKey: 'mail.app_password_guide_other', + guideUrl: '', + }, +]; + +/** + * Bottom-Sheet (65% Screen-Höhe) zum Verbinden eines Postfachs. + * + * Zwei Ansichten im selben Sheet: + * 1. Provider-Grid (6 Tiles) + * 2. Formular-View: Email + App-Passwort + Guide-Link (nach Provider-Tap) + */ +export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) { + const { t } = useTranslation(); + const insets = useSafeAreaInsets(); + const { connect, connecting, error: connectError } = useMailConnect(); + + const [view, setView] = useState<'grid' | 'form'>('grid'); + const [selectedProvider, setSelectedProvider] = useState(null); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [passwordVisible, setPasswordVisible] = useState(false); + const [formError, setFormError] = useState(null); + + const translateY = useRef(new Animated.Value(SHEET_HEIGHT)).current; + const backdropOpacity = useRef(new Animated.Value(0)).current; + + useEffect(() => { + if (visible) { + translateY.setValue(SHEET_HEIGHT); + backdropOpacity.setValue(0); + Animated.parallel([ + Animated.timing(translateY, { + toValue: 0, + duration: 280, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + Animated.timing(backdropOpacity, { + toValue: 1, + duration: 220, + useNativeDriver: true, + }), + ]).start(); + } + }, [visible, translateY, backdropOpacity]); + + function handleClose() { + setView('grid'); + setSelectedProvider(null); + setEmail(''); + setPassword(''); + setPasswordVisible(false); + setFormError(null); + onClose(); + } + + function handleProviderSelect(provider: ProviderConfig) { + setSelectedProvider(provider); + setView('form'); + setFormError(null); + } + + function handleBack() { + setView('grid'); + setSelectedProvider(null); + setFormError(null); + } + + async function handleConnect() { + if (!email.trim() || !password.trim()) { + setFormError(t('mail.form_fields_required')); + return; + } + setFormError(null); + + const body: Parameters[0] = { email: email.trim(), password }; + + // Für "other" Provider: User muss imapHost selbst eingeben — aktuell nicht + // unterstützt in dieser Sheet-Version. Custom-IMAP bleibt TODO für Phase 11. + // Provider-Detection passiert server-seitig via Email-Domain. + + const result = await connect(body); + if (result.ok) { + handleClose(); + onSuccess(); + } else { + setFormError(result.error ?? t('mail.connect_failed')); + } + } + + // Wenn User Email tippt → Provider-Icon in Echtzeit updaten + const detectedProvider = email.includes('@') ? detectProvider(email) : null; + const currentProvider = selectedProvider ?? null; + + return ( + + {/* Backdrop */} + + + + + {/* Sheet */} + + + {/* Drag-Handle */} + + + + + {/* Header */} + + {view === 'form' ? ( + + + {t('common.back')} + + + ) : ( + + + {t('common.cancel')} + + + )} + + + {view === 'form' && currentProvider + ? t(currentProvider.labelKey) + : t('mail.connect_sheet_title')} + + + + + + {/* Content */} + {view === 'grid' ? ( + + ) : ( + { setEmail(v); setFormError(null); }} + password={password} + onPasswordChange={(v) => { setPassword(v); setFormError(null); }} + passwordVisible={passwordVisible} + onTogglePasswordVisible={() => setPasswordVisible((p) => !p)} + error={formError ?? connectError} + connecting={connecting} + onConnect={handleConnect} + insets={insets} + t={t} + /> + )} + + + + ); +} + +// --------------------------------------------------------------------------- +// Sub-View: Provider-Grid +// --------------------------------------------------------------------------- + +function ProviderGrid({ + providers, + onSelect, + t, +}: { + providers: ProviderConfig[]; + onSelect: (p: ProviderConfig) => void; + t: (key: string) => string; +}) { + return ( + + + {t('mail.connect_sheet_subtitle')} + + + + {providers.map((p) => ( + onSelect(p)} + style={({ pressed }) => ({ + width: '47%', + flexDirection: 'row', + alignItems: 'center', + gap: 10, + backgroundColor: '#f9f9f9', + borderWidth: 1, + borderColor: '#e5e5e5', + borderRadius: 14, + padding: 14, + opacity: pressed ? 0.7 : 1, + })} + > + + + + + + {t(p.labelKey)} + + + + + ))} + + + ); +} + +// --------------------------------------------------------------------------- +// Sub-View: Formular (Email + App-Passwort) +// --------------------------------------------------------------------------- + +type FormViewProps = { + provider: ProviderConfig | null; + detectedProvider: MailProvider | null; + email: string; + onEmailChange: (v: string) => void; + password: string; + onPasswordChange: (v: string) => void; + passwordVisible: boolean; + onTogglePasswordVisible: () => void; + error: string | null; + connecting: boolean; + onConnect: () => void; + insets: ReturnType; + t: (key: string) => string; +}; + +function FormView({ + provider, + email, + onEmailChange, + password, + onPasswordChange, + passwordVisible, + onTogglePasswordVisible, + error, + connecting, + onConnect, + insets, + t, +}: FormViewProps) { + const canConnect = email.trim().length > 0 && password.trim().length > 0 && !connecting; + + return ( + + {/* App-Password-Guide-Hinweis */} + {provider && provider.id !== 'other' && ( + + + + + {t('mail.app_password_required_title')} + + + {t(provider.guideKey)} + + {provider.guideUrl.length > 0 && ( + Linking.openURL(provider.guideUrl)}> + + {t('mail.app_password_open_link')} → + + + )} + + + )} + + {/* Email-Input */} + + + {t('mail.form_email_label')} + + + + + {/* Passwort-Input */} + + + {t('mail.form_password_label')} + + + + + + + + + + {/* Datenschutz-Hinweis */} + + + + {t('mail.form_privacy_note')} + + + + {/* Error */} + {error && ( + + {error} + + )} + + {/* Connect-Button */} + ({ + backgroundColor: canConnect ? '#007AFF' : '#d4d4d4', + borderRadius: 14, + paddingVertical: 14, + alignItems: 'center', + opacity: pressed ? 0.85 : 1, + marginTop: 4, + marginBottom: insets.bottom > 0 ? 8 : 12, + })} + > + {connecting ? ( + + ) : ( + + {t('mail.form_connect_btn')} + + )} + + + ); +} diff --git a/apps/rebreak-native/components/mail/EditMailAccountSheet.tsx b/apps/rebreak-native/components/mail/EditMailAccountSheet.tsx new file mode 100644 index 0000000..b0e6974 --- /dev/null +++ b/apps/rebreak-native/components/mail/EditMailAccountSheet.tsx @@ -0,0 +1,249 @@ +import { useEffect, useRef, useState } from 'react'; +import { + ActivityIndicator, + Animated, + Dimensions, + Easing, + KeyboardAvoidingView, + Modal, + Platform, + Pressable, + Text, + TextInput, + View, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { useMailConnect } from '../../hooks/useMailConnect'; + +const SCREEN_HEIGHT = Dimensions.get('window').height; +const SHEET_HEIGHT = SCREEN_HEIGHT * 0.5; + +type Props = { + visible: boolean; + email: string; + onClose: () => void; + onSuccess: () => void; +}; + +/** + * Sheet zum Aktualisieren des App-Passworts eines bereits verbundenen Postfachs. + * Nutzt POST /api/mail/connect (upsert) — Backend ersetzt verschlüsseltes Passwort. + */ +export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Props) { + const { t } = useTranslation(); + const insets = useSafeAreaInsets(); + const { connect, connecting, error: connectError } = useMailConnect(); + + const [password, setPassword] = useState(''); + const [passwordVisible, setPasswordVisible] = useState(false); + const [formError, setFormError] = useState(null); + + const translateY = useRef(new Animated.Value(SHEET_HEIGHT)).current; + const backdropOpacity = useRef(new Animated.Value(0)).current; + + useEffect(() => { + if (visible) { + setPassword(''); + setPasswordVisible(false); + setFormError(null); + translateY.setValue(SHEET_HEIGHT); + backdropOpacity.setValue(0); + Animated.parallel([ + Animated.timing(translateY, { + toValue: 0, + duration: 280, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + Animated.timing(backdropOpacity, { + toValue: 1, + duration: 220, + useNativeDriver: true, + }), + ]).start(); + } + }, [visible, translateY, backdropOpacity]); + + async function handleSave() { + if (!password.trim()) { + setFormError(t('mail.form_fields_required')); + return; + } + setFormError(null); + const result = await connect({ email, password }); + if (result.ok) { + onClose(); + onSuccess(); + } else { + setFormError(result.error ?? t('mail.connect_failed')); + } + } + + return ( + + + + + + + + {/* Drag-Handle */} + + + + + {/* Header */} + + + + {t('common.cancel')} + + + + {t('mail.edit_account_title')} + + + + + + + {t('mail.edit_account_subtitle', { email })} + + + + + { + setPassword(v); + setFormError(null); + }} + placeholder={t('mail.app_password_placeholder')} + placeholderTextColor="#a3a3a3" + secureTextEntry={!passwordVisible} + autoCapitalize="none" + autoCorrect={false} + style={{ + flex: 1, + paddingVertical: 14, + fontSize: 15, + fontFamily: 'Nunito_400Regular', + color: '#0a0a0a', + }} + /> + setPasswordVisible((p) => !p)} hitSlop={8}> + + + + + {(formError ?? connectError) && ( + + + + {formError ?? connectError} + + + )} + + ({ + marginTop: 4, + paddingVertical: 14, + borderRadius: 12, + backgroundColor: !password.trim() || connecting ? '#bfdbfe' : '#007AFF', + alignItems: 'center', + opacity: pressed ? 0.85 : 1, + })} + > + {connecting ? ( + + ) : ( + + {t('mail.edit_account_save')} + + )} + + + + + + + + ); +} diff --git a/apps/rebreak-native/components/mail/MailAccountCard.tsx b/apps/rebreak-native/components/mail/MailAccountCard.tsx new file mode 100644 index 0000000..c856aea --- /dev/null +++ b/apps/rebreak-native/components/mail/MailAccountCard.tsx @@ -0,0 +1,391 @@ +import { useState } from 'react'; +import { + ActivityIndicator, + LayoutAnimation, + Platform, + Pressable, + Text, + UIManager, + View, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { ConfirmAlert } from '../ConfirmAlert'; +import { EditMailAccountSheet } from './EditMailAccountSheet'; +import { useMailInterval } from '../../hooks/useMailInterval'; +import type { MailAccount } from '../../hooks/useMailStatus'; + +if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} + +type Props = { + account: MailAccount; + plan: 'free' | 'pro' | 'legend'; + expanded: boolean; + onToggle: () => void; + onDisconnect: (id: string) => Promise; + onIntervalChanged: () => void; + onEditSuccess: () => void; + disconnecting?: boolean; +}; + +function resolveProviderIcon(provider: string): { + icon: React.ComponentProps['name']; + color: string; +} { + const p = provider.toLowerCase(); + if (p.includes('gmail') || p.includes('google')) return { icon: 'mail', color: '#EA4335' }; + if (p.includes('icloud') || p.includes('apple')) return { icon: 'cloud', color: '#007AFF' }; + if (p.includes('outlook') || p.includes('hotmail') || p.includes('microsoft')) + return { icon: 'mail-open', color: '#0078D4' }; + if (p.includes('yahoo')) return { icon: 'at', color: '#7C3AED' }; + if (p.includes('gmx') || p.includes('web.de')) + return { icon: 'mail-unread', color: '#E87A22' }; + return { icon: 'server', color: '#737373' }; +} + +function formatRelativeTime(iso: string | null, t: (k: string) => string): string { + if (!iso) return t('mail.account_never_scanned'); + const diff = Date.now() - new Date(iso).getTime(); + const mins = Math.floor(diff / 60_000); + if (mins < 2) return t('mail.account_just_now'); + if (mins < 60) return `${mins} min`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h`; + return `${Math.floor(hours / 24)}d`; +} + +const INTERVAL_OPTIONS_BY_PLAN: Record<'free' | 'pro' | 'legend', number[]> = { + free: [4], + pro: [1, 4, 8], + legend: [1, 4, 8], +}; + +// Solid styles outside of render — no gap, no callback layout. +const HEADER_ROW = { + flexDirection: 'row' as const, + alignItems: 'center' as const, + paddingHorizontal: 14, + paddingVertical: 14, +}; + +const ACTION_BTN_BASE = { + flex: 1, + flexDirection: 'row' as const, + alignItems: 'center' as const, + justifyContent: 'center' as const, + paddingVertical: 12, + borderRadius: 10, +}; + +export function MailAccountCard({ + account, + plan, + expanded, + onToggle, + onDisconnect, + onIntervalChanged, + onEditSuccess, + disconnecting, +}: Props) { + const { t } = useTranslation(); + const [confirmVisible, setConfirmVisible] = useState(false); + const [editVisible, setEditVisible] = useState(false); + const { setInterval, updating } = useMailInterval(); + const { icon, color } = resolveProviderIcon(account.provider); + + const isLegend = plan === 'legend'; + const intervalOptions = INTERVAL_OPTIONS_BY_PLAN[plan]; + + function handleToggle() { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + onToggle(); + } + + async function handleSetInterval(value: number) { + const res = await setInterval(account.id, value); + if (res.ok) onIntervalChanged(); + } + + return ( + <> + + {/* ── Header ── */} + + + + + + + + + {account.email} + + + + + {account.isActive + ? isLegend + ? t('mail.live') + : t('mail.account_active') + : t('mail.account_inactive')} + + + · {formatRelativeTime(account.lastScannedAt, t)} + + + + + + + + + {/* ── Body ── */} + {expanded && ( + + {/* Big stat: Blocked */} + + + + + {t('mail.account_stat_blocked')} + + + {account.totalBlocked.toLocaleString()} + + + + {t('mail.account_of_scanned', { + scanned: account.totalScanned.toLocaleString(), + })} + + + + {/* Scan Mode */} + {isLegend ? ( + + + + {t('mail.realtime_desc')} + + + ) : ( + + + {t('mail.scan_interval_label')} + + + {intervalOptions.map((opt, idx) => { + const active = account.scanInterval === opt; + const disabled = plan === 'free' || updating === account.id; + return ( + handleSetInterval(opt)} + style={{ + flex: 1, + paddingVertical: 9, + borderRadius: 10, + alignItems: 'center', + backgroundColor: active ? '#007AFF' : '#f5f5f5', + marginLeft: idx === 0 ? 0 : 6, + opacity: disabled && !active ? 0.5 : 1, + }} + > + + {opt}h + + + ); + })} + + {plan === 'free' && ( + + {t('mail.free_scan_interval_hint')} + + )} + + )} + + {/* Action Row */} + + setEditVisible(true)} + style={{ ...ACTION_BTN_BASE, backgroundColor: '#f5f5f5', marginRight: 6 }} + > + + + {t('mail.account_change_password')} + + + setConfirmVisible(true)} + disabled={disconnecting} + style={{ + ...ACTION_BTN_BASE, + backgroundColor: '#fef2f2', + marginLeft: 6, + opacity: disconnecting ? 0.6 : 1, + }} + > + {disconnecting ? ( + + ) : ( + <> + + + {t('mail.disconnect')} + + + )} + + + + )} + + + { + setConfirmVisible(false); + await onDisconnect(account.id); + }} + onCancel={() => setConfirmVisible(false)} + /> + + setEditVisible(false)} + onSuccess={onEditSuccess} + /> + + ); +} diff --git a/apps/rebreak-native/components/mail/MailActivityLog.tsx b/apps/rebreak-native/components/mail/MailActivityLog.tsx new file mode 100644 index 0000000..de75ffe --- /dev/null +++ b/apps/rebreak-native/components/mail/MailActivityLog.tsx @@ -0,0 +1,218 @@ +import { + LayoutAnimation, + Platform, + Pressable, + Text, + UIManager, + View, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { useMailResults, type MailBlockedItem } from '../../hooks/useMailResults'; + +if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} + +type Props = { + expanded: boolean; + onToggle: () => void; +}; + +function formatDate(iso: string, t: (k: string) => string): string { + const diff = Date.now() - new Date(iso).getTime(); + const mins = Math.floor(diff / 60_000); + if (mins < 2) return t('mail.account_just_now'); + if (mins < 60) return `${mins} min`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h`; + return `${Math.floor(hours / 24)}d`; +} + +export function MailActivityLog({ expanded, onToggle }: Props) { + const { t } = useTranslation(); + const { results, total, loading, refresh } = useMailResults(expanded); + + function handleToggle() { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + onToggle(); + } + + return ( + + + + + + + + + {t('mail.activity_log_title')} + + + {t('mail.activity_log_subtitle')} + + + + + + + {expanded && ( + + {loading && results.length === 0 ? ( + + + {t('mail.loading')} + + + ) : results.length === 0 ? ( + + + + {t('mail.activity_log_empty')} + + + ) : ( + <> + {results.slice(0, 10).map((item) => ( + + ))} + + + {total > 10 + ? t('mail.activity_log_more', { count: total - 10 }) + : t('mail.activity_log_count', { count: total })} + + + + + + + )} + + )} + + ); +} + +function ActivityItem({ + item, + t, +}: { + item: MailBlockedItem; + t: (k: string, opts?: any) => string; +}) { + return ( + + + + + + + {item.subject || t('mail.activity_no_subject')} + + + {item.sender_name || item.sender_email} + + + + {formatDate(item.received_at, t)} + + + ); +} diff --git a/apps/rebreak-native/components/mail/MailEmptyState.tsx b/apps/rebreak-native/components/mail/MailEmptyState.tsx new file mode 100644 index 0000000..ee64e0c --- /dev/null +++ b/apps/rebreak-native/components/mail/MailEmptyState.tsx @@ -0,0 +1,100 @@ +import { Pressable, Text, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; + +type Props = { + onConnectPress: () => void; +}; + +/** + * Leerer Zustand wenn kein Postfach verbunden ist. + * Hero-CTA öffnet ConnectMailSheet. + */ +export function MailEmptyState({ onConnectPress }: Props) { + const { t } = useTranslation(); + + return ( + + {/* Icon-Circle */} + + + + + + {t('mail.empty_state_title')} + + + + {t('mail.empty_state_subtitle')} + + + {/* Privacy-Punkte */} + + {(['privacy_1', 'privacy_2', 'privacy_3'] as const).map((key) => ( + + + + {t(`mail.${key}`)} + + + ))} + + + {/* CTA */} + ({ + backgroundColor: '#007AFF', + borderRadius: 14, + paddingVertical: 14, + paddingHorizontal: 28, + alignSelf: 'stretch', + alignItems: 'center', + opacity: pressed ? 0.85 : 1, + })} + > + + {t('mail.empty_state_cta')} + + + + ); +} diff --git a/apps/rebreak-native/components/mail/MailStatsRow.tsx b/apps/rebreak-native/components/mail/MailStatsRow.tsx new file mode 100644 index 0000000..9c84d25 --- /dev/null +++ b/apps/rebreak-native/components/mail/MailStatsRow.tsx @@ -0,0 +1,95 @@ +import { Text, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; + +type Props = { + totalBlocked: number; + accountCount: number; + isLegend?: boolean; +}; + +/** + * Kompakte Stats: nur "Blockiert gesamt" prominent + kleines Status-Pill. + * Kein Block-Rate, kein Next-Scan — Mode steht auf den Cards. + */ +export function MailStatsRow({ totalBlocked, accountCount, isLegend }: Props) { + const { t } = useTranslation(); + + return ( + + + + + {t('mail.stats_blocked')} + + + {totalBlocked.toLocaleString()} + + + {t('mail.stats_account_summary', { count: accountCount })} + + + + {/* Mode pill */} + + + + {isLegend ? t('mail.live') : t('mail.scheduled')} + + + + + ); +} diff --git a/apps/rebreak-native/components/urge/Breathing.tsx b/apps/rebreak-native/components/urge/Breathing.tsx new file mode 100644 index 0000000..2312075 --- /dev/null +++ b/apps/rebreak-native/components/urge/Breathing.tsx @@ -0,0 +1,145 @@ +// 4-7-8 Atemübung: Card (in-chat) + Drawer (bottom sheet). +import { useEffect, useRef, useState } from 'react'; +import { View, Text, Pressable, Animated, StyleSheet } from 'react-native'; +import { BREATH_PHASES, TOTAL_ROUNDS, type BreathState } from '../../lib/sosConstants'; +import { colors } from '../../lib/theme'; + +type Props = { onDone: () => void; onSpeak?: (text: string) => Promise | void }; + +export function BreathingCard({ onDone, onSpeak }: Props) { + const [breathState, setBreathState] = useState('idle'); + const [countdown, setCountdown] = useState(3); + const [round, setRound] = useState(1); + const [phaseIndex, setPhaseIndex] = useState(0); + const [count, setCount] = useState(BREATH_PHASES[0]!.duration); + const pulse = useRef(new Animated.Value(1)).current; + const timerRef = useRef | null>(null); + const animRef = useRef(null); + const currentPhase = BREATH_PHASES[phaseIndex]!; + + function runPulse(target: number, seconds: number) { + animRef.current?.stop(); + animRef.current = Animated.timing(pulse, { toValue: target, duration: seconds * 1000, useNativeDriver: true }); + animRef.current.start(); + } + + // Countdown: visually 3 → 2 → 1 → 0 ("Los!"), then transition to active + useEffect(() => { + if (breathState !== 'countdown') return; + if (countdown > 0) { + const t = setTimeout(() => setCountdown((c) => c - 1), 1000); + return () => clearTimeout(t); + } else { + // Kein "Los!" sprechen — würde laufende Lyra-Antwort abbrechen + const t = setTimeout(() => setBreathState('active'), 500); + return () => clearTimeout(t); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [breathState, countdown]); + + // Breathing phases with TTS guidance + useEffect(() => { + if (breathState !== 'active') return; + let remaining = currentPhase.duration; + setCount(remaining); + runPulse(currentPhase.phase === 'exhale' ? 1 : 1.22, currentPhase.duration); + + // Speak only if speakLine defined — short words avoid overlap (inhale=4s, exhale=8s) + let speakTimer: ReturnType | null = null; + if (currentPhase.speakLine) { + speakTimer = setTimeout(() => onSpeak?.(currentPhase.speakLine!), 350); + } + + timerRef.current = setInterval(() => { + remaining -= 1; + setCount(remaining); + if (remaining <= 0) { + clearInterval(timerRef.current!); + const next = phaseIndex + 1; + if (next >= BREATH_PHASES.length) { + if (round >= TOTAL_ROUNDS) { + // Lob ZUERST komplett ausspielen, DANN onDone (das triggert Lyras nächste Frage) + (async () => { + try { await onSpeak?.('Sehr gut! Du hast alle drei Runden geschafft. Wunderbar gemacht!'); } catch {} + onDone(); + })(); + return; + } + // Nächste Runde still starten + setRound((r) => r + 1); + setPhaseIndex(0); + } else { + setPhaseIndex(next); + } + } + }, 1000); + return () => { + if (timerRef.current) clearInterval(timerRef.current); + if (speakTimer) clearTimeout(speakTimer); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [breathState, phaseIndex, round]); + + return ( + + {breathState === 'idle' ? ( + + 4-7-8 Atemübung + 3 Runden · beruhigt dein Nervensystem + { setCountdown(3); setBreathState('countdown'); }}> + Starten + + + ) : breathState === 'countdown' ? ( + + Gleich geht's los... + + {countdown > 0 ? countdown : '✓'} + + + ) : ( + + Runde {round} / {TOTAL_ROUNDS} + + + {count} + {currentPhase.label} + + + + )} + + ); +} + +// ── BreathingDrawer (bottom sheet, covers input, slides up) ─────────────────── +export function BreathingDrawer({ onDone, onSpeak }: Props) { + const slideAnim = useRef(new Animated.Value(500)).current; + useEffect(() => { + Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, damping: 22, mass: 1, stiffness: 200 }).start(); + }, []); + return ( + <> + + + + + + + ); +} + +const st = StyleSheet.create({ + breathBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.28)', zIndex: 20 }, + breathDrawerContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 21, backgroundColor: '#ffffff', borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingBottom: 36, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.18, shadowRadius: 20, elevation: 24 }, + breathDrawerHandle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#d1d5db', alignSelf: 'center', marginTop: 14, marginBottom: 4 }, + breathCardInner: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 8, alignItems: 'center', gap: 16 }, + breathCircleLg: { width: 190, height: 190, borderRadius: 95, alignItems: 'center', justifyContent: 'center', borderWidth: 5 }, + breathCountLg: { fontFamily: 'Nunito_800ExtraBold', fontSize: 60, color: '#111827', lineHeight: 68 }, + breathTitle: { fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' }, + breathSub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#6b7280', textAlign: 'center' }, + breathStartBtn: { borderRadius: 12, backgroundColor: colors.brandOrange, paddingHorizontal: 28, paddingVertical: 10, marginTop: 4 }, + breathStartTxt: { color: '#fff', fontFamily: 'Nunito_700Bold', fontSize: 14 }, + breathRound: { fontFamily: 'Nunito_600SemiBold', fontSize: 12, color: '#9ca3af' }, + breathPhaseLabel: { fontFamily: 'Nunito_700Bold', fontSize: 13 }, +}); diff --git a/apps/rebreak-native/components/urge/GamePickerDrawer.tsx b/apps/rebreak-native/components/urge/GamePickerDrawer.tsx new file mode 100644 index 0000000..1b46e9b --- /dev/null +++ b/apps/rebreak-native/components/urge/GamePickerDrawer.tsx @@ -0,0 +1,38 @@ +// Bottom-Sheet mit 4 Mini-Spielen zur Ablenkung im SOS-Flow. +import { useEffect, useRef } from 'react'; +import { View, Text, Pressable, Animated, StyleSheet } from 'react-native'; +import { type GameType, GamePickerGrid } from './UrgeGames'; + +type Props = { onSelect: (game: GameType) => void; onClose: () => void }; + +export default function GamePickerDrawer({ onSelect, onClose }: Props) { + const slideAnim = useRef(new Animated.Value(500)).current; + useEffect(() => { + Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, damping: 22, mass: 1, stiffness: 200 }).start(); + }, []); + return ( + <> + + + + + + Wähl ein Spiel + + + Lenk deinen Kopf ab — nur ein paar Minuten + + + + + + + + ); +} + +const st = StyleSheet.create({ + backdrop: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.28)', zIndex: 20 }, + drawerContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 21, backgroundColor: '#ffffff', borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingBottom: 36, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.18, shadowRadius: 20, elevation: 24 }, + drawerHandle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#d1d5db', alignSelf: 'center', marginTop: 14, marginBottom: 4 }, +}); diff --git a/apps/rebreak-native/components/urge/InlineIndicators.tsx b/apps/rebreak-native/components/urge/InlineIndicators.tsx new file mode 100644 index 0000000..0c1ac13 --- /dev/null +++ b/apps/rebreak-native/components/urge/InlineIndicators.tsx @@ -0,0 +1,53 @@ +// Kleine Indikator-Komponenten für den SOS-Header / Chat: +// - ThinkingDots: 3 hüpfende Punkte ("Lyra denkt nach") +// - VoiceBars: animierte Balken für Sprach-Aktivität +import { useEffect, useRef } from 'react'; +import { View, Animated, StyleSheet } from 'react-native'; + +export function ThinkingDots() { + const anim = useRef([new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)]).current; + useEffect(() => { + const animations = anim.map((a, i) => + Animated.loop(Animated.sequence([ + Animated.delay(i * 160), + Animated.timing(a, { toValue: 1, duration: 300, useNativeDriver: true }), + Animated.timing(a, { toValue: 0, duration: 300, useNativeDriver: true }), + ])), + ); + animations.forEach((a) => a.start()); + return () => animations.forEach((a) => a.stop()); + }, []); + return ( + + {anim.map((a, i) => ( + + ))} + + ); +} + +export function VoiceBars({ count, baseColor }: { count: number; baseColor: string }) { + const anims = useRef(Array.from({ length: count }, () => new Animated.Value(4))).current; + useEffect(() => { + const animations = anims.map((a, i) => + Animated.loop(Animated.sequence([ + Animated.timing(a, { toValue: 4 + Math.random() * 14, duration: 450 + (i % 5) * 80, useNativeDriver: false }), + Animated.timing(a, { toValue: 4, duration: 450 + (i % 5) * 80, useNativeDriver: false }), + ])), + ); + animations.forEach((a) => a.start()); + return () => animations.forEach((a) => a.stop()); + }, []); + return ( + + {anims.map((a, i) => ( + + ))} + + ); +} + +const st = StyleSheet.create({ + thinkingRow: { flexDirection: 'row', gap: 4, paddingHorizontal: 4, paddingVertical: 2, alignItems: 'center' }, + thinkingDot: { width: 7, height: 7, borderRadius: 3.5, backgroundColor: '#9ca3af' }, +}); diff --git a/apps/rebreak-native/components/urge/InlineRatingDrawer.tsx b/apps/rebreak-native/components/urge/InlineRatingDrawer.tsx new file mode 100644 index 0000000..da2d657 --- /dev/null +++ b/apps/rebreak-native/components/urge/InlineRatingDrawer.tsx @@ -0,0 +1,205 @@ +import { useEffect, useRef, useState } from 'react'; +import { + View, + Text, + Pressable, + TextInput, + StyleSheet, + Animated, + KeyboardAvoidingView, + Platform, + ScrollView, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { colors } from '../../lib/theme'; +import type { SosFeedback } from './SosFeedbackModal'; + +/** + * Inline-Bewertungs-Drawer (Bottom-Sheet) als Alternative zum Exit-Modal. + * Wenn der User hier bewertet, soll der Exit-Modal nicht mehr erscheinen. + */ +export function InlineRatingDrawer({ + onSubmit, + onClose, +}: { + onSubmit: (feedback: SosFeedback) => Promise | void; + onClose: () => void; +}) { + const slide = useRef(new Animated.Value(600)).current; + const [better, setBetter] = useState(null); + const [rating, setRating] = useState(0); + const [text, setText] = useState(''); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + Animated.spring(slide, { + toValue: 0, + useNativeDriver: true, + damping: 22, + mass: 1, + stiffness: 200, + }).start(); + }, []); + + async function submit() { + if (submitting) return; + setSubmitting(true); + try { + await onSubmit({ + better, + rating: rating > 0 ? rating : null, + text: text.trim(), + }); + } finally { + setSubmitting(false); + } + } + + return ( + <> + + + + + + + + Bewerte diese Session + + + Dein Feedback hilft uns, Lyra besser zu machen. + + + Fühlst du dich besser? + + setBetter(true)} + > + + Ja + + setBetter(false)} + > + + Nein + + + + Bewertung + + {[1, 2, 3, 4, 5].map((n) => ( + setRating(n)} hitSlop={6}> + + + ))} + + + Bemerkung (optional) + + + + + Abbrechen + + + {submitting ? 'Sende…' : 'Senden'} + + + + + + + ); +} + +const s = StyleSheet.create({ + backdrop: { + position: 'absolute', + top: 0, left: 0, right: 0, bottom: 0, + backgroundColor: 'rgba(0,0,0,0.35)', + }, + drawer: { + position: 'absolute', + left: 0, right: 0, bottom: 0, + backgroundColor: '#fff', + borderTopLeftRadius: 24, borderTopRightRadius: 24, + paddingHorizontal: 18, paddingTop: 8, paddingBottom: 22, + maxHeight: '88%', + ...Platform.select({ + ios: { shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.18, shadowRadius: 12 }, + android: { elevation: 12 }, + }), + }, + handle: { + width: 40, height: 4, borderRadius: 2, + backgroundColor: '#e2e8f0', + alignSelf: 'center', + marginBottom: 12, + }, + header: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + title: { fontFamily: 'Nunito_800ExtraBold', fontSize: 19, color: '#111827' }, + sub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#6b7280', marginTop: 4 }, + q: { fontFamily: 'Nunito_700Bold', fontSize: 13, color: '#374151', marginTop: 14 }, + btnRow: { flexDirection: 'row', gap: 10, marginTop: 6 }, + choiceBtn: { + flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, + borderWidth: 1, borderColor: '#cbd5e1', backgroundColor: '#f1f5f9', + borderRadius: 12, paddingVertical: 11, + }, + choiceBtnYes: { backgroundColor: '#16a34a', borderColor: '#16a34a' }, + choiceBtnNo: { backgroundColor: '#dc2626', borderColor: '#dc2626' }, + choiceTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#374151' }, + starsRow: { flexDirection: 'row', justifyContent: 'center', gap: 8, marginTop: 8 }, + textArea: { + marginTop: 6, backgroundColor: '#f8fafc', borderRadius: 12, + borderWidth: 1, borderColor: '#e2e8f0', + paddingHorizontal: 12, paddingVertical: 10, minHeight: 80, + fontFamily: 'Nunito_400Regular', fontSize: 14, color: '#111827', + textAlignVertical: 'top', + }, + actions: { flexDirection: 'row', gap: 10, marginTop: 18 }, + cancelBtn: { + flex: 1, paddingVertical: 12, borderRadius: 12, + alignItems: 'center', backgroundColor: '#f1f5f9', + }, + cancelTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' }, + submitBtn: { + flex: 2, paddingVertical: 12, borderRadius: 12, + alignItems: 'center', backgroundColor: colors.brandOrange, + }, + submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' }, +}); diff --git a/apps/rebreak-native/components/urge/MessageRow.tsx b/apps/rebreak-native/components/urge/MessageRow.tsx new file mode 100644 index 0000000..421e303 --- /dev/null +++ b/apps/rebreak-native/components/urge/MessageRow.tsx @@ -0,0 +1,90 @@ +// Chat-Bubble + Spezial-Cards (Spiele/Überwunden) für den SOS-Chat-Stream +// sowie GameHeader für die aktive Spiel-Session. +import { View, Text, Pressable, StyleSheet } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { type GameType, GAME_META, GamePickerGrid } from './UrgeGames'; +import { RiveAvatar, type Emotion as LyraEmotion } from '../RiveAvatar'; + +export type CardType = 'games' | 'overcome'; +export type SosMsg = { id: string; role: 'user' | 'assistant'; content: string; cardType?: CardType; timestamp: Date }; + +// ── GameCard ───────────────────────────────────────────────────────────────── +export function GameCard({ onSelect }: { onSelect: (game: GameType) => void }) { + return ( + + Welches Spiel? + + + ); +} + +// ── OvercomeCard ───────────────────────────────────────────────────────────── +export function OvercomeCard() { + return ( + + 🎉 + Gut gemacht. + Du hast den Impuls überwunden. + + ); +} + +// ── MessageRow ─────────────────────────────────────────────────────────────── +type MessageRowProps = { + item: SosMsg; + onGameSelect: (game: GameType) => void; + onBreathingDone: () => void; + onSpeak?: (text: string) => Promise | void; +}; + +export default function MessageRow({ item, onGameSelect }: MessageRowProps) { + const isUser = item.role === 'user'; + if (item.cardType === 'games') return ; + if (item.cardType === 'overcome') return ; + return ( + + + + {item.content} + + + + ); +} + +// ── GameHeader ─────────────────────────────────────────────────────────────── +export function GameHeader({ game, emotion, onBack }: { game: GameType; emotion: LyraEmotion; onBack: () => void }) { + const meta = GAME_META.find((g) => g.id === game); + return ( + + + + + {meta?.id ?? game} + + + + ); +} + +const st = StyleSheet.create({ + msgRow: { flexDirection: 'row', marginBottom: 4, alignItems: 'flex-end' }, + msgRowUser: { justifyContent: 'flex-end' }, + msgRowAssistant: { justifyContent: 'flex-start', marginBottom: 6 }, + bubbleCol: { maxWidth: '75%', gap: 2 }, + bubbleColUser: { alignItems: 'flex-end' }, + bubbleColAssistant: { alignItems: 'flex-start' }, + bubble: { borderRadius: 20, paddingHorizontal: 14, paddingVertical: 9 }, + bubbleUser: { backgroundColor: '#007AFF', borderBottomRightRadius: 4 }, + bubbleAssistant: { backgroundColor: '#f0f0f0', borderBottomLeftRadius: 4 }, + bubbleText: { fontSize: 15, lineHeight: 22 }, + bubbleTextUser: { color: '#ffffff', fontFamily: 'Nunito_400Regular' }, + bubbleTextAssistant: { color: '#1a1a1a', fontFamily: 'Nunito_400Regular' }, + gameCard: { backgroundColor: '#f0f9ff', borderRadius: 16, borderWidth: 1, borderColor: '#bae6fd', padding: 12, maxWidth: '92%' }, + gameCardTitle: { fontFamily: 'Nunito_700Bold', color: '#0369a1', fontSize: 13, marginBottom: 10 }, + overcomeCard: { backgroundColor: '#f0fdf4', borderRadius: 16, borderWidth: 1, borderColor: '#86efac', padding: 16, alignItems: 'center', maxWidth: '88%', gap: 4 }, + overcomeTitle: { fontFamily: 'Nunito_800ExtraBold', fontSize: 18, color: '#15803d' }, + overcomeSub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#166534', textAlign: 'center' }, + gameHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f3f4f6', backgroundColor: '#fff' }, + backBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#f3f4f6', alignItems: 'center', justifyContent: 'center' }, +}); diff --git a/apps/rebreak-native/components/urge/ShareSuccessDrawer.tsx b/apps/rebreak-native/components/urge/ShareSuccessDrawer.tsx new file mode 100644 index 0000000..2e00899 --- /dev/null +++ b/apps/rebreak-native/components/urge/ShareSuccessDrawer.tsx @@ -0,0 +1,211 @@ +import { useEffect, useRef, useState } from 'react'; +import { + View, + Text, + Pressable, + TextInput, + StyleSheet, + Animated, + KeyboardAvoidingView, + Platform, + ActivityIndicator, + ScrollView, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { colors } from '../../lib/theme'; + +export interface ShareSuccessPayload { + text: string; +} + +/** + * Bottom-Sheet zum Teilen einer Erfolgs-Story in der Community. + * AI-generierter Vorschlagstext (vom Parent via prop), editierbar. + */ +export function ShareSuccessDrawer({ + initialText, + generating, + onShare, + onClose, + onRegenerate, +}: { + initialText: string; + generating: boolean; + onShare: (text: string) => Promise | void; + onClose: () => void; + onRegenerate?: () => void; +}) { + const slide = useRef(new Animated.Value(600)).current; + const [text, setText] = useState(initialText); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + Animated.spring(slide, { + toValue: 0, + useNativeDriver: true, + damping: 22, + mass: 1, + stiffness: 200, + }).start(); + }, []); + + // Sync wenn initialText neu generiert wird + useEffect(() => { + setText(initialText); + }, [initialText]); + + async function handleShare() { + if (!text.trim() || submitting) return; + setSubmitting(true); + try { + await onShare(text.trim()); + } finally { + setSubmitting(false); + } + } + + return ( + <> + + + + + + + + Erfolg teilen + + + Inspiriere andere — dein Beitrag wird anonym in der Community gepostet. + + + {generating ? ( + + + Lyra schreibt einen Vorschlag… + + ) : ( + + )} + + + {onRegenerate && ( + + + Neu generieren + + )} + + Abbrechen + + + {submitting ? ( + + ) : ( + <> + + Teilen + + )} + + + + + + + ); +} + +const s = StyleSheet.create({ + backdrop: { + position: 'absolute', + top: 0, left: 0, right: 0, bottom: 0, + backgroundColor: 'rgba(0,0,0,0.35)', + }, + drawer: { + position: 'absolute', + left: 0, right: 0, bottom: 0, + backgroundColor: '#fff', + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + paddingHorizontal: 18, + paddingTop: 8, + paddingBottom: 22, + maxHeight: '85%', + ...Platform.select({ + ios: { shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.18, shadowRadius: 12 }, + android: { elevation: 12 }, + }), + }, + handle: { + width: 40, height: 4, borderRadius: 2, + backgroundColor: '#e2e8f0', + alignSelf: 'center', + marginBottom: 12, + }, + header: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + title: { fontFamily: 'Nunito_800ExtraBold', fontSize: 19, color: '#111827' }, + sub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#6b7280', marginTop: 4, marginBottom: 12 }, + textArea: { + backgroundColor: '#f8fafc', + borderRadius: 14, + borderWidth: 1, borderColor: '#e2e8f0', + paddingHorizontal: 14, paddingVertical: 12, + minHeight: 110, maxHeight: 220, + fontFamily: 'Nunito_400Regular', fontSize: 15, color: '#111827', + textAlignVertical: 'top', + }, + loadingBox: { + backgroundColor: '#f8fafc', + borderRadius: 14, borderWidth: 1, borderColor: '#e2e8f0', + paddingVertical: 28, paddingHorizontal: 14, + alignItems: 'center', gap: 10, + minHeight: 110, + justifyContent: 'center', + }, + loadingTxt: { fontFamily: 'Nunito_500Medium', fontSize: 13, color: '#64748b' }, + row: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 14, flexWrap: 'wrap' }, + secondaryBtn: { + flexDirection: 'row', alignItems: 'center', gap: 6, + paddingHorizontal: 12, paddingVertical: 10, + borderRadius: 12, backgroundColor: '#f1f5f9', + borderWidth: 1, borderColor: '#cbd5e1', + }, + secondaryTxt: { fontFamily: 'Nunito_700Bold', fontSize: 13, color: '#475569' }, + cancelBtn: { + paddingHorizontal: 14, paddingVertical: 10, + borderRadius: 12, backgroundColor: '#f1f5f9', + }, + cancelTxt: { fontFamily: 'Nunito_700Bold', fontSize: 13, color: '#475569' }, + shareBtn: { + flex: 1, minWidth: 110, + flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, + backgroundColor: colors.brandOrange, + borderRadius: 12, paddingVertical: 12, + }, + shareBtnDisabled: { opacity: 0.5 }, + shareTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' }, +}); diff --git a/apps/rebreak-native/components/urge/SosFeedbackModal.tsx b/apps/rebreak-native/components/urge/SosFeedbackModal.tsx new file mode 100644 index 0000000..85f57fe --- /dev/null +++ b/apps/rebreak-native/components/urge/SosFeedbackModal.tsx @@ -0,0 +1,141 @@ +import { useState } from 'react'; +import { View, Text, Pressable, TextInput, Modal, StyleSheet, Platform, KeyboardAvoidingView, ScrollView } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { colors } from '../../lib/theme'; + +export interface SosFeedback { + better: boolean | null; + rating: number | null; + text: string; +} + +export function SosFeedbackModal({ + visible, + onSubmit, + onSkip, +}: { + visible: boolean; + onSubmit: (feedback: SosFeedback) => void; + onSkip: () => void; +}) { + const [better, setBetter] = useState(null); + const [rating, setRating] = useState(0); + const [text, setText] = useState(''); + + function reset() { + setBetter(null); setRating(0); setText(''); + } + + function submit() { + onSubmit({ better, rating: rating > 0 ? rating : null, text: text.trim() }); + reset(); + } + function skip() { onSkip(); reset(); } + + return ( + + + + + Wie war diese Session? + Dein Feedback hilft Lyra besser zu werden. + + {/* Better Yes/No */} + Fühlst du dich besser? + + setBetter(true)} + > + + Ja + + setBetter(false)} + > + + Nein + + + + {/* Stars */} + Bewertung + + {[1, 2, 3, 4, 5].map((n) => ( + setRating(n)} hitSlop={6}> + + + ))} + + + {/* Comment */} + Bemerkung (optional) + + + {/* Actions */} + + + Überspringen + + + Senden + + + + + + + ); +} + +const s = StyleSheet.create({ + backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.45)' }, + scrollContent: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', padding: 20 }, + card: { width: '100%', maxWidth: 420, backgroundColor: '#fff', borderRadius: 24, padding: 22, gap: 8, ...Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.25, shadowRadius: 20 }, android: { elevation: 10 } }) }, + title: { fontFamily: 'Nunito_800ExtraBold', fontSize: 19, color: '#111827' }, + sub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#6b7280', marginBottom: 8 }, + q: { fontFamily: 'Nunito_700Bold', fontSize: 13, color: '#374151', marginTop: 12 }, + btnRow: { flexDirection: 'row', gap: 10, marginTop: 6 }, + choiceBtn: { + flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, + borderWidth: 1, borderColor: '#cbd5e1', backgroundColor: '#f1f5f9', + borderRadius: 12, paddingVertical: 11, + }, + choiceBtnYes: { backgroundColor: '#16a34a', borderColor: '#16a34a' }, + choiceBtnNo: { backgroundColor: '#dc2626', borderColor: '#dc2626' }, + choiceTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#374151' }, + starsRow: { flexDirection: 'row', justifyContent: 'center', gap: 8, marginTop: 8 }, + textArea: { + marginTop: 6, backgroundColor: '#f8fafc', borderRadius: 12, + borderWidth: 1, borderColor: '#e2e8f0', + paddingHorizontal: 12, paddingVertical: 10, minHeight: 70, + fontFamily: 'Nunito_400Regular', fontSize: 14, color: '#111827', + textAlignVertical: 'top', + }, + actions: { flexDirection: 'row', gap: 10, marginTop: 18 }, + skipBtn: { flex: 1, paddingVertical: 12, borderRadius: 12, alignItems: 'center', backgroundColor: '#f1f5f9' }, + skipTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' }, + submitBtn: { flex: 2, paddingVertical: 12, borderRadius: 12, alignItems: 'center', backgroundColor: colors.brandOrange }, + submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' }, +}); diff --git a/apps/rebreak-native/components/urge/TtsProviderToggle.tsx b/apps/rebreak-native/components/urge/TtsProviderToggle.tsx new file mode 100644 index 0000000..9797b0a --- /dev/null +++ b/apps/rebreak-native/components/urge/TtsProviderToggle.tsx @@ -0,0 +1,60 @@ +import { Pressable, Text, View } from 'react-native'; +import { TTS_PROVIDER_LABEL, type TtsProvider, useTtsProvider } from '../../lib/ttsProvider'; + +const PROVIDERS: TtsProvider[] = ['openai', 'gemini', 'google-cloud']; + +export function TtsProviderToggle() { + const [current, set] = useTtsProvider(); + return ( + + + TTS + + {PROVIDERS.map((p) => { + const active = p === current; + return ( + { void set(p); }} + hitSlop={6} + style={{ + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 999, + backgroundColor: active ? '#1f2937' : '#f9fafb', + borderWidth: 1, + borderColor: active ? '#1f2937' : '#e5e7eb', + }} + > + + {TTS_PROVIDER_LABEL[p]} + + + ); + })} + + ); +} diff --git a/apps/rebreak-native/components/urge/UrgeGames.tsx b/apps/rebreak-native/components/urge/UrgeGames.tsx new file mode 100644 index 0000000..dfeabbb --- /dev/null +++ b/apps/rebreak-native/components/urge/UrgeGames.tsx @@ -0,0 +1,1056 @@ +import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { View, Text, Pressable, Dimensions, PanResponder, Platform } from 'react-native'; +import Svg, { Defs, Pattern, Path, Rect, Polyline, Circle, Line } from 'react-native-svg'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { SvgXml } from 'react-native-svg'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import * as Haptics from 'expo-haptics'; +import Slider from '@react-native-community/slider'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { memorySvg, snakeSvg, tetrisSvg, tictactoeSvg } from './gameSvgs'; + +// Haptic helper — fire-and-forget, swallow errors on platforms without taptic engine +function tapHaptic() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {}); +} +function mediumHaptic() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {}); +} + +export type GameType = 'memory' | 'tictactoe' | 'snake' | 'tetris'; + +export const GAME_META: Array<{ id: GameType; svg: string; titleKey: string; descKey: string }> = [ + { id: 'memory', svg: memorySvg, titleKey: 'urge.game_memory', descKey: 'urge.game_memory_desc' }, + { id: 'tictactoe', svg: tictactoeSvg, titleKey: 'urge.game_tictactoe', descKey: 'urge.game_tictactoe_desc' }, + { id: 'snake', svg: snakeSvg, titleKey: 'urge.game_snake', descKey: 'urge.game_snake_desc' }, + { id: 'tetris', svg: tetrisSvg, titleKey: 'urge.game_tetris', descKey: 'urge.game_tetris_desc' }, +]; + +// ── Game picker grid ────────────────────────────────────────────────────────── + +export function GamePickerGrid({ onSelect }: { onSelect: (game: GameType) => void }) { + const { t } = useTranslation(); + return ( + + {GAME_META.map((game) => ( + onSelect(game.id)} + style={({ pressed }) => ({ + width: '47%', + aspectRatio: 1, + borderRadius: 16, + borderWidth: 1, + borderColor: '#e5e7eb', + backgroundColor: pressed ? '#f0f9ff' : '#f9fafb', + alignItems: 'center', + justifyContent: 'center', + padding: 12, + })} + > + + + {t(game.titleKey)} + + + {t(game.descKey)} + + + ))} + + ); +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function shuffle(arr: T[]): T[] { + const out = [...arr]; + for (let i = out.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const tmp = out[i]!; + out[i] = out[j]!; + out[j] = tmp; + } + return out; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// SNAKE — 1:1 Port von apps/rebreak/app/components/sos/GameSnake.vue +// ═══════════════════════════════════════════════════════════════════════════════ + +const SNAKE_ROWS = 20; +const SNAKE_COLS = 15; +const SNAKE_TICK_MS = 180; +type Dir = 'up' | 'down' | 'left' | 'right'; +interface Pos { row: number; col: number } +const OPPOSITES: Record = { up: 'down', down: 'up', left: 'right', right: 'left' }; + +export function SnakeGame({ + onComplete, + onAbandon, +}: { + onComplete: (score: number) => void; + onAbandon: () => void; +}) { + const insets = useSafeAreaInsets(); + // Cell size aus Bildschirmgröße — Header(80) + Drawer-Padding(40) + DPad(220) + Spacing(40) + Home-Indicator + const cell = useMemo(() => { + const win = Dimensions.get('window'); + const maxW = Math.min(win.width - 32, 400); + const maxH = win.height - 80 - 40 - 220 - 40 - Math.max(insets.bottom, 16); + const cellByW = Math.floor(maxW / SNAKE_COLS); + const cellByH = Math.floor(maxH / SNAKE_ROWS); + return Math.max(12, Math.min(cellByW, cellByH)); + }, [insets.bottom]); + const boardW = SNAKE_COLS * cell; + const boardH = SNAKE_ROWS * cell; + + const [snake, setSnake] = useState([ + { row: 10, col: 7 }, { row: 10, col: 6 }, { row: 10, col: 5 }, + ]); + const [food, setFood] = useState({ row: 3, col: 10 }); + const dirRef = useRef

    ('right'); + const nextDirRef = useRef('right'); + const [score, setScore] = useState(0); + const [highScore, setHighScore] = useState(0); + const [gameOver, setGameOver] = useState(false); + const [activeDPad, setActiveDPad] = useState('right'); + const [, forceRender] = useState(0); + const intervalRef = useRef | null>(null); + + // Load high score + useEffect(() => { + AsyncStorage.getItem('rebreak-snake-highscore').then((v) => { + if (v) setHighScore(parseInt(v) || 0); + }); + }, []); + + function setDir(d: Dir) { + if (OPPOSITES[d] !== dirRef.current) nextDirRef.current = d; + } + function onDPad(d: Dir) { + setDir(d); + setActiveDPad(d); + } + + function randomFood(currentSnake: Pos[]): Pos { + const occupied = new Set(currentSnake.map((p) => p.row * SNAKE_COLS + p.col)); + let pos: Pos; + do { + pos = { row: Math.floor(Math.random() * SNAKE_ROWS), col: Math.floor(Math.random() * SNAKE_COLS) }; + } while (occupied.has(pos.row * SNAKE_COLS + pos.col)); + return pos; + } + + function endGame(finalScore: number) { + if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } + setGameOver(true); + if (finalScore > highScore) { + setHighScore(finalScore); + AsyncStorage.setItem('rebreak-snake-highscore', String(finalScore)).catch(() => {}); + } + setTimeout(() => onComplete(finalScore), 500); + } + + // Game tick loop + useEffect(() => { + if (gameOver) return; + intervalRef.current = setInterval(() => { + dirRef.current = nextDirRef.current; + setSnake((prev) => { + const head = prev[0]; + if (!head) return prev; + const next: Pos = { row: head.row, col: head.col }; + if (dirRef.current === 'up') next.row--; + else if (dirRef.current === 'down') next.row++; + else if (dirRef.current === 'left') next.col--; + else if (dirRef.current === 'right') next.col++; + if (next.row < 0 || next.row >= SNAKE_ROWS || next.col < 0 || next.col >= SNAKE_COLS) { + setTimeout(() => endGame(score), 0); + return prev; + } + if (prev.some((s) => s.row === next.row && s.col === next.col)) { + setTimeout(() => endGame(score), 0); + return prev; + } + const ate = next.row === food.row && next.col === food.col; + const newSnake = [next, ...prev]; + if (!ate) newSnake.pop(); + else { + setScore((s) => s + 1); + setFood(randomFood(newSnake)); + } + return newSnake; + }); + forceRender((x) => x + 1); + }, SNAKE_TICK_MS); + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [gameOver, food, score, highScore]); + + // Swipe gestures + const panResponder = useMemo( + () => + PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onPanResponderRelease: (_, g) => { + const dx = g.dx, dy = g.dy; + if (Math.abs(dx) < 10 && Math.abs(dy) < 10) return; + if (Math.abs(dx) > Math.abs(dy)) onDPad(dx > 0 ? 'right' : 'left'); + else onDPad(dy > 0 ? 'down' : 'up'); + }, + }), + [], + ); + + // Lyra message based on score + const lyraMessage = + score >= 8 ? 'Voll im Flow – der Impuls hat keine Chance!' : + score >= 5 ? 'Sehr gut! Bleib konzentriert.' : + score >= 3 ? 'Super! Weiter so.' : + 'Sammle die roten Äpfel.'; + + // SVG geometry + const snakePoints = snake + .map((s) => `${s.col * cell + cell / 2},${s.row * cell + cell / 2}`) + .join(' '); + + const head = snake[0]; + const eyePositions = head + ? (() => { + const cx = head.col * cell + cell / 2; + const cy = head.row * cell + cell / 2; + const off = cell * 0.22; + switch (dirRef.current) { + case 'right': return [{ x: cx + off * 0.8, y: cy - off }, { x: cx + off * 0.8, y: cy + off }]; + case 'left': return [{ x: cx - off * 0.8, y: cy - off }, { x: cx - off * 0.8, y: cy + off }]; + case 'up': return [{ x: cx - off, y: cy - off * 0.8 }, { x: cx + off, y: cy - off * 0.8 }]; + case 'down': return [{ x: cx - off, y: cy + off * 0.8 }, { x: cx + off, y: cy + off * 0.8 }]; + } + })() + : []; + const pupilPositions = eyePositions.map((e) => { + const d = cell * 0.04; + switch (dirRef.current) { + case 'right': return { x: e.x + d, y: e.y }; + case 'left': return { x: e.x - d, y: e.y }; + case 'up': return { x: e.x, y: e.y - d }; + case 'down': return { x: e.x, y: e.y + d }; + } + }); + const tongue = head + ? (() => { + const cx = head.col * cell + cell / 2; + const cy = head.row * cell + cell / 2; + const len = cell * 0.45; + const base = cell * 0.4; + switch (dirRef.current) { + case 'right': return { x1: cx + base, y1: cy, x2: cx + base + len, y2: cy }; + case 'left': return { x1: cx - base, y1: cy, x2: cx - base - len, y2: cy }; + case 'up': return { x1: cx, y1: cy - base, x2: cx, y2: cy - base - len }; + case 'down': return { x1: cx, y1: cy + base, x2: cx, y2: cy + base + len }; + } + })() + : null; + + return ( + + {/* Header */} + + {lyraMessage} + + + Score + {score} + + + Best + {highScore} + + + + + + + + {/* Board */} + + + + + + + + + + {snake.length >= 2 && ( + + )} + {head && } + {eyePositions.map((eye, ei) => ( + + ))} + {pupilPositions.map((p, pi) => ( + + ))} + {tongue && !gameOver && ( + + )} + + + + + + {/* D-Pad */} + {!gameOver && ( + + onDPad('up')} /> + + onDPad('left')} /> + + + + onDPad('right')} /> + + onDPad('down')} /> + + )} + + {gameOver && ( + + Game Over + {score} {score === 1 ? 'Apfel' : 'Äpfel'} gesammelt + + )} + + ); +} + +function DPadBtn({ dir, active, onPress }: { dir: Dir; active: boolean; onPress: () => void }) { + const icons: Record = { + up: 'chevron-up', down: 'chevron-down', left: 'chevron-back', right: 'chevron-forward', + }; + // FIX 1 (prev agent): icon color follows pressed-OR-active so it stays visible against dark pressed-bg. + // FIX 2 (this agent): idle button was #ffffff on a #ffffff screen → invisible. Idle is now light-gray + // with stronger border, pressed becomes mid-gray, active stays dark. Guarantees ≥ 3:1 contrast in all states. + const isHighlighted = active; + return ( + { tapHaptic(); onPress(); }} + hitSlop={12} + android_ripple={{ color: 'rgba(31,41,55,0.18)', borderless: true, radius: 36 }} + style={({ pressed }) => ({ + width: 64, height: 64, borderRadius: 32, + backgroundColor: isHighlighted ? '#1f2937' : (pressed ? '#d1d5db' : '#f3f4f6'), + borderWidth: 1.5, + borderColor: isHighlighted ? '#1f2937' : (pressed ? '#6b7280' : '#9ca3af'), + alignItems: 'center', justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: pressed ? 0.06 : 0.12, + shadowRadius: 4, + elevation: pressed ? 1 : 3, + transform: [{ scale: pressed ? 0.94 : 1 }], + })} + > + {({ pressed }) => ( + + )} + + ); +} + +// Action button für Tetris (Rotate, Drop) — größer & mit Label. +// Idle: tönt sich leicht in accent-Farbe ein (kein white-on-white-Verlust auf weißem Screen). +function TetrisActionBtn({ + icon, label, onPress, accent, +}: { + icon: 'sync' | 'arrow-down'; + label: string; + onPress: () => void; + accent?: string; +}) { + const accentColor = accent || '#1f2937'; + return ( + { mediumHaptic(); onPress(); }} + hitSlop={12} + android_ripple={{ color: accentColor + '33', borderless: false }} + style={({ pressed }) => ({ + width: 72, height: 72, borderRadius: 20, + // accent + '14' = ~8% Tönung im Idle-State, accent solid auf Press + backgroundColor: pressed ? accentColor : accentColor + '14', + borderWidth: 1.5, + borderColor: accentColor, + alignItems: 'center', justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: pressed ? 0.05 : 0.12, + shadowRadius: 5, + elevation: pressed ? 1 : 3, + transform: [{ scale: pressed ? 0.95 : 1 }], + })} + > + {({ pressed }) => ( + <> + + {label} + + )} + + ); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// MEMORY — 1:1 Port von apps/rebreak/app/components/sos/GameMemory.vue +// ═══════════════════════════════════════════════════════════════════════════════ + +const MEMORY_PAIRS = 8; +const MEMORY_EMOJIS = ['🛡️', '💪', '🌟', '🧠', '🌊', '🎯', '🌱', '🔑']; + +export function MemoryGame({ + onComplete, + onAbandon, +}: { + onComplete: (score: number) => void; + onAbandon: () => void; +}) { + type Card = { id: number; emoji: string; matched: boolean; revealed: boolean }; + const [cards, setCards] = useState([]); + const [flipped, setFlipped] = useState([]); + const [moveCount, setMoveCount] = useState(0); + const [matchedCount, setMatchedCount] = useState(0); + const [blocked, setBlocked] = useState(false); + + function init() { + const pairs = shuffle([...MEMORY_EMOJIS, ...MEMORY_EMOJIS]); + setCards(pairs.map((emoji, id) => ({ id, emoji, matched: false, revealed: false }))); + setFlipped([]); + setMoveCount(0); + setMatchedCount(0); + setBlocked(false); + } + useEffect(() => { init(); }, []); + + const lyraMessage = (() => { + const r = matchedCount / MEMORY_PAIRS; + if (r === 0) return 'Dreh die erste Karte um – nimm dir Zeit.'; + if (r < 0.25) return 'Gut. Merk dir die Positionen genau.'; + if (r < 0.5) return 'Du machst das super! Konzentrier dich weiter.'; + if (r < 0.75) return 'Mehr als die Hälfte! Du bist fast da.'; + return 'Wow – nur noch wenige Paare! 🔥'; + })(); + + function flip(id: number) { + if (blocked) return; + const card = cards[id]; + if (!card || card.matched || card.revealed) return; + if (flipped.length >= 2) return; + const next = cards.slice(); + next[id] = { ...next[id]!, revealed: true }; + setCards(next); + const nextFlipped = [...flipped, id]; + setFlipped(nextFlipped); + if (nextFlipped.length === 2) { + const newMoveCount = moveCount + 1; + setMoveCount(newMoveCount); + const [a, b] = nextFlipped; + const ca = next[a]!, cb = next[b]!; + if (ca.emoji === cb.emoji) { + const matched = next.slice(); + matched[a] = { ...ca, matched: true }; + matched[b] = { ...cb, matched: true }; + setCards(matched); + setFlipped([]); + const newMatched = matchedCount + 1; + setMatchedCount(newMatched); + if (newMatched === MEMORY_PAIRS) { + setTimeout(() => onComplete(newMoveCount), 600); + } + } else { + setBlocked(true); + setTimeout(() => { + const reverted = next.slice(); + reverted[a] = { ...reverted[a]!, revealed: false }; + reverted[b] = { ...reverted[b]!, revealed: false }; + setCards(reverted); + setFlipped([]); + setBlocked(false); + }, 900); + } + } + } + + return ( + + {/* Lyra Header */} + + + Lyra + {lyraMessage} + + + Züge + {moveCount} + + + + + + + {/* Progress */} + + + + + {/* Card Grid 4x4 */} + + {cards.map((card) => { + const showFace = card.revealed || card.matched; + return ( + flip(card.id)} + style={{ + width: '22.7%', aspectRatio: 1, borderRadius: 12, borderWidth: 1.5, + borderColor: card.matched ? '#86efac' : showFace ? '#93c5fd' : '#e5e7eb', + backgroundColor: card.matched ? '#f0fdf4' : showFace ? '#eff6ff' : '#f9fafb', + alignItems: 'center', justifyContent: 'center', + opacity: blocked && !showFace ? 0.6 : 1, + transform: [{ scale: card.matched ? 0.95 : 1 }], + }} + > + {showFace ? card.emoji : '🛡️'} + + ); + })} + + + ); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// TIC-TAC-TOE — 1:1 Port von apps/rebreak/app/components/sos/GameTicTacToe.vue +// ═══════════════════════════════════════════════════════════════════════════════ + +type TTCell = 'X' | 'O' | null; +type TTResult = 'player' | 'lyra' | 'draw' | null; +const TT_WIN_LINES = [ + [0, 1, 2], [3, 4, 5], [6, 7, 8], + [0, 3, 6], [1, 4, 7], [2, 5, 8], + [0, 4, 8], [2, 4, 6], +]; + +function ttCheckWinner(b: TTCell[]): { winner: 'X' | 'O' | null; line: number[] } { + for (const line of TT_WIN_LINES) { + const [a, c, d] = line as [number, number, number]; + if (b[a] && b[a] === b[c] && b[a] === b[d]) return { winner: b[a]!, line }; + } + return { winner: null, line: [] }; +} +function ttEmpty(b: TTCell[]) { return b.map((c, i) => c === null ? i : -1).filter((i) => i !== -1); } +function ttWouldWin(b: TTCell[], idx: number, mark: 'X' | 'O') { + const next = [...b]; next[idx] = mark; + return ttCheckWinner(next).winner === mark; +} +function ttLyraAI(b: TTCell[]): number { + const empty = ttEmpty(b); + for (const i of empty) if (ttWouldWin(b, i, 'O')) return i; // win + for (const i of empty) if (ttWouldWin(b, i, 'X')) return i; // block + if (b[4] === null) return 4; // center + const corners = [0, 2, 6, 8].filter((i) => b[i] === null); + if (corners.length) return corners[Math.floor(Math.random() * corners.length)]!; + return empty[Math.floor(Math.random() * empty.length)]!; +} + +export function TicTacToeGame({ + onComplete, + onAbandon, +}: { + onComplete: (score: number) => void; + onAbandon: () => void; +}) { + const [board, setBoard] = useState(Array(9).fill(null)); + const [gameOver, setGameOver] = useState(false); + const [result, setResult] = useState(null); + const [winLine, setWinLine] = useState([]); + const [lyraThinking, setLyraThinking] = useState(false); + const [playerScore, setPlayerScore] = useState(0); + const [lyraScore, setLyraScore] = useState(0); + const [round, setRound] = useState(1); + + const resultText = + result === 'player' ? '🎉 Du gewinnst diese Runde!' : + result === 'lyra' ? '🤖 Lyra gewinnt diese Runde' : + '🤝 Unentschieden'; + const resultColor = result === 'player' ? '#16a34a' : result === 'lyra' ? '#f43f5e' : '#eab308'; + + const lyraMessage = (() => { + if (lyraThinking) return 'Hmm, lass mich kurz nachdenken…'; + if (result === 'player') return 'Gut gespielt! Du hast diese Runde gewonnen. 👏'; + if (result === 'lyra') return 'Diesmal war ich schneller. Versuch es nochmal!'; + if (result === 'draw') return 'Unentschieden! Gut gekämpft.'; + const empty = board.filter((c) => c === null).length; + if (empty === 9) return 'Du fängst an – ich bin Lyra, du spielst X.'; + if (empty <= 3) return 'Das wird spannend – noch wenige Felder frei!'; + return 'Dein Zug. Ich beobachte genau.'; + })(); + + function applyResult(mark: 'X' | 'O', line: number[]) { + setWinLine(line); + setGameOver(true); + if (mark === 'X') { setResult('player'); setPlayerScore((s) => s + 1); } + else { setResult('lyra'); setLyraScore((s) => s + 1); } + } + + function playerMove(i: number) { + if (board[i] || gameOver || lyraThinking) return; + const next = [...board]; next[i] = 'X'; + setBoard(next); + const r = ttCheckWinner(next); + if (r.winner) { applyResult('X', r.line); return; } + if (ttEmpty(next).length === 0) { setGameOver(true); setResult('draw'); return; } + + setLyraThinking(true); + setTimeout(() => { + const idx = ttLyraAI(next); + const after = [...next]; after[idx] = 'O'; + setBoard(after); + const r2 = ttCheckWinner(after); + if (r2.winner) applyResult('O', r2.line); + else if (ttEmpty(after).length === 0) { setGameOver(true); setResult('draw'); } + setLyraThinking(false); + }, 600); + } + + function newRound() { + setBoard(Array(9).fill(null)); + setGameOver(false); + setResult(null); + setWinLine([]); + setRound((r) => r + 1); + } + + return ( + + {/* Lyra Header */} + + + Lyra + {lyraMessage} + + + + {playerScore} + Du + + : + + {lyraScore} + Lyra + + + + + {/* Board */} + + {board.map((cell, i) => { + const isWin = winLine.includes(i); + return ( + playerMove(i)} + disabled={!!cell || gameOver || lyraThinking} + style={{ + width: '31%', aspectRatio: 1, borderRadius: 16, borderWidth: 1.5, + borderColor: isWin ? '#facc15' : cell === 'X' ? '#3b82f6' : cell === 'O' ? '#f43f5e' : '#e5e7eb', + backgroundColor: isWin ? '#fef9c3' : cell === 'X' ? '#eff6ff' : cell === 'O' ? '#fee2e2' : '#f9fafb', + alignItems: 'center', justifyContent: 'center', + }} + > + {cell ?? ''} + + ); + })} + + + {/* Status */} + + {lyraThinking ? ( + Lyra denkt nach… + ) : gameOver ? ( + <> + {resultText} + Runde {round} + + ) : ( + Dein Zug – du spielst X + )} + + + {/* Actions */} + {gameOver ? ( + + + Nochmal + + onComplete(playerScore)} style={{ flex: 1, paddingVertical: 12, borderRadius: 12, backgroundColor: '#16a34a', alignItems: 'center' }}> + Fertig → + + + ) : ( + + Abbrechen + + )} + + ); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// TETRIS — 1:1 Port von apps/rebreak/app/components/sos/GameTetris.vue +// ═══════════════════════════════════════════════════════════════════════════════ + +const TETRIS_COLS = 10; +const TETRIS_ROWS = 20; +const TETRIS_SPEED_BASES = [1400, 1000, 700, 450, 250] as const; +const TETRIS_PIECES = [ + { shape: [[1, 1, 1, 1]], color: '#22d3ee' }, // I + { shape: [[1, 1], [1, 1]], color: '#fbbf24' }, // O + { shape: [[0, 1, 0], [1, 1, 1]], color: '#a78bfa' }, // T + { shape: [[0, 1, 1], [1, 1, 0]], color: '#34d399' }, // S + { shape: [[1, 1, 0], [0, 1, 1]], color: '#f87171' }, // Z + { shape: [[1, 0, 0], [1, 1, 1]], color: '#60a5fa' }, // J + { shape: [[0, 0, 1], [1, 1, 1]], color: '#fb923c' }, // L +]; +type TetrisPiece = { shape: number[][]; color: string; x: number; y: number }; + +function tetrisEmptyBoard(): string[][] { + return Array.from({ length: TETRIS_ROWS }, () => Array(TETRIS_COLS).fill('')); +} +function tetrisRandomPiece() { + const p = TETRIS_PIECES[Math.floor(Math.random() * TETRIS_PIECES.length)]!; + return { shape: p.shape.map((r) => [...r]), color: p.color }; +} +function tetrisRotate(shape: number[][]) { + return shape[0]!.map((_, i) => shape.map((row) => row[i]!).reverse()); +} + +export function TetrisGame({ + onComplete, + onAbandon, +}: { + onComplete: (score: number) => void; + onAbandon: () => void; +}) { + const insets = useSafeAreaInsets(); + // CELL aus Bildschirmgröße — Header(80) + Padding(40) + Speed-Stepper(50) + Controls(110) + Spacing(20) + Home-Indicator + const CELL = useMemo(() => { + const win = Dimensions.get('window'); + const maxW = Math.min(win.width - 32, 400); + const maxH = win.height - 80 - 40 - 50 - 110 - 20 - Math.max(insets.bottom, 16); + const cellByW = Math.floor(maxW / TETRIS_COLS); + const cellByH = Math.floor(maxH / TETRIS_ROWS); + return Math.max(12, Math.min(cellByW, cellByH)); + }, [insets.bottom]); + + const [board, setBoard] = useState(tetrisEmptyBoard()); + const [current, setCurrent] = useState(null); + const nextPieceRef = useRef(tetrisRandomPiece()); + const [score, setScore] = useState(0); + const [level, setLevel] = useState(1); + const [lines, setLines] = useState(0); + const [gameOver, setGameOver] = useState(false); + const [highScore, setHighScore] = useState(0); + const [speedLevel, setSpeedLevel] = useState(3); + const tickTimerRef = useRef | null>(null); + + const boardRef = useRef(board); + const currentRef = useRef(current); + useEffect(() => { boardRef.current = board; }, [board]); + useEffect(() => { currentRef.current = current; }, [current]); + + // Load high score + useEffect(() => { + AsyncStorage.getItem('rebreak-tetris-highscore').then((v) => { + if (v) setHighScore(parseInt(v) || 0); + }); + }, []); + + function isValid(piece: TetrisPiece, px: number, py: number, shape = piece.shape): boolean { + const b = boardRef.current; + for (let r = 0; r < shape.length; r++) { + for (let c = 0; c < shape[r]!.length; c++) { + if (!shape[r]![c]) continue; + const nx = px + c, ny = py + r; + if (nx < 0 || nx >= TETRIS_COLS || ny >= TETRIS_ROWS) return false; + if (ny >= 0 && b[ny]![nx]) return false; + } + } + return true; + } + + const spawnPiece = useCallback(() => { + const p = nextPieceRef.current; + nextPieceRef.current = tetrisRandomPiece(); + const x = Math.floor((TETRIS_COLS - p.shape[0]!.length) / 2); + const newPiece: TetrisPiece = { ...p, x, y: 0 }; + if (!isValid(newPiece, newPiece.x, newPiece.y)) { + setGameOver(true); + stopTick(); + const finalScore = score; + if (finalScore > highScore) { + setHighScore(finalScore); + AsyncStorage.setItem('rebreak-tetris-highscore', String(finalScore)).catch(() => {}); + } + setTimeout(() => onComplete(finalScore), 500); + return; + } + setCurrent(newPiece); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [score, highScore, onComplete]); + + function lockPiece() { + const piece = currentRef.current; + if (!piece) return; + const b = boardRef.current.map((r) => [...r]); + for (let r = 0; r < piece.shape.length; r++) { + for (let c = 0; c < piece.shape[r]!.length; c++) { + if (!piece.shape[r]![c]) continue; + const ny = piece.y + r, nx = piece.x + c; + if (ny >= 0) b[ny]![nx] = piece.color; + } + } + const cleared = b.filter((row) => row.every((c) => c !== '')); + const kept = b.filter((row) => row.some((c) => c === '')); + const newLines = cleared.length; + if (newLines > 0) { + setLines((l) => l + newLines); + const pts = [0, 100, 300, 500, 800][newLines] ?? 800; + setScore((s) => s + pts * level); + setLevel((lv) => Math.floor((lines + newLines) / 10) + 1); + resetTick(); + } + const nextBoard = [ + ...Array.from({ length: newLines }, () => Array(TETRIS_COLS).fill('')), + ...kept, + ]; + setBoard(nextBoard); + boardRef.current = nextBoard; + setCurrent(null); + setTimeout(() => spawnPiece(), 0); + } + + function moveLeft() { + const p = currentRef.current; if (!p || gameOver) return; + if (isValid(p, p.x - 1, p.y)) setCurrent({ ...p, x: p.x - 1 }); + } + function moveRight() { + const p = currentRef.current; if (!p || gameOver) return; + if (isValid(p, p.x + 1, p.y)) setCurrent({ ...p, x: p.x + 1 }); + } + function rotatePiece() { + const p = currentRef.current; if (!p || gameOver) return; + const rotated = tetrisRotate(p.shape); + for (const kick of [0, -1, 1, -2, 2]) { + if (isValid(p, p.x + kick, p.y, rotated)) { + setCurrent({ ...p, shape: rotated, x: p.x + kick }); + return; + } + } + } + function softDrop() { + const p = currentRef.current; if (!p || gameOver) return; + if (isValid(p, p.x, p.y + 1)) { + setCurrent({ ...p, y: p.y + 1 }); + setScore((s) => s + 1); + } else { + lockPiece(); + } + } + + function tickInterval() { + const base = TETRIS_SPEED_BASES[speedLevel - 1]!; + return Math.max(80, base - (level - 1) * 40); + } + function startTick() { + tickTimerRef.current = setInterval(() => { + const p = currentRef.current; + if (!p || gameOver) return; + if (isValid(p, p.x, p.y + 1)) setCurrent({ ...p, y: p.y + 1 }); + else lockPiece(); + }, tickInterval()); + } + function stopTick() { + if (tickTimerRef.current) { clearInterval(tickTimerRef.current); tickTimerRef.current = null; } + } + function resetTick() { stopTick(); startTick(); } + + // Init + useEffect(() => { + spawnPiece(); + startTick(); + return () => stopTick(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Re-tick when speed/level changes + useEffect(() => { if (!gameOver) resetTick(); /* eslint-disable-next-line */ }, [speedLevel, level]); + + // Display board (board + current piece) + const displayBoard = useMemo(() => { + const b = board.map((r) => [...r]); + if (current) { + for (let r = 0; r < current.shape.length; r++) { + for (let c = 0; c < current.shape[r]!.length; c++) { + if (!current.shape[r]![c]) continue; + const ny = current.y + r, nx = current.x + c; + if (ny >= 0 && ny < TETRIS_ROWS && nx >= 0 && nx < TETRIS_COLS) b[ny]![nx] = current.color; + } + } + } + return b; + }, [board, current]); + + // Ghost piece + const ghostCells = useMemo(() => { + if (!current) return []; + let gy = current.y; + while (isValid(current, current.x, gy + 1)) gy++; + if (gy === current.y) return []; + const cells: [number, number][] = []; + for (let r = 0; r < current.shape.length; r++) + for (let c = 0; c < current.shape[r]!.length; c++) + if (current.shape[r]![c]) cells.push([current.x + c, gy + r]); + return cells; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [current, board]); + + const lyraMessage = + lines >= 10 ? 'Unglaublich! Du bist voll im Flow! 🔥' : + lines >= 3 ? 'Super Linie! Weiter so.' : + 'Stapel die Blöcke – du schaffst das!'; + + const speedColors = ['#22c55e', '#84cc16', '#eab308', '#f97316', '#ef4444']; + + return ( + + {/* Header */} + + {lyraMessage} + + + {highScore > 0 && } + + + + + + + {/* Board */} + + + {displayBoard.map((row, y) => ( + + {row.map((color, x) => ( + + + + ))} + + ))} + {/* Ghost piece overlay */} + {ghostCells.map(([gx, gy], i) => ( + + ))} + + + + {/* Speed — native rendered slider (UISlider on iOS, SeekBar on Android) */} + + + + + Tempo + + + Speed {speedLevel} + + + { + const nv = Math.round(v); + if (nv !== speedLevel) { + tapHaptic(); + setSpeedLevel(nv); + } + }} + minimumTrackTintColor={speedColors[speedLevel - 1]} + maximumTrackTintColor="#e5e7eb" + thumbTintColor={Platform.OS === 'android' ? speedColors[speedLevel - 1] : undefined} + /> + + + {/* Controls — Move Pad (links) + Action Pad (rechts) */} + + {/* Move Pad */} + + + + + {/* Action Pad */} + + + + + + + {gameOver && ( + + Game Over + {score} Punkte · {lines} Linien + + )} + + ); +} + +function Stat({ label, value, color }: { label: string; value: number; color: string }) { + return ( + + {label} + {value} + + ); +} diff --git a/apps/rebreak-native/components/urge/UrgeStats.tsx b/apps/rebreak-native/components/urge/UrgeStats.tsx new file mode 100644 index 0000000..783a51b --- /dev/null +++ b/apps/rebreak-native/components/urge/UrgeStats.tsx @@ -0,0 +1,367 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { View, Text, ActivityIndicator } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { apiFetch } from '../../lib/api'; +import { colors } from '../../lib/theme'; + +type Emotion = 'stress' | 'sadness' | 'anger' | 'empty' | 'boredom' | 'other'; + +type UrgeLog = { + id: string; + timestamp: string; + emotion: Emotion; + wasOvercome: boolean; + breathingDone: boolean; +}; + +const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; + +function emotionLabel(key: string, t: (k: string) => string): string { + const map: Record = { + stress: t('urge.emotion_stress'), + sadness: t('urge.emotion_sadness'), + anger: t('urge.emotion_anger'), + empty: t('urge.emotion_empty'), + boredom: t('urge.emotion_boredom'), + other: t('urge.emotion_other'), + }; + return map[key] ?? key; +} + +function StatCard({ label, value, color }: { label: string; value: string; color: string }) { + return ( + + {value} + + {label} + + + ); +} + +export function UrgeStats() { + const { t } = useTranslation(); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + + const load = useCallback(async () => { + try { + setLoading(true); + const data = await apiFetch('/api/urge?limit=100'); + setLogs(Array.isArray(data) ? data : []); + } catch { + setLogs([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + load(); + }, [load]); + + const weeklyStats = useMemo(() => { + const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; + const thisWeek = logs.filter((log) => new Date(log.timestamp).getTime() > weekAgo); + return { + total: thisWeek.length, + overcome: thisWeek.filter((log) => log.wasOvercome).length, + breathingDone: thisWeek.filter((log) => log.breathingDone).length, + }; + }, [logs]); + + const patterns = useMemo(() => { + if (logs.length < 5) return null; + + const weekday = new Array(7).fill(0) as number[]; + const timeBlockCounts = [0, 0, 0, 0] as number[]; + const emotionCount: Record = {}; + + for (const log of logs) { + const d = new Date(log.timestamp); + const jsDay = d.getDay(); + const mondayIndex = jsDay === 0 ? 6 : jsDay - 1; + weekday[mondayIndex]!++; + + const hour = d.getHours(); + if (hour >= 6 && hour < 12) timeBlockCounts[0]!++; + else if (hour >= 12 && hour < 18) timeBlockCounts[1]!++; + else if (hour >= 18 && hour < 23) timeBlockCounts[2]!++; + else timeBlockCounts[3]!++; + + emotionCount[log.emotion] = (emotionCount[log.emotion] ?? 0) + 1; + } + + const maxWeekday = Math.max(...weekday, 1); + const maxTime = Math.max(...timeBlockCounts, 1); + const topEmotions = Object.entries(emotionCount) + .sort((a, b) => b[1] - a[1]) + .slice(0, 3); + + const peakDayIdx = weekday.indexOf(Math.max(...weekday)); + const peakTimeIdx = timeBlockCounts.indexOf(Math.max(...timeBlockCounts)); + const peakTimeLabel = ['morgens', 'mittags', 'abends', 'nachts'][peakTimeIdx] ?? ''; + const peakDayLabel = peakDayIdx >= 5 ? 'am Wochenende' : `${WEEKDAY_LABELS[peakDayIdx]}s`; + + return { + weekday: weekday.map((countValue, i) => ({ + label: WEEKDAY_LABELS[i]!, + count: countValue, + pct: Math.round((countValue / maxWeekday) * 100), + })), + timeBlocks: [ + { + emoji: '🌅', + label: t('urge.block_morning'), + count: timeBlockCounts[0]!, + pct: Math.round((timeBlockCounts[0]! / maxTime) * 100), + }, + { + emoji: '☀️', + label: t('urge.block_noon'), + count: timeBlockCounts[1]!, + pct: Math.round((timeBlockCounts[1]! / maxTime) * 100), + }, + { + emoji: '🌆', + label: t('urge.block_evening'), + count: timeBlockCounts[2]!, + pct: Math.round((timeBlockCounts[2]! / maxTime) * 100), + }, + { + emoji: '🌙', + label: t('urge.block_night'), + count: timeBlockCounts[3]!, + pct: Math.round((timeBlockCounts[3]! / maxTime) * 100), + }, + ], + topEmotions, + insight: `${t('urge.pattern_insight_prefix')} ${peakDayLabel} ${peakTimeLabel}.`, + }; + }, [logs, t]); + + if (loading) { + return ( + + + + ); + } + + return ( + + {/* Weekly counters */} + + + {t('urge.this_week')} + + + + + + + + + {patterns && ( + <> + {/* Insight */} + + + + {patterns.insight} + + + + {/* Weekday chart */} + + + {t('urge.chart_weekday_title')} + + + {patterns.weekday.map((day) => ( + + 0 ? Math.max(6, day.pct * 0.5) : 4, + borderRadius: 5, + backgroundColor: + day.pct >= 80 ? '#fb7185' : day.pct >= 50 ? '#f59e0b' : '#60a5fa', + marginBottom: 6, + }} + /> + + {day.label} + + + ))} + + + + {/* Time blocks */} + + + {t('urge.chart_time_title')} + + + {patterns.timeBlocks.map((b) => ( + + {b.emoji} + + {b.label} + + + + + + {b.count} + + + ))} + + + + {/* Top emotions */} + + + {t('urge.chart_top_emotions')} + + + {patterns.topEmotions.map(([emo, c]) => ( + + + {emotionLabel(emo, t)} x{c} + + + ))} + + + + )} + + ); +} diff --git a/apps/rebreak-native/components/urge/gameSvgs.ts b/apps/rebreak-native/components/urge/gameSvgs.ts new file mode 100644 index 0000000..f034d40 --- /dev/null +++ b/apps/rebreak-native/components/urge/gameSvgs.ts @@ -0,0 +1,7 @@ +export const memorySvg = ``; + +export const tictactoeSvg = ``; + +export const snakeSvg = ``; + +export const tetrisSvg = ``; diff --git a/apps/rebreak-native/dev-ios.sh b/apps/rebreak-native/dev-ios.sh new file mode 100755 index 0000000..bdf8e6d --- /dev/null +++ b/apps/rebreak-native/dev-ios.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Rebreak Native: Dev-Server (Metro) + iOS Build & Run +# Pendant zu apps/rebreak/dev-ios.sh, aber für React Native + Expo statt Capacitor. +set -e + +cd "$(dirname "$0")" + +echo "🧠 Rebreak Native iOS Dev" +echo "=========================" + +# Modi: +# ./dev-ios.sh → öffnet Xcode-Workspace (Default — User baut auf iPhone) +# ./dev-ios.sh --device → physisches iPhone via USB (CLI-Build + Auto-Launch) +# ./dev-ios.sh --simulator → iOS Simulator (schnellster Test-Loop, weniger Auth/Push-Test) +MODE="${1:-xcode}" + +# Metro-Port wird NICHT mehr automatisch gekillt — falls du Metro in einem +# anderen Terminal laufen hast, würde das hier die Session zerstören. +# Falls Metro hängt: manuell `lsof -ti:8081 | xargs kill -9` ausführen. + +# Cocoapods: läuft beim ersten Run automatisch via expo run:ios. +# Falls "objectVersion 70 not supported" Fehler unter Xcode 26 → CocoaPods updaten: +# sudo gem install cocoapods --pre +# +# Podfile-Fixes werden durch Config-Plugins automatisch reinpatcht: +# - plugins/with-fmt-consteval-fix.js → FMT_USE_CONSTEVAL=0 (Xcode 16 + RN 0.79) +# - plugins/with-rebreak-protection-ios.js → NEFilter Extension Target +# → expo prebuild --clean ist daher SAFE (Plugins regenerieren die Patches). +# +# Für radikalen Cache-Reset: ./clean-ios.sh +# Bei Build-Errors aus dem Nichts: ./clean-ios.sh --build + +# 3. Run je nach Mode +case "$MODE" in + --xcode|xcode|"") + # Falls Xcode bereits mit Rebreak.xcodeproj (statt .xcworkspace) offen ist, + # schließe ihn erst. Sonst kriegst du zwei Project-Windows. + osascript -e 'tell application "Xcode" to close every window whose name contains "Rebreak.xcodeproj"' 2>/dev/null || true + + echo "🔨 Opening Xcode-Workspace..." + open -a Xcode ios/Rebreak.xcworkspace + echo "" + echo "✅ Xcode offen — In Xcode:" + echo " 1. iPhone via USB anschließen (falls nicht schon)" + echo " 2. Top-Bar: iPhone als Run-Target wählen (links neben Play-Button)" + echo " 3. Cmd+R für Build & Run auf iPhone" + echo "" + echo "ℹ️ Metro: separat starten via 'pnpm expo start --dev-client' falls noch nicht läuft" + ;; + + --device|device) + echo "📱 Building für physisches iPhone (USB)..." + echo "ℹ️ Erste Mal: Xcode wird geöffnet zum Signing-Setup." + pnpm expo run:ios --device + ;; + + --simulator|simulator) + echo "📱 Building für iOS Simulator..." + pnpm expo run:ios + ;; + + *) + echo "Unknown mode: $MODE" + echo "Usage: ./dev-ios.sh [--xcode|--device|--simulator]" + exit 1 + ;; +esac diff --git a/apps/rebreak-native/dev-iphone.sh b/apps/rebreak-native/dev-iphone.sh new file mode 100755 index 0000000..4003d0b --- /dev/null +++ b/apps/rebreak-native/dev-iphone.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Rebreak Native — Dev auf physischem iPhone (kein Simulator). +# +# Was es macht: +# - Killt alte Metro-Instanzen auf 8081 (saubere Session) +# - Startet Metro mit --host lan damit iPhone via WiFi connecten kann +# - Druckt deine LAN-IP zum manuellen Eintragen falls Bonjour failt +# +# Auf iPhone: +# 1. Mac + iPhone müssen im SELBEN WiFi sein +# 2. App komplett killen (App-Switcher → swipe up) +# 3. App neu öffnen — dev-client sollte Metro automatisch finden +# 4. Falls nicht: dev-launcher → "Enter URL manually" → http://:8081 +# +# WICHTIG: Im Metro-Terminal NICHT `i` drücken — sonst startet Simulator! +# Nur `r` für Reload. +set -e +cd "$(dirname "$0")" + +echo "🧹 Killing old Metro on port 8081..." +lsof -ti:8081 | xargs kill -9 2>/dev/null || true + +echo "" +echo "📡 Mac LAN-IP für iPhone:" +ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)" +echo "" +echo "ℹ️ Falls dev-client Metro nicht automatisch findet:" +echo " im iPhone-Launcher → 'Enter URL manually' → http://:8081" +echo "" +echo "🚀 Starting Metro with --host lan..." +echo " (Drücke 'r' für Reload, NICHT 'i' — sonst startet Simulator!)" +echo "" + +exec pnpm expo start --host lan --clear --dev-client diff --git a/apps/rebreak-native/global.css b/apps/rebreak-native/global.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/apps/rebreak-native/global.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/rebreak-native/hooks/useBlocklistSync.ts b/apps/rebreak-native/hooks/useBlocklistSync.ts new file mode 100644 index 0000000..fea8d16 --- /dev/null +++ b/apps/rebreak-native/hooks/useBlocklistSync.ts @@ -0,0 +1,54 @@ +import { useCallback, useState } from 'react'; +import Constants from 'expo-constants'; +import { supabase } from '../lib/supabase'; +import { protection } from '../lib/protection'; + +type SyncResult = { ok: boolean; count?: number; plan?: string; error?: string }; + +/** + * Synct die binary Blocklist (`blocklist.bin`) vom Server in die App-Group. + * Die NEFilter-Extension memory-mapped diese Datei — ohne Sync = leere + * Blocklist = nichts wird geblockt. + * + * Triggers: + * - direkt nach activateUrlFilter() success + * - nach Domain-Add/-Submit/-Delete + * - bei App-Resume (in case Server-Updates kamen) + * + * Backend respondet 304 wenn ETag matched → kein Re-Download. + */ +export function useBlocklistSync() { + const [syncing, setSyncing] = useState(false); + const [lastResult, setLastResult] = useState(null); + + const sync = useCallback(async (): Promise => { + if (syncing) return { ok: false, error: 'already_syncing' }; + setSyncing(true); + try { + const baseURL = Constants.expoConfig?.extra?.apiUrl as string; + const session = (await supabase.auth.getSession()).data.session; + const authToken = session?.access_token; + + if (!baseURL || !authToken) { + const result = { ok: false, error: 'missing_baseURL_or_token' }; + setLastResult(result); + return result; + } + + const res = await protection.syncBlocklist({ baseURL, authToken }); + const result = { ok: true, count: res.count, plan: res.plan }; + setLastResult(result); + console.log('[blocklist-sync] ok:', res); + return result; + } catch (e: any) { + const result = { ok: false, error: e?.message ?? 'sync_failed' }; + setLastResult(result); + console.error('[blocklist-sync] failed:', e); + return result; + } finally { + setSyncing(false); + } + }, [syncing]); + + return { sync, syncing, lastResult }; +} diff --git a/apps/rebreak-native/hooks/useChatRealtime.ts b/apps/rebreak-native/hooks/useChatRealtime.ts new file mode 100644 index 0000000..e0b9934 --- /dev/null +++ b/apps/rebreak-native/hooks/useChatRealtime.ts @@ -0,0 +1,129 @@ +import { useEffect } from "react"; +import { supabase } from "../lib/supabase"; +import type { RealtimeChannel } from "@supabase/supabase-js"; + +/** + * Realtime-Subscription für DM-Konversation: + * Lauscht auf INSERT in rebreak.direct_messages mit sender_id=eq.{partnerId}. + * Filter: Wir bekommen nur Nachrichten DES Partners (eigene werden lokal optimistisch + * hinzugefügt). callback erhält die rohe Postgres-Row. + */ +export function useDmRealtime( + partnerId: string | undefined, + onInsert: (row: any) => void, + enabled: boolean = true, +) { + useEffect(() => { + if (!enabled || !partnerId) return; + let channel: RealtimeChannel | null = null; + let cancelled = false; + let reconnectTimer: ReturnType | null = null; + + async function subscribe() { + const { data } = await supabase.auth.getSession(); + if (cancelled || !data.session?.access_token) return; + supabase.realtime.setAuth(data.session.access_token); + + channel = supabase + .channel(`dm:${partnerId}:${Date.now()}`) + .on( + "postgres_changes", + { + event: "INSERT", + schema: "rebreak", + table: "direct_messages", + filter: `sender_id=eq.${partnerId}`, + }, + (payload: any) => { + onInsert(payload.new); + }, + ) + .subscribe((status) => { + if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { + cleanup(); + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(() => { + if (!cancelled) subscribe(); + }, 3000); + } + }); + } + + function cleanup() { + if (channel) { + supabase.removeChannel(channel); + channel = null; + } + } + + subscribe(); + return () => { + cancelled = true; + if (reconnectTimer) clearTimeout(reconnectTimer); + cleanup(); + }; + }, [partnerId, enabled, onInsert]); +} + +/** + * Realtime für Gruppen-Chat: lauscht auf INSERT in rebreak.chat_messages mit room_id=eq.{roomId}. + */ +export function useRoomRealtime( + roomId: string | undefined, + myUserId: string | undefined, + onInsert: (row: any) => void, + enabled: boolean = true, +) { + useEffect(() => { + if (!enabled || !roomId) return; + let channel: RealtimeChannel | null = null; + let cancelled = false; + let reconnectTimer: ReturnType | null = null; + + async function subscribe() { + const { data } = await supabase.auth.getSession(); + if (cancelled || !data.session?.access_token) return; + supabase.realtime.setAuth(data.session.access_token); + + channel = supabase + .channel(`room:${roomId}:${Date.now()}`) + .on( + "postgres_changes", + { + event: "INSERT", + schema: "rebreak", + table: "chat_messages", + filter: `room_id=eq.${roomId}`, + }, + (payload: any) => { + // Eigene Nachrichten überspringen (lokal optimistisch hinzugefügt) + if (payload.new?.user_id === myUserId) return; + onInsert(payload.new); + }, + ) + .subscribe((status) => { + if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { + cleanup(); + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(() => { + if (!cancelled) subscribe(); + }, 3000); + } + }); + } + + function cleanup() { + if (channel) { + supabase.removeChannel(channel); + channel = null; + } + } + + subscribe(); + return () => { + cancelled = true; + if (reconnectTimer) clearTimeout(reconnectTimer); + cleanup(); + }; + }, [roomId, myUserId, enabled, onInsert]); +} diff --git a/apps/rebreak-native/hooks/useCommunityRealtime.ts b/apps/rebreak-native/hooks/useCommunityRealtime.ts new file mode 100644 index 0000000..4238cda --- /dev/null +++ b/apps/rebreak-native/hooks/useCommunityRealtime.ts @@ -0,0 +1,153 @@ +import { useEffect } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { supabase } from "../lib/supabase"; +import type { RealtimeChannel } from "@supabase/supabase-js"; +import type { CommunityPost } from "../stores/community"; + +/** + * Realtime-Subscription für die Community-Feed-Page. + * Lauscht auf: + * - INSERT auf community_posts → invalidiert die Feed-Query (frischer Refetch) + * - UPDATE auf community_posts → patcht likes/comments-Counts inline + * - UPDATE auf domain_submissions → patcht domain_vote-Posts mit neuem Status + * - UPDATE auf game_challenges → patcht challenge-Status + * + * Pendant zum Nuxt-`communityStore.startRealtime()` aus apps/rebreak/. + */ +export function useCommunityRealtime(enabled: boolean = true) { + const queryClient = useQueryClient(); + + useEffect(() => { + if (!enabled) return; + let channel: RealtimeChannel | null = null; + let cancelled = false; + let reconnectTimer: ReturnType | null = null; + + async function subscribe() { + const { data } = await supabase.auth.getSession(); + const session = data.session; + if (!session?.access_token) return; + if (cancelled) return; + + supabase.realtime.setAuth(session.access_token); + const myId = session.user.id; + + channel = supabase + .channel(`community:posts:${Date.now()}`) + .on( + "postgres_changes", + { event: "INSERT", schema: "rebreak", table: "community_posts" }, + (payload: any) => { + const r = payload.new; + if (r.user_id === myId) return; // eigene Posts schon optimistisch hinzugefügt + if (r.is_moderated) return; + // Einfacher als Detail-Fetch: alle Feed-Queries invalidieren + queryClient.invalidateQueries({ queryKey: ["community-posts"] }); + }, + ) + .on( + "postgres_changes", + { event: "UPDATE", schema: "rebreak", table: "community_posts" }, + (payload: any) => { + const r = payload.new; + patchPostInAllQueries(queryClient, r.id, (p) => ({ + ...p, + likesCount: r.likes_count ?? p.likesCount, + dislikesCount: r.dislikes_count ?? p.dislikesCount, + commentsCount: r.comments_count ?? p.commentsCount, + repostsCount: r.reposts_count ?? p.repostsCount, + })); + }, + ) + .on( + "postgres_changes", + { event: "UPDATE", schema: "rebreak", table: "domain_submissions" }, + (payload: any) => { + const r = payload.new; + if ( + r.status !== "approved" && + r.status !== "rejected" && + r.status !== "in_review" + ) { + return; + } + patchPostInAllQueries(queryClient, null, (p) => { + if (p.submission?.domain == null) return p; + // Wir kennen die submissionId nicht direkt am Post, also matche per domain. + // Realistisch unique pro user_id, aber Feed enthält Post mit submission-Objekt. + // Wenn der Post diese submission referenziert, patchen. + if (!p.submission || (p as any).submissionId !== r.id) { + // Falls du submissionId an Post-Schema hängst, hier nutzen. + // Solange nicht: invalidate fallback unten. + return p; + } + return { + ...p, + submission: { + ...p.submission, + status: r.status, + yesVotes: r.yes_votes ?? p.submission.yesVotes, + noVotes: r.no_votes ?? p.submission.noVotes, + reviewedAt: r.reviewed_at ?? p.submission.reviewedAt, + }, + }; + }); + // Sicherheitshalber auch invalidieren — domain_vote ist selten genug. + queryClient.invalidateQueries({ queryKey: ["community-posts"] }); + }, + ) + .on( + "postgres_changes", + { event: "UPDATE", schema: "rebreak", table: "game_challenges" }, + (payload: any) => { + const r = payload.new; + patchPostInAllQueries(queryClient, null, (p) => + p.challengeId === r.id ? { ...p, challengeStatus: r.status } : p, + ); + }, + ) + .subscribe((status) => { + if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { + cleanup(); + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(() => { + if (!cancelled) subscribe(); + }, 3000); + } + }); + } + + function cleanup() { + if (channel) { + supabase.removeChannel(channel); + channel = null; + } + } + + subscribe(); + + return () => { + cancelled = true; + if (reconnectTimer) clearTimeout(reconnectTimer); + cleanup(); + }; + }, [enabled, queryClient]); +} + +function patchPostInAllQueries( + queryClient: ReturnType, + postId: string | null, + patcher: (p: CommunityPost) => CommunityPost, +) { + const queries = queryClient.getQueriesData({ + queryKey: ["community-posts"], + }); + for (const [key, data] of queries) { + if (!Array.isArray(data)) continue; + const next = data.map((p) => { + if (postId !== null && p.id !== postId) return p; + return patcher(p); + }); + queryClient.setQueryData(key, next); + } +} diff --git a/apps/rebreak-native/hooks/useCustomDomains.ts b/apps/rebreak-native/hooks/useCustomDomains.ts new file mode 100644 index 0000000..6faabf4 --- /dev/null +++ b/apps/rebreak-native/hooks/useCustomDomains.ts @@ -0,0 +1,178 @@ +import { useCallback, useEffect, useState } from 'react'; +import { apiFetch } from '../lib/api'; + +export type DomainStatus = 'active' | 'submitted' | 'approved' | 'rejected'; + +export type CustomDomain = { + id: string; + domain: string; + status: DomainStatus; + addedAt?: string; + postId?: string | null; + submission?: { id: string; yesVotes: number; noVotes: number; status: string } | null; +}; + +export type Plan = 'free' | 'pro' | 'legend'; + +export type Tier = { + plan: Plan; + domainLimit: number; // free=5, pro=5, legend=10 + refillEnabled: boolean; // free=false, pro/legend=true + globalBlocklist: boolean; // free=false, pro/legend=true + canSubmit: boolean; // free=false, pro/legend=true + usedSlots: number; // active+submitted (NICHT approved/rejected) + atLimit: boolean; +}; + +function deriveTier(plan: Plan, domains: CustomDomain[]): Tier { + const limit = plan === 'legend' ? 10 : 5; + const refill = plan !== 'free'; + const usedSlots = domains.filter((d) => d.status === 'active' || d.status === 'submitted').length; + return { + plan, + domainLimit: limit, + refillEnabled: refill, + globalBlocklist: refill, + canSubmit: refill, + usedSlots, + atLimit: usedSlots >= limit, + }; +} + +export type UseCustomDomainsReturn = { + domains: CustomDomain[]; + tier: Tier; + loading: boolean; + error: string | null; + refresh: () => Promise; + addDomain: (domain: string) => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>; + submitDomain: (id: string) => Promise<{ ok: boolean; error?: string }>; + removeDomain: (id: string) => Promise<{ ok: boolean; error?: string }>; + /** Live-Validate (regex) ob string gültiger Domain-Name ist. */ + isValidDomain: (s: string) => boolean; + /** Normalize: lowercase, http(s)://, /path stripping, www. weg. */ + normalizeDomain: (s: string) => string; +}; + +const DOMAIN_REGEX = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i; + +export function normalizeDomain(input: string): string { + let s = input.trim().toLowerCase(); + if (s.startsWith('https://')) s = s.slice(8); + else if (s.startsWith('http://')) s = s.slice(7); + const slash = s.indexOf('/'); + if (slash >= 0) s = s.slice(0, slash); + if (s.startsWith('www.')) s = s.slice(4); + return s; +} + +export function isValidDomain(input: string): boolean { + const n = normalizeDomain(input); + if (!n || n.length > 253) return false; + return DOMAIN_REGEX.test(n); +} + +/** + * Custom-Domain CRUD gegen `/api/custom-domains/*` mit Tier-aware Limits. + * + * Tier-Logik (Single-Source-of-Truth: User.plan): + * Free → 5 Slots, kein Refill, keine Submit + * Pro → 5 Slots, Refill bei approved/rejected, Submit erlaubt + * Legend → 10 Slots, Refill, Submit + */ +export function useCustomDomains(plan: Plan): UseCustomDomainsReturn { + const [domains, setDomains] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchDomains = useCallback(async () => { + try { + // Backend (`server/api/custom-domains/index.get.ts`) gibt Array DIREKT zurück, + // kein { domains: [...] }-Wrapper. + const res = await apiFetch( + '/api/custom-domains', + ); + const arr = Array.isArray(res) ? res : (res?.domains ?? []); + console.log('[useCustomDomains] fetched:', arr.length, 'domains', arr.slice(0, 3)); + setDomains(arr); + setError(null); + } catch (e: any) { + console.error('[useCustomDomains] fetch failed:', e?.message ?? e); + setError(e?.message ?? 'unknown'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchDomains(); + }, [fetchDomains]); + + const addDomain = useCallback( + async (input: string) => { + if (!isValidDomain(input)) return { ok: false, error: 'invalid_domain' }; + const tier = deriveTier(plan, domains); + if (tier.atLimit) return { ok: false, error: 'limit_reached' }; + const normalized = normalizeDomain(input); + try { + // Backend könnte einen `alreadyGlobal`-Flag setzen wenn die Domain + // bereits in der globalen Blocklist ist (Slot wird nicht verbraucht). + const res = await apiFetch('/api/custom-domains', { + method: 'POST', + body: { domain: normalized }, + }); + if (res?.alreadyGlobal) { + return { ok: false, alreadyGlobal: true }; + } + await fetchDomains(); + return { ok: true }; + } catch (e: any) { + return { ok: false, error: e?.message ?? 'add_failed' }; + } + }, + [plan, domains, fetchDomains], + ); + + const submitDomain = useCallback( + async (id: string) => { + const tier = deriveTier(plan, domains); + if (!tier.canSubmit) return { ok: false, error: 'plan_does_not_support_submit' }; + try { + await apiFetch(`/api/custom-domains/${id}/submit`, { method: 'POST', body: {} }); + await fetchDomains(); + return { ok: true }; + } catch (e: any) { + return { ok: false, error: e?.message ?? 'submit_failed' }; + } + }, + [plan, domains, fetchDomains], + ); + + const removeDomain = useCallback( + async (id: string) => { + try { + await apiFetch(`/api/custom-domains/${id}`, { method: 'DELETE' }); + await fetchDomains(); + return { ok: true }; + } catch (e: any) { + return { ok: false, error: e?.message ?? 'remove_failed' }; + } + }, + [fetchDomains], + ); + + const tier = deriveTier(plan, domains); + + return { + domains, + tier, + loading, + error, + refresh: fetchDomains, + addDomain, + submitDomain, + removeDomain, + isValidDomain, + normalizeDomain, + }; +} diff --git a/apps/rebreak-native/hooks/useDomainSubmissionRealtime.ts b/apps/rebreak-native/hooks/useDomainSubmissionRealtime.ts new file mode 100644 index 0000000..7557f7e --- /dev/null +++ b/apps/rebreak-native/hooks/useDomainSubmissionRealtime.ts @@ -0,0 +1,96 @@ +import { useEffect } from "react"; +import { supabase } from "../lib/supabase"; +import type { RealtimeChannel } from "@supabase/supabase-js"; + +/** + * Realtime-Subscription für die Blocker-Page. + * Lauscht auf: + * - UPDATE auf rebreak.domain_submissions → ruft `onChange()` (refetch) + * - INSERT auf rebreak.notifications mit type=domain_accepted für eigene recipient_id → refetch + * + * Pendant zum Nuxt-Code in apps/rebreak/app/pages/app/blocker/index.vue. + */ +export function useDomainSubmissionRealtime( + onChange: () => void, + enabled: boolean = true, +) { + useEffect(() => { + if (!enabled) return; + let channel: RealtimeChannel | null = null; + let cancelled = false; + let reconnectTimer: ReturnType | null = null; + + async function subscribe() { + const { data } = await supabase.auth.getSession(); + const session = data.session; + if (!session?.access_token) return; + if (cancelled) return; + + supabase.realtime.setAuth(session.access_token); + const myId = session.user.id; + + channel = supabase + .channel(`blocker:domains:${myId}:${Date.now()}`) + .on( + "postgres_changes", + { + event: "*", + schema: "rebreak", + table: "domain_submissions", + filter: `submitter_id=eq.${myId}`, + }, + () => onChange(), + ) + .on( + "postgres_changes", + { + event: "*", + schema: "rebreak", + table: "user_custom_domains", + filter: `user_id=eq.${myId}`, + }, + () => onChange(), + ) + .on( + "postgres_changes", + { + event: "INSERT", + schema: "rebreak", + table: "notifications", + filter: `recipient_id=eq.${myId}`, + }, + (payload: any) => { + const t = payload.new?.type; + if (t === "domain_accepted" || t === "domain_rejected") { + onChange(); + } + }, + ) + .subscribe((status, err) => { + if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { + console.warn("[domainRealtime] error:", status, err ?? ""); + cleanup(); + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(() => { + if (!cancelled) subscribe(); + }, 3000); + } + }); + } + + function cleanup() { + if (channel) { + supabase.removeChannel(channel); + channel = null; + } + } + + subscribe(); + + return () => { + cancelled = true; + if (reconnectTimer) clearTimeout(reconnectTimer); + cleanup(); + }; + }, [enabled, onChange]); +} diff --git a/apps/rebreak-native/hooks/useMailConnect.ts b/apps/rebreak-native/hooks/useMailConnect.ts new file mode 100644 index 0000000..cfa4968 --- /dev/null +++ b/apps/rebreak-native/hooks/useMailConnect.ts @@ -0,0 +1,94 @@ +import { useCallback, useState } from 'react'; +import { apiFetch } from '../lib/api'; + +export type MailProvider = + | 'gmail' + | 'icloud' + | 'outlook' + | 'yahoo' + | 'gmx' + | 'other'; + +type ConnectBody = { + email: string; + password: string; + // Provider-Feld wird NICHT an das Backend gesendet — der Server erkennt + // den Provider automatisch via Email-Domain (detectImapProvider in connect.post.ts). + // Optionale Custom-IMAP-Felder für "other": + imapHost?: string; + imapPort?: number; + useTls?: boolean; + rejectUnauthorized?: boolean; +}; + +type ConnectResult = { + connected: boolean; + email: string; + provider: string; + custom: boolean; +}; + +export type UseMailConnectReturn = { + connect: (params: ConnectBody) => Promise<{ ok: boolean; error?: string }>; + connecting: boolean; + error: string | null; + /** Leitet aus der Email-Domain den Provider ab (rein client-seitig, zur UI-Hilfe). */ + detectProvider: (email: string) => MailProvider; +}; + +const PROVIDER_DOMAIN_MAP: Record = { + 'gmail.com': 'gmail', + 'googlemail.com': 'gmail', + 'icloud.com': 'icloud', + 'me.com': 'icloud', + 'mac.com': 'icloud', + 'outlook.com': 'outlook', + 'hotmail.com': 'outlook', + 'live.com': 'outlook', + 'msn.com': 'outlook', + 'yahoo.com': 'yahoo', + 'yahoo.de': 'yahoo', + 'yahoo.co.uk': 'yahoo', + 'ymail.com': 'yahoo', + 'gmx.de': 'gmx', + 'gmx.net': 'gmx', + 'gmx.at': 'gmx', + 'gmx.ch': 'gmx', + 'web.de': 'gmx', +}; + +export function detectProvider(email: string): MailProvider { + const domain = email.trim().toLowerCase().split('@')[1] ?? ''; + return PROVIDER_DOMAIN_MAP[domain] ?? 'other'; +} + +/** + * Kapselt POST /api/mail/connect. + * + * Backend erwartet: { email, password, imapHost?, imapPort?, useTls?, rejectUnauthorized? } + * Provider-Detection passiert server-seitig — wir senden keinen provider-Key. + */ +export function useMailConnect(): UseMailConnectReturn { + const [connecting, setConnecting] = useState(false); + const [error, setError] = useState(null); + + const connect = useCallback(async (params: ConnectBody) => { + setConnecting(true); + setError(null); + try { + await apiFetch('/api/mail/connect', { + method: 'POST', + body: params, + }); + return { ok: true }; + } catch (e: any) { + const msg = e?.message ?? 'Verbindung fehlgeschlagen'; + setError(msg); + return { ok: false, error: msg }; + } finally { + setConnecting(false); + } + }, []); + + return { connect, connecting, error, detectProvider }; +} diff --git a/apps/rebreak-native/hooks/useMailDisconnect.ts b/apps/rebreak-native/hooks/useMailDisconnect.ts new file mode 100644 index 0000000..14a8f8a --- /dev/null +++ b/apps/rebreak-native/hooks/useMailDisconnect.ts @@ -0,0 +1,39 @@ +import { useCallback, useState } from 'react'; +import { apiFetch } from '../lib/api'; + +export type UseMailDisconnectReturn = { + disconnect: (connectionId: string) => Promise<{ ok: boolean; error?: string }>; + disconnecting: boolean; + error: string | null; +}; + +/** + * Kapselt DELETE /api/mail/disconnect für ein einzelnes Konto. + * + * Backend erwartet: Body { connectionId } (nicht als URL-Param). + * Gibt { ok: true } zurück wenn erfolgreich. + */ +export function useMailDisconnect(): UseMailDisconnectReturn { + const [disconnecting, setDisconnecting] = useState(false); + const [error, setError] = useState(null); + + const disconnect = useCallback(async (connectionId: string) => { + setDisconnecting(true); + setError(null); + try { + await apiFetch<{ ok: boolean }>('/api/mail/disconnect', { + method: 'DELETE', + body: { connectionId }, + }); + return { ok: true }; + } catch (e: any) { + const msg = e?.message ?? 'Trennen fehlgeschlagen'; + setError(msg); + return { ok: false, error: msg }; + } finally { + setDisconnecting(false); + } + }, []); + + return { disconnect, disconnecting, error }; +} diff --git a/apps/rebreak-native/hooks/useMailInterval.ts b/apps/rebreak-native/hooks/useMailInterval.ts new file mode 100644 index 0000000..f28dcca --- /dev/null +++ b/apps/rebreak-native/hooks/useMailInterval.ts @@ -0,0 +1,37 @@ +import { useCallback, useState } from "react"; +import { apiFetch } from "../lib/api"; + +/** + * PATCH /api/mail/interval — Setzt das Scan-Intervall (in Stunden) für eine + * bestimmte Mail-Connection. Plan-Limits werden serverseitig geprüft. + */ +export function useMailInterval() { + const [updating, setUpdating] = useState(null); + const [error, setError] = useState(null); + + const setInterval = useCallback( + async (connectionId: string, interval: number) => { + setUpdating(connectionId); + setError(null); + try { + await apiFetch<{ ok: boolean; interval: number }>( + "/api/mail/interval", + { + method: "PATCH", + body: { connectionId, interval }, + }, + ); + return { ok: true }; + } catch (e: any) { + const msg = e?.message ?? "unknown"; + setError(msg); + return { ok: false, error: msg }; + } finally { + setUpdating(null); + } + }, + [], + ); + + return { setInterval, updating, error }; +} diff --git a/apps/rebreak-native/hooks/useMailResults.ts b/apps/rebreak-native/hooks/useMailResults.ts new file mode 100644 index 0000000..3f0cd4f --- /dev/null +++ b/apps/rebreak-native/hooks/useMailResults.ts @@ -0,0 +1,52 @@ +import { useCallback, useEffect, useState } from "react"; +import { apiFetch } from "../lib/api"; + +export type MailBlockedItem = { + id: string; + subject: string; + sender_email: string; + sender_name: string | null; + received_at: string; + connection_id: string; +}; + +export type MailResultsResponse = { + results: MailBlockedItem[]; + total: number; + page: number; + pages: number; +}; + +/** + * GET /api/mail/results — Liste der in den letzten 24h gelöschten Mails. + * Backend räumt selbst nach 24h auf (deleteOldMailBlocked). + */ +export function useMailResults(enabled: boolean = true) { + const [results, setResults] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const refresh = useCallback(async () => { + if (!enabled) return; + setLoading(true); + try { + const res = await apiFetch( + "/api/mail/results?page=1", + ); + setResults(res.results ?? []); + setTotal(res.total ?? 0); + setError(null); + } catch (e: any) { + setError(e?.message ?? "unknown"); + } finally { + setLoading(false); + } + }, [enabled]); + + useEffect(() => { + if (enabled) refresh(); + }, [enabled, refresh]); + + return { results, total, loading, error, refresh }; +} diff --git a/apps/rebreak-native/hooks/useMailStatus.ts b/apps/rebreak-native/hooks/useMailStatus.ts new file mode 100644 index 0000000..509a03f --- /dev/null +++ b/apps/rebreak-native/hooks/useMailStatus.ts @@ -0,0 +1,137 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { AppState, type AppStateStatus } from 'react-native'; +import { apiFetch } from '../lib/api'; + +export type MailAccount = { + id: string; + email: string; + provider: string; + isActive: boolean; + lastScannedAt: string | null; + nextScanAt: string | null; + totalBlocked: number; + totalScanned: number; + scanInterval: number; + blockRate: number; +}; + +export type DailyStat = { + date: string; + label: string; + count: number; +}; + +export type MailStatusResponse = { + connected: boolean; + accounts: MailAccount[]; + totalBlocked: number; + totalScanned: number; + dailyStats: DailyStat[]; +}; + +export type Plan = 'free' | 'pro' | 'legend'; + +export type UseMailStatusReturn = { + connected: boolean; + accounts: MailAccount[]; + totalBlocked: number; + totalScanned: number; + dailyStats: DailyStat[]; + /** Plan-derived account limit: free=1, pro=3, legend=Infinity */ + maxAccounts: number; + loading: boolean; + error: string | null; + refresh: () => Promise; +}; + +const POLL_INTERVAL_MS = 30_000; + +function deriveMaxAccounts(plan: Plan): number { + if (plan === 'free') return 1; + if (plan === 'pro') return 3; + return Infinity; +} + +/** + * Fetched GET /api/mail/status mit: + * - initialem Fetch on mount + * - 30s-Polling solange App im Vordergrund (AppState === 'active') + * - manuell triggerbar via refresh() + * + * TODO: Ersetze Polling durch IDLE-Realtime-Websocket wenn Phase-10-Backend fertig ist. + */ +export function useMailStatus(plan: Plan): UseMailStatusReturn { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const intervalRef = useRef | null>(null); + const appStateRef = useRef(AppState.currentState); + + const fetchStatus = useCallback(async () => { + try { + const res = await apiFetch('/api/mail/status'); + setData(res); + setError(null); + } catch (e: any) { + console.error('[useMailStatus] fetch failed:', e?.message ?? e); + setError(e?.message ?? 'unknown'); + } finally { + setLoading(false); + } + }, []); + + // Polling starten / stoppen je nach AppState + const startPolling = useCallback(() => { + if (intervalRef.current) return; + intervalRef.current = setInterval(() => { + fetchStatus(); + }, POLL_INTERVAL_MS); + }, [fetchStatus]); + + const stopPolling = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }, []); + + useEffect(() => { + fetchStatus(); + startPolling(); + + const sub = AppState.addEventListener('change', (nextState: AppStateStatus) => { + const wasActive = appStateRef.current === 'active'; + const isNowActive = nextState === 'active'; + appStateRef.current = nextState; + + if (!wasActive && isNowActive) { + // App kommt in Vordergrund — sofort refreshen + Polling neu starten + fetchStatus(); + startPolling(); + } else if (wasActive && !isNowActive) { + // App geht in Hintergrund — Polling stoppen + stopPolling(); + } + }); + + return () => { + stopPolling(); + sub.remove(); + }; + }, [fetchStatus, startPolling, stopPolling]); + + const maxAccounts = deriveMaxAccounts(plan); + + return { + connected: data?.connected ?? false, + accounts: data?.accounts ?? [], + totalBlocked: data?.totalBlocked ?? 0, + totalScanned: data?.totalScanned ?? 0, + dailyStats: data?.dailyStats ?? [], + maxAccounts, + loading, + error, + refresh: fetchStatus, + }; +} diff --git a/apps/rebreak-native/hooks/useMe.ts b/apps/rebreak-native/hooks/useMe.ts new file mode 100644 index 0000000..c04b3c2 --- /dev/null +++ b/apps/rebreak-native/hooks/useMe.ts @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react'; +import { apiFetch } from '../lib/api'; + +export type Plan = 'free' | 'pro' | 'legend'; + +/** + * Single source of truth für den eingeloggten User. /api/auth/me joint + * `auth.users` mit `rebreak.profiles` server-side — wir bekommen alles in + * einem Request: plan, avatar, nickname, streak. + * + * WICHTIG: nicht aus `supabase.auth.getUser().user_metadata` lesen — das + * sind nur die JWT-Claims vom Signup-Zeitpunkt, NICHT der aktuelle Profile- + * Stand (Avatar/Nickname/Plan werden via Profile-Edit-API geupdated, landen + * in der DB, NICHT zurück ins JWT-Claim). + */ +export type Me = { + id: string; + email: string; + username: string; + nickname: string | null; + avatar: string | null; + plan: Plan; + streak: number; + created_at?: string; +}; + +let cachedMe: Me | null = null; + +export function useMe(): { me: Me | null; loading: boolean; reload: () => void } { + const [me, setMe] = useState(cachedMe); + const [loading, setLoading] = useState(cachedMe === null); + const [version, setVersion] = useState(0); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const res = await apiFetch('/api/auth/me'); + if (cancelled) return; + cachedMe = res; + setMe(res); + } catch (e) { + console.warn('[useMe] fetch failed:', e); + } finally { + if (!cancelled) setLoading(false); + } + })(); + return () => { + cancelled = true; + }; + }, [version]); + + return { + me, + loading, + reload: () => { + cachedMe = null; + setLoading(true); + setVersion((v) => v + 1); + }, + }; +} diff --git a/apps/rebreak-native/hooks/useProtectionState.ts b/apps/rebreak-native/hooks/useProtectionState.ts new file mode 100644 index 0000000..23be816 --- /dev/null +++ b/apps/rebreak-native/hooks/useProtectionState.ts @@ -0,0 +1,163 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { AppState, type AppStateStatus } from 'react-native'; +import { + protection, + type ProtectionState, + type ProtectionPhase, + formatCooldownRemaining, +} from '../lib/protection'; + +const POLL_MS_ACTIVE_COOLDOWN = 5_000; +const POLL_MS_NORMAL = 30_000; + +type UseProtectionStateReturn = { + state: ProtectionState | null; + loading: boolean; + error: string | null; + /** Live Countdown-String "23:59:42" während Cooldown läuft. */ + cooldownRemainingFormatted: string; + /** Refetch ohne loading-flicker. */ + refresh: () => Promise; + /** Aktiviert ALLE Layers (legacy, beide Dialoge nacheinander). */ + activate: () => Promise<{ allLayersOn: boolean; missingLayers: string[] }>; + /** Aktiviert NUR den URL-Filter (NEFilter). */ + activateUrlFilter: () => Promise<{ enabled: boolean; error?: string }>; + /** Aktiviert NUR Family Controls (= der Lock — danach nur per Cooldown abschaltbar). */ + activateFamilyControls: () => Promise<{ enabled: boolean; error?: string }>; + /** Startet 24h Cooldown via Backend. UI muss Friction-Flow vorher durchlaufen. */ + requestDeactivation: (reason?: string) => Promise; + /** Bricht laufenden Cooldown ab. Schutz bleibt aktiv. */ + cancelDeactivation: () => Promise; +}; + +/** + * Single-Source-of-Truth-Hook für Protection-State. + * + * - Initial-Fetch on mount + * - Polling: alle 30s normal, 5s während aktivem Cooldown (Live-Countdown) + * - Refresh on AppState 'active' (User kommt aus Background zurück) + * - Layer-Change-Listener vom Native-Modul (Bypass-Detection) + */ +export function useProtectionState(): UseProtectionStateReturn { + const [state, setState] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [tickSeconds, setTickSeconds] = useState(0); + + const pollTimer = useRef | null>(null); + const tickTimer = useRef | null>(null); + + const fetchState = useCallback(async (showLoading = false) => { + if (showLoading) setLoading(true); + try { + const next = await protection.getCombinedState(); + setState(next); + setTickSeconds(next.cooldown.remainingSeconds); + setError(null); + } catch (e: any) { + setError(e?.message ?? 'unknown'); + } finally { + if (showLoading) setLoading(false); + } + }, []); + + // Initial fetch + useEffect(() => { + fetchState(true); + }, [fetchState]); + + // Adaptive poll-rate: 5s während Cooldown, 30s sonst + useEffect(() => { + const interval = state?.cooldown.active ? POLL_MS_ACTIVE_COOLDOWN : POLL_MS_NORMAL; + if (pollTimer.current) clearInterval(pollTimer.current); + pollTimer.current = setInterval(() => fetchState(false), interval); + return () => { + if (pollTimer.current) clearInterval(pollTimer.current); + }; + }, [state?.cooldown.active, fetchState]); + + // Live-Countdown-Tick (nur während Cooldown — 1s-Decrement client-side) + useEffect(() => { + if (!state?.cooldown.active) { + if (tickTimer.current) { + clearInterval(tickTimer.current); + tickTimer.current = null; + } + return; + } + tickTimer.current = setInterval(() => { + setTickSeconds((s) => Math.max(0, s - 1)); + }, 1000); + return () => { + if (tickTimer.current) clearInterval(tickTimer.current); + }; + }, [state?.cooldown.active]); + + // AppState-Listener: Refresh wenn App aus Background zurückkommt. + // KEIN auto-disable hier — Backend's canDisableProtection-Flag ist auch + // in initial-state true, würde sonst den Filter killen ohne dass User + // jemals einen Cooldown gestartet hat. Auto-Disable nur über expliziten + // UI-Pfad nach Cooldown-Ablauf (kommt in Step 5b). + useEffect(() => { + const sub = AppState.addEventListener('change', (status: AppStateStatus) => { + if (status === 'active') { + fetchState(false); + } + }); + return () => sub.remove(); + }, [fetchState]); + + // Native Layer-Change-Listener (User schaltet VPN extern aus etc.) + useEffect(() => { + const sub = protection.addLayerChangeListener(() => fetchState(false)); + return () => sub?.remove(); + }, [fetchState]); + + // ─── Public Actions ──────────────────────────────────────────────── + + const activate = useCallback(async () => { + const result = await protection.activate(); + await fetchState(false); + return result; + }, [fetchState]); + + const activateUrlFilter = useCallback(async () => { + const result = await protection.activateUrlFilter(); + await fetchState(false); + return result; + }, [fetchState]); + + const activateFamilyControls = useCallback(async () => { + const result = await protection.activateFamilyControls(); + await fetchState(false); + return result; + }, [fetchState]); + + const requestDeactivation = useCallback( + async (reason?: string) => { + await protection.requestDeactivation(reason); + await fetchState(false); + }, + [fetchState], + ); + + const cancelDeactivation = useCallback(async () => { + await protection.cancelDeactivation(); + await fetchState(false); + }, [fetchState]); + + return { + state, + loading, + error, + cooldownRemainingFormatted: formatCooldownRemaining(tickSeconds), + refresh: () => fetchState(false), + activate, + activateUrlFilter, + activateFamilyControls, + requestDeactivation, + cancelDeactivation, + }; +} + +export type { ProtectionPhase }; diff --git a/apps/rebreak-native/hooks/useUserPlan.ts b/apps/rebreak-native/hooks/useUserPlan.ts new file mode 100644 index 0000000..3fd1275 --- /dev/null +++ b/apps/rebreak-native/hooks/useUserPlan.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import { apiFetch } from "../lib/api"; + +export type Plan = "free" | "pro" | "legend"; + +type MeResponse = { + id: string; + email: string; + username: string; + plan: Plan; +}; + +let cachedPlan: Plan | null = null; + +/** + * Holt den User-Plan vom Backend (/api/auth/me). + * Plan wird in DB gespeichert (nicht in user_metadata) — daher BFF-Call nötig. + */ +export function useUserPlan(): { plan: Plan; loading: boolean } { + const [plan, setPlan] = useState(cachedPlan ?? "free"); + const [loading, setLoading] = useState(cachedPlan === null); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const res = await apiFetch("/api/auth/me"); + if (cancelled) return; + cachedPlan = res.plan ?? "free"; + setPlan(cachedPlan); + } catch (e) { + console.warn("[useUserPlan] failed:", e); + } finally { + if (!cancelled) setLoading(false); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + return { plan, loading }; +} diff --git a/apps/rebreak-native/install-android.sh b/apps/rebreak-native/install-android.sh new file mode 100755 index 0000000..f78cc88 --- /dev/null +++ b/apps/rebreak-native/install-android.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Rebreak Native — Build debug APK + install on connected Android device. +# +# Usage: +# ./install-android.sh # build + install + launch +# ./install-android.sh --no-build # skip Gradle build, just install last APK +# ./install-android.sh --no-launch # install but don't auto-launch +# +# Multi-Device: +# ANDROID_SERIAL= ./install-android.sh +# +# Wireless-ADB (einmalig): +# adb pair : # PIN vom Phone-Display eingeben +# adb connect : +# +# Phone-Setup: +# Einstellungen → Entwickleroptionen → USB-Debugging an +# Beim ersten Connect: "Diesem Computer vertrauen?" → Erlauben +set -euo pipefail + +cd "$(dirname "$0")" + +PACKAGE="org.rebreak.app" +APK="android/app/build/outputs/apk/debug/app-debug.apk" +SKIP_BUILD=0 +LAUNCH=1 + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-build) SKIP_BUILD=1; shift ;; + --no-launch) LAUNCH=0; shift ;; + -h|--help) + awk '/^#!/{next} /^#/{sub(/^# ?/, ""); print; next} {exit}' "$0" + exit 0 ;; + *) echo "Unbekanntes Flag: $1"; echo " --help für Hilfe"; exit 1 ;; + esac +done + +if ! command -v adb >/dev/null 2>&1; then + echo "adb nicht im PATH. Android Platform-Tools installieren:" + echo " brew install --cask android-platform-tools" + exit 1 +fi + +# Lines that end with literal "device" (excludes "unauthorized", "offline", header). +DEVICE_LINES=$(adb devices | grep -E '[[:space:]]device$' || true) +DEVICE_COUNT=$(printf '%s\n' "$DEVICE_LINES" | grep -c '.' || true) + +if [[ "$DEVICE_COUNT" -eq 0 ]]; then + echo "Kein verfügbares Android-Gerät via ADB." + echo "" + adb devices + echo "" + echo "Mögliche Ursachen:" + echo " - USB nicht angeschlossen / Kabel nur Strom (nicht Daten)" + echo " - USB-Debugging auf dem Phone aus" + echo " - 'Diesem Computer vertrauen?' Dialog noch nicht bestätigt → 'unauthorized'" + echo " - Wireless-ADB nicht verbunden → adb connect :" + exit 1 +fi + +if [[ "$DEVICE_COUNT" -gt 1 && -z "${ANDROID_SERIAL:-}" ]]; then + echo "Mehrere Geräte verbunden:" + adb devices + echo "" + echo "Eines auswählen via ANDROID_SERIAL:" + echo " ANDROID_SERIAL= $0" + exit 1 +fi + +if [[ "$SKIP_BUILD" -eq 0 ]]; then + echo "→ Building debug APK (gradlew assembleDebug)..." + ( cd android && ./gradlew assembleDebug --console=plain ) +fi + +if [[ ! -f "$APK" ]]; then + echo "APK nicht gefunden: $APK" + echo " --no-build weglassen oder Build-Fehler oben prüfen." + exit 1 +fi + +echo "" +echo "→ Installing $APK..." +adb install -r -d "$APK" + +if [[ "$LAUNCH" -eq 1 ]]; then + echo "" + echo "→ Launching $PACKAGE..." + adb shell monkey -p "$PACKAGE" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1 || { + echo "Launch via monkey schlug fehl — App ist installiert, du kannst sie manuell öffnen." + } +fi + +echo "" +echo "Fertig." diff --git a/apps/rebreak-native/install-ios.sh b/apps/rebreak-native/install-ios.sh new file mode 100755 index 0000000..9de5f2c --- /dev/null +++ b/apps/rebreak-native/install-ios.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Rebreak Native — Build standalone iOS Release on connected iPhone. +# Bundle ist eingebettet → läuft OHNE Metro / OHNE WiFi-zum-Mac. +# Backend zeigt auf staging.rebreak.org (siehe extra.apiUrl in app.config.ts). +# +# Usage: +# ./install-ios.sh # Release-Build + Install + Launch +# ./install-ios.sh --debug # Debug-Build (braucht Metro!) — nur fürs Testen +# +# Voraussetzungen (einmalig): +# - Xcode installiert + Apple-ID in Xcode → Settings → Accounts hinzugefügt +# - iPhone via USB angeschlossen, "Diesem Computer vertrauen?" bestätigt +# - In Xcode: Window → Devices and Simulators → iPhone → 'Use for Development' +# - Auf iPhone (nach erstem Install): Einstellungen → Allgemein → VPN & Geräteverwaltung +# → Apple-Dev-Profil "Vertrauen" +# +# Free-Apple-Account-Hinweis: Release-Build läuft 7 Tage, danach muss neu installiert werden. +# Mit Paid Developer Account: 1 Jahr. +set -euo pipefail + +cd "$(dirname "$0")" + +CONFIGURATION="Release" +while [[ $# -gt 0 ]]; do + case "$1" in + --debug) CONFIGURATION="Debug"; shift ;; + -h|--help) awk '/^#!/{next} /^#/{sub(/^# ?/, ""); print; next} {exit}' "$0"; exit 0 ;; + *) echo "Unbekanntes Flag: $1"; exit 1 ;; + esac +done + +if ! command -v xcrun >/dev/null 2>&1; then + echo "Xcode Command-Line-Tools fehlen. Installieren:" + echo " xcode-select --install" + exit 1 +fi + +# Device-Detection via xctrace. +# == Devices == → physisch connected + entsperrt + trusted +# == Devices Offline == → schon mal gepairt, aber gerade nicht erreichbar +# (iPhone gesperrt, abgesteckt, oder Trust-Dialog wartet) +XCTRACE_OUT=$(xcrun xctrace list devices 2>&1) + +ONLINE=$(printf '%s\n' "$XCTRACE_OUT" \ + | awk '/^== Devices ==/{f=1; next} /^== /{f=0} f' \ + | grep -E "iPhone|iPad" || true) + +OFFLINE=$(printf '%s\n' "$XCTRACE_OUT" \ + | awk '/^== Devices Offline ==/{f=1; next} /^== /{f=0} f' \ + | grep -E "iPhone|iPad" || true) + +if [[ -z "$ONLINE" ]]; then + echo "Kein iPhone/iPad ONLINE." + if [[ -n "$OFFLINE" ]]; then + echo "" + echo "Aber diese Geräte sind gepairt aber offline:" + printf '%s\n' "$OFFLINE" | sed 's/^/ /' + echo "" + echo "Häufigste Ursachen:" + echo " 1. iPhone ist gesperrt → entsperren, Mac wartet darauf" + echo " 2. Kabel nur Strom, keine Daten → anderes Kabel probieren" + echo " 3. 'Diesem Computer vertrauen?'-Dialog → bestätigen" + echo " 4. Erst kürzlich angesteckt → 5-10 Sek warten und erneut probieren" + else + echo "" + echo "Setup nötig:" + echo " - iPhone via USB-Kabel anschließen + entsperren" + echo " - 'Diesem Computer vertrauen?' am iPhone bestätigen" + echo " - Xcode öffnen, dort einmalig 'Use for Development' aktivieren" + fi + exit 1 +fi +echo "→ Gerät online: $(printf '%s\n' "$ONLINE" | head -1)" + +echo "→ Building iOS $CONFIGURATION bundle + installing on device..." +echo " (erster Release-Build dauert 5-10 min wegen Pod-Install + Bundle)" +echo "" + +# expo run:ios kümmert sich um Pods + xcodebuild + Code-Signing + Install + Launch. +# --device wählt ein USB-Gerät statt Simulator. +# --configuration Release embeddet das JS-Bundle → keine Metro-Verbindung nötig. +npx expo run:ios --device --configuration "$CONFIGURATION" + +echo "" +echo "Fertig — App läuft jetzt standalone auf deinem iPhone." +if [[ "$CONFIGURATION" == "Release" ]]; then + echo "Backend: https://staging.rebreak.org (siehe extra.apiUrl in app.config.ts)" + echo "Free-Account: 7 Tage gültig, danach Skript erneut laufen lassen." +fi diff --git a/apps/rebreak-native/lib/api.ts b/apps/rebreak-native/lib/api.ts new file mode 100644 index 0000000..0776a4d --- /dev/null +++ b/apps/rebreak-native/lib/api.ts @@ -0,0 +1,49 @@ +import Constants from 'expo-constants'; +import { supabase } from './supabase'; + +const apiUrl = Constants.expoConfig?.extra?.apiUrl as string; + +type FetchOptions = Omit & { + body?: any; +}; + +/** + * Wrapper für Backend-API-Calls mit automatischem Auth-Token. + * Pendant zum Nuxt-`useSafeFetch` aus apps/rebreak/. + * + * Backend antwortet mit { success, data, status } — wir entpacken `data`. + */ +export async function apiFetch( + path: string, + options: FetchOptions = {} +): Promise { + const session = (await supabase.auth.getSession()).data.session; + + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record), + }; + + if (session?.access_token) { + headers.Authorization = `Bearer ${session.access_token}`; + } + + const res = await fetch(`${apiUrl}${path}`, { + ...options, + headers, + body: options.body ? JSON.stringify(options.body) : undefined, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`API ${res.status}: ${text}`); + } + + const json = await res.json(); + + // Unwrap { success, data, status } — siehe useSafeFetch-Pattern in der Vue-App + if (json && typeof json === 'object' && 'success' in json && 'data' in json) { + return json.data as T; + } + return json as T; +} diff --git a/apps/rebreak-native/lib/avatars.ts b/apps/rebreak-native/lib/avatars.ts new file mode 100644 index 0000000..9caf10d --- /dev/null +++ b/apps/rebreak-native/lib/avatars.ts @@ -0,0 +1,31 @@ +export interface HeroAvatar { + id: string; + name: string; + color: string; // NativeWind border color class + url: string; +} + +const DICEBEAR_BASE = 'https://api.dicebear.com/9.x/adventurer/svg'; + +export const HERO_AVATARS: HeroAvatar[] = [ + { id: 'spider', name: 'Spider', color: 'border-red-500', url: `${DICEBEAR_BASE}?seed=spiderman&backgroundColor=b71c1c` }, + { id: 'hulk', name: 'Hulk', color: 'border-green-500', url: `${DICEBEAR_BASE}?seed=hulk&backgroundColor=1b5e20` }, + { id: 'iron', name: 'Iron', color: 'border-yellow-500', url: `${DICEBEAR_BASE}?seed=ironman&backgroundColor=e65100` }, + { id: 'cap', name: 'Captain', color: 'border-blue-500', url: `${DICEBEAR_BASE}?seed=captain&backgroundColor=0d47a1` }, + { id: 'storm', name: 'Storm', color: 'border-purple-500', url: `${DICEBEAR_BASE}?seed=storm&backgroundColor=4a148c` }, + { id: 'wolf', name: 'Wolf', color: 'border-gray-400', url: `${DICEBEAR_BASE}?seed=wolverine&backgroundColor=37474f` }, + { id: 'flash', name: 'Flash', color: 'border-red-400', url: `${DICEBEAR_BASE}?seed=flash&backgroundColor=c62828` }, + { id: 'panther', name: 'Panther', color: 'border-indigo-500', url: `${DICEBEAR_BASE}?seed=panther&backgroundColor=1a237e` }, + { id: 'phoenix', name: 'Phoenix', color: 'border-orange-500', url: `${DICEBEAR_BASE}?seed=phoenix&backgroundColor=bf360c` }, + { id: 'frost', name: 'Frost', color: 'border-cyan-400', url: `${DICEBEAR_BASE}?seed=frost&backgroundColor=006064` }, + { id: 'shadow', name: 'Shadow', color: 'border-gray-600', url: `${DICEBEAR_BASE}?seed=shadow&backgroundColor=212121` }, + { id: 'nova', name: 'Nova', color: 'border-pink-500', url: `${DICEBEAR_BASE}?seed=nova&backgroundColor=880e4f` }, +]; + +export function getAvatarById(id: string): HeroAvatar | undefined { + return HERO_AVATARS.find((a) => a.id === id); +} + +export function getAvatarUrl(id: string): string { + return getAvatarById(id)?.url ?? `${DICEBEAR_BASE}?seed=anonym&backgroundColor=374151`; +} diff --git a/apps/rebreak-native/lib/formatTime.ts b/apps/rebreak-native/lib/formatTime.ts new file mode 100644 index 0000000..2494b65 --- /dev/null +++ b/apps/rebreak-native/lib/formatTime.ts @@ -0,0 +1,7 @@ +export function formatRelativeTime(ts: string): string { + const diff = Date.now() - new Date(ts).getTime(); + if (diff < 60_000) return 'gerade eben'; + if (diff < 3_600_000) return `vor ${Math.floor(diff / 60_000)}m`; + if (diff < 86_400_000) return `vor ${Math.floor(diff / 3_600_000)}h`; + return new Date(ts).toLocaleDateString('de-DE'); +} diff --git a/apps/rebreak-native/lib/i18n.ts b/apps/rebreak-native/lib/i18n.ts new file mode 100644 index 0000000..13ab397 --- /dev/null +++ b/apps/rebreak-native/lib/i18n.ts @@ -0,0 +1,26 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import * as Localization from 'expo-localization'; +import de from '../locales/de.json'; +import en from '../locales/en.json'; + +const deviceLocale = Localization.getLocales()[0]?.languageCode ?? 'en'; + +i18n.use(initReactI18next).init({ + resources: { + de: { translation: de }, + en: { translation: en }, + }, + lng: deviceLocale === 'de' ? 'de' : 'en', + fallbackLng: 'en', + // RN hat kein eingebautes Intl.PluralRules — v3 funktioniert nativ ohne Polyfill + compatibilityJSON: 'v3', + interpolation: { + escapeValue: false, + // Locale-Dateien verwenden Vue-i18n-Style %{var} (1:1 portiert aus der Nuxt-App). + prefix: '%{', + suffix: '}', + }, +}); + +export default i18n; diff --git a/apps/rebreak-native/lib/lyraResponse.ts b/apps/rebreak-native/lib/lyraResponse.ts new file mode 100644 index 0000000..5c4f2cf --- /dev/null +++ b/apps/rebreak-native/lib/lyraResponse.ts @@ -0,0 +1,61 @@ +// Parser für Lyras LLM-JSON-Antworten + Emotion-Detection. +import type { Emotion as LyraEmotion } from '../components/RiveAvatar'; +import { EMPATHY_RE, HAPPY_RE } from './sosConstants'; + +export type { LyraEmotion }; +export type ChipSpec = { label: string; action: string }; + +// Parse LLM JSON response — robust gegen Markdown-Fences UND abgeschnittenes JSON +export function parseLyraResponse(raw: string): { message: string; chips: ChipSpec[] } { + if (!raw) return { message: '', chips: [] }; + // Strip ALL markdown fences (auch wenn nur am Anfang) + const text = raw.trim() + .replace(/^```(?:json)?\s*/i, '') + .replace(/```\s*$/i, '') + .trim(); + const start = text.indexOf('{'); + if (start === -1) return { message: raw.trim(), chips: [] }; + // Erst echtes JSON-Parse versuchen (komplett) + const end = text.lastIndexOf('}'); + if (end > start) { + try { + const obj = JSON.parse(text.slice(start, end + 1)); + const message = typeof obj.message === 'string' ? obj.message.trim() : ''; + const chipsRaw = Array.isArray(obj.chips) ? obj.chips : []; + const chips: ChipSpec[] = chipsRaw + .filter((c: { label?: unknown; action?: unknown }) => c && typeof c.label === 'string' && typeof c.action === 'string') + .slice(0, 5) + .map((c: { label: string; action: string }) => ({ label: c.label.trim(), action: c.action.trim() })); + if (message) return { message, chips }; + } catch {/* fall through to recovery */} + } + // RECOVERY: JSON ist abgeschnitten (z.B. max_tokens hit). + // 1) message-Feld per Regex extrahieren + const msgMatch = text.match(/"message"\s*:\s*"((?:[^"\\]|\\.)*)"/); + let message = ''; + if (msgMatch) { + try { message = JSON.parse('"' + msgMatch[1] + '"'); } catch { message = msgMatch[1]; } + } + // 2) Chips: alle vollständigen {label,action}-Objekte einsammeln + const chips: ChipSpec[] = []; + const chipRe = /\{\s*"label"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"action"\s*:\s*"((?:[^"\\]|\\.)*)"\s*\}/g; + let m: RegExpExecArray | null; + while ((m = chipRe.exec(text)) !== null && chips.length < 5) { + try { + chips.push({ + label: JSON.parse('"' + m[1] + '"').trim(), + action: JSON.parse('"' + m[2] + '"').trim(), + }); + } catch { + chips.push({ label: m[1].trim(), action: m[2].trim() }); + } + } + if (message) return { message, chips }; + return { message: raw.trim(), chips: [] }; +} + +export function detectEmotion(text: string): LyraEmotion { + if (HAPPY_RE.test(text)) return 'happy'; + if (EMPATHY_RE.test(text)) return 'empathy'; + return 'idle'; +} diff --git a/apps/rebreak-native/lib/protection.ts b/apps/rebreak-native/lib/protection.ts new file mode 100644 index 0000000..81a57d0 --- /dev/null +++ b/apps/rebreak-native/lib/protection.ts @@ -0,0 +1,270 @@ +/** + * Protection orchestration layer (JS-side). + * + * Verbindet das native rebreak-protection-Modul (Device-Layer-State) mit + * dem Backend-Cooldown-API (`/api/cooldown/*` + `/api/protection/state`). + * + * Cooldown ist Backend-driven (JWT mit `cooldown_ends_at`-Claim, server-time + * = single source of truth gegen lokale-Uhr-Manipulation). Native-Modul + * kümmert sich nur um echten Device-State (NEFilter, Family Controls etc.). + */ +import { Platform } from "react-native"; +import RebreakProtection from "../modules/rebreak-protection"; +import type { + ActivateResult, + DeviceLayers, + HealthProbeOpts, + HealthProbeResult, + SyncBlocklistOpts, + SyncBlocklistResult, + SystemSettingsTarget, +} from "../modules/rebreak-protection"; +import { apiFetch } from "./api"; + +// ─── Public Types ────────────────────────────────────────────────────────── + +export type ProtectionPhase = + | "inactive" + | "activating" + | "active" + | "cooldownPending" + | "cooldownActive" + | "recoveringFromBypass"; + +export type CooldownState = { + active: boolean; + endsAt: string | null; + remainingSeconds: number; + reason: string | null; +}; + +export type ProtectionState = { + phase: ProtectionPhase; + layers: DeviceLayers; + cooldown: CooldownState; + blocklistCount: number; + plan: "free" | "pro" | "legend"; +}; + +// ─── Backend Response-Types ──────────────────────────────────────────────── + +type BackendCooldownStatus = { + active: boolean; + remainingSeconds: number; + cooldownEndsAt: string | null; + token: string | null; + canDisableProtection: boolean; + reason?: string; +}; + +// Matches actual response from `apps/rebreak/server/api/protection/state.get.ts` +// (apiFetch unwrapt das `data`-Feld bereits, daher hier nur die Inner-Shape). +type BackendProtectionState = { + protectionShouldBeActive: boolean; + cooldown: { + active: boolean; + remainingSeconds: number; + cooldownEndsAt: string | null; + }; + plan: "free" | "pro" | "legend"; +}; + +// ─── Public API ──────────────────────────────────────────────────────────── + +export const protection = { + // ─── Native-Calls (Device-Layer) ───────────────────────────────────────── + + activate(): Promise { + return RebreakProtection.activate(); + }, + + async activateUrlFilter(): Promise<{ enabled: boolean; error?: string }> { + if (Platform.OS === "android") { + // Android Layer-1 = VpnService (DNS-Filter). iOS-API erwartet hier + // {enabled, error?}, also Native-`activate()`-Result re-shapen. + const res = await RebreakProtection.activate(); + const enabled = !res.missingLayers.includes("vpn"); + return enabled ? { enabled: true } : { enabled: false, error: res.errors?.[0] }; + } + return RebreakProtection.activateUrlFilter(); + }, + + async activateFamilyControls(): Promise<{ enabled: boolean; error?: string }> { + if (Platform.OS === "android") { + // Android Layer-2 = AccessibilityService (Browser-URL-Filter) + Tamper-Lock. + // Two-step UX: + // (1) A11y nicht aktiv → Settings öffnen, return {enabled:false} mit + // Marker-Error. UI fragt nach Return den State neu ab und tappt + // erneut auf "App lock" → wir landen in Step (2). + // (2) A11y aktiv → tamperLock armen → return {enabled:true}. + const a11y = await RebreakProtection.isAccessibilityEnabled(); + if (!a11y.enabled) { + await RebreakProtection.openAccessibilitySettings(); + return { enabled: false, error: "accessibility_pending" }; + } + try { + await RebreakProtection.armTamperLock(); + return { enabled: true }; + } catch (e: any) { + return { + enabled: false, + error: e?.message ?? "tamper_lock_failed", + }; + } + } + return RebreakProtection.activateFamilyControls(); + }, + + /** Schaltet alle Layer ab. NUR aufrufen wenn JS-Layer Cooldown verifiziert. */ + forceDisable() { + return RebreakProtection.disable(); + }, + + getDeviceState(): Promise { + return RebreakProtection.getDeviceState(); + }, + + syncBlocklist(opts: SyncBlocklistOpts): Promise { + return RebreakProtection.syncBlocklist(opts); + }, + + runHealthProbe(opts?: HealthProbeOpts): Promise { + return RebreakProtection.runHealthProbe(opts); + }, + + openSystemSettings(target?: SystemSettingsTarget): Promise { + return RebreakProtection.openSystemSettings(target); + }, + + addLayerChangeListener(cb: (layers: DeviceLayers) => void) { + return RebreakProtection.addListener("onLayerChange", cb); + }, + + // ─── Backend-Cooldown ──────────────────────────────────────────────────── + + /** Startet 24h Cooldown. Schutz BLEIBT aktiv, kann erst nach Ablauf disabled werden. */ + async requestDeactivation( + reason?: string, + ): Promise<{ cooldownEndsAt: string }> { + const res = await apiFetch<{ + cooldownEndsAt: string; + token: string; + remainingSeconds: number; + }>("/api/cooldown/request", { method: "POST", body: { reason } }); + return { cooldownEndsAt: res.cooldownEndsAt }; + }, + + /** Bricht laufenden Cooldown ab. Schutz BLEIBT aktiv. */ + async cancelDeactivation(): Promise<{ cancelled: boolean }> { + const res = await apiFetch<{ cancelled: boolean }>("/api/cooldown/cancel", { + method: "POST", + body: {}, + }); + return res; + }, + + async getCooldownStatus(): Promise { + try { + const res = await apiFetch("/api/cooldown/status"); + return { + active: res.active, + endsAt: res.cooldownEndsAt, + remainingSeconds: res.remainingSeconds, + reason: res.reason ?? null, + }; + } catch { + // Offline / Backend down → konservativ: kein Cooldown angenommen + return { active: false, endsAt: null, remainingSeconds: 0, reason: null }; + } + }, + + async getBackendProtectionState(): Promise { + try { + return await apiFetch("/api/protection/state"); + } catch { + return null; + } + }, + + // ─── Combined State (für UI) ───────────────────────────────────────────── + + /** + * Holt nativen Device-State + Backend-Cooldown parallel und merged. + * Phase-Berechnung folgt der State-Machine im Plan. + */ + async getCombinedState(): Promise { + const [layers, cooldown, backend] = await Promise.all([ + this.getDeviceState(), + this.getCooldownStatus(), + this.getBackendProtectionState(), + ]); + + const allLayersOn = isAllLayersOn(layers); + const iosLockActive = + layers.appDeletionLock ?? layers.familyControls ?? false; + const phase: ProtectionPhase = cooldown.active + ? "cooldownActive" + : backend?.protectionShouldBeActive === true && + layers.urlFilter === true && + iosLockActive !== true + ? "recoveringFromBypass" + : allLayersOn + ? "active" + : "inactive"; + + return { + phase, + layers, + cooldown, + blocklistCount: layers.blocklistCount, + plan: backend?.plan ?? "free", + }; + }, + + /** + * Wenn ein Cooldown TATSÄCHLICH gelaufen ist und jetzt elapsed → native disable. + * + * Defensiv: prüft `cooldownEndsAt` (heißt es gab einen Cooldown) UND + * `remainingSeconds <= 0` (heißt er ist abgelaufen) UND `canDisableProtection`. + * Backend kann `canDisableProtection: true` auch im initial-state geben; + * der `cooldownEndsAt`-Check verhindert dann False-Positives. + */ + async applyCooldownDisableIfElapsed(): Promise { + const status = await apiFetch( + "/api/cooldown/status", + ).catch(() => null); + if (!status) return false; + if (!status.canDisableProtection) return false; + if (!status.cooldownEndsAt) return false; // nie ein Cooldown gewesen + if (status.remainingSeconds > 0) return false; // Cooldown noch nicht abgelaufen + await this.forceDisable(); + return true; + }, +}; + +// ─── Helpers ─────────────────────────────────────────────────────────────── + +export function isAllLayersOn(layers: DeviceLayers): boolean { + // iOS: urlFilter + appDeletionLock (fallback: familyControls für ältere Builds). + // Android: vpn + accessibility (+ tamperLock optional). + if ( + layers.urlFilter !== undefined || + layers.familyControls !== undefined || + layers.appDeletionLock !== undefined + ) { + const lockLayer = layers.appDeletionLock ?? layers.familyControls; + return layers.urlFilter === true && lockLayer === true; + } + if (layers.vpn !== undefined || layers.accessibility !== undefined) { + return layers.vpn === true && layers.accessibility === true; + } + return false; +} + +export function formatCooldownRemaining(seconds: number): string { + if (seconds <= 0) return "00:00:00"; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return [h, m, s].map((n) => String(n).padStart(2, "0")).join(":"); +} diff --git a/apps/rebreak-native/lib/resolveAvatar.ts b/apps/rebreak-native/lib/resolveAvatar.ts new file mode 100644 index 0000000..4b044b8 --- /dev/null +++ b/apps/rebreak-native/lib/resolveAvatar.ts @@ -0,0 +1,29 @@ +import { getAvatarById, getAvatarUrl } from './avatars'; + +const DICEBEAR_BASE = 'https://api.dicebear.com/9.x/adventurer/svg'; + +/** + * Resolves the `profiles.avatar` field zu einer renderbaren URL. + * + * Drei Quellen-Formate werden unterstützt: + * 1. Hero-Avatar-ID (z.B. "spider", "hulk") → DiceBear-URL aus HERO_AVATARS + * 2. Custom-Photo-URL (https://... — User hat Foto via Profile-Edit + * hochgeladen, gespeichert in Supabase-Storage) → unverändert durchreichen + * 3. Leer / unbekannt → Dicebear-Initials-Fallback per nickname + * + * Wichtig: NICHT blindly getAvatarUrl(avatarId) aufrufen — das gab vorher den + * Dicebear-anonym-Fallback zurück wenn avatarId zwar truthy aber kein Hero + * (z.B. Foto-URL). Jetzt wird zuerst auf URL geprüft. + */ +export function resolveAvatar(avatarId: string | null | undefined, nickname: string): string { + if (avatarId) { + if (/^https?:\/\//i.test(avatarId)) { + return avatarId; + } + if (getAvatarById(avatarId)) { + return getAvatarUrl(avatarId); + } + } + const seed = encodeURIComponent(nickname || 'anonym'); + return `${DICEBEAR_BASE}?seed=${seed}&backgroundColor=374151`; +} diff --git a/apps/rebreak-native/lib/sosConstants.ts b/apps/rebreak-native/lib/sosConstants.ts new file mode 100644 index 0000000..99d3a4f --- /dev/null +++ b/apps/rebreak-native/lib/sosConstants.ts @@ -0,0 +1,56 @@ +// Konstanten für den SOS-Screen: Chip-Sets, Atemphasen, Emotion-Regex. + +export type ChipSet = 'start' | 'help' | 'after_breathing' | 'after_game' | 'overcome_check' | 'overcome_done' | 'none'; +type Chip = { label: string; action: string }; + +export const CHIP_SETS: Record = { + start: [ + { label: '😤 Wütend', action: 'feel:Ich bin gerade sehr wütend.' }, + { label: '😰 Ängstlich', action: 'feel:Ich bin ängstlich und nervös.' }, + { label: '😔 Traurig', action: 'feel:Ich bin traurig.' }, + { label: '😤 Gestresst', action: 'feel:Ich bin gerade gestresst.' }, + { label: '😶 Leer', action: 'feel:Ich fühle mich innerlich leer.' }, + { label: '🤔 Etwas anderes...', action: 'need_help' }, + ], + help: [ + { label: '🫁 Atemübung', action: 'breathing' }, + { label: '🎮 Spiel starten', action: 'game_picker' }, + ], + after_breathing: [ + { label: '😌 Besser', action: 'send_text:Ich fühle mich nach der Atemübung besser.' }, + { label: '🔄 Nochmal', action: 'breathing' }, + { label: '🎮 Spiel', action: 'game_picker' }, + { label: '❤️ Überwunden', action: 'overcome' }, + ], + after_game: [ + { label: '😌 Ruhiger', action: 'send_text:Das Spiel hat geholfen, ich bin ruhiger.' }, + { label: '🔄 Nochmal', action: 'game_picker' }, + { label: '❤️ Überwunden', action: 'overcome' }, + ], + overcome_check: [ + { label: '✅ Ja, ich habe es geschafft', action: 'overcome' }, + { label: '💪 Noch nicht ganz', action: 'send_text:Der Spielimpuls ist noch da, ich brauche noch Hilfe.' }, + ], + overcome_done: [ + { label: '✨ Erfolg teilen', action: 'share_success' }, + { label: '⭐ Diese Session bewerten', action: 'rate_session' }, + { label: '📊 Meine Statistik', action: 'show_stats' }, + { label: '✅ Fertig', action: 'close' }, + ], + none: [], +}; + +// ── Breathing guide ────────────────────────────────────────────────────────── +export type BreathPhase = 'inhale' | 'hold' | 'exhale'; +export type BreathState = 'idle' | 'countdown' | 'active'; +// speakLine bewusst durchgehend null — Phase-TTS würde Lyras laufende Audio abbrechen +// (User-Wahrnehmung: "Stimme ändert sich"). Visuelles Pulsieren + Countdown reicht. +export const BREATH_PHASES: { phase: BreathPhase; duration: number; label: string; color: string; speakLine: string | null }[] = [ + { phase: 'inhale', duration: 4, label: 'Einatmen', color: '#6366f1', speakLine: null }, + { phase: 'hold', duration: 7, label: 'Halten', color: '#f97316', speakLine: null }, + { phase: 'exhale', duration: 8, label: 'Ausatmen', color: '#16a34a', speakLine: null }, +]; +export const TOTAL_ROUNDS = 3; + +export const EMPATHY_RE = /schwer|rückfall|traurig|schlimm|hoffnungslos|verloren|scham|schuld|verzweifelt/i; +export const HAPPY_RE = /toll|super|geschafft|stark|stolz|fantastisch|prima/i; diff --git a/apps/rebreak-native/lib/sosPrompts.ts b/apps/rebreak-native/lib/sosPrompts.ts new file mode 100644 index 0000000..1663712 --- /dev/null +++ b/apps/rebreak-native/lib/sosPrompts.ts @@ -0,0 +1,36 @@ +// SOS-Modus System-Prompt — wird als erste user-message in den LLM-Kontext gelegt. +// Definiert Lyras Rolle + Gesprächsführung. Antwort-FORMAT wird vom Server +// festgelegt (siehe `apps/rebreak/server/api/coach/sos-stream.get.ts` +// SOS_INSTRUCTION) — Prosa + EINE Schluss-Zeile mit `[[CHIPS]]:[...]` Marker. +// Wir wiederholen das Format-Spec hier NICHT — das hatte vorher zu konfliktierenden +// Anweisungen geführt (LLM dumpte zusätzlich JSON-Wrapper ans Ende). +export const SOS_BOOT = [{ + role: 'user' as const, + content: + '[SOS-MODUS — SEHR WICHTIG]\n\n' + + 'Ich habe gerade die Notfallhilfe geöffnet, weil ich einen akuten Spielimpuls spüre.\n\n' + + 'DEINE ROLLE:\n' + + '- Du bist Lyra, meine ruhige, warme Begleitung in dieser Krise.\n' + + '- DEIN ZIEL: Impuls runterfahren → Trigger/Ursache erkennen → spätestens nach 2-3 Fragen Atemübung oder Spiel anbieten.\n' + + '- Sprich kurz (max. 2-3 Sätze), menschlich, kein Therapeut-Sprech, kein Druck.\n' + + '- Zeige Mitgefühl. Spiegele, was ich sage. Zwinge mich nicht.\n' + + '\n' + + 'GESPRÄCHSFÜHRUNG (PFLICHT):\n' + + '- KURZ: 1-2 Sätze, max 3. Mehr ist verboten.\n' + + '- JEDE Antwort endet mit einer konkreten Frage ODER einem klaren Angebot. NIE offen lassen.\n' + + '- JEDE Antwort liefert 2-4 Chips als Antwort-Optionen. NIE ohne Chips.\n' + + '- Spätestens nach meiner 3. Antwort: biete Atemübung ODER Spiel an (Chips: "🫁 Atemübung" + "🎮 Spiel" + "💬 Weiter reden").\n' + + '- Frag NIE "Magst du mir mehr erzählen?" ohne Chips — immer 2-3 Antworten als Chips anbieten.\n' + + '- ABSOLUT KRITISCH zur SPRACHE: NIEMALS Chip-Optionen im Prosa-Text auflisten oder paraphrasieren.\n' + + ' Der Prosa-Text wird VORGELESEN (TTS) → Chip-Aufzählung klingt unnatürlich.\n' + + ' ✗ FALSCH: "Magst du atmen oder lieber spielen?"\n' + + ' ✗ FALSCH: "Du kannst eine Atemübung oder ein Spiel machen."\n' + + ' ✓ RICHTIG: "Magst du was probieren?"\n' + + ' ✓ RICHTIG: "Was hilft dir gerade?"\n' + + '\n' + + 'CHIPS-INHALTE (Beispiele, nicht Format — Format ist im System-Prompt definiert):\n' + + '- Erste Antworten (Trigger erkunden): "💬 Ja, lass uns reden", "😶 Lieber nicht", "💥 Es war Stress", "🌙 Es war Einsamkeit"\n' + + '- Nach 2-3 Fragen (Angebot): "🫁 Atemübung", "🎮 Spiel", "💬 Weiter reden"\n' + + '- Nach Atmen/Spiel: "😌 Besser", "🔄 Nochmal", "❤️ Überwunden"\n\n' + + 'Jetzt los — erste Begrüßung mit warmer Frage + 3-4 Gefühls-Chips zum Erkunden. Antwort-Format folgt der System-Prompt-Spec, nicht meiner Beschreibung hier.', +}]; diff --git a/apps/rebreak-native/lib/sosStream.ts b/apps/rebreak-native/lib/sosStream.ts new file mode 100644 index 0000000..329b29b --- /dev/null +++ b/apps/rebreak-native/lib/sosStream.ts @@ -0,0 +1,141 @@ +// SSE Streaming Helper für Lyras SOS-Antworten. +// +// Architektur (bewusst entkoppelt): +// - Step 1: POST /api/coach/sos-session → liefert sessionId +// - Step 2: EventSource auf /api/coach/sos-stream?session= +// - Events: 'message' (chunk), 'chips' (parsed), 'done', 'error' +// +// Phase B (sentence-level TTS): zusätzlich `onSentence?` Callback. Wenn gesetzt, +// feuert er sobald ein vollständiger Satz erkannt wird (live während des +// Streams) + einmal am Stream-Ende für den Tail. Aufrufer kann den Satz dann +// direkt in eine TTS-Queue schieben → erste Audio-Wiedergabe ~3s früher als +// "warten bis fullText fertig". +import EventSource from 'react-native-sse'; + +type SseEvents = 'message' | 'chips' | 'done'; + +export type StreamSosLyraOpts = { + apiBase: string; + token: string; + messages: Array<{ role: 'user' | 'assistant'; content: string }>; + locale: string; + onTextUpdate: (full: string) => void; + onChips: (chips: Array<{ label: string; action: string }>) => void; + /** Phase B: feuert pro fertigem Satz live während des Streams + Tail beim + * done. Bei aktivem TTS-Streaming sollte der Aufrufer hier seine Queue + * füllen statt im onDone den ganzen Text zu sprechen. */ + onSentence?: (sentence: string) => void; + onDone: (full: string) => void; + onError: (err: unknown) => void; +}; + +// Min-Länge für sentence-level TTS — winzige "Hm." / "Ja." kommen mit dem +// nächsten Satz mit, sonst klingt's choppy. +const MIN_SENTENCE_CHARS = 8; + +/** + * Findet vollständige Sätze im Text. Ein Satz endet bei `[.!?]` GEFOLGT VON + * Whitespace + Großbuchstaben (oder Zitat-Anfang). Das filtert die häufigen + * deutschen Abkürzungen "z.B. einfach", "d.h. nichts" etc. ohne explizite + * Abkürzungs-Liste — der nächste Char ist dann meistens lowercase. + * + * Returns: { sentences[], consumed } — wieviele chars vom Anfang von `text` + * bereits in `sentences` enthalten sind (inkl. trailing whitespace). + */ +function consumeCompletedSentences(text: string): { sentences: string[]; consumed: number } { + const sentences: string[] = []; + const re = /[.!?](?=\s+[A-ZÄÖÜ"„])/g; + let lastEnd = 0; + let m: RegExpExecArray | null; + while ((m = re.exec(text)) !== null) { + const endOfSentence = m.index + 1; + sentences.push(text.slice(lastEnd, endOfSentence)); + const wsMatch = text.slice(endOfSentence).match(/^\s+/); + lastEnd = endOfSentence + (wsMatch ? wsMatch[0].length : 0); + re.lastIndex = lastEnd; + } + return { sentences, consumed: lastEnd }; +} + +export async function streamSosLyra(opts: StreamSosLyraOpts): Promise<() => void> { + // Step 1: POST zu /api/coach/sos-session → sessionId holen + const sessRes = await fetch(`${opts.apiBase}/api/coach/sos-session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${opts.token}`, + }, + body: JSON.stringify({ messages: opts.messages, locale: opts.locale }), + }); + if (!sessRes.ok) throw new Error(`session: ${sessRes.status}`); + const { sessionId } = await sessRes.json(); + + // Step 2: EventSource für SSE-Stream + // pollingInterval: 0 → KEIN Auto-Reconnect (Session ist one-time-use) + const es = new EventSource(`${opts.apiBase}/api/coach/sos-stream?session=${sessionId}`, { + headers: { Authorization: `Bearer ${opts.token}` }, + pollingInterval: 0, + lineEndingCharacter: '\n', + }); + + let fullText = ''; + let sentenceConsumedIndex = 0; + + const flushNewSentences = () => { + if (!opts.onSentence) return; + const remaining = fullText.slice(sentenceConsumedIndex); + const { sentences, consumed } = consumeCompletedSentences(remaining); + for (const s of sentences) { + const trimmed = s.trim(); + if (trimmed.length >= MIN_SENTENCE_CHARS) { + opts.onSentence(trimmed); + } + } + sentenceConsumedIndex += consumed; + }; + + es.addEventListener('message', (event) => { + if (!event.data) return; + // Backend sendet JSON-encoded String → parse für korrekte Whitespace-Behandlung + let chunk: string; + try { + chunk = JSON.parse(event.data); + } catch { + chunk = event.data; + } + if (!chunk) return; + fullText += chunk; + opts.onTextUpdate(fullText); + // Phase B: live sentence-detection für TTS-Queue + flushNewSentences(); + }); + + es.addEventListener('chips', (event) => { + if (!event.data) return; + try { + const chips = JSON.parse(event.data); + if (Array.isArray(chips)) opts.onChips(chips); + } catch { /* ignore */ } + }); + + es.addEventListener('done', () => { + // Phase B: Tail flushen (letzter Satz ohne folgendes Capital-Letter wird + // sonst nie als "complete" erkannt). Trim leeren Tail away. + if (opts.onSentence) { + const tail = fullText.slice(sentenceConsumedIndex).trim(); + if (tail.length > 0) { + opts.onSentence(tail); + } + } + opts.onDone(fullText); + es.close(); + }); + + es.addEventListener('error', (err) => { + opts.onError(err); + es.close(); + }); + + // Return cancel-Funktion + return () => { es.close(); }; +} diff --git a/apps/rebreak-native/lib/sosTtsQueue.ts b/apps/rebreak-native/lib/sosTtsQueue.ts new file mode 100644 index 0000000..0ee06fd --- /dev/null +++ b/apps/rebreak-native/lib/sosTtsQueue.ts @@ -0,0 +1,209 @@ +// Sentence-Level TTS Queue für SOS-Streaming. +// +// Aufrufer (urge.tsx) erstellt eine neue Queue pro sendToLyra-Call und füttert +// sie via `enqueue(sentence)` aus dem `onSentence`-Callback von streamSosLyra. +// Die Queue fetched + spielt sequenziell — wenn n+1 reinkommt während n noch +// spielt, wartet der Fetch bis n's Audio durch ist (kein doppeltes Sprechen). +// +// Lifecycle: +// - new SosTtsQueue({...}) → bereit, nichts spielt +// - enqueue(s1) → fetch + play s1 +// - enqueue(s2) während s1 spielt → s2 wartet in queue, fetch+play sobald s1 fertig +// - abort() → in-flight fetch cancelled, current sound stopped+unloaded, queue cleared +// +// State-Reporting via Callbacks: onStart (erster Satz beginnt zu spielen), +// onIdle (Queue komplett durch + nichts mehr spielt). UI-Layer kann darauf +// `setIsSpeaking` triggern. +import { Audio } from 'expo-av'; +import * as FileSystem from 'expo-file-system'; + +export type SosTtsFetchOpts = { + apiBase: string; + accessToken: string; + locale: string; + /** Server-Pfad zum TTS-Endpoint, default: OpenAI. Erlaubt A/B zwischen + * /api/coach/speak-openai, /api/coach/speak-gemini, /api/coach/speak-google. */ + endpoint?: string; +}; + +export type SosTtsQueueOpts = SosTtsFetchOpts & { + /** Erster Satz beginnt zu spielen. */ + onStart?: () => void; + /** Queue ist leer + nichts spielt mehr. */ + onIdle?: () => void; + /** Single-sentence-fetch oder -playback ist gescheitert. Queue läuft weiter. */ + onError?: (err: unknown, sentence: string) => void; +}; + +const EMOJI_RE = /[\p{Extended_Pictographic}\p{Emoji_Component}]/gu; + +function cleanForTts(text: string): string { + return text.replace(EMOJI_RE, '').replace(/\s+/g, ' ').trim(); +} + +export type SosTtsMode = 'sos' | 'sos-continuation'; + +type QueueItem = { + text: string; + mode: SosTtsMode; + controller: AbortController; + /** Pre-fetch starts beim enqueue → wenn play dran ist, ist Audio meist schon + * fertig oder fast fertig. Eliminiert Gap zwischen Items im Hybrid-Mode. */ + audioPromise: Promise<{ uri: string } | null>; +}; + +export class SosTtsQueue { + private queue: QueueItem[] = []; + private playing = false; + private currentSound: Audio.Sound | null = null; + private aborted = false; + private startedOnce = false; + private opts: SosTtsQueueOpts; + + constructor(opts: SosTtsQueueOpts) { + this.opts = opts; + } + + /** + * Enqueue a text segment for TTS playback. + * @param mode Default 'sos' (warm-empathic-opening). Use 'sos-continuation' + * für Folge-Blöcke im Hybrid-Mode → server passt OpenAI's + * `instructions`-Feld an damit der Voice-Boundary weicher klingt. + */ + enqueue(sentence: string, mode: SosTtsMode = 'sos'): void { + if (this.aborted) return; + const cleaned = cleanForTts(sentence); + if (!cleaned) return; + // Pre-fetch SOFORT beim enqueue → läuft parallel zum Playback der vorigen + // Items. Heißt: wenn Item 1 fertig spielt, ist Item 2's Audio meist schon + // im Cache → null Gap zwischen den Sätzen/Blöcken. + const controller = new AbortController(); + const audioPromise = this.fetchAudio(cleaned, mode, controller.signal).catch((err) => { + this.opts.onError?.(err, cleaned); + return null; + }); + this.queue.push({ text: cleaned, mode, controller, audioPromise }); + void this.tick(); + } + + abort(): void { + this.aborted = true; + // Alle in-flight fetches cancelen (auch pre-fetched ones) + for (const item of this.queue) { + item.controller.abort(); + } + this.queue = []; + if (this.currentSound) { + const s = this.currentSound; + this.currentSound = null; + s.stopAsync().catch(() => {}); + s.unloadAsync().catch(() => {}); + } + } + + /** True wenn noch was läuft (in queue oder gerade spielend). */ + isActive(): boolean { + return !this.aborted && (this.playing || this.queue.length > 0); + } + + private async tick(): Promise { + if (this.aborted || this.playing) return; + const item = this.queue.shift(); + if (!item) return; + this.playing = true; + + if (!this.startedOnce) { + this.startedOnce = true; + this.opts.onStart?.(); + } + + try { + const audio = await item.audioPromise; + if (this.aborted || !audio) return; + + const { sound } = await Audio.Sound.createAsync( + { uri: audio.uri }, + { shouldPlay: true }, + ); + if (this.aborted) { + await sound.unloadAsync().catch(() => {}); + return; + } + this.currentSound = sound; + await new Promise((resolve) => { + sound.setOnPlaybackStatusUpdate((status) => { + if (this.aborted) { + sound.setOnPlaybackStatusUpdate(null); + resolve(); + return; + } + if (status.isLoaded && status.didJustFinish) { + sound.setOnPlaybackStatusUpdate(null); + sound.unloadAsync().catch(() => {}); + resolve(); + } + }); + }); + this.currentSound = null; + } catch (err) { + this.opts.onError?.(err, item.text); + } finally { + this.playing = false; + if (this.aborted) return; + if (this.queue.length > 0) { + void this.tick(); + } else { + this.opts.onIdle?.(); + } + } + } + + private async fetchAudio(text: string, mode: SosTtsMode, signal: AbortSignal): Promise<{ uri: string } | null> { + const endpoint = this.opts.endpoint ?? '/api/coach/speak-openai'; + const isGoogleCloud = endpoint.endsWith('/speak-google'); + const res = await fetch(`${this.opts.apiBase}${endpoint}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.opts.accessToken}`, + }, + body: JSON.stringify({ text, locale: this.opts.locale, mode }), + signal, + }); + if (!res.ok || signal.aborted) return null; + + // /speak-google liefert JSON { audio: "data:audio/mp3;base64,..." }. + // /speak-openai (audio/mpeg) und /speak-gemini (audio/wav) liefern den + // Body als raw bytes — gleiche Pipeline reicht für beide. + let base64: string; + let ext: 'mp3' | 'wav'; + if (isGoogleCloud) { + const json = (await res.json()) as { audio?: string }; + const dataUri = json.audio ?? ''; + const comma = dataUri.indexOf(','); + if (comma === -1) return null; + base64 = dataUri.slice(comma + 1); + ext = 'mp3'; + } else { + const buffer = await res.arrayBuffer(); + if (signal.aborted || buffer.byteLength === 0) return null; + const bytes = new Uint8Array(buffer); + const chunks: string[] = []; + const cs = 0x8000; + for (let i = 0; i < bytes.length; i += cs) { + chunks.push( + String.fromCharCode(...bytes.subarray(i, Math.min(i + cs, bytes.length))), + ); + } + base64 = btoa(chunks.join('')); + ext = endpoint.endsWith('/speak-gemini') ? 'wav' : 'mp3'; + } + + const tmpPath = `${FileSystem.cacheDirectory}sos-tts-q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; + await FileSystem.writeAsStringAsync(tmpPath, base64, { + encoding: FileSystem.EncodingType.Base64, + }); + if (signal.aborted) return null; + return { uri: tmpPath }; + } +} diff --git a/apps/rebreak-native/lib/supabase.ts b/apps/rebreak-native/lib/supabase.ts new file mode 100644 index 0000000..3304a52 --- /dev/null +++ b/apps/rebreak-native/lib/supabase.ts @@ -0,0 +1,28 @@ +import "react-native-url-polyfill/auto"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { createClient } from "@supabase/supabase-js"; +import Constants from "expo-constants"; + +const supabaseUrl = Constants.expoConfig?.extra?.supabaseUrl as string; +const supabaseAnonKey = Constants.expoConfig?.extra?.supabaseAnonKey as string; + +if (!supabaseUrl || !supabaseAnonKey) { + throw new Error( + "Supabase URL und Anon Key müssen in app.config.ts (extra) gesetzt sein. " + + "EXPO_PUBLIC_SUPABASE_URL + EXPO_PUBLIC_SUPABASE_ANON_KEY in env.", + ); +} + +export const supabase = createClient(supabaseUrl, supabaseAnonKey, { + auth: { + storage: AsyncStorage, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + }, + realtime: { + params: { + apikey: supabaseAnonKey, + }, + }, +}); diff --git a/apps/rebreak-native/lib/tabIcons.ts b/apps/rebreak-native/lib/tabIcons.ts new file mode 100644 index 0000000..5670d5c --- /dev/null +++ b/apps/rebreak-native/lib/tabIcons.ts @@ -0,0 +1,35 @@ +// Cross-platform Tab-Bar Icons. +// +// react-native-bottom-tabs akzeptiert `ImageSource | AppleIcon` als tabBarIcon. +// AppleIcon = SF Symbols → iOS-only. Auf Android müssen wir ImageSource liefern. +// +// Lösung: 5 PNG-Files (Ionicons-rastered) im assets/tabs/-Ordner. Metro bundelt +// sie automatisch + Android RES-Drawable wird via require()-Asset-Hash erzeugt. +// react-native-bottom-tabs wendet tabBarActiveTintColor automatisch als Tint an. +// +// Warum nicht Ionicons.getImageSource() at runtime? @expo/vector-icons v14 +// expose-d die statische Methode nicht mehr — und der Vendor-Pfad nutzt +// RNVectorIconsManager (native), das Expo nicht bundled. Bundle-PNGs sind die +// einzige zuverlässige Lösung. +import { Platform, type ImageSourcePropType } from 'react-native'; + +export type TabKey = 'home' | 'chat' | 'coach' | 'blocker' | 'mail'; + +const ANDROID_ICONS: Record = { + home: require('../assets/tabs/home.png'), + chat: require('../assets/tabs/chatbubble.png'), + coach: require('../assets/tabs/sparkles.png'), + blocker: require('../assets/tabs/shield-checkmark.png'), + mail: require('../assets/tabs/mail.png'), +}; + +export function getTabIcon(key: TabKey): ImageSourcePropType | undefined { + if (Platform.OS !== 'android') return undefined; + return ANDROID_ICONS[key]; +} + +// Backwards-compat: noop. Andere Module rufen `preloadTabIcons()` aus dem alten +// async-getImageSource-Pattern auf. +export function preloadTabIcons(): Promise { + return Promise.resolve(); +} diff --git a/apps/rebreak-native/lib/theme.ts b/apps/rebreak-native/lib/theme.ts new file mode 100644 index 0000000..7afc4e4 --- /dev/null +++ b/apps/rebreak-native/lib/theme.ts @@ -0,0 +1,26 @@ +export const theme = { + bg: 'bg-white', + surface: 'bg-neutral-50', + surfaceElevated: 'bg-neutral-100', + border: 'border-neutral-200', + text: 'text-neutral-900', + textMuted: 'text-neutral-500', + brandOrange: 'text-rebreak-500', + brandOrangeBg: 'bg-rebreak-500', + brandBlue: 'bg-midnight-800', +} as const; + +export const colors = { + bg: '#ffffff', + surface: '#fafafa', + surfaceElevated: '#f5f5f5', + border: '#e5e5e5', + text: '#0a0a0a', + textMuted: '#737373', + // TEMP zum Testen: iOS native blue. Wieder auf '#f59e0b' wenn du zur Brand zurück willst. + brandOrange: '#007AFF', + brandBlue: '#0e1f3a', + success: '#16a34a', + error: '#dc2626', + warning: '#f59e0b', +} as const; diff --git a/apps/rebreak-native/lib/ttsProvider.ts b/apps/rebreak-native/lib/ttsProvider.ts new file mode 100644 index 0000000..de5b870 --- /dev/null +++ b/apps/rebreak-native/lib/ttsProvider.ts @@ -0,0 +1,54 @@ +// SOS-TTS-Provider mit AsyncStorage-Persist + Listener-Pattern. +// Live-Switch im SOS-Screen: Hook holt aktuelle Wahl + reagiert auf Änderungen +// während des Mounts. Endpoint-Path wird beim Erzeugen der TTS-Queue gelesen. +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { useEffect, useState } from 'react'; + +export type TtsProvider = 'openai' | 'gemini' | 'google-cloud'; + +const STORAGE_KEY = 'rebreak-sos-tts-provider'; +const DEFAULT_PROVIDER: TtsProvider = 'openai'; + +export const TTS_PROVIDER_LABEL: Record = { + openai: 'OpenAI', + gemini: 'Gemini', + 'google-cloud': 'Cloud', +}; + +export const TTS_PROVIDER_ENDPOINT: Record = { + openai: '/api/coach/speak-openai', + gemini: '/api/coach/speak-gemini', + 'google-cloud': '/api/coach/speak-google', +}; + +const listeners = new Set<(p: TtsProvider) => void>(); +let cached: TtsProvider | null = null; + +export async function loadTtsProvider(): Promise { + if (cached) return cached; + const raw = await AsyncStorage.getItem(STORAGE_KEY).catch(() => null); + cached = raw === 'gemini' || raw === 'google-cloud' ? raw : DEFAULT_PROVIDER; + return cached; +} + +export async function setTtsProvider(p: TtsProvider): Promise { + cached = p; + await AsyncStorage.setItem(STORAGE_KEY, p).catch(() => {}); + for (const cb of listeners) cb(p); +} + +export function endpointForProvider(p: TtsProvider): string { + return TTS_PROVIDER_ENDPOINT[p]; +} + +export function useTtsProvider(): [TtsProvider, (p: TtsProvider) => Promise] { + const [p, setP] = useState(cached ?? DEFAULT_PROVIDER); + useEffect(() => { + let mounted = true; + loadTtsProvider().then((v) => { if (mounted) setP(v); }); + const cb = (v: TtsProvider) => { if (mounted) setP(v); }; + listeners.add(cb); + return () => { mounted = false; listeners.delete(cb); }; + }, []); + return [p, setTtsProvider]; +} diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json new file mode 100644 index 0000000..c84d8a1 --- /dev/null +++ b/apps/rebreak-native/locales/de.json @@ -0,0 +1,591 @@ +{ + "common": { + "loading": "Einen Moment...", + "cancel": "Abbrechen", + "continue": "Weiter", + "back": "Zurück", + "error": "Fehler", + "success": "Erfolgreich", + "ok": "OK", + "confirm": "Bestätigen", + "retry": "Erneut versuchen", + "unknown_error": "Unbekannter Fehler" + }, + "auth": { + "welcomeBack": "Willkommen zurück", + "signinSubtitle": "Melde dich an, um weiterzumachen.", + "signin": "Anmelden", + "signingIn": "Einen Moment...", + "signup": "Registrieren", + "signupTitle": "Konto erstellen", + "signupSubtitle": "Werde Teil der Community.", + "signOut": "Abmelden", + "email": "E-Mail", + "emailPlaceholder": "E-Mail", + "emailRequired": "E-Mail *", + "password": "Passwort", + "passwordPlaceholder": "Passwort", + "passwordRequired": "Passwort * (min. 8 Zeichen)", + "passwordMin8": "Passwort muss mindestens 8 Zeichen haben.", + "newPassword": "Neues Passwort", + "firstName": "Vorname", + "lastName": "Nachname", + "nickname": "Benutzername", + "nicknamePlaceholder": "Benutzername * (sichtbar für andere)", + "noAccount": "Noch kein Konto?", + "alreadyRegistered": "Bereits registriert?", + "fillRequired": "Bitte alle Pflichtfelder ausfüllen.", + "googleSignin": "Mit Google anmelden", + "appleSignin": "Mit Apple anmelden", + "googleSignup": "Mit Google registrieren", + "appleSignup": "Mit Apple registrieren", + "orWithEmail": "oder mit E-Mail", + "forgotPassword": "Passwort vergessen?", + "resetPasswordTitle": "Passwort zurücksetzen", + "resetPasswordSubtitle": "Gib deine E-Mail-Adresse ein und wir senden dir einen Link zum Zurücksetzen.", + "resetPasswordSend": "Link senden", + "resetPasswordSent": "E-Mail gesendet", + "resetPasswordSentDesc": "Prüfe dein Postfach. Der Link ist 60 Minuten gültig.", + "resetPasswordSentDescPrefix": "Prüfe dein Postfach für ", + "resetPasswordSentDescSuffix": ". Der Link ist 60 Minuten gültig.", + "backToLogin": "← Zurück zum Login", + "backToLoginPlain": "Zurück zum Login", + "backToSignup": "← Zurück zur Registrierung", + "chooseAvatar": "Avatar wählen", + "privacyNotice": "Deine Daten werden sicher auf Servern in Deutschland gespeichert. Wir verkaufen keine Daten an Dritte.", + "acceptTerms": "Ich akzeptiere die", + "acceptTermsSuffix": " und habe die Datenschutzerklärung gelesen.", + "termsLink": "Nutzungsbedingungen", + "pleaseAcceptTerms": "Bitte akzeptiere die Nutzungsbedingungen.", + "confirmEmailTitle": "E-Mail bestätigen", + "confirmEmailDesc": "Wir haben einen 6-stelligen Code an %{email} gesendet.", + "confirmEmailLine1": "Wir haben einen 6-stelligen Code an", + "confirmEmailLine2": "gesendet.", + "confirmBtn": "Bestätigen", + "confirmed": "Bestätigt! Du wirst weitergeleitet...", + "confirming": "Anmeldung wird bestätigt...", + "confirmSuccess": "Erfolgreich angemeldet!", + "confirmTimeout": "Zeitüberschreitung – bitte erneut versuchen.", + "confirmFailed": "Bestätigung fehlgeschlagen.", + "resend": "Erneut senden", + "resendCooldown": "Erneut senden (%{seconds}s)", + "noCode": "Keinen Code erhalten?", + "deviceLimitTitle": "Geräte-Limit erreicht", + "deviceLimitDesc": "Dein aktueller Plan erlaubt nicht mehr Geräte. Gib ein anderes Gerät frei oder upgrade deinen Plan, um auf diesem Gerät weiterzumachen.", + "deviceLimitUpgrade": "Plan upgraden", + "toLogin": "Zur Anmeldung", + "oauthFailed": "Anmeldung fehlgeschlagen", + "loginFailed": "Anmeldung fehlgeschlagen", + "registerFailed": "Registrierung fehlgeschlagen" + }, + "landing": { + "appName": "Rebreak", + "tagline": "Du gehst nicht allein.", + "start": "Loslegen", + "version": "v0.1.0 — RN Migration Phase 1 Skeleton" + }, + "splash": { + "tagline": "You will never walk alone!", + "subtitle": "Zusammen schaffen wir das.", + "madeInGermany": "Made in Germany" + }, + "appHeader": { + "appName": "ReBreak", + "sosLabel": "SOS — Atemübung", + "sosSubtitle": "Sofort-Hilfe bei Druck", + "editProfile": "Profil bearbeiten", + "settings": "Einstellungen", + "signOut": "Abmelden" + }, + "tabs": { + "home": "Home", + "chat": "Chat", + "coach": "Coach", + "blocker": "Blocker", + "mail": "Mail" + }, + "home": { + "tagline": "Du gehst nicht allein.", + "start": "Loslegen", + "greeting_morning": "Guten Morgen", + "greeting_day": "Guten Tag", + "greeting_evening": "Guten Abend", + "streak_days_one": "Tag clean", + "streak_days_other": "Tage clean", + "streak_start": "Starte deinen ersten Tag", + "quote_of_day": "Gedanke des Tages", + "quick_access": "Schnellzugriff", + "stats_urges": "Impulse", + "stats_chats": "Gespräche", + "stats_mails": "Mails blockiert" + }, + "coach": { + "title": "Lyra", + "subtitle": "Dein CBT-Coach", + "welcome": "Hallo! Ich bin Lyra, dein persönlicher Coach. Wie geht es dir heute? Ich bin hier, um dir zuzuhören und zu helfen.", + "input_placeholder": "Schreib mir...", + "new_chat": "Neues Gespräch", + "lyra": "Lyra", + "placeholder": "Was beschäftigt dich?", + "speaking": "Lyra spricht...", + "recording": "Aufnahme läuft...", + "transcribing": "Wird verarbeitet...", + "feedback_saved": "Feedback gespeichert", + "welcome_back": "Willkommen zurück", + "online": "online", + "thinking": "schreibt …", + "error": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + }, + "blocker": { + "title": "Blocker", + "subtitle": "208.000+ Domains blockiert", + "status_active": "Aktiv", + "status_inactive": "Inaktiv", + "filter_label": "Gambling-Filter", + "filter_active_desc": "Alle Gambling-Seiten werden blockiert", + "filter_inactive_desc": "Filter ist deaktiviert", + "tamper_title": "Manipulationsschutz", + "tamper_desc": "Der Filter ist gegen einfaches Deaktivieren gesichert. Eine Entsperrung erfordert eine 6-Stunden-Abkühlphase.", + "custom_domains": "Eigene Domains", + "add_domain": "Hinzufügen", + "help_link": "Hilfe & FAQ zum Blocker", + "status_approved": "Genehmigt", + "status_rejected": "Abgelehnt", + "status_pending": "Ausstehend", + + "add_sheet_title": "Domain blockieren", + "add_sheet_label": "Domain", + "add_sheet_placeholder": "z.B. bet365.com", + "add_sheet_invalid": "Bitte gültige Domain eingeben (z.B. example.com)", + "add_sheet_warning_free": "Diese Domain bleibt dauerhaft auf deiner Liste — du kannst sie später nicht entfernen.", + "add_sheet_warning_pro": "Diese Domain ist permanent. Du kannst sie zur globalen Blocklist freigeben — dann wird der Slot frei und sie schützt alle ReBreak-User.", + "add_sheet_confirm_permanent": "Ich verstehe dass diese Domain permanent ist.", + "add_sheet_add_failed": "Hinzufügen fehlgeschlagen.", + "add_sheet_already_global": "%{domain} steht bereits in der globalen Sperrliste — kein Slot nötig.", + + "cooldown_banner_title": "Cooldown läuft", + + "deactivation_actionsheet_title": "24-Stunden-Cooldown starten?", + "deactivation_actionsheet_message": "Schutz bleibt während dieser Zeit aktiv. Du kannst jederzeit abbrechen.", + "deactivation_start_cta": "Cooldown starten", + "deactivation_failed_msg": "Cooldown konnte nicht gestartet werden.", + "deactivation_heading": "Bevor du deaktivierst", + "deactivation_title": "Wir verstehen das.", + "deactivation_intro": "Bevor du den Schutz abschaltest, hier was du wissen solltest:", + "deactivation_bullet1_title": "24 Stunden Cooldown", + "deactivation_bullet1_text": "Der Schutz bleibt 24h aktiv, selbst wenn du den Cooldown startest. Diese Zeit gibt dir Raum den Drang abklingen zu lassen.", + "deactivation_bullet2_title": "Du kannst jederzeit abbrechen", + "deactivation_bullet2_text": "Wenn der Drang nachlässt: ein Tap und der Cooldown ist weg. Der Schutz bleibt einfach an.", + "deactivation_bullet3_title": "Andere Werkzeuge sind da", + "deactivation_bullet3_text": "Atemübung, Lyra, deine Streak — alles bleibt verfügbar während du wartest.", + "deactivation_breathe_cta": "Jetzt 3 min atmen", + "deactivation_start_anyway": "Cooldown trotzdem starten", + "deactivation_starting": "Cooldown wird gestartet…", + "deactivation_cancel_failed": "Cooldown konnte nicht abgebrochen werden.", + + "domain_section_title": "Eigene Domains", + "domain_add_a11y": "Domain hinzufügen", + "domain_limit_title": "Limit erreicht", + "domain_limit_desc": "Pro: 208k+ Domains, Refill bei Freigabe — tippe für Details", + "domain_empty": "Noch keine eigenen Domains.\nTippe + um eine hinzuzufügen.", + "domain_badge_voting": "Voting", + "domain_badge_pruefung": "Prüfung", + "domain_badge_rejected": "Abgelehnt", + "domain_badge_active": "Aktiv", + "domain_btn_freigeben": "Freigeben", + "domain_btn_erneut": "Erneut", + "domain_btn_in_abstimmung": "In Abstimmung", + "domain_btn_rebreak_prueft": "ReBreak prüft", + "domain_confirm_legend_resubmit": "Erneut an ReBreak senden?", + "domain_confirm_legend_first": "Domain an ReBreak senden?", + "domain_confirm_community_resubmit": "Erneut zur Abstimmung freigeben?", + "domain_confirm_community_first": "Domain zur Abstimmung freigeben?", + "domain_confirm_legend_message": "%{domain} wird direkt an das ReBreak-Team weitergeleitet und manuell geprüft.", + "domain_confirm_community_message": "%{domain} wird zur Community-Abstimmung freigegeben (Yes/No-Voting).", + "domain_success_legend_title": "Domain eingereicht", + "domain_success_community_title": "Domain in Abstimmung", + "domain_success_legend_message": "Das ReBreak-Team prüft die Domain manuell. Du bekommst eine Benachrichtigung beim Ergebnis.", + "domain_success_community_message": "Die Community kann jetzt abstimmen. Du wirst beim Ergebnis benachrichtigt.", + + "upgrade_alert_title": "Pro-Upgrade", + "upgrade_alert_desc": "Stripe-Checkout kommt in Step 11.", + + "protection_card_title": "ReBreak-Schutz", + "protection_card_locked_title": "ReBreak-Schutz aktiv", + "protection_subtitle_inactive": "Tippe um den Schutz zu aktivieren", + "protection_subtitle_cooldown": "Cooldown läuft — Schutz weiter aktiv", + "protection_subtitle_free": "Filter aktiv — %{count} eigene Domains", + "protection_subtitle_legend": "Geschützt vor 208.000+ Domains + bis zu 10 eigenen", + "protection_subtitle_pro": "Geschützt vor 208.000+ Domains + 5 eigenen", + "protection_settings_a11y": "Schutz-Einstellungen", + "protection_stat_domains": "Domains", + "protection_stat_method": "Methode", + "protection_stat_method_dns": "DNS", + "protection_stat_method_native": "Native", + "protection_stat_status": "Status", + "protection_stat_status_live": "Live", + + "activate_url_failed_title": "URL-Filter konnte nicht aktiviert werden", + "activate_url_failed_msg": "Unbekannter Fehler.\nDu kannst es nochmal versuchen oder System-Einstellungen prüfen.", + "activate_settings_btn": "Einstellungen", + "activate_app_lock_failed_title": "App-Lock konnte nicht aktiviert werden", + "activate_app_lock_failed_msg": "Bildschirmzeit-Berechtigung wurde verweigert. Du kannst es nochmal versuchen.", + "sync_list_failed_title": "Filter-Liste konnte nicht geladen werden", + "sync_list_failed_msg": "Bitte später nochmal versuchen.", + "activation_failed_title": "Aktivierung fehlgeschlagen", + + "details_done": "Fertig", + "details_title": "Schutz-Details", + "details_active_title": "Schutz aktiv", + "details_domains_blocked": "%{value} Domains blockiert", + "details_layers_heading": "Aktive Layer", + "details_layer_url_label": "Network-Filter", + "details_layer_url_desc": "Blockt Gambling-Domains system-weit (NEFilter Extension)", + "details_layer_applock_label": "App-Lock", + "details_layer_applock_desc": "ReBreak kann nicht impulsiv gelöscht werden", + "details_layer_vpn_label": "VPN-Filter", + "details_layer_vpn_desc": "Lokaler DNS-Filter via VpnService", + "details_layer_a11y_label": "Browser-Filter", + "details_layer_a11y_desc": "Erkennt URL-Eingaben in Browser-Apps", + "details_layer_tamper_label": "Tamper-Lock", + "details_layer_tamper_desc": "Watchdog gegen externes Deaktivieren", + "details_lyra_cta_title": "Brauchst du den Schutz nicht mehr?", + "details_lyra_cta_subtitle": "Sprich mit Lyra darüber — sie hört zu.", + "details_deactivate_link": "Ich will trotzdem deaktivieren", + + "layers_url_filter_title": "URL-Filter", + "layers_url_filter_subtitle_active": "System-weiter Filter aktiv", + "layers_url_filter_subtitle_inactive": "Blockt Gambling-Seiten in Safari + Apps", + "layers_app_lock_title": "App-Lock", + "layers_app_lock_subtitle_active": "Familienzugriff aktiv", + "layers_app_lock_subtitle_inactive": "Verhindert dass du ReBreak im Impuls löschst", + "layers_app_lock_warning": "Sobald aktiv kannst du den Schutz nur über einen 24-Stunden-Cooldown abschalten. Das ist gewollt.", + + "kpi_global_label": "Geblockte Domains weltweit", + "kpi_global_subtitle": "Aktive Einträge in der globalen Blockliste", + "delta_week": "diese Woche", + "delta_month": "diesen Monat", + "kpi_submissions_title": "Deine eingereichten Domains", + "kpi_submissions_subtitle": "Status deiner Beiträge zur globalen Liste", + "kpi_my_submissions": "insgesamt", + "kpi_status_active": "aktiv", + "kpi_status_vote": "im Vote", + "kpi_status_review": "in Prüfung", + "kpi_in_vote": "Im Vote", + "kpi_in_review": "In Prüfung", + "kpi_avg_per_user": "Ø Domains pro User", + "kpi_avg_wait": "Ø Wartezeit", + "kpi_days_suffix": "Tage", + + "faq_heading": "Häufige Fragen", + "faq1_q": "Wie funktioniert der Schutz?", + "faq1_a": "Der Schutz läuft direkt im iOS-System als Inhaltsfilter. Glücksspielseiten werden lokal auf deinem Gerät blockiert — kein Datenverkehr verlässt dein iPhone.", + "faq2_q": "Wie viele Seiten werden blockiert?", + "faq2_a": "Über 208.000 Domains aus einer kuratierten globalen Blockliste — Online-Casinos, Sportwetten, Glücksspiel-Plattformen und verwandte Seiten. Die Liste wird regelmäßig aktualisiert.", + "faq3_q": "Kann ich eigene Domains hinzufügen?", + "faq3_a": "Ja. Über die Domain-Liste auf der Blocker-Seite kannst du eigene Domains hinzufügen, die zusätzlich zur globalen Liste blockiert werden.", + "faq4_q": "Warum kann ich den Schutz nicht sofort abschalten?", + "faq4_a": "Wenn du im Drang bist, willst du oft schnell deaktivieren — und es danach bereuen. Der 24-Stunden-Cooldown gibt dir Zeit, den Drang abklingen zu lassen. Du kannst den Cooldown jederzeit abbrechen — der Schutz bleibt dann einfach an.", + + "more_info_title": "Wie funktioniert der Cooldown?" + }, + "mail": { + "title": "Mail-Schutz", + "subtitle": "Gambling-Mails automatisch blockieren", + "plan_free": "Free", + "stat_accounts": "Postfach", + "stat_domains": "Domains", + "stat_interval": "Scan-Intervall", + "connect_title": "Verbinde dein Postfach", + "connect_desc": "Rebreak scannt automatisch nach Gambling-Mails und blockiert sie — ohne deine E-Mails zu lesen.", + "connect_cta": "Jetzt verbinden", + "privacy_1": "Nur Betreff + Absender werden geprüft", + "privacy_2": "Kein Zugriff auf Mail-Inhalte", + "privacy_3": "DSGVO-konform, Server in DE", + "providers_title": "Unterstützte Anbieter", + "provider_other": "Andere", + "empty_title": "Noch keine Mails blockiert", + "empty_subtitle": "Verbinde dein Postfach, damit Rebreak automatisch schützt.", + + "connect_sheet_title": "Postfach verbinden", + "connect_sheet_subtitle": "Wähle deinen E-Mail-Anbieter. Rebreak löscht Gambling-Mails automatisch — Inhalte werden nie gelesen.", + + "provider_gmail": "Gmail", + "provider_icloud": "iCloud Mail", + "provider_outlook": "Outlook", + "provider_yahoo": "Yahoo Mail", + "provider_gmx": "GMX / Web.de", + + "app_password_required_title": "App-Passwort erforderlich", + "app_password_guide_gmail": "Gmail erfordert ein App-spezifisches Passwort (kein normales Google-Passwort). Aktiviere 2FA und erstelle ein App-Passwort unter myaccount.google.com/apppasswords.", + "app_password_guide_icloud": "iCloud erfordert ein App-spezifisches Passwort. Gehe zu appleid.apple.com → Anmelden → App-spezifische Passwörter.", + "app_password_guide_outlook": "Outlook mit Microsoft-Konto: Aktiviere 2FA und erstelle ein App-Passwort unter account.microsoft.com/security.", + "app_password_guide_yahoo": "Yahoo erfordert ein App-Passwort. Aktiviere 2FA und erstelle es unter login.yahoo.com/account/security.", + "app_password_guide_gmx": "GMX / Web.de: Aktiviere IMAP in den Einstellungen und verwende dein normales Passwort oder ein App-Passwort falls 2FA aktiv.", + "app_password_guide_other": "Gib die IMAP-Zugangsdaten deines E-Mail-Anbieters ein. App-Passwort empfohlen wenn vorhanden.", + "app_password_open_link": "Jetzt App-Passwort erstellen", + + "form_email_label": "E-Mail-Adresse", + "form_email_placeholder": "deine@email.de", + "form_password_label": "App-Passwort", + "form_password_placeholder": "App-Passwort (nicht dein Login-Passwort)", + "form_privacy_note": "Dein Passwort wird AES-verschlüsselt gespeichert. Inhalte deiner Mails werden nie gelesen — nur Betreff und Absender.", + "form_connect_btn": "Postfach verbinden", + "form_fields_required": "E-Mail und Passwort sind erforderlich.", + "connect_failed": "Verbindung fehlgeschlagen. Prüfe deine Zugangsdaten.", + + "section_accounts": "Postfächer", + "add_account_a11y": "Postfach hinzufügen", + + "empty_state_title": "Kein Postfach verbunden", + "empty_state_subtitle": "Verbinde dein erstes Postfach — Rebreak löscht Gambling-Mails automatisch, bevor du sie siehst.", + "empty_state_cta": "Erstes Postfach verbinden", + + "account_active": "Aktiv", + "account_inactive": "Inaktiv", + "account_last_scan": "Zuletzt vor %{time}", + "account_never_scanned": "Noch nicht gescannt", + "account_just_now": "gerade eben", + "account_stat_blocked": "Blockiert", + "account_stat_scanned": "Gescannt", + "account_stat_block_rate": "Block-Rate", + "account_disconnect_confirm_title": "Postfach trennen?", + "account_disconnect_confirm_message": "%{email} wird getrennt und alle Scan-Daten werden gelöscht.", + "account_disconnect_confirm_btn": "Trennen", + + "stats_blocked": "Blockiert", + "stats_accounts": "Postfächer", + "stats_next_scan": "Nächster Scan", + "stats_next_scan_soon": "gleich", + "stats_mode": "Modus", + "stats_account_summary": "über %{count} Postfach/Postfächer", + "scheduled": "Geplant", + "account_of_scanned": "von %{scanned} gescannt", + "activity_log_count": "%{count} Mail(s) blockiert", + + "connect_success_title": "Postfach verbunden", + "connect_success_message": "Rebreak scannt ab jetzt automatisch nach Gambling-Mails.", + + "add_account": "Postfach hinzufügen", + "section_accounts_count": "%{used} von %{max} verbunden", + "section_accounts_count_unlimited": "%{used} verbunden · unbegrenzt", + "live": "Live", + "disconnect": "Trennen", + "loading": "Lädt…", + "app_password_placeholder": "App-Passwort", + + "scan_interval_label": "Scan-Intervall", + "realtime_desc": "Echtzeit-Blockierung via IMAP IDLE", + "free_scan_interval_hint": "Free-Plan: fest 4h. Upgrade für 1h.", + + "account_change_password": "Passwort ändern", + "edit_account_title": "Passwort aktualisieren", + "edit_account_subtitle": "Gib das neue App-Passwort für %{email} ein. Das alte Passwort wird ersetzt.", + "edit_account_save": "Speichern", + + "activity_log_title": "Kürzlich blockiert", + "activity_log_subtitle": "In den letzten 24h blockierte Mails", + "activity_log_empty": "Keine Mails in den letzten 24h blockiert", + "activity_log_more": "+ %{count} weitere", + "activity_no_subject": "(kein Betreff)", + + "upgrade_alert_title": "Mehr Postfächer", + "upgrade_alert_desc": "Upgrade auf Pro für bis zu 3 Postfächer, auf Legend für unbegrenzte Postfächer." + }, + "settings": { + "title": "Einstellungen", + "account_section": "Konto", + "prefs_section": "Einstellungen", + "danger_section": "Danger Zone", + "edit_profile": "Profil bearbeiten", + "devices": "Geräte", + "devices_desc": "Registrierte Geräte verwalten", + "subscription": "Abonnement", + "plan_free": "Free", + "push_notifications": "Push-Benachrichtigungen", + "streak_reminders": "Streak-Erinnerungen", + "language": "Sprache", + "language_current": "Deutsch", + "upgrade_cta": "Auf Pro upgraden — 29 €/Jahr", + "delete_account": "Konto löschen", + "delete_desc": "Alle Daten werden unwiderruflich gelöscht.", + "sign_out": "Abmelden" + }, + "urge": { + "title": "SOS — Atemübung", + "step_dashboard": "Start", + "step_emotion": "Emotion", + "step_breathing": "Atmung", + "step_games": "Lyra Games", + "step_result": "Reflexion", + "step_done": "Fertig", + "feel_urge": "Spürst du gerade einen starken Impuls?", + "feel_urge_desc": "Wir führen dich in kleinen Schritten durch einen sicheren Reset.", + "yes_urge": "Ja, ich brauche Hilfe", + "just_play": "Nur kurz spielen", + "this_week": "Diese Woche", + "total_urges": "Impulse", + "overcome_count": "Überwunden", + "breathing_exercises": "Atemübungen", + "having_urge": "Du bist nicht allein.", + "how_feeling": "Wie fühlst du dich gerade?", + "emotion_stress": "Stress", + "emotion_sadness": "Trauer", + "emotion_anger": "Wut", + "emotion_empty": "Leere", + "emotion_boredom": "Langeweile", + "emotion_other": "Anderes", + "lets_breathe": "Lass uns kurz atmen", + "breathing_desc": "Nur 3 Runden. Danach ist dein Kopf meist deutlich ruhiger.", + "round": "Runde %{current} / %{total}", + "round_simple": "Runde %{current} / %{total}", + "intro": "Tief durchatmen hilft, den Impuls zu überwältigen.", + "inhale": "Einatmen", + "hold": "Halten", + "exhale": "Ausatmen", + "start": "Übung starten", + "start_exercise": "Atemübung starten", + "skip": "Überspringen", + "game_offer_title": "Lyra Games", + "game_offer_text": "Wähle ein kurzes Spiel. 2-3 Minuten reichen oft, um den Impuls zu brechen.", + "just_play_lyra": "Kleiner Fokus-Reset gefällig? Such dir ein Spiel aus.", + "game_memory": "Memory", + "game_tictactoe": "Tic-Tac-Toe", + "game_snake": "Snake", + "game_tetris": "Tetris", + "game_memory_desc": "Paare finden, Fokus zurückholen", + "game_tictactoe_desc": "Schnelles Duell für klare Entscheidungen", + "game_snake_desc": "Rhythmus statt Grübeln", + "game_tetris_desc": "Muster ordnen, Kopf beruhigen", + "skip_games": "Spiele überspringen", + "back": "Zurück", + "open_lyra": "Mit Lyra öffnen", + "game_start_title": "Spiel starten", + "game_start_desc": "%{game} wird mit Lyra gestartet.", + "how_overcome": "Wie ging es danach?", + "answer_helps": "Deine Antwort hilft dir, Muster zu erkennen und stärker zu werden.", + "i_overcame": "Ich habe den Impuls überwunden", + "i_gave_in": "Ich habe nachgegeben", + "overcame_msg": "Stark. Jeder überwundene Impuls trainiert dein Gehirn neu.", + "gave_in_msg": "Kein Urteil. Ehrlichkeit ist der Startpunkt für den nächsten Sieg.", + "save": "Speichern", + "done_title": "Sehr gut!", + "done_desc": "Du hast die Atemübung abgeschlossen. Dein Nervensystem hat sich beruhigt.", + "done_back": "Zurück", + "well_done": "Stark gemacht", + "chin_up": "Kopf hoch", + "overcame_result": "Du hast den Impuls durchbrochen. Bleib bei dem, was dir gut tut.", + "gave_in_result": "Ein Rückschritt ist kein Ende. Atme durch und starte neu.", + "back_to_dashboard": "Zurück zum Dashboard" + }, + "notifications": { + "title": "Benachrichtigungen", + "empty_title": "Keine Benachrichtigungen", + "empty_subtitle": "Du bist auf dem neuesten Stand.", + "mark_all_read": "Alle als gelesen markieren", + "liked_post": "hat deinen Beitrag geliked", + "commented_post": "hat deinen Beitrag kommentiert", + "voted_domain": "hat über deine Domain abgestimmt", + "domain_accepted": "ist jetzt in der globalen Sperrliste", + "domain_accepted_sub": "Tippe um deine Sperrliste zu öffnen", + "domain_rejected": "wurde abgelehnt und aus deiner Liste entfernt", + "new_follower": "folgt dir jetzt", + "generic": "hat dich benachrichtigt", + "just_now": "gerade eben", + "min_ago": "vor %{n} Min", + "hours_ago": "vor %{n} Std", + "days_ago": "vor %{n} T" + }, + "chat": { + "title": "Chat", + "dms": "Direktnachrichten", + "rooms": "Gruppen", + "groups": "Gruppen", + "direct": "Direkt", + "no_chats": "Noch keine Chats", + "no_rooms": "Noch keine Gruppen", + "start_dm": "Neuen DM starten", + "placeholder": "Nachricht schreiben…", + "you": "Du: ", + "just_now": "gerade", + "loading": "Laden…", + "send_failed": "Nachricht konnte nicht gesendet werden.", + "create_group": "Gruppe erstellen", + "create": "Erstellen", + "room_name": "Gruppenname", + "room_description": "Beschreibung (optional)", + "public_room": "Öffentliche Gruppe", + "join_mode": "Beitrittsmodus", + "join_mode_approval": "Mit Freigabe", + "join_mode_invite": "Nur Einladung", + "join": "Beitreten", + "join_pending": "Beitritt wird geprüft…", + "join_required": "Tritt der Gruppe bei, um mitzuschreiben.", + "members": "Mitglieder", + "settings": "Einstellungen", + "info": "Info", + "leave_room": "Gruppe verlassen", + "reply": "Antworten", + "reply_to": "Antwort an", + "like": "Liken", + "unlike": "Like entfernen", + "copy": "Kopieren", + "image_attachment": "Bild", + "file_attachment": "Datei", + "upload_failed": "Upload fehlgeschlagen", + "member_count": "%{n} Mitglieder", + "pending_request": "Beitrittsanfragen", + "approve": "Annehmen", + "reject": "Ablehnen", + "avatar_updated": "Gruppenbild aktualisiert", + "send": "Senden" + }, + "community": { + "compose_placeholder": "Was bewegt dich gerade?", + "compose_default_user": "Du", + "compose_photo_perm_title": "Foto-Zugriff", + "compose_photo_perm_desc": "Bitte erlaube den Zugriff auf deine Fotos in den iOS-Einstellungen.", + "image": "Bild", + "cancel": "Abbrechen", + "share": "Teilen", + "no_posts": "Sei der Erste der was teilt", + "cat_all": "Alle", + "cat_games": "Games", + "cat_domain": "Domain-Votes", + "cat_lyra": "Lyra", + "cat_rebreak": "ReBreak", + "like": "Gefällt mir", + "comment": "Kommentar", + "comments_title": "Kommentare", + "comments_empty": "Noch keine Kommentare – sei der Erste!", + "reply": "Antworten", + "reply_to": "Antwort an", + "send": "Senden", + "comment_placeholder": "Kommentar schreiben…", + "filter": "Filter", + "published": "Veröffentlicht", + "post_failed": "Post konnte nicht veröffentlicht werden.", + "anonymous_label": "Anonym", + "tier_starter": "Starter", + "tier_pro": "Pro", + "tier_legend": "Legend", + "bot_admin": "Admin", + "bot_ai": "KI", + "reposted_suffix": "hat repostet", + "domain_proposal_label": "Sperrlisten-Vorschlag", + "domain_added_to_blocklist": "Zur globalen Sperrliste hinzugefügt", + "domain_added": "In der globalen Sperrliste", + "domain_proposed": "Zur Aufnahme vorgeschlagen", + "domain_vote_own": "Du kannst nicht über deinen eigenen Vorschlag abstimmen.", + "vote_yes": "Ja", + "vote_no": "Nein", + "vote_rejected": "Abgelehnt", + "vote_in_review": "In Prüfung", + "voted_thanks": "Danke für deine Stimme!" + }, + "streak": { + "label_one": "Tag", + "label_other": "Tage", + "label_suffix": "clean" + } +} diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json new file mode 100644 index 0000000..ba1c229 --- /dev/null +++ b/apps/rebreak-native/locales/en.json @@ -0,0 +1,591 @@ +{ + "common": { + "loading": "One moment...", + "cancel": "Cancel", + "continue": "Continue", + "back": "Back", + "error": "Error", + "success": "Success", + "ok": "OK", + "confirm": "Confirm", + "retry": "Try again", + "unknown_error": "Unknown error" + }, + "auth": { + "welcomeBack": "Welcome back", + "signinSubtitle": "Sign in to continue.", + "signin": "Sign in", + "signingIn": "One moment...", + "signup": "Sign up", + "signupTitle": "Create account", + "signupSubtitle": "Join the community.", + "signOut": "Sign out", + "email": "Email", + "emailPlaceholder": "Email", + "emailRequired": "Email *", + "password": "Password", + "passwordPlaceholder": "Password", + "passwordRequired": "Password * (min. 8 characters)", + "passwordMin8": "Password must be at least 8 characters.", + "newPassword": "New password", + "firstName": "First name", + "lastName": "Last name", + "nickname": "Username", + "nicknamePlaceholder": "Username * (visible to others)", + "noAccount": "No account yet?", + "alreadyRegistered": "Already registered?", + "fillRequired": "Please fill in all required fields.", + "googleSignin": "Sign in with Google", + "appleSignin": "Sign in with Apple", + "googleSignup": "Sign up with Google", + "appleSignup": "Sign up with Apple", + "orWithEmail": "or with email", + "forgotPassword": "Forgot password?", + "resetPasswordTitle": "Reset password", + "resetPasswordSubtitle": "Enter your email and we'll send you a reset link.", + "resetPasswordSend": "Send link", + "resetPasswordSent": "Email sent", + "resetPasswordSentDesc": "Check your inbox. The link is valid for 60 minutes.", + "resetPasswordSentDescPrefix": "Check your inbox for ", + "resetPasswordSentDescSuffix": ". The link is valid for 60 minutes.", + "backToLogin": "← Back to sign in", + "backToLoginPlain": "Back to sign in", + "backToSignup": "← Back to sign up", + "chooseAvatar": "Choose avatar", + "privacyNotice": "Your data is stored securely on servers in Germany. We never sell data to third parties.", + "acceptTerms": "I accept the", + "acceptTermsSuffix": " and have read the privacy policy.", + "termsLink": "Terms of Service", + "pleaseAcceptTerms": "Please accept the Terms of Service.", + "confirmEmailTitle": "Confirm email", + "confirmEmailDesc": "We sent a 6-digit code to %{email}.", + "confirmEmailLine1": "We sent a 6-digit code to", + "confirmEmailLine2": "", + "confirmBtn": "Confirm", + "confirmed": "Confirmed! Redirecting...", + "confirming": "Confirming sign-in...", + "confirmSuccess": "Successfully signed in!", + "confirmTimeout": "Timed out – please try again.", + "confirmFailed": "Confirmation failed.", + "resend": "Resend", + "resendCooldown": "Resend (%{seconds}s)", + "noCode": "Didn't receive a code?", + "deviceLimitTitle": "Device limit reached", + "deviceLimitDesc": "Your current plan doesn't allow more devices. Free up another device or upgrade your plan to continue on this device.", + "deviceLimitUpgrade": "Upgrade plan", + "toLogin": "Back to sign in", + "oauthFailed": "Sign in failed", + "loginFailed": "Sign in failed", + "registerFailed": "Registration failed" + }, + "landing": { + "appName": "Rebreak", + "tagline": "You're not walking alone.", + "start": "Get started", + "version": "v0.1.0 — RN Migration Phase 1 Skeleton" + }, + "splash": { + "tagline": "You will never walk alone!", + "subtitle": "Together we'll make it.", + "madeInGermany": "Made in Germany" + }, + "appHeader": { + "appName": "ReBreak", + "sosLabel": "SOS — Breathing exercise", + "sosSubtitle": "Instant help under pressure", + "editProfile": "Edit profile", + "settings": "Settings", + "signOut": "Sign out" + }, + "tabs": { + "home": "Home", + "chat": "Chat", + "coach": "Coach", + "blocker": "Blocker", + "mail": "Mail" + }, + "home": { + "tagline": "You're not walking alone.", + "start": "Get started", + "greeting_morning": "Good morning", + "greeting_day": "Good afternoon", + "greeting_evening": "Good evening", + "streak_days_one": "day clean", + "streak_days_other": "days clean", + "streak_start": "Start your first day", + "quote_of_day": "Thought of the day", + "quick_access": "Quick access", + "stats_urges": "Urges", + "stats_chats": "Chats", + "stats_mails": "Mails blocked" + }, + "coach": { + "title": "Lyra", + "subtitle": "Your CBT coach", + "welcome": "Hi! I'm Lyra, your personal coach. How are you doing today? I'm here to listen and help.", + "input_placeholder": "Write to me...", + "new_chat": "New chat", + "lyra": "Lyra", + "placeholder": "What's on your mind?", + "speaking": "Lyra is speaking...", + "recording": "Recording...", + "transcribing": "Processing...", + "feedback_saved": "Feedback saved", + "welcome_back": "Welcome back", + "online": "online", + "thinking": "typing …", + "error": "Something went wrong. Please try again." + }, + "blocker": { + "title": "Blocker", + "subtitle": "208,000+ domains blocked", + "status_active": "Active", + "status_inactive": "Inactive", + "filter_label": "Gambling Filter", + "filter_active_desc": "All gambling sites are being blocked", + "filter_inactive_desc": "Filter is disabled", + "tamper_title": "Tamper protection", + "tamper_desc": "The filter is secured against easy disabling. Unlocking requires a 6-hour cooldown period.", + "custom_domains": "Custom Domains", + "add_domain": "Add", + "help_link": "Help & FAQ about Blocker", + "status_approved": "Approved", + "status_rejected": "Rejected", + "status_pending": "Pending", + + "add_sheet_title": "Block domain", + "add_sheet_label": "Domain", + "add_sheet_placeholder": "e.g. bet365.com", + "add_sheet_invalid": "Please enter a valid domain (e.g. example.com)", + "add_sheet_warning_free": "This domain stays on your list permanently — you cannot remove it later.", + "add_sheet_warning_pro": "This domain is permanent. You can release it to the global blocklist — the slot becomes free again and it will protect every ReBreak user.", + "add_sheet_confirm_permanent": "I understand this domain is permanent.", + "add_sheet_add_failed": "Failed to add domain.", + "add_sheet_already_global": "%{domain} is already on the global blocklist — no slot needed.", + + "cooldown_banner_title": "Cooldown running", + + "deactivation_actionsheet_title": "Start 24-hour cooldown?", + "deactivation_actionsheet_message": "Protection stays active during this time. You can cancel anytime.", + "deactivation_start_cta": "Start cooldown", + "deactivation_failed_msg": "Could not start cooldown.", + "deactivation_heading": "Before you deactivate", + "deactivation_title": "We get it.", + "deactivation_intro": "Before you turn off protection, here's what you should know:", + "deactivation_bullet1_title": "24-hour cooldown", + "deactivation_bullet1_text": "Protection stays active for 24 hours even after you start the cooldown. This time gives you space to let the urge pass.", + "deactivation_bullet2_title": "You can cancel anytime", + "deactivation_bullet2_text": "If the urge fades: one tap and the cooldown is gone. Protection just stays on.", + "deactivation_bullet3_title": "Other tools are here", + "deactivation_bullet3_text": "Breathing exercise, Lyra, your streak — everything stays available while you wait.", + "deactivation_breathe_cta": "Breathe for 3 min", + "deactivation_start_anyway": "Start cooldown anyway", + "deactivation_starting": "Starting cooldown…", + "deactivation_cancel_failed": "Could not cancel cooldown.", + + "domain_section_title": "Custom domains", + "domain_add_a11y": "Add domain", + "domain_limit_title": "Limit reached", + "domain_limit_desc": "Pro: 208k+ domains, refill on release — tap for details", + "domain_empty": "No custom domains yet.\nTap + to add one.", + "domain_badge_voting": "Voting", + "domain_badge_pruefung": "Review", + "domain_badge_rejected": "Rejected", + "domain_badge_active": "Active", + "domain_btn_freigeben": "Release", + "domain_btn_erneut": "Retry", + "domain_btn_in_abstimmung": "In voting", + "domain_btn_rebreak_prueft": "ReBreak reviewing", + "domain_confirm_legend_resubmit": "Resubmit to ReBreak?", + "domain_confirm_legend_first": "Send domain to ReBreak?", + "domain_confirm_community_resubmit": "Resubmit to community vote?", + "domain_confirm_community_first": "Release domain to community vote?", + "domain_confirm_legend_message": "%{domain} will be sent directly to the ReBreak team for manual review.", + "domain_confirm_community_message": "%{domain} will be released to the community vote (yes/no voting).", + "domain_success_legend_title": "Domain submitted", + "domain_success_community_title": "Domain in voting", + "domain_success_legend_message": "The ReBreak team is reviewing this domain manually. You'll get a notification with the result.", + "domain_success_community_message": "The community can now vote. You'll be notified once the result is in.", + + "upgrade_alert_title": "Pro upgrade", + "upgrade_alert_desc": "Stripe checkout is coming in step 11.", + + "protection_card_title": "ReBreak protection", + "protection_card_locked_title": "ReBreak protection active", + "protection_subtitle_inactive": "Tap to activate protection", + "protection_subtitle_cooldown": "Cooldown running — protection still active", + "protection_subtitle_free": "Filter active — %{count} custom domains", + "protection_subtitle_legend": "Protected against 208,000+ domains + up to 10 custom", + "protection_subtitle_pro": "Protected against 208,000+ domains + 5 custom", + "protection_settings_a11y": "Protection settings", + "protection_stat_domains": "Domains", + "protection_stat_method": "Method", + "protection_stat_method_dns": "DNS", + "protection_stat_method_native": "Native", + "protection_stat_status": "Status", + "protection_stat_status_live": "Live", + + "activate_url_failed_title": "Could not activate URL filter", + "activate_url_failed_msg": "Unknown error.\nYou can try again or check System Settings.", + "activate_settings_btn": "Settings", + "activate_app_lock_failed_title": "Could not activate App Lock", + "activate_app_lock_failed_msg": "Screen Time permission was denied. You can try again.", + "sync_list_failed_title": "Filter list could not be loaded", + "sync_list_failed_msg": "Please try again later.", + "activation_failed_title": "Activation failed", + + "details_done": "Done", + "details_title": "Protection details", + "details_active_title": "Protection active", + "details_domains_blocked": "%{value} domains blocked", + "details_layers_heading": "Active layers", + "details_layer_url_label": "Network filter", + "details_layer_url_desc": "Blocks gambling domains system-wide (NEFilter Extension)", + "details_layer_applock_label": "App lock", + "details_layer_applock_desc": "ReBreak cannot be deleted impulsively", + "details_layer_vpn_label": "VPN filter", + "details_layer_vpn_desc": "Local DNS filter via VpnService", + "details_layer_a11y_label": "Browser filter", + "details_layer_a11y_desc": "Detects URL input in browser apps", + "details_layer_tamper_label": "Tamper lock", + "details_layer_tamper_desc": "Watchdog against external deactivation", + "details_lyra_cta_title": "Don't need protection anymore?", + "details_lyra_cta_subtitle": "Talk to Lyra about it — she's listening.", + "details_deactivate_link": "Deactivate anyway", + + "layers_url_filter_title": "URL filter", + "layers_url_filter_subtitle_active": "System-wide filter active", + "layers_url_filter_subtitle_inactive": "Blocks gambling sites in Safari + apps", + "layers_app_lock_title": "App lock", + "layers_app_lock_subtitle_active": "Family access active", + "layers_app_lock_subtitle_inactive": "Prevents you from deleting ReBreak on impulse", + "layers_app_lock_warning": "Once active, you can only disable protection through a 24-hour cooldown. That's by design.", + + "kpi_global_label": "Domains blocked worldwide", + "kpi_global_subtitle": "Active entries in the global blocklist", + "delta_week": "this week", + "delta_month": "this month", + "kpi_submissions_title": "Your submitted domains", + "kpi_submissions_subtitle": "Status of your contributions to the global list", + "kpi_my_submissions": "total", + "kpi_status_active": "active", + "kpi_status_vote": "in vote", + "kpi_status_review": "in review", + "kpi_in_vote": "In vote", + "kpi_in_review": "In review", + "kpi_avg_per_user": "Avg. domains per user", + "kpi_avg_wait": "Avg. wait", + "kpi_days_suffix": "days", + + "faq_heading": "FAQ", + "faq1_q": "How does protection work?", + "faq1_a": "Protection runs directly in iOS as a content filter. Gambling sites are blocked locally on your device — no traffic leaves your iPhone.", + "faq2_q": "How many sites are blocked?", + "faq2_a": "Over 208,000 domains from a curated global blocklist — online casinos, sports betting, gambling platforms and related sites. The list is updated regularly.", + "faq3_q": "Can I add my own domains?", + "faq3_a": "Yes. From the domain list on the blocker page you can add custom domains that get blocked in addition to the global list.", + "faq4_q": "Why can't I turn protection off immediately?", + "faq4_a": "In the moment of urge, you often want to disable fast — and regret it after. The 24-hour cooldown gives you time for the urge to pass. You can cancel the cooldown anytime — protection then simply stays on.", + + "more_info_title": "How does the cooldown work?" + }, + "mail": { + "title": "Mail Shield", + "subtitle": "Automatically block gambling emails", + "plan_free": "Free", + "stat_accounts": "Mailbox", + "stat_domains": "Domains", + "stat_interval": "Scan interval", + "connect_title": "Connect your mailbox", + "connect_desc": "Rebreak automatically scans for gambling emails and blocks them — without reading your emails.", + "connect_cta": "Connect now", + "privacy_1": "Only subject + sender are checked", + "privacy_2": "No access to email content", + "privacy_3": "GDPR-compliant, servers in Germany", + "providers_title": "Supported providers", + "provider_other": "Other", + "empty_title": "No emails blocked yet", + "empty_subtitle": "Connect your mailbox so Rebreak can protect you automatically.", + + "connect_sheet_title": "Connect mailbox", + "connect_sheet_subtitle": "Choose your email provider. Rebreak deletes gambling emails automatically — your message content is never read.", + + "provider_gmail": "Gmail", + "provider_icloud": "iCloud Mail", + "provider_outlook": "Outlook", + "provider_yahoo": "Yahoo Mail", + "provider_gmx": "GMX / Web.de", + + "app_password_required_title": "App password required", + "app_password_guide_gmail": "Gmail requires an app-specific password (not your regular Google password). Enable 2FA and create an app password at myaccount.google.com/apppasswords.", + "app_password_guide_icloud": "iCloud requires an app-specific password. Go to appleid.apple.com → Sign in → App-specific passwords.", + "app_password_guide_outlook": "Outlook with Microsoft account: Enable 2FA and create an app password at account.microsoft.com/security.", + "app_password_guide_yahoo": "Yahoo requires an app password. Enable 2FA and create it at login.yahoo.com/account/security.", + "app_password_guide_gmx": "GMX / Web.de: Enable IMAP in settings and use your regular password or an app password if 2FA is active.", + "app_password_guide_other": "Enter the IMAP credentials of your email provider. An app password is recommended if available.", + "app_password_open_link": "Create app password now", + + "form_email_label": "Email address", + "form_email_placeholder": "your@email.com", + "form_password_label": "App password", + "form_password_placeholder": "App password (not your login password)", + "form_privacy_note": "Your password is stored AES-encrypted. The content of your emails is never read — only subject and sender.", + "form_connect_btn": "Connect mailbox", + "form_fields_required": "Email and password are required.", + "connect_failed": "Connection failed. Please check your credentials.", + + "section_accounts": "Mailboxes", + "add_account_a11y": "Add mailbox", + + "empty_state_title": "No mailbox connected", + "empty_state_subtitle": "Connect your first mailbox — Rebreak will delete gambling emails automatically before you see them.", + "empty_state_cta": "Connect first mailbox", + + "account_active": "Active", + "account_inactive": "Inactive", + "account_last_scan": "%{time} ago", + "account_never_scanned": "Not scanned yet", + "account_just_now": "just now", + "account_stat_blocked": "Blocked", + "account_stat_scanned": "Scanned", + "account_stat_block_rate": "Block rate", + "account_disconnect_confirm_title": "Disconnect mailbox?", + "account_disconnect_confirm_message": "%{email} will be disconnected and all scan data will be deleted.", + "account_disconnect_confirm_btn": "Disconnect", + + "stats_blocked": "Blocked", + "stats_accounts": "Mailboxes", + "stats_next_scan": "Next scan", + "stats_next_scan_soon": "soon", + "stats_mode": "Mode", + "stats_account_summary": "across %{count} mailbox(es)", + "scheduled": "Scheduled", + "account_of_scanned": "of %{scanned} scanned", + "activity_log_count": "%{count} mail(s) blocked", + + "connect_success_title": "Mailbox connected", + "connect_success_message": "Rebreak will now automatically scan for gambling emails.", + + "upgrade_alert_title": "More mailboxes", + "upgrade_alert_desc": "Upgrade to Pro for up to 3 mailboxes, or Legend for unlimited.", + + "add_account": "Add mailbox", + "section_accounts_count": "%{used} of %{max} connected", + "section_accounts_count_unlimited": "%{used} connected · unlimited", + "live": "Live", + "disconnect": "Disconnect", + "loading": "Loading…", + "app_password_placeholder": "App password", + + "scan_interval_label": "Scan interval", + "realtime_desc": "Real-time blocking via IMAP IDLE", + "free_scan_interval_hint": "Free plan: fixed 4h interval. Upgrade for 1h.", + + "account_change_password": "Change password", + "edit_account_title": "Update password", + "edit_account_subtitle": "Enter the new app password for %{email}. The previous password will be replaced.", + "edit_account_save": "Save", + + "activity_log_title": "Recently blocked", + "activity_log_subtitle": "Mails blocked in the last 24h", + "activity_log_empty": "No mails blocked in the last 24h", + "activity_log_more": "+ %{count} more", + "activity_no_subject": "(no subject)" + }, + "settings": { + "title": "Settings", + "account_section": "Account", + "prefs_section": "Preferences", + "danger_section": "Danger Zone", + "edit_profile": "Edit profile", + "devices": "Devices", + "devices_desc": "Manage registered devices", + "subscription": "Subscription", + "plan_free": "Free", + "push_notifications": "Push notifications", + "streak_reminders": "Streak reminders", + "language": "Language", + "language_current": "English", + "upgrade_cta": "Upgrade to Pro — €29/year", + "delete_account": "Delete account", + "delete_desc": "All data will be permanently deleted.", + "sign_out": "Sign out" + }, + "urge": { + "title": "SOS — Breathing exercise", + "step_dashboard": "Start", + "step_emotion": "Emotion", + "step_breathing": "Breathing", + "step_games": "Lyra games", + "step_result": "Reflection", + "step_done": "Done", + "feel_urge": "Feeling a strong urge right now?", + "feel_urge_desc": "We'll guide you through a short reset, step by step.", + "yes_urge": "Yes, I need help", + "just_play": "Just play", + "this_week": "This week", + "total_urges": "Urges", + "overcome_count": "Overcome", + "breathing_exercises": "Breathing sessions", + "having_urge": "You're not alone.", + "how_feeling": "How are you feeling right now?", + "emotion_stress": "Stress", + "emotion_sadness": "Sadness", + "emotion_anger": "Anger", + "emotion_empty": "Emptiness", + "emotion_boredom": "Boredom", + "emotion_other": "Other", + "lets_breathe": "Let's breathe for a minute", + "breathing_desc": "Just 3 rounds. Your mind usually feels calmer afterwards.", + "round": "Round %{current} / %{total}", + "round_simple": "Round %{current} / %{total}", + "intro": "Deep breathing helps overcome the urge.", + "inhale": "Inhale", + "hold": "Hold", + "exhale": "Exhale", + "start": "Start exercise", + "start_exercise": "Start breathing", + "skip": "Skip", + "game_offer_title": "Lyra games", + "game_offer_text": "Pick a short game. 2-3 minutes are often enough to break the urge.", + "just_play_lyra": "Need a quick focus reset? Pick a game.", + "game_memory": "Memory", + "game_tictactoe": "Tic-Tac-Toe", + "game_snake": "Snake", + "game_tetris": "Tetris", + "game_memory_desc": "Find pairs and regain focus", + "game_tictactoe_desc": "Quick duel for clear decisions", + "game_snake_desc": "Rhythm over rumination", + "game_tetris_desc": "Organize patterns, calm your mind", + "skip_games": "Skip games", + "back": "Back", + "open_lyra": "Open with Lyra", + "game_start_title": "Start game", + "game_start_desc": "%{game} will be started with Lyra.", + "how_overcome": "How did it go afterwards?", + "answer_helps": "Your answer helps you spot patterns and get stronger.", + "i_overcame": "I overcame the urge", + "i_gave_in": "I gave in", + "overcame_msg": "Strong. Every resisted urge rewires your brain.", + "gave_in_msg": "No judgment. Honesty is the start of the next win.", + "save": "Save", + "done_title": "Well done!", + "done_desc": "You completed the breathing exercise. Your nervous system has calmed down.", + "done_back": "Back", + "well_done": "Great job", + "chin_up": "Keep your head up", + "overcame_result": "You broke the urge loop. Stay close to what helps you.", + "gave_in_result": "A setback is not the end. Breathe and restart.", + "back_to_dashboard": "Back to dashboard" + }, + "notifications": { + "title": "Notifications", + "empty_title": "No notifications", + "empty_subtitle": "You're all caught up.", + "mark_all_read": "Mark all as read", + "liked_post": "liked your post", + "commented_post": "commented on your post", + "voted_domain": "voted on your domain", + "domain_accepted": "is now in the global blocklist", + "domain_accepted_sub": "Tap to open your blocklist", + "domain_rejected": "was rejected and removed from your list", + "new_follower": "started following you", + "generic": "sent you a notification", + "just_now": "just now", + "min_ago": "%{n} min ago", + "hours_ago": "%{n} h ago", + "days_ago": "%{n} d ago" + }, + "chat": { + "title": "Chat", + "dms": "Direct Messages", + "rooms": "Groups", + "groups": "Groups", + "direct": "Direct", + "no_chats": "No chats yet", + "no_rooms": "No groups yet", + "start_dm": "Start new DM", + "placeholder": "Write a message…", + "you": "You: ", + "just_now": "just now", + "loading": "Loading…", + "send_failed": "Failed to send message.", + "create_group": "Create group", + "create": "Create", + "room_name": "Group name", + "room_description": "Description (optional)", + "public_room": "Public group", + "join_mode": "Join mode", + "join_mode_approval": "With approval", + "join_mode_invite": "Invite only", + "join": "Join", + "join_pending": "Join request pending…", + "join_required": "Join the group to participate.", + "members": "Members", + "settings": "Settings", + "info": "Info", + "leave_room": "Leave group", + "reply": "Reply", + "reply_to": "Replying to", + "like": "Like", + "unlike": "Unlike", + "copy": "Copy", + "image_attachment": "Image", + "file_attachment": "File", + "upload_failed": "Upload failed", + "member_count": "%{n} members", + "pending_request": "Join requests", + "approve": "Approve", + "reject": "Reject", + "avatar_updated": "Group photo updated", + "send": "Send" + }, + "community": { + "compose_placeholder": "What's on your mind?", + "compose_default_user": "You", + "compose_photo_perm_title": "Photo access", + "compose_photo_perm_desc": "Please allow access to your photos in iOS Settings.", + "image": "Image", + "cancel": "Cancel", + "share": "Share", + "no_posts": "Be the first to share something", + "cat_all": "All", + "cat_games": "Games", + "cat_domain": "Domain Votes", + "cat_lyra": "Lyra", + "cat_rebreak": "ReBreak", + "like": "Like", + "comment": "Comment", + "comments_title": "Comments", + "comments_empty": "No comments yet – be the first!", + "reply": "Reply", + "reply_to": "Replying to", + "send": "Send", + "comment_placeholder": "Write a comment…", + "filter": "Filter", + "published": "Published", + "post_failed": "Failed to publish post.", + "anonymous_label": "Anonymous", + "tier_starter": "Starter", + "tier_pro": "Pro", + "tier_legend": "Legend", + "bot_admin": "Admin", + "bot_ai": "AI", + "reposted_suffix": "reposted", + "domain_proposal_label": "Blocklist proposal", + "domain_added_to_blocklist": "Added to global blocklist", + "domain_added": "In the global blocklist", + "domain_proposed": "Proposed for inclusion", + "domain_vote_own": "You can't vote on your own proposal.", + "vote_yes": "Yes", + "vote_no": "No", + "vote_rejected": "Rejected", + "vote_in_review": "Under review", + "voted_thanks": "Thanks for your vote!" + }, + "streak": { + "label_one": "day", + "label_other": "days", + "label_suffix": "clean" + } +} diff --git a/apps/rebreak-native/metro.config.js b/apps/rebreak-native/metro.config.js new file mode 100644 index 0000000..72d7b59 --- /dev/null +++ b/apps/rebreak-native/metro.config.js @@ -0,0 +1,31 @@ +// Metro config für Expo + pnpm Monorepo +// Quelle: https://docs.expo.dev/guides/monorepos/ + +const { getDefaultConfig } = require('expo/metro-config'); +const { withNativeWind } = require('nativewind/metro'); +const path = require('path'); + +const projectRoot = __dirname; +const monorepoRoot = path.resolve(projectRoot, '../..'); + +const config = getDefaultConfig(projectRoot); + +// 1. Watch alle Workspace-Pakete +config.watchFolders = [monorepoRoot]; + +// 2. Auflösung über Workspace-root + lokale node_modules +config.resolver.nodeModulesPaths = [ + path.resolve(projectRoot, 'node_modules'), + path.resolve(monorepoRoot, 'node_modules'), +]; + +// 3. Symlinks via pnpm +config.resolver.unstable_enableSymlinks = true; +config.resolver.unstable_enablePackageExports = true; + +// 4. .riv (Rive-Animation) als Asset registrieren +config.resolver.assetExts = [...(config.resolver.assetExts ?? []), 'riv']; + +module.exports = withNativeWind(config, { + input: './global.css', +}); diff --git a/apps/rebreak-native/metro.sh b/apps/rebreak-native/metro.sh new file mode 100755 index 0000000..29fb8cc --- /dev/null +++ b/apps/rebreak-native/metro.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Rebreak Native: Metro Bundler (Kill + Clean Restart). +# Killt jede laufende Instanz auf 8081, dann frischer Start mit --clear-Cache. +# Aufruf: ./metro.sh (mit cache-clear, Default) +# ./metro.sh --keep (ohne --clear, schneller wenn keine Dependency-Changes) +set -e + +cd "$(dirname "$0")" + +echo "🚇 Metro Bundler" +echo "================" + +# 1) Existierende Metro-Instanz auf Port 8081 killen +PIDS=$(lsof -iTCP:8081 -sTCP:LISTEN -n -P 2>/dev/null | awk 'NR>1 {print $2}' | sort -u) +if [ -n "$PIDS" ]; then + echo "→ Killing existing Metro on :8081 (PIDs: $PIDS)" + echo "$PIDS" | xargs kill -9 2>/dev/null || true + sleep 1 +else + echo "→ Kein Metro auf :8081 aktiv" +fi + +# 2) Stale node-Prozesse die expo CLI gestartet haben (Belt-and-Suspenders) +pkill -f "expo start" 2>/dev/null || true +pkill -f "react-native/cli/build" 2>/dev/null || true + +# 3) Start +if [ "$1" = "--keep" ]; then + echo "→ Starte Metro (Cache behalten)" + exec npx expo start +else + echo "→ Starte Metro mit --clear (Haste-Map + Transformer-Cache reset)" + exec npx expo start --clear +fi diff --git a/apps/rebreak-native/modules/rebreak-protection/expo-module.config.json b/apps/rebreak-native/modules/rebreak-protection/expo-module.config.json new file mode 100644 index 0000000..4a6ed44 --- /dev/null +++ b/apps/rebreak-native/modules/rebreak-protection/expo-module.config.json @@ -0,0 +1,9 @@ +{ + "platforms": ["apple", "android"], + "apple": { + "modules": ["RebreakProtectionModule"] + }, + "android": { + "modules": ["expo.modules.rebreakprotection.RebreakProtectionModule"] + } +} diff --git a/apps/rebreak-native/modules/rebreak-protection/index.ts b/apps/rebreak-native/modules/rebreak-protection/index.ts new file mode 100644 index 0000000..19650f0 --- /dev/null +++ b/apps/rebreak-native/modules/rebreak-protection/index.ts @@ -0,0 +1,17 @@ +import RebreakProtectionModule from './src/RebreakProtectionModule'; + +export type { + ActivateResult, + DeviceLayers, + DisableResult, + HealthProbeOpts, + HealthProbeOutcome, + HealthProbeResult, + ProtectionLayerKey, + RebreakProtectionEvents, + SyncBlocklistOpts, + SyncBlocklistResult, + SystemSettingsTarget, +} from './src/RebreakProtection.types'; + +export default RebreakProtectionModule; diff --git a/apps/rebreak-native/modules/rebreak-protection/package.json b/apps/rebreak-native/modules/rebreak-protection/package.json new file mode 100644 index 0000000..434b291 --- /dev/null +++ b/apps/rebreak-native/modules/rebreak-protection/package.json @@ -0,0 +1,8 @@ +{ + "name": "rebreak-protection", + "version": "0.1.0", + "private": true, + "description": "ReBreak unified protection module — NEFilter + Family Controls (iOS) + VpnService + AccessibilityService + Tamper Lock (Android).", + "main": "index.ts", + "types": "index.ts" +} diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts new file mode 100644 index 0000000..f0a652e --- /dev/null +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts @@ -0,0 +1,92 @@ +/** + * Device-local layer state — was tatsächlich an/aus ist auf dem Gerät. + * Plattform-spezifisch: iOS hat urlFilter+familyControls, Android hat + * vpn+accessibility+tamperLock. Felder die auf der jeweils anderen + * Plattform irrelevant sind, sind undefined. + */ +export type DeviceLayers = { + // iOS + urlFilter?: boolean; + familyControls?: boolean; + appDeletionLock?: boolean; + // Android + vpn?: boolean; + accessibility?: boolean; + tamperLock?: boolean; + // Shared + blocklistCount: number; + blocklistLastSyncAt: string | null; +}; + +export type ActivateResult = { + allLayersOn: boolean; + /** + * Welche Layer der User noch aktivieren muss (z.B. weil ein + * System-Permission-Dialog abgelehnt wurde). Wenn allLayersOn=true → leer. + */ + missingLayers: ProtectionLayerKey[]; + /** + * Detaillierte Native-Errors für UX-relevante Diagnose. Leer wenn alle + * Layers durchgekommen sind. Beispiele: NEFilter saveToPreferences-Failure, + * Family Controls Authorization denied. + */ + errors?: string[]; +}; + +export type DisableResult = { + allLayersOff: boolean; +}; + +export type ProtectionLayerKey = + | "urlFilter" + | "familyControls" + | "appDeletionLock" + | "vpn" + | "accessibility" + | "tamperLock"; + +export type SyncBlocklistOpts = { + baseURL: string; + authToken: string; +}; + +export type SyncBlocklistResult = { + updated: boolean; + count: number; + plan?: string; +}; + +export type HealthProbeOutcome = "blocked" | "loaded" | "offline" | "timeout"; + +export type HealthProbeOpts = { + /** Default: https://bet365.com — sollte sicher in der Blocklist stehen. */ + target?: string; + /** Default 5s. */ + timeoutSeconds?: number; +}; + +export type HealthProbeResult = { + outcome: HealthProbeOutcome; + reason: string; + durationMs: number; + target: string; +}; + +export type SystemSettingsTarget = + /** Android: Settings → Network → VPN. */ + | "vpn" + /** Android: Settings → Accessibility (Bedienungshilfen). */ + | "accessibility" + /** iOS: Settings → Screen Time → Family Controls. */ + | "screenTime" + /** iOS+Android: App-Notifications-Settings. */ + | "notifications"; + +/** + * Events die das native Modul fired wenn sich Device-Layer State ändert + * (z.B. User schaltet VPN extern aus, deaktiviert A11y, oder NEFilter-Config + * wurde von außen entfernt). Der App-State-Watchdog hängt darauf. + */ +export type RebreakProtectionEvents = { + onLayerChange: (state: DeviceLayers) => void; +}; diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts new file mode 100644 index 0000000..4958390 --- /dev/null +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts @@ -0,0 +1,92 @@ +import { NativeModule, requireNativeModule } from 'expo'; + +import type { + ActivateResult, + DeviceLayers, + DisableResult, + HealthProbeOpts, + HealthProbeResult, + RebreakProtectionEvents, + SyncBlocklistOpts, + SyncBlocklistResult, + SystemSettingsTarget, +} from './RebreakProtection.types'; + +declare class RebreakProtectionModule extends NativeModule { + /** + * iOS: aktiviert NUR den NEFilter (URL-Filter Layer). + * Triggert iOS-Dialog "Filter-Konfiguration zulassen". + */ + activateUrlFilter(): Promise<{ enabled: boolean; error?: string }>; + + /** + * iOS: aktiviert NUR Family Controls (Auth + denyAppRemoval = der Lock). + * Triggert iOS-Dialog "Bildschirmzeit verwalten". + * Sobald aktiv, kann der User den Schutz nur über Cooldown deaktivieren. + */ + activateFamilyControls(): Promise<{ enabled: boolean; error?: string }>; + + /** + * Aktiviert ALLE Schutz-Layer in einem Call (legacy, beide Dialoge nacheinander). + * Bevorzugt activateUrlFilter() + activateFamilyControls() einzeln aufrufen. + */ + activate(): Promise; + + /** + * Schaltet ALLE Schutz-Layer ab. NUR aufrufen wenn JS-Layer verifiziert + * hat dass der 24h-Cooldown abgelaufen ist. Native-Modul prüft das nicht + * — der Backend-Cooldown ist Single Source of Truth, das ist Aufgabe der + * JS-Schicht. + */ + disable(): Promise; + + /** Aktueller Device-State. Polling- und Health-Check-Pfad. */ + getDeviceState(): Promise; + + /** + * Lädt blocklist.bin vom Server, schreibt atomisch in App-Group/internal + * storage, postet Reload-Notification an die Filter-Extension. Server + * respondet 304 wenn ETag matched → updated=false. Plan-aware: + * Free → nur personal-domains (≤5), Pro/Legend → 208k+ + personal. + */ + syncBlocklist(opts: SyncBlocklistOpts): Promise; + + /** + * E2E-Verifikation: Hidden WebView lädt eine bekannte Gambling-Domain, + * prüft ob WebKit/Browser den Load aborted (Filter funktioniert) oder die + * Page lädt (Filter ist tot — Alarm). + */ + runHealthProbe(opts?: HealthProbeOpts): Promise; + + /** Öffnet System-Settings auf dem entsprechenden Tab. */ + openSystemSettings(target?: SystemSettingsTarget): Promise; + + // ─── Android-spezifische Methoden (auf iOS undefined zur Laufzeit) ─────── + + /** Android: Live-Check ob unser AccessibilityService aktuell als enabled + * registriert ist (Settings.Secure + AccessibilityManager). */ + isAccessibilityEnabled(): Promise<{ enabled: boolean }>; + + /** Android: Öffnet Settings → Bedienungshilfen, möglichst tief auf die + * Rebreak-Detail-Page (deep-link). Fallback: generelle A11y-Liste. */ + openAccessibilitySettings(): Promise<{ opened: boolean }>; + + /** Android: Aktiviert Tamper-Lock-Watchdog (Settings-Page-Blockade durch + * AccessibilityService). Wirft `preconditions_not_met` wenn VPN oder A11y + * nicht beide live. */ + armTamperLock(): Promise<{ armed: boolean }>; + + /** Android: Disarm Tamper-Lock. Schutz-Layers laufen weiter, aber Settings- + * Watchdog blockt nicht mehr. Im normalen Flow nur nach Cooldown-Ablauf. */ + disarmTamperLock(): Promise<{ armed: boolean }>; + + /** Android: kombinierter Status aller 3 Layers + Blocklist-Count. */ + getProtectionStatus(): Promise<{ + vpnEnabled: boolean; + accessibilityEnabled: boolean; + blocklistCount: number; + tamperArmed: boolean; + }>; +} + +export default requireNativeModule('RebreakProtection'); diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts new file mode 100644 index 0000000..ceca35c --- /dev/null +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts @@ -0,0 +1,69 @@ +/** + * Web-Stub. Ergibt im Browser keinen funktionalen Schutz — der Filter ist + * inhärent device-bound. Verhindert nur dass Imports auf Web crashen. + */ +import { registerWebModule, NativeModule } from 'expo'; + +import type { + ActivateResult, + DeviceLayers, + DisableResult, + HealthProbeResult, + RebreakProtectionEvents, + SyncBlocklistResult, +} from './RebreakProtection.types'; + +class RebreakProtectionModuleWeb extends NativeModule { + async activate(): Promise { + return { allLayersOn: false, missingLayers: [] }; + } + + async disable(): Promise { + return { allLayersOff: true }; + } + + async getDeviceState(): Promise { + return { blocklistCount: 0, blocklistLastSyncAt: null }; + } + + async syncBlocklist(): Promise { + return { updated: false, count: 0 }; + } + + async runHealthProbe(): Promise { + return { + outcome: 'offline', + reason: 'web_stub', + durationMs: 0, + target: '', + }; + } + + async openSystemSettings(): Promise { + // no-op + } + + // Android-only stubs (Web nutzt keinen davon, aber Type-Compat). + async isAccessibilityEnabled() { + return { enabled: false }; + } + async openAccessibilitySettings() { + return { opened: false }; + } + async armTamperLock() { + return { armed: false }; + } + async disarmTamperLock() { + return { armed: false }; + } + async getProtectionStatus() { + return { + vpnEnabled: false, + accessibilityEnabled: false, + blocklistCount: 0, + tamperArmed: false, + }; + } +} + +export default registerWebModule(RebreakProtectionModuleWeb, 'RebreakProtection'); diff --git a/apps/rebreak-native/nativewind-env.d.ts b/apps/rebreak-native/nativewind-env.d.ts new file mode 100644 index 0000000..c0d8380 --- /dev/null +++ b/apps/rebreak-native/nativewind-env.d.ts @@ -0,0 +1,3 @@ +/// + +// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. \ No newline at end of file diff --git a/apps/rebreak-native/package.json b/apps/rebreak-native/package.json new file mode 100644 index 0000000..34c0d01 --- /dev/null +++ b/apps/rebreak-native/package.json @@ -0,0 +1,71 @@ +{ + "name": "@trucko/rebreak-native", + "version": "0.1.0", + "private": true, + "main": "expo-router/entry", + "scripts": { + "start": "expo start --dev-client", + "ios": "expo run:ios", + "android": "expo run:android", + "prebuild": "expo prebuild --clean", + "lint": "expo lint", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@expo-google-fonts/nunito": "^0.2.3", + "@expo/vector-icons": "^14.0.0", + "@react-native-async-storage/async-storage": "^2.1.2", + "@react-native-community/slider": "^5.2.0", + "@react-navigation/native": "^7.0.0", + "@supabase/supabase-js": "^2.46.0", + "@tanstack/react-query": "^5.59.0", + "expo": "^53.0.0", + "expo-apple-authentication": "~7.2.4", + "expo-application": "~6.1.5", + "expo-av": "~15.1.7", + "expo-build-properties": "~0.14.8", + "expo-clipboard": "^55.0.13", + "expo-constants": "~17.1.8", + "expo-dev-client": "~5.2.4", + "expo-file-system": "~18.1.11", + "expo-font": "~13.0.0", + "expo-haptics": "^55.0.14", + "expo-image-picker": "~16.1.4", + "expo-linking": "~7.1.7", + "expo-localization": "~16.1.6", + "expo-modules-core": "^2.0.0", + "expo-notifications": "~0.31.5", + "expo-router": "~5.1.11", + "expo-speech": "~13.1.7", + "expo-splash-screen": "~0.30.10", + "expo-status-bar": "~2.2.3", + "expo-web-browser": "~14.2.0", + "i18next": "^23.16.0", + "lottie-react-native": "7.2.2", + "nativewind": "^4.1.0", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-hook-form": "^7.53.0", + "react-i18next": "^15.1.0", + "react-native": "0.79.6", + "react-native-bottom-tabs": "^1.2.0", + "react-native-gesture-handler": "~2.24.0", + "react-native-mmkv": "^3.1.0", + "react-native-reanimated": "~4.0.0", + "react-native-safe-area-context": "5.4.0", + "react-native-screens": "~4.11.1", + "react-native-sse": "^1.2.1", + "react-native-svg": "15.11.2", + "react-native-url-polyfill": "^2.0.0", + "react-native-worklets": "~0.4.0", + "rive-react-native": "^9.0.1", + "tailwindcss": "^3.4.14", + "valibot": "^1.2.0", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@babel/core": "^7.25.0", + "@types/react": "~19.0.14", + "typescript": "~5.8.3" + } +} diff --git a/apps/rebreak-native/plugins/with-fmt-consteval-fix.js b/apps/rebreak-native/plugins/with-fmt-consteval-fix.js new file mode 100644 index 0000000..7667806 --- /dev/null +++ b/apps/rebreak-native/plugins/with-fmt-consteval-fix.js @@ -0,0 +1,144 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/** + * Workaround für Xcode 16 + RN 0.79 + fmt 11.0.2: + * "Call to consteval function 'fmt::basic_format_string<...>' is not a constant expression" + * + * Grund: fmt/include/fmt/base.h definiert FMT_USE_CONSTEVAL UNCONDITIONAL — + * kein `#ifndef`-Guard. Daher hilft `-DFMT_USE_CONSTEVAL=0` als Compiler-Flag + * NICHT — fmt's eigener Header überschreibt es. + * + * Wir patchen daher direkt die Source-Datei nach `pod install`: + * - In ios/Pods/fmt/include/fmt/base.h einen Override-Block einfügen, der + * nach fmt's eigener Detection FMT_USE_CONSTEVAL auf 0 zwingt. + * + * Wirkung: + * - basic_format_string-Konstruktor ist nicht mehr consteval, sondern constexpr + * - FMT_STRING("{}{}") expansion compiliert wieder unter Apple Clang 16+ + * + * Idempotent — markiert die Patch-Stelle mit einem Magic-Comment. + */ +const { withDangerousMod } = require('@expo/config-plugins'); +const fs = require('fs'); +const path = require('path'); + +const PATCH_MARKER = '/* REBREAK_FMT_CONSTEVAL_FIX */'; + +const SOURCE_PATCH = ` + +${PATCH_MARKER} +// Xcode 16 + Apple Clang 16+ haben einen consteval-Bug der fmt's +// FMT_STRING-basierte format_to-Calls bricht. Wir zwingen daher +// FMT_USE_CONSTEVAL=0 nach fmt's eigener Detection. +#undef FMT_USE_CONSTEVAL +#define FMT_USE_CONSTEVAL 0 +#undef FMT_CONSTEVAL +#define FMT_CONSTEVAL +#undef FMT_CONSTEXPR20 +#define FMT_CONSTEXPR20 +${PATCH_MARKER} +`; + +function patchFmtBaseHeader(podsDir) { + const baseHeader = path.join(podsDir, 'fmt', 'include', 'fmt', 'base.h'); + if (!fs.existsSync(baseHeader)) { + console.warn('[with-fmt-consteval-fix] fmt/base.h not found at', baseHeader); + return false; + } + + let content = fs.readFileSync(baseHeader, 'utf-8'); + if (content.includes(PATCH_MARKER)) { + return false; // schon gepatcht + } + + // Patch nach der Detection-Block einfügen, vor dem `#if FMT_USE_CONSTEVAL` + // Suche nach dem End-of-Detection (line endet mit `#endif` direkt vor + // `#if FMT_USE_CONSTEVAL`). + const anchor = '#if FMT_USE_CONSTEVAL'; + const idx = content.indexOf(anchor); + if (idx === -1) { + console.warn('[with-fmt-consteval-fix] anchor not found in base.h'); + return false; + } + + // Patch direkt vor dem anchor einfügen + content = content.slice(0, idx) + SOURCE_PATCH + '\n' + content.slice(idx); + fs.writeFileSync(baseHeader, content); + console.log('[with-fmt-consteval-fix] patched', baseHeader); + return true; +} + +module.exports = function withFmtConstevalFix(config) { + return withDangerousMod(config, [ + 'ios', + async (cfg) => { + const podsDir = path.join(cfg.modRequest.platformProjectRoot, 'Pods'); + // Wenn Pods/ noch nicht existiert (= prebuild Phase, vor pod install): + // Patch wird automatisch beim nächsten Run angewendet sobald Pods da sind. + // Damit der User aber NICHT manuell nachpatchen muss, packen wir den + // Patch zusätzlich in einen Podfile-pre_install-Hook der bei JEDEM + // pod install läuft (auch beim ersten). + if (fs.existsSync(podsDir)) { + patchFmtBaseHeader(podsDir); + } + + // Podfile pre_install-Hook injizieren — patched die fmt-Source bei + // jedem pod install (nachdem Pods/fmt/ bereits gedownloaded ist). + const podfilePath = path.join(cfg.modRequest.platformProjectRoot, 'Podfile'); + let podfile = fs.readFileSync(podfilePath, 'utf-8'); + + // Alten Patch (frühere Plugin-Version) entfernen + const oldPatchRegex = /\s*# ─── Rebreak: fmt consteval fix[\s\S]*?# ───────────────────────────────────────────────────────/g; + podfile = podfile.replace(oldPatchRegex, ''); + const oldPostInstallPatchRegex = /\s*# ═══ Rebreak: fmt consteval source-patch[\s\S]*?# ═══════════════════════════════════════════════════════/g; + podfile = podfile.replace(oldPostInstallPatchRegex, ''); + + // Neuen pre_install-Hook injizieren (vor `target` block). + // Wir patchen die fmt/base.h-Datei direkt im pre_install — pod install + // hat dann zu dem Zeitpunkt schon den Pod gedownloaded. + const PRE_INSTALL = ` +# ═══ Rebreak: fmt consteval source-patch (Xcode 16 + RN 0.79) ═══ +pre_install do |installer| + fmt_base_h = File.join(installer.sandbox.root, 'fmt', 'include', 'fmt', 'base.h') + if File.exist?(fmt_base_h) + content = File.read(fmt_base_h) + marker = '/* REBREAK_FMT_CONSTEVAL_FIX */' + unless content.include?(marker) + patch = <<~PATCH + +#{marker} +// Xcode 16 + Apple Clang 16+ consteval-Bug Workaround +#undef FMT_USE_CONSTEVAL +#define FMT_USE_CONSTEVAL 0 +#undef FMT_CONSTEVAL +#define FMT_CONSTEVAL +#undef FMT_CONSTEXPR20 +#define FMT_CONSTEXPR20 +#{marker} + PATCH + anchor = '#if FMT_USE_CONSTEVAL' + if content.include?(anchor) + content = content.sub(anchor, patch + "\\n" + anchor) + File.write(fmt_base_h, content) + Pod::UI.puts " -> Patched fmt/base.h with consteval workaround".green + end + end + end +end +# ═══════════════════════════════════════════════════════ +`; + + // Inject vor dem ersten `target` block (Top-level) + const targetMatch = podfile.match(/^target\s+['"][^'"]+['"]\s+do/m); + if (targetMatch) { + const insertAt = targetMatch.index; + podfile = podfile.slice(0, insertAt) + PRE_INSTALL + '\n' + podfile.slice(insertAt); + } else { + // Fallback: ans Ende anhängen + podfile += PRE_INSTALL; + } + + fs.writeFileSync(podfilePath, podfile); + return cfg; + }, + ]); +}; diff --git a/apps/rebreak-native/plugins/with-rebreak-protection-android.js b/apps/rebreak-native/plugins/with-rebreak-protection-android.js new file mode 100644 index 0000000..f9b3111 --- /dev/null +++ b/apps/rebreak-native/plugins/with-rebreak-protection-android.js @@ -0,0 +1,127 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/** + * Expo Config-Plugin — wires the Android VpnService (DNS-Filter) + + * AccessibilityService (URL filter Layer 2) into AndroidManifest.xml at + * prebuild time. + * + * Was es macht: + * 1) Sorgt für `xmlns:tools` auf . + * 2) Registriert mit + * foregroundServiceType="systemExempted" + intent-filter + * android.net.VpnService + permission BIND_VPN_SERVICE. + * (`systemExempted` ist seit Android 14 der korrekte Type für + * VPN-/Filter-Foreground-Services — vorher war `specialUse`+content_filter + * angedacht aber bringt mehr Probleme als Nutzen.) + * 3) Registriert mit + * android:permission=BIND_ACCESSIBILITY_SERVICE + intent-filter + * android.accessibilityservice.AccessibilityService + meta-data + * android.accessibilityservice → @xml/accessibility_service_config. + * + * Wird aus app.config.ts via `plugins: ['./plugins/with-rebreak-protection-android']` + * registriert. Idempotent — kann beliebig oft via `expo prebuild` laufen. + * + * Native Source: `modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/` + * - VpnService: expo.modules.rebreakprotection.vpn.RebreakVpnService + * - AccessibilityService: expo.modules.rebreakprotection.accessibility.RebreakAccessibilityService + */ + +const { + withAndroidManifest, + AndroidConfig, +} = require('@expo/config-plugins'); + +const VPN_SERVICE_CLASS = + 'expo.modules.rebreakprotection.vpn.RebreakVpnService'; +const A11Y_SERVICE_CLASS = + 'expo.modules.rebreakprotection.accessibility.RebreakAccessibilityService'; + +// ─── 1) tools-Namespace auf ────────────────────────────────────── + +function ensureToolsNamespace(manifest) { + if (!manifest.manifest.$) manifest.manifest.$ = {}; + if (!manifest.manifest.$['xmlns:tools']) { + manifest.manifest.$['xmlns:tools'] = 'http://schemas.android.com/tools'; + } +} + +// ─── 2) -Tag für RebreakVpnService ───────────────────────────────── + +function ensureVpnService(manifest) { + const application = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest); + + if (!application.service) application.service = []; + + const alreadyDeclared = application.service.some( + (svc) => svc.$ && svc.$['android:name'] === VPN_SERVICE_CLASS, + ); + if (alreadyDeclared) return; + + application.service.push({ + $: { + 'android:name': VPN_SERVICE_CLASS, + 'android:permission': 'android.permission.BIND_VPN_SERVICE', + 'android:foregroundServiceType': 'systemExempted', + 'android:exported': 'false', + }, + 'intent-filter': [ + { + action: [{ $: { 'android:name': 'android.net.VpnService' } }], + }, + ], + }); +} + +// ─── 3) -Tag für RebreakAccessibilityService ─────────────────────── + +function ensureAccessibilityService(manifest) { + const application = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest); + + if (!application.service) application.service = []; + + const alreadyDeclared = application.service.some( + (svc) => svc.$ && svc.$['android:name'] === A11Y_SERVICE_CLASS, + ); + if (alreadyDeclared) return; + + application.service.push({ + $: { + 'android:name': A11Y_SERVICE_CLASS, + 'android:permission': 'android.permission.BIND_ACCESSIBILITY_SERVICE', + 'android:label': '@string/accessibility_service_summary', + 'android:exported': 'true', + }, + 'intent-filter': [ + { + action: [ + { + $: { + 'android:name': + 'android.accessibilityservice.AccessibilityService', + }, + }, + ], + }, + ], + 'meta-data': [ + { + $: { + 'android:name': 'android.accessibilityservice', + 'android:resource': '@xml/accessibility_service_config', + }, + }, + ], + }); +} + +// ─── Composition ──────────────────────────────────────────────────────────── + +function withRebreakProtectionAndroid(config) { + return withAndroidManifest(config, (cfg) => { + ensureToolsNamespace(cfg.modResults); + ensureVpnService(cfg.modResults); + ensureAccessibilityService(cfg.modResults); + return cfg; + }); +} + +module.exports = withRebreakProtectionAndroid; diff --git a/apps/rebreak-native/plugins/with-rebreak-protection-ios.js b/apps/rebreak-native/plugins/with-rebreak-protection-ios.js new file mode 100644 index 0000000..d35b84c --- /dev/null +++ b/apps/rebreak-native/plugins/with-rebreak-protection-ios.js @@ -0,0 +1,192 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/** + * Expo Config-Plugin — wires the NEFilter Extension target into the iOS + * project at prebuild time. + * + * Was es macht: + * 1) Setzt die Entitlements der Haupt-App (family-controls, network- + * extension, app-groups). + * 2) Kopiert `modules/rebreak-protection/ios/RebreakURLFilter/` nach + * `ios/RebreakURLFilter/` (idempotent). + * 3) Fügt einen neuen Xcode-Target `RebreakURLFilter` (Bundle-ID + * `org.rebreak.app.RebreakURLFilter`) zum Projekt hinzu, mit: + * - Source-File: FilterControlProvider.swift + * - NetworkExtension.framework + * - Embed-App-Extensions Build-Phase im Haupt-Target + * - Entitlements via `RebreakURLFilter.entitlements` + * + * Wird aus app.config.ts via `plugins: ['./plugins/with-rebreak-protection-ios']` + * registriert. Idempotent — kann beliebig oft via `expo prebuild` laufen. + */ + +const fs = require('fs'); +const path = require('path'); + +const { + withEntitlementsPlist, + withDangerousMod, + withXcodeProject, +} = require('@expo/config-plugins'); + +const APP_GROUP = 'group.org.rebreak.app'; +const TARGET_NAME = 'RebreakURLFilter'; +const EXT_BUNDLE_SUFFIX = 'RebreakURLFilter'; +const MODULE_DIR = path.join( + __dirname, + '..', + 'modules', + 'rebreak-protection', + 'ios', + TARGET_NAME, +); + +// ─── 1) Haupt-App Entitlements ────────────────────────────────────────────── + +function withMainAppEntitlements(config) { + return withEntitlementsPlist(config, (cfg) => { + cfg.modResults['com.apple.developer.networking.networkextension'] = [ + 'content-filter-provider', + ]; + cfg.modResults['com.apple.developer.family-controls'] = true; + const groups = cfg.modResults['com.apple.security.application-groups'] || []; + if (!groups.includes(APP_GROUP)) { + cfg.modResults['com.apple.security.application-groups'] = [...groups, APP_GROUP]; + } + return cfg; + }); +} + +// ─── 2) Extension-Sources ins ios/-Verzeichnis kopieren ───────────────────── + +function withCopyExtensionSources(config) { + return withDangerousMod(config, [ + 'ios', + async (cfg) => { + const platformProjectRoot = cfg.modRequest.platformProjectRoot; + const dest = path.join(platformProjectRoot, TARGET_NAME); + if (!fs.existsSync(MODULE_DIR)) { + throw new Error( + `[with-rebreak-protection-ios] Extension source dir missing: ${MODULE_DIR}`, + ); + } + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + for (const file of fs.readdirSync(MODULE_DIR)) { + const srcFile = path.join(MODULE_DIR, file); + const destFile = path.join(dest, file); + fs.copyFileSync(srcFile, destFile); + } + return cfg; + }, + ]); +} + +// ─── 3) Xcode-Target hinzufügen ────────────────────────────────────────────── + +function withExtensionTarget(config) { + return withXcodeProject(config, async (cfg) => { + const proj = cfg.modResults; + + // Idempotenz: skip wenn Target schon angelegt + if (proj.pbxTargetByName(TARGET_NAME)) { + return cfg; + } + + const mainBundleId = cfg.ios?.bundleIdentifier; + if (!mainBundleId) { + throw new Error('[with-rebreak-protection-ios] ios.bundleIdentifier fehlt in app.config'); + } + const extBundleId = `${mainBundleId}.${EXT_BUNDLE_SUFFIX}`; + + // ── Target anlegen (Type: app_extension) ── + const target = proj.addTarget(TARGET_NAME, 'app_extension', TARGET_NAME, extBundleId); + + // ── Build-Phasen: Sources + Frameworks + Resources ── + proj.addBuildPhase( + ['FilterControlProvider.swift'], + 'PBXSourcesBuildPhase', + 'Sources', + target.uuid, + ); + proj.addBuildPhase( + ['NetworkExtension.framework'], + 'PBXFrameworksBuildPhase', + 'Frameworks', + target.uuid, + ); + // Info.plist gehört NICHT als Resource — wird via INFOPLIST_FILE referenziert. + + // ── PBXGroup für die Sources ── + const pbxGroup = proj.addPbxGroup( + ['FilterControlProvider.swift', 'Info.plist', 'RebreakURLFilter.entitlements'], + TARGET_NAME, + TARGET_NAME, + ); + // Group ans CustomTemplate-Group hängen damit sie im Project Navigator erscheint + const groups = proj.hash.project.objects.PBXGroup; + Object.keys(groups).forEach((key) => { + if ( + groups[key].name === 'CustomTemplate' || + (groups[key].name === undefined && groups[key].path === undefined) + ) { + proj.addToPbxGroup(pbxGroup.uuid, key); + } + }); + + // ── Build-Settings auf der Target-Configuration anpassen ── + const configurations = proj.pbxXCBuildConfigurationSection(); + Object.keys(configurations) + .filter((k) => typeof configurations[k] === 'object') + .forEach((k) => { + const buildSettingsObj = configurations[k].buildSettings; + if ( + buildSettingsObj && + buildSettingsObj.PRODUCT_NAME && + buildSettingsObj.PRODUCT_NAME.replace(/"/g, '') === TARGET_NAME + ) { + buildSettingsObj.INFOPLIST_FILE = `"${TARGET_NAME}/Info.plist"`; + buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${TARGET_NAME}/${TARGET_NAME}.entitlements"`; + buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '15.1'; + buildSettingsObj.SWIFT_VERSION = '5.9'; + buildSettingsObj.TARGETED_DEVICE_FAMILY = '"1,2"'; + buildSettingsObj.CODE_SIGN_STYLE = 'Automatic'; + } + }); + + // ── Embed App Extensions Build-Phase im Haupt-Target ── + // Suche nach existierender CopyFilesBuildPhase mit Comment "Embed App Extensions" + const mainTargetUuid = proj.getFirstTarget().uuid; + const buildPhases = proj.hash.project.objects.PBXNativeTarget[mainTargetUuid].buildPhases; + const copyFilesPhases = proj.hash.project.objects.PBXCopyFilesBuildPhase || {}; + const hasEmbedPhase = Object.keys(copyFilesPhases).some((key) => { + const phase = copyFilesPhases[key]; + return ( + typeof phase === 'object' && + phase.dstSubfolderSpec === 13 && // 13 = PluginsAndFrameworks (App Extensions) + buildPhases.some((bp) => bp.value === key) + ); + }); + if (!hasEmbedPhase) { + proj.addBuildPhase( + [`${TARGET_NAME}.appex`], + 'PBXCopyFilesBuildPhase', + 'Embed App Extensions', + mainTargetUuid, + 'app_extension', // dstSubfolderSpec=13 + ); + } + + // ── Target-Dependency: Haupt-App muss Extension vor sich bauen ── + proj.addTargetDependency(mainTargetUuid, [target.uuid]); + + return cfg; + }); +} + +// ─── Composition ──────────────────────────────────────────────────────────── + +module.exports = function withRebreakProtectionIos(config) { + config = withMainAppEntitlements(config); + config = withCopyExtensionSources(config); + config = withExtensionTarget(config); + return config; +}; diff --git a/apps/rebreak-native/plugins/with-rive-asset-android.js b/apps/rebreak-native/plugins/with-rive-asset-android.js new file mode 100644 index 0000000..c2fc81d --- /dev/null +++ b/apps/rebreak-native/plugins/with-rive-asset-android.js @@ -0,0 +1,50 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/** + * Expo Config-Plugin — kopiert das Rive-Asset (lyra-avatar.riv) bei prebuild + * nach android/app/src/main/res/raw/lyra_avatar.riv damit Rive-Native auf + * Android es als raw-resource laden kann. + * + * Hintergrund: rive-react-native auf Android akzeptiert NUR + * - resourceName="lyra_avatar" (raw-resource-Name, lowercase+underscores) + * - url="https://..." (remote) + * + * Mit `source={{ uri: 'file:///...' }}` (was iOS via expo-asset gut findet) + * crasht Android: "File resource not found. You must provide correct url + * or resourceName!" + * + * Fix: bei jedem prebuild .riv aus assets/ in res/raw/ kopieren. + * + * Usage in app.config.ts: plugins: ['./plugins/with-rive-asset-android'] + */ + +const fs = require('fs'); +const path = require('path'); +const { withDangerousMod } = require('@expo/config-plugins'); + +const SOURCE = path.join(__dirname, '..', 'assets', 'lyra-avatar.riv'); +// Android raw-resource convention: lowercase + underscores (NICHT hyphens). +const TARGET_FILENAME = 'lyra_avatar.riv'; + +function withRiveAssetAndroid(config) { + return withDangerousMod(config, [ + 'android', + async (cfg) => { + const platformProjectRoot = cfg.modRequest.platformProjectRoot; + const targetDir = path.join(platformProjectRoot, 'app', 'src', 'main', 'res', 'raw'); + const target = path.join(targetDir, TARGET_FILENAME); + + if (!fs.existsSync(SOURCE)) { + throw new Error(`[with-rive-asset-android] Quelle fehlt: ${SOURCE}`); + } + + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + fs.copyFileSync(SOURCE, target); + return cfg; + }, + ]); +} + +module.exports = withRiveAssetAndroid; diff --git a/apps/rebreak-native/scripts/fix-embed-extension.js b/apps/rebreak-native/scripts/fix-embed-extension.js new file mode 100644 index 0000000..4dbcd6b --- /dev/null +++ b/apps/rebreak-native/scripts/fix-embed-extension.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node +/** + * One-shot Fix: fügt die fehlende "Embed App Extensions" Build-Phase ins + * existierende Xcode-Projekt ein, ohne den ganzen Prebuild zu wiederholen. + * + * Wird auch in `with-rebreak-protection-ios.js` als Logik referenziert, + * damit zukünftige Prebuilds das richtig hinkriegen. + * + * Usage: node scripts/fix-embed-extension.js + */ +const fs = require('fs'); +const path = require('path'); +const xcode = require('xcode'); + +const PROJECT_ROOT = path.resolve(__dirname, '..'); +const PBXPROJ = path.join(PROJECT_ROOT, 'ios', 'Rebreak.xcodeproj', 'project.pbxproj'); +const TARGET_NAME = 'RebreakURLFilter'; + +if (!fs.existsSync(PBXPROJ)) { + console.error('❌ pbxproj not found at:', PBXPROJ); + process.exit(1); +} + +const project = xcode.project(PBXPROJ); +project.parseSync(); + +// 1) Find Main + Extension targets +const mainTarget = project.getFirstTarget(); +if (!mainTarget) { + console.error('❌ no main target found'); + process.exit(1); +} +const mainTargetUuid = mainTarget.uuid; +const extTargetUuid = project.findTargetKey(TARGET_NAME); +if (!extTargetUuid) { + console.error(`❌ Extension target "${TARGET_NAME}" not found`); + process.exit(1); +} + +console.log(`✓ Main target: ${mainTarget.firstTarget.name} (${mainTargetUuid})`); +console.log(`✓ Extension target: ${TARGET_NAME} (${extTargetUuid})`); + +// 2) Check existing Embed phase +const buildPhases = project.hash.project.objects.PBXNativeTarget[mainTargetUuid].buildPhases || []; +const copyFilesPhases = project.hash.project.objects.PBXCopyFilesBuildPhase || {}; + +let hasEmbedPhase = false; +for (const key of Object.keys(copyFilesPhases)) { + const phase = copyFilesPhases[key]; + if (typeof phase !== 'object') continue; + if (!buildPhases.some((bp) => bp.value === key)) continue; + // dstSubfolderSpec kann String '13' oder Number 13 sein — beide checken + if (phase.dstSubfolderSpec === 13 || phase.dstSubfolderSpec === '13') { + hasEmbedPhase = true; + console.log(`✓ Embed phase already exists: ${key}`); + break; + } +} + +if (hasEmbedPhase) { + console.log('✓ Already has Embed App Extensions phase'); +} else { + console.log('→ Adding Embed App Extensions phase to main target...'); + const newPhase = project.addBuildPhase( + [`${TARGET_NAME}.appex`], + 'PBXCopyFilesBuildPhase', + 'Embed App Extensions', + mainTargetUuid, + 'app_extension', + ); + const phaseObj = project.hash.project.objects.PBXCopyFilesBuildPhase[newPhase.uuid]; + if (phaseObj && phaseObj.dstSubfolderSpec !== 13 && phaseObj.dstSubfolderSpec !== '13') { + phaseObj.dstSubfolderSpec = 13; + phaseObj.dstPath = ''; + } +} + +// Target-Dependency: Main → Extension (zwingt Build-Order) +console.log('→ Ensuring target dependency (Main → Extension)...'); +const mainObj = project.hash.project.objects.PBXNativeTarget[mainTargetUuid]; +const existingDeps = mainObj.dependencies || []; +const dependencyTargets = project.hash.project.objects.PBXTargetDependency || {}; +const hasDepToExt = existingDeps.some((dep) => { + const depObj = dependencyTargets[dep.value]; + return depObj && depObj.target === extTargetUuid; +}); +if (!hasDepToExt) { + project.addTargetDependency(mainTargetUuid, [extTargetUuid]); + console.log('✓ Target dependency added'); +} else { + console.log('✓ Target dependency already exists'); +} + +// Save & exit (von hier — die ursprüngliche save-Logik unten wurde durch dieses Block ersetzt) +fs.writeFileSync(PBXPROJ, project.writeSync()); +console.log('✅ Saved pbxproj'); +console.log('Now: in Xcode → Cmd+Shift+K (clean) → Cmd+B (build)'); +process.exit(0); + +// 3) Add Embed phase +console.log('→ Adding Embed App Extensions phase to main target...'); +const newPhase = project.addBuildPhase( + [`${TARGET_NAME}.appex`], + 'PBXCopyFilesBuildPhase', + 'Embed App Extensions', + mainTargetUuid, + 'app_extension', +); +console.log(`✓ New phase UUID: ${newPhase.uuid}`); + +// 4) Verify dstSubfolderSpec was set correctly +const phaseObj = project.hash.project.objects.PBXCopyFilesBuildPhase[newPhase.uuid]; +if (phaseObj && phaseObj.dstSubfolderSpec !== 13 && phaseObj.dstSubfolderSpec !== '13') { + console.log( + `⚠️ dstSubfolderSpec was ${phaseObj.dstSubfolderSpec}, forcing to 13`, + ); + phaseObj.dstSubfolderSpec = 13; + phaseObj.dstPath = ''; +} + +// 5) Add target dependency: Main → Extension +// Sorgt dafür dass Xcode die Extension VOR der Main-App baut. +console.log('→ Adding target dependency: Main → Extension...'); +try { + project.addTargetDependency(mainTargetUuid, [extTargetUuid]); + console.log('✓ Target dependency added'); +} catch (e) { + console.log('⚠️ Target dependency might already exist:', e.message); +} + +// 6) Add Extension's appex to the main app's Frameworks/PBXFileReference linkage +// so Xcode knows where to find it during embedding. +// (xcode npm package's addBuildPhase handles file references automatically +// if the .appex was registered via addTarget — sollte schon da sein.) + +// 7) Save +fs.writeFileSync(PBXPROJ, project.writeSync()); +console.log('✅ Saved pbxproj'); +console.log(''); +console.log('Now rebuild in Xcode (Cmd+B). The .appex should be embedded into Rebreak.app/PlugIns/'); diff --git a/apps/rebreak-native/stores/auth.ts b/apps/rebreak-native/stores/auth.ts new file mode 100644 index 0000000..935bd7c --- /dev/null +++ b/apps/rebreak-native/stores/auth.ts @@ -0,0 +1,157 @@ +import { create } from 'zustand'; +import type { Session, User } from '@supabase/supabase-js'; +import * as WebBrowser from 'expo-web-browser'; +import * as Linking from 'expo-linking'; +import { supabase } from '../lib/supabase'; + +WebBrowser.maybeCompleteAuthSession(); + +type AuthState = { + user: User | null; + session: Session | null; + loading: boolean; + + init: () => Promise; + signInWithPassword: (email: string, password: string) => Promise<{ error?: string }>; + signUp: ( + email: string, + password: string, + metadata: { username: string; firstName?: string; lastName?: string; avatarId: string; avatarUrl: string } + ) => Promise<{ error?: string }>; + signOut: () => Promise; + signInWithOAuth: (provider: 'google' | 'apple') => Promise<{ error?: string }>; + resetPasswordForEmail: (email: string) => Promise<{ error?: string }>; + verifyOtp: (email: string, token: string) => Promise<{ error?: string }>; + resendConfirmation: (email: string) => Promise<{ error?: string }>; +}; + +export const useAuthStore = create((set) => ({ + user: null, + session: null, + loading: true, + + init: async () => { + const { data } = await supabase.auth.getSession(); + set({ + session: data.session, + user: data.session?.user ?? null, + loading: false, + }); + + supabase.auth.onAuthStateChange((_event, session) => { + set({ session, user: session?.user ?? null }); + }); + }, + + signInWithPassword: async (email, password) => { + const { data, error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) return { error: error.message }; + set({ session: data.session, user: data.user }); + return {}; + }, + + signUp: async (email, password, metadata) => { + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { + username: metadata.username, + first_name: metadata.firstName ?? '', + last_name: metadata.lastName ?? '', + avatar_id: metadata.avatarId, + avatar_url: metadata.avatarUrl, + }, + // Deep-link redirect for email confirmation — scheme registered in app.config.ts + emailRedirectTo: 'rebreak://auth/confirm', + }, + }); + if (error) return { error: error.message }; + set({ session: data.session, user: data.user ?? null }); + return {}; + }, + + signOut: async () => { + await supabase.auth.signOut(); + set({ session: null, user: null }); + }, + + signInWithOAuth: async (provider) => { + const redirectUri = Linking.createURL('auth/callback'); + + if (provider === 'apple') { + // TODO: configure Apple Sign-In + // Requires expo-apple-authentication to be installed + Apple Developer entitlement. + // Apple Client ID = Bundle ID (org.rebreak.app) for native flow. + // For now we fall through to the Supabase OAuth web flow as a temporary path. + } + + const { data, error } = await supabase.auth.signInWithOAuth({ + provider, + options: { + redirectTo: redirectUri, + skipBrowserRedirect: true, + }, + }); + + if (error) return { error: error.message }; + if (!data.url) return { error: 'Kein OAuth-URL erhalten' }; + + const result = await WebBrowser.openAuthSessionAsync(data.url, redirectUri); + + if (result.type !== 'success') { + return result.type === 'cancel' ? {} : { error: 'OAuth fehlgeschlagen' }; + } + + // Extract tokens from the deep-link URL fragment + const url = result.url; + const params = new URLSearchParams(url.split('#')[1] ?? url.split('?')[1] ?? ''); + const accessToken = params.get('access_token'); + const refreshToken = params.get('refresh_token'); + + if (!accessToken || !refreshToken) { + // Session may already be set via onAuthStateChange — check + const { data: sessionData } = await supabase.auth.getSession(); + if (sessionData.session) { + set({ session: sessionData.session, user: sessionData.session.user }); + return {}; + } + return { error: 'Session konnte nicht gelesen werden' }; + } + + const { data: sessionData, error: sessionError } = await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }); + + if (sessionError) return { error: sessionError.message }; + set({ session: sessionData.session, user: sessionData.session?.user ?? null }); + return {}; + }, + + resetPasswordForEmail: async (email) => { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: 'rebreak://auth/reset-password', + }); + if (error) return { error: error.message }; + return {}; + }, + + verifyOtp: async (email, token) => { + const { data, error } = await supabase.auth.verifyOtp({ + email, + token, + type: 'signup', + }); + if (error) return { error: error.message }; + if (!data.session) return { error: 'Bestätigung fehlgeschlagen – bitte erneut versuchen.' }; + set({ session: data.session, user: data.user ?? null }); + return {}; + }, + + resendConfirmation: async (email) => { + const { error } = await supabase.auth.resend({ type: 'signup', email }); + if (error) return { error: error.message }; + return {}; + }, +})); diff --git a/apps/rebreak-native/stores/coach.ts b/apps/rebreak-native/stores/coach.ts new file mode 100644 index 0000000..f7bee35 --- /dev/null +++ b/apps/rebreak-native/stores/coach.ts @@ -0,0 +1,86 @@ +import { create } from 'zustand'; +import { apiFetch } from '../lib/api'; + +let historyLoaded = false; + +export type Message = { + id: string; + role: 'user' | 'assistant'; + content: string; + isError?: boolean; + feedbackSaved?: boolean; +}; + +type CoachState = { + messages: Message[]; + thinking: boolean; + historyLoaded: boolean; + /** True sobald der Welcome-Back-Call in dieser App-Session geantwortet hat + * — verhindert dass jede Tab-Rückkehr eine neue Lyra-Begrüßung anhängt. */ + welcomeBackShownThisSession: boolean; + + loadHistory: () => Promise; + clearHistory: () => Promise; + sendMessage: (content: string, locale: string) => Promise<{ message: string; feedbackSaved?: boolean }>; + prependMessage: (msg: Message) => void; + pushMessage: (msg: Message) => void; + markFeedbackSaved: (id: string) => void; + setThinking: (v: boolean) => void; + setWelcomeBackShown: (v: boolean) => void; + reset: () => void; +}; + +export const useCoachStore = create((set, get) => ({ + messages: [], + thinking: false, + historyLoaded: false, + welcomeBackShownThisSession: false, + + loadHistory: async () => { + if (historyLoaded) { + set({ historyLoaded: true }); + return; + } + historyLoaded = true; + const res = await apiFetch<{ messages: Array<{ role: 'user' | 'assistant'; content: string }> }>( + '/api/coach/history' + ); + set({ + messages: res.messages?.length + ? res.messages.map((m, i) => ({ id: i.toString(), role: m.role, content: m.content })) + : [], + historyLoaded: true, + }); + }, + + clearHistory: async () => { + await apiFetch('/api/coach/history', { method: 'DELETE' }).catch(() => null); + historyLoaded = false; + set({ messages: [], historyLoaded: false, welcomeBackShownThisSession: false }); + }, + + sendMessage: async (content, locale) => { + const { messages } = get(); + const res = await apiFetch<{ message: string; feedbackSaved?: boolean }>('/api/coach/message', { + method: 'POST', + body: { + messages: messages.filter((m) => !m.isError).map((m) => ({ role: m.role, content: m.content })), + locale, + }, + }); + return res; + }, + + prependMessage: (msg) => set((s) => ({ messages: [msg, ...s.messages] })), + pushMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })), + markFeedbackSaved: (id) => + set((s) => ({ + messages: s.messages.map((m) => (m.id === id ? { ...m, feedbackSaved: true } : m)), + })), + setThinking: (v) => set({ thinking: v }), + setWelcomeBackShown: (v) => set({ welcomeBackShownThisSession: v }), + reset: () => { + historyLoaded = false; + set({ messages: [], thinking: false, historyLoaded: false, welcomeBackShownThisSession: false }); + }, +})); diff --git a/apps/rebreak-native/stores/community.ts b/apps/rebreak-native/stores/community.ts new file mode 100644 index 0000000..1584404 --- /dev/null +++ b/apps/rebreak-native/stores/community.ts @@ -0,0 +1,105 @@ +import { create } from 'zustand'; + +export type CommunityCategory = 'all' | 'games' | 'domain_vote' | 'lyra' | 'rebreak'; + +export interface CommunityPostAuthor { + id: string | null; + username: string; + nickname: string; + avatar: string | null; + plan: string; + tier?: string; +} + +export interface CommunityPost { + id: string; + category: string; + content: string; + imageUrl?: string | null; + likesCount: number; + dislikesCount: number; + commentsCount: number; + repostsCount: number; + isAnonymous: boolean; + createdAt: string; + userLike: 'like' | 'dislike' | null; + isBot?: boolean; + botType?: 'lyra' | 'rebreak'; + gameName?: string | null; + challengeId?: string | null; + challengeStatus?: 'OPEN' | 'ACTIVE' | 'FINISHED' | 'CANCELLED' | null; + opponentName?: string | null; + isLive?: boolean; + userVote?: 'yes' | 'no' | null; + submission?: { + id: string; + domain: string; + status: 'pending' | 'approved' | 'rejected' | 'in_review'; + yesVotes: number; + noVotes: number; + reviewedAt?: string | null; + yesVoters?: Array<{ id: string; nickname: string; avatar: string | null }>; + noVoters?: Array<{ id: string; nickname: string; avatar: string | null }>; + } | null; + author: CommunityPostAuthor; + repostOf?: { + author: CommunityPostAuthor; + content: string; + imageUrl?: string | null; + } | null; +} + +export interface CommunityComment { + id: string; + content: string; + createdAt: string; + parentCommentId: string | null; + authorNickname: string; + authorAvatar: string | null; + authorId: string | null; + likesCount: number; + userLike: boolean; +} + +type CommunityState = { + activeCategory: CommunityCategory; + setCategory: (cat: CommunityCategory) => void; + optimisticLikes: Record; + applyOptimisticLike: (postId: string, currentLike: 'like' | null, currentCount: number) => { newLike: 'like' | null; newCount: number }; + revertOptimisticLike: (postId: string) => void; + clearOptimisticLike: (postId: string) => void; +}; + +export const useCommunityStore = create((set, get) => ({ + activeCategory: 'all', + optimisticLikes: {}, + + setCategory: (cat) => set({ activeCategory: cat }), + + applyOptimisticLike: (postId, currentLike, currentCount) => { + const isLiked = currentLike === 'like'; + const newLike: 'like' | null = isLiked ? null : 'like'; + const newCount = isLiked ? Math.max(0, currentCount - 1) : currentCount + 1; + set((s) => ({ + optimisticLikes: { + ...s.optimisticLikes, + [postId]: { delta: newCount - currentCount, userLike: newLike }, + }, + })); + return { newLike, newCount }; + }, + + revertOptimisticLike: (postId) => { + set((s) => { + const { [postId]: _, ...rest } = s.optimisticLikes; + return { optimisticLikes: rest }; + }); + }, + + clearOptimisticLike: (postId) => { + set((s) => { + const { [postId]: _, ...rest } = s.optimisticLikes; + return { optimisticLikes: rest }; + }); + }, +})); diff --git a/apps/rebreak-native/stores/notifications.ts b/apps/rebreak-native/stores/notifications.ts new file mode 100644 index 0000000..285e9a7 --- /dev/null +++ b/apps/rebreak-native/stores/notifications.ts @@ -0,0 +1,149 @@ +import { create } from "zustand"; +import { apiFetch } from "../lib/api"; +import { supabase } from "../lib/supabase"; +import type { RealtimeChannel } from "@supabase/supabase-js"; + +export interface AppNotification { + id: string; + type: string; + actorName: string; + actorAvatar: string | null; + postId: string | null; + preview: string | null; + readAt: string | null; + createdAt: string; +} + +type NotificationState = { + items: AppNotification[]; + unread: number; + loaded: boolean; + load: () => Promise; + markRead: () => Promise; + remove: (id: string) => Promise; + startRealtime: () => Promise; + stopRealtime: () => void; + reset: () => void; +}; + +let realtimeSub: RealtimeChannel | null = null; +let reconnectTimer: ReturnType | null = null; + +export const useNotificationStore = create((set, get) => ({ + items: [], + unread: 0, + loaded: false, + + load: async () => { + try { + const res = await apiFetch<{ items: AppNotification[]; unread: number }>( + "/api/notifications", + ); + set({ items: res.items ?? [], unread: res.unread ?? 0, loaded: true }); + } catch (err) { + console.warn("[notifications] load failed:", err); + } + }, + + markRead: async () => { + if (get().unread === 0) return; + const now = new Date().toISOString(); + set((s) => ({ + unread: 0, + items: s.items.map((n) => ({ ...n, readAt: n.readAt ?? now })), + })); + try { + await apiFetch("/api/notifications/read", { method: "POST" }); + } catch (err) { + console.warn("[notifications] markRead failed:", err); + } + }, + + remove: async (id) => { + set((s) => ({ items: s.items.filter((n) => n.id !== id) })); + try { + await apiFetch(`/api/notifications/${id}`, { method: "DELETE" }); + } catch (err) { + console.warn("[notifications] remove failed:", err); + } + }, + + startRealtime: async () => { + if (realtimeSub) return; + const { data } = await supabase.auth.getSession(); + const session = data.session; + if (!session?.user?.id) return; + + supabase.realtime.setAuth(session.access_token); + const myId = session.user.id; + + realtimeSub = supabase + .channel(`notifications:${myId}:${Date.now()}`) + .on( + "postgres_changes", + { + event: "INSERT", + schema: "rebreak", + table: "notifications", + filter: `recipient_id=eq.${myId}`, + }, + (payload: any) => { + const r = payload.new; + const notif: AppNotification = { + id: r.id, + type: r.type, + actorName: r.actor_name, + actorAvatar: r.actor_avatar ?? null, + postId: r.post_id ?? null, + preview: r.preview ?? null, + readAt: null, + createdAt: r.created_at, + }; + set((s) => { + if (s.items.find((n) => n.id === notif.id)) return s; + return { items: [notif, ...s.items], unread: s.unread + 1 }; + }); + }, + ) + .on( + "postgres_changes", + { + event: "DELETE", + schema: "rebreak", + table: "notifications", + filter: `recipient_id=eq.${myId}`, + }, + (payload: any) => { + const id = payload.old?.id; + if (!id) return; + set((s) => ({ items: s.items.filter((n) => n.id !== id) })); + }, + ) + .subscribe((status) => { + if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { + console.warn("[notifRealtime] error:", status); + get().stopRealtime(); + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(() => { + get().startRealtime(); + }, 3000); + } + }); + }, + + stopRealtime: () => { + if (realtimeSub) { + supabase.removeChannel(realtimeSub); + realtimeSub = null; + } + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + }, + + reset: () => { + get().stopRealtime(); + set({ items: [], unread: 0, loaded: false }); + }, +})); diff --git a/apps/rebreak-native/tailwind.config.js b/apps/rebreak-native/tailwind.config.js new file mode 100644 index 0000000..79e80ce --- /dev/null +++ b/apps/rebreak-native/tailwind.config.js @@ -0,0 +1,50 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './app/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + ], + presets: [require('nativewind/preset')], + theme: { + extend: { + colors: { + // Rebreak brand colors — orange burst + dark blue from app-icon.png + // TEMP: iOS native blue palette zum Testen. + // Original Brand-Orange: 50:#fff7ed → 500:#f59e0b → 900:#78350f + rebreak: { + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#007AFF', + 600: '#0062cc', + 700: '#0050a3', + 800: '#003e7a', + 900: '#002b52', + }, + // Dark blue background palette aus dem Brand-Icon + midnight: { + 50: '#e6edf6', + 100: '#cad9eb', + 200: '#92b4d6', + 300: '#5a8ec1', + 400: '#3a6da3', + 500: '#264e7d', + 600: '#1a3a60', + 700: '#13284a', + 800: '#0e1f3a', + 900: '#091428', + 950: '#040a16', + }, + }, + fontFamily: { + sans: ['Nunito_400Regular', 'system-ui'], + semibold: ['Nunito_600SemiBold'], + bold: ['Nunito_700Bold'], + extrabold: ['Nunito_800ExtraBold'], + }, + }, + }, + plugins: [], +}; diff --git a/apps/rebreak-native/tools/gen-android-launcher.sh b/apps/rebreak-native/tools/gen-android-launcher.sh new file mode 100755 index 0000000..df851c1 --- /dev/null +++ b/apps/rebreak-native/tools/gen-android-launcher.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Generates Android launcher icons from assets/adaptive-icon-android.png. +# - Trims white border, re-centers chain on 1024×1024 transparent canvas +# (logo size unchanged, only position). +# - Writes ic_launcher_foreground.webp (transparent bg) for adaptive icon. +# - Writes ic_launcher{,_round}.webp on #0a0a0a square as legacy fallback. +# - Background color matches values/colors.xml (iconBackground = #0a0a0a). +set -euo pipefail + +cd "$(dirname "$0")/.." + +SOURCE="assets/adaptive-icon-android.png" +RES="android/app/src/main/res" +BG="#0a0a0a" + +if [[ ! -f "$SOURCE" ]]; then + echo "Missing $SOURCE" >&2 + exit 1 +fi + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# 1. Trim near-white borders, re-embed centered on 1024×1024 transparent canvas. +# -fuzz 5% absorbs JPEG-style artifacts around the white background. +magick "$SOURCE" \ + -fuzz 5% -trim +repage \ + -background "rgba(0,0,0,0)" -gravity center -extent 1024x1024 \ + "$TMP/master.png" + +# Overwrite source so app.config.ts stays the single source of truth. +cp "$TMP/master.png" "$SOURCE" +echo "Wrote centered master → $SOURCE" + +# bash 3.2 (macOS default) has no assoc arrays — use parallel positional pairs: +# "::". +BUCKETS=( "mdpi:108:48" "hdpi:162:72" "xhdpi:216:96" "xxhdpi:324:144" "xxxhdpi:432:192" ) + +for entry in "${BUCKETS[@]}"; do + bucket="${entry%%:*}" + rest="${entry#*:}" + fg_size="${rest%%:*}" + lg_size="${rest#*:}" + dir="$RES/mipmap-$bucket" + mkdir -p "$dir" + + # Foreground layer (transparent bg) for adaptive icon. + magick "$TMP/master.png" -resize "${fg_size}x${fg_size}" "$TMP/fg-$bucket.png" + cwebp -quiet -q 95 "$TMP/fg-$bucket.png" -o "$dir/ic_launcher_foreground.webp" + echo " ic_launcher_foreground.webp $bucket ${fg_size}px" + + # Legacy ic_launcher / ic_launcher_round on dark background. + magick -size "${lg_size}x${lg_size}" "xc:$BG" \ + \( "$TMP/master.png" -resize "${lg_size}x${lg_size}" \) -gravity center -composite \ + "$TMP/lg-$bucket.png" + cwebp -quiet -q 95 "$TMP/lg-$bucket.png" -o "$dir/ic_launcher.webp" + cp "$dir/ic_launcher.webp" "$dir/ic_launcher_round.webp" + echo " ic_launcher{,_round}.webp $bucket ${lg_size}px" +done + +echo "Done." diff --git a/apps/rebreak-native/tsconfig.json b/apps/rebreak-native/tsconfig.json new file mode 100644 index 0000000..c5ad5b1 --- /dev/null +++ b/apps/rebreak-native/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "paths": { + "@/*": [ + "./*" + ], + "@/components/*": [ + "./components/*" + ], + "@/hooks/*": [ + "./hooks/*" + ], + "@/stores/*": [ + "./stores/*" + ], + "@/lib/*": [ + "./lib/*" + ], + "@/locales/*": [ + "./locales/*" + ] + }, + "types": [ + "expo/types" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ".expo/types/**/*.ts", + "expo-env.d.ts", + "nativewind-env.d.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/backend/nitro.config.ts b/backend/nitro.config.ts new file mode 100644 index 0000000..b49b574 --- /dev/null +++ b/backend/nitro.config.ts @@ -0,0 +1,42 @@ +import { defineNitroConfig } from "nitropack/config"; + +export default defineNitroConfig({ + compatibilityDate: "latest", + srcDir: "server", + preset: "node-server", + + // Supabase als external dep — nicht bundlen + externals: { + inline: [/^(?!@supabase\/supabase-js)/], + }, + + imports: { + dirs: ["db", "db/**", "utils", "utils/**"], + exclude: ["**/node_modules/**"], + }, + + runtimeConfig: { + databaseUrl: process.env.DATABASE_URL ?? "", + adminSecret: process.env.ADMIN_SECRET ?? "", + openrouterApiKey: process.env.OPENROUTER_API_KEY ?? "", + deepgramApiKey: process.env.DEEPGRAM_API_KEY ?? "", + googleApiKey: process.env.GOOGLE_API_KEY ?? "", + googleAiApiKey: process.env.GOOGLE_AI_API_KEY ?? "", + azureTtsKey: process.env.AZURE_TTS_KEY ?? "", + azureTtsRegion: process.env.AZURE_TTS_REGION ?? "", + openaiApiKey: process.env.OPENAI_API_KEY ?? "", + stripeSecretKey: process.env.STRIPE_SECRET_KEY ?? "", + stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? "", + resendApiKey: process.env.RESEND_API_KEY ?? "", + encryptionKey: process.env.ENCRYPTION_KEY ?? "", + lyraBotUserId: process.env.LYRA_BOT_USER_ID ?? "", + rebreakBotUserId: process.env.REBREAK_BOT_USER_ID ?? "", + groqApiKey: process.env.GROQ_API_KEY ?? "", + cronSecret: process.env.CRON_SECRET ?? "", + public: { + stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY ?? "", + appUrl: process.env.APP_URL ?? "https://staging.rebreak.org", + apiBase: process.env.API_BASE ?? "https://staging.rebreak.org", + }, + }, +}); diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..517b344 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,34 @@ +{ + "name": "rebreak-backend", + "private": true, + "type": "module", + "version": "0.1.0", + "scripts": { + "build": "prisma generate --schema prisma/schema.prisma && nitro build", + "dev": "nitro dev", + "preview": "node .output/server/index.mjs", + "start": "node .output/server/index.mjs", + "prisma:generate": "prisma generate --schema prisma/schema.prisma" + }, + "dependencies": { + "@prisma/adapter-pg": "^7.2.0", + "@prisma/client": "^7.2.0", + "@supabase/supabase-js": "^2.39.7", + "groq-sdk": "^0.7.0", + "imapflow": "^1.2.18", + "jose": "^6.0.0", + "openai": "^4.65.0", + "pg": "^8.16.3", + "resend": "^4.0.0", + "stripe": "^17.0.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/pg": "^8.11.10", + "h3": "^1.15.4", + "nitropack": "^2.12.4", + "prisma": "^7.2.0", + "typescript": "^5.9.3" + } +} diff --git a/backend/prisma.config.ts b/backend/prisma.config.ts new file mode 100644 index 0000000..82da075 --- /dev/null +++ b/backend/prisma.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env.DATABASE_URL ?? "", + }, +}); diff --git a/backend/prisma/migrations/0_init/migration.sql b/backend/prisma/migrations/0_init/migration.sql new file mode 100644 index 0000000..a02a3d8 --- /dev/null +++ b/backend/prisma/migrations/0_init/migration.sql @@ -0,0 +1,404 @@ + +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "rebreak"; + +-- CreateEnum +CREATE TYPE "rebreak"."FeedbackStatus" AS ENUM ('PENDING', 'REVIEWING', 'PLANNED', 'SHIPPED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "rebreak"."GameChallengeStatus" AS ENUM ('OPEN', 'ACTIVE', 'FINISHED', 'CANCELLED'); + +-- CreateTable +CREATE TABLE "rebreak"."profiles" ( + "id" UUID NOT NULL, + "username" TEXT, + "nickname" TEXT, + "avatar" TEXT, + "plan" TEXT NOT NULL DEFAULT 'free', + "streak" INTEGER NOT NULL DEFAULT 0, + "followers_count" INTEGER NOT NULL DEFAULT 0, + "stripe_customer_id" TEXT, + "stripe_subscription_id" TEXT, + "premium_until" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "profiles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."streaks" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "start_date" DATE NOT NULL, + "current_days" INTEGER NOT NULL DEFAULT 0, + "longest_days" INTEGER NOT NULL DEFAULT 0, + "avg_monthly_savings" DOUBLE PRECISION, + "is_active" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "streaks_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."streak_events" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "type" TEXT NOT NULL, + "meta" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "streak_events_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."urge_logs" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "emotion" TEXT NOT NULL, + "was_overcome" BOOLEAN NOT NULL DEFAULT false, + "breathing_done" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "urge_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."community_posts" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "category" TEXT NOT NULL, + "content" TEXT NOT NULL, + "image_url" TEXT, + "upvotes" INTEGER NOT NULL DEFAULT 0, + "likes_count" INTEGER NOT NULL DEFAULT 0, + "dislikes_count" INTEGER NOT NULL DEFAULT 0, + "comments_count" INTEGER NOT NULL DEFAULT 0, + "reposts_count" INTEGER NOT NULL DEFAULT 0, + "is_anonymous" BOOLEAN NOT NULL DEFAULT false, + "is_moderated" BOOLEAN NOT NULL DEFAULT false, + "repost_of_id" UUID, + "challenge_id" UUID, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "community_posts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."post_likes" ( + "user_id" UUID NOT NULL, + "post_id" UUID NOT NULL, + "type" TEXT NOT NULL, + + CONSTRAINT "post_likes_pkey" PRIMARY KEY ("user_id","post_id") +); + +-- CreateTable +CREATE TABLE "rebreak"."community_replies" ( + "id" UUID NOT NULL, + "post_id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "content" TEXT NOT NULL, + "parent_reply_id" UUID, + "is_anonymous" BOOLEAN NOT NULL DEFAULT false, + "likes_count" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "community_replies_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."comment_likes" ( + "user_id" UUID NOT NULL, + "comment_id" UUID NOT NULL, + + CONSTRAINT "comment_likes_pkey" PRIMARY KEY ("user_id","comment_id") +); + +-- CreateTable +CREATE TABLE "rebreak"."chat_messages" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "content" TEXT NOT NULL, + "room_id" UUID, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "chat_messages_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."direct_messages" ( + "id" UUID NOT NULL, + "sender_id" UUID NOT NULL, + "receiver_id" UUID NOT NULL, + "content" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "read_at" TIMESTAMP(3), + + CONSTRAINT "direct_messages_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."user_follows" ( + "follower_id" UUID NOT NULL, + "following_id" UUID NOT NULL, + + CONSTRAINT "user_follows_pkey" PRIMARY KEY ("follower_id","following_id") +); + +-- CreateTable +CREATE TABLE "rebreak"."user_scores" ( + "user_id" UUID NOT NULL, + "total_points" INTEGER NOT NULL DEFAULT 0, + "tier" TEXT NOT NULL DEFAULT 'beginner', + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "user_scores_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "rebreak"."score_events" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "event_type" TEXT NOT NULL, + "points" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "meta" JSONB, + + CONSTRAINT "score_events_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."user_custom_domains" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "domain" TEXT NOT NULL, + "source" TEXT NOT NULL DEFAULT 'manual', + "status" TEXT NOT NULL DEFAULT 'active', + "post_id" UUID, + "added_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "user_custom_domains_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."domain_submissions" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "domain" TEXT NOT NULL, + "custom_domain_id" UUID NOT NULL, + "post_id" UUID, + "status" TEXT NOT NULL DEFAULT 'pending', + "yes_votes" INTEGER NOT NULL DEFAULT 0, + "no_votes" INTEGER NOT NULL DEFAULT 0, + "review_note" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "reviewed_at" TIMESTAMP(3), + + CONSTRAINT "domain_submissions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."domain_votes" ( + "user_id" UUID NOT NULL, + "submission_id" UUID NOT NULL, + "vote" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "domain_votes_pkey" PRIMARY KEY ("user_id","submission_id") +); + +-- CreateTable +CREATE TABLE "rebreak"."feedback_items" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "content" TEXT NOT NULL, + "category" TEXT, + "status" "rebreak"."FeedbackStatus" NOT NULL DEFAULT 'PENDING', + "admin_note" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "feedback_items_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."blocklist_domains" ( + "id" UUID NOT NULL, + "domain" TEXT NOT NULL, + "source" TEXT NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "report_count" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "blocklist_domains_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."trusted_contacts" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "name" TEXT NOT NULL, + "phone" TEXT, + "email" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "trusted_contacts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."coach_sessions" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "content" JSONB NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "coach_sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."mail_connections" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "email" TEXT NOT NULL, + "provider" TEXT NOT NULL DEFAULT 'imap', + "provider_name" TEXT, + "imap_host" TEXT NOT NULL, + "imap_port" INTEGER NOT NULL, + "password_encrypted" TEXT NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "scan_interval" INTEGER NOT NULL DEFAULT 24, + "last_scanned_at" TIMESTAMP(3), + "next_scan_at" TIMESTAMP(3), + "emails_blocked" INTEGER NOT NULL DEFAULT 0, + "emails_scanned" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "mail_connections_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."game_challenges" ( + "id" UUID NOT NULL, + "challenger_id" UUID NOT NULL, + "challenger_name" TEXT NOT NULL, + "opponent_id" UUID, + "opponent_name" TEXT, + "status" "rebreak"."GameChallengeStatus" NOT NULL DEFAULT 'OPEN', + "board" TEXT NOT NULL DEFAULT '---------', + "current_turn" TEXT NOT NULL DEFAULT 'X', + "winner" TEXT, + "post_id" UUID, + "game_type" TEXT NOT NULL DEFAULT 'tictactoe', + "is_live" BOOLEAN NOT NULL DEFAULT false, + "memory_state" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "game_challenges_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."notifications" ( + "id" UUID NOT NULL, + "recipient_id" UUID NOT NULL, + "type" TEXT NOT NULL, + "actor_name" TEXT NOT NULL, + "post_id" UUID, + "preview" TEXT, + "read_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "notifications_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."game_scores" ( + "user_id" UUID NOT NULL, + "player_name" TEXT NOT NULL, + "wins" INTEGER NOT NULL DEFAULT 0, + "losses" INTEGER NOT NULL DEFAULT 0, + "draws" INTEGER NOT NULL DEFAULT 0, + "points" INTEGER NOT NULL DEFAULT 0, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "game_scores_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "rebreak"."mail_blocked" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "connection_id" UUID NOT NULL, + "gmail_message_id" TEXT NOT NULL, + "sender_email" TEXT NOT NULL, + "sender_name" TEXT, + "subject" TEXT NOT NULL, + "received_at" TIMESTAMP(3) NOT NULL, + "action" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "mail_blocked_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rebreak"."imap_proxy_accounts" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "proxy_username" TEXT NOT NULL, + "proxy_password" TEXT NOT NULL, + "connection_id" UUID NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "imap_proxy_accounts_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_custom_domains_user_id_domain_key" ON "rebreak"."user_custom_domains"("user_id", "domain"); + +-- CreateIndex +CREATE UNIQUE INDEX "domain_submissions_custom_domain_id_key" ON "rebreak"."domain_submissions"("custom_domain_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "blocklist_domains_domain_key" ON "rebreak"."blocklist_domains"("domain"); + +-- CreateIndex +CREATE UNIQUE INDEX "mail_connections_user_id_email_key" ON "rebreak"."mail_connections"("user_id", "email"); + +-- CreateIndex +CREATE INDEX "notifications_recipient_id_read_at_idx" ON "rebreak"."notifications"("recipient_id", "read_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "mail_blocked_gmail_message_id_user_id_key" ON "rebreak"."mail_blocked"("gmail_message_id", "user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "imap_proxy_accounts_proxy_username_key" ON "rebreak"."imap_proxy_accounts"("proxy_username"); + +-- CreateIndex +CREATE UNIQUE INDEX "imap_proxy_accounts_connection_id_key" ON "rebreak"."imap_proxy_accounts"("connection_id"); + +-- AddForeignKey +ALTER TABLE "rebreak"."community_posts" ADD CONSTRAINT "community_posts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "rebreak"."profiles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rebreak"."community_posts" ADD CONSTRAINT "community_posts_repost_of_id_fkey" FOREIGN KEY ("repost_of_id") REFERENCES "rebreak"."community_posts"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rebreak"."post_likes" ADD CONSTRAINT "post_likes_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "rebreak"."community_posts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rebreak"."community_replies" ADD CONSTRAINT "community_replies_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "rebreak"."community_posts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rebreak"."community_replies" ADD CONSTRAINT "community_replies_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "rebreak"."profiles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rebreak"."comment_likes" ADD CONSTRAINT "comment_likes_comment_id_fkey" FOREIGN KEY ("comment_id") REFERENCES "rebreak"."community_replies"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rebreak"."domain_submissions" ADD CONSTRAINT "domain_submissions_custom_domain_id_fkey" FOREIGN KEY ("custom_domain_id") REFERENCES "rebreak"."user_custom_domains"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rebreak"."domain_votes" ADD CONSTRAINT "domain_votes_submission_id_fkey" FOREIGN KEY ("submission_id") REFERENCES "rebreak"."domain_submissions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rebreak"."mail_blocked" ADD CONSTRAINT "mail_blocked_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "rebreak"."mail_connections"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/backend/prisma/migrations/20250712_chat_rooms_and_features/migration.sql b/backend/prisma/migrations/20250712_chat_rooms_and_features/migration.sql new file mode 100644 index 0000000..afd1d82 --- /dev/null +++ b/backend/prisma/migrations/20250712_chat_rooms_and_features/migration.sql @@ -0,0 +1,79 @@ +-- CreateTable: chat_rooms +CREATE TABLE "rebreak"."chat_rooms" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "name" TEXT NOT NULL, + "description" TEXT, + "is_public" BOOLEAN NOT NULL DEFAULT false, + "avatar_url" TEXT, + "created_by" UUID NOT NULL, + "join_mode" TEXT NOT NULL DEFAULT 'open', + "invite_code" TEXT, + "member_count" INTEGER NOT NULL DEFAULT 0, + "is_default" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "chat_rooms_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex: chat_rooms.invite_code +CREATE UNIQUE INDEX "chat_rooms_invite_code_key" ON "rebreak"."chat_rooms"("invite_code"); + +-- CreateTable: chat_room_members +CREATE TABLE "rebreak"."chat_room_members" ( + "room_id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "role" TEXT NOT NULL DEFAULT 'member', + "status" TEXT NOT NULL DEFAULT 'active', + "joined_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "chat_room_members_pkey" PRIMARY KEY ("room_id","user_id") +); + +-- AlterTable: chat_messages – neue Spalten +ALTER TABLE "rebreak"."chat_messages" ADD COLUMN "reply_to_id" UUID; +ALTER TABLE "rebreak"."chat_messages" ADD COLUMN "attachment_url" TEXT; +ALTER TABLE "rebreak"."chat_messages" ADD COLUMN "attachment_type" TEXT; +ALTER TABLE "rebreak"."chat_messages" ADD COLUMN "attachment_name" TEXT; +ALTER TABLE "rebreak"."chat_messages" ADD COLUMN "likes_count" INTEGER NOT NULL DEFAULT 0; + +-- CreateTable: chat_message_likes +CREATE TABLE "rebreak"."chat_message_likes" ( + "user_id" UUID NOT NULL, + "message_id" UUID NOT NULL, + + CONSTRAINT "chat_message_likes_pkey" PRIMARY KEY ("user_id","message_id") +); + +-- AlterTable: direct_messages – neue Spalten +ALTER TABLE "rebreak"."direct_messages" ADD COLUMN "reply_to_id" UUID; +ALTER TABLE "rebreak"."direct_messages" ADD COLUMN "attachment_url" TEXT; +ALTER TABLE "rebreak"."direct_messages" ADD COLUMN "attachment_type" TEXT; +ALTER TABLE "rebreak"."direct_messages" ADD COLUMN "attachment_name" TEXT; +ALTER TABLE "rebreak"."direct_messages" ADD COLUMN "likes_count" INTEGER NOT NULL DEFAULT 0; + +-- CreateTable: direct_message_likes +CREATE TABLE "rebreak"."direct_message_likes" ( + "user_id" UUID NOT NULL, + "message_id" UUID NOT NULL, + + CONSTRAINT "direct_message_likes_pkey" PRIMARY KEY ("user_id","message_id") +); + +-- AddForeignKey: chat_room_members -> chat_rooms +ALTER TABLE "rebreak"."chat_room_members" ADD CONSTRAINT "chat_room_members_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "rebreak"."chat_rooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: chat_messages -> chat_rooms +ALTER TABLE "rebreak"."chat_messages" ADD CONSTRAINT "chat_messages_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "rebreak"."chat_rooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: chat_messages self-reply +ALTER TABLE "rebreak"."chat_messages" ADD CONSTRAINT "chat_messages_reply_to_id_fkey" FOREIGN KEY ("reply_to_id") REFERENCES "rebreak"."chat_messages"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey: chat_message_likes -> chat_messages +ALTER TABLE "rebreak"."chat_message_likes" ADD CONSTRAINT "chat_message_likes_message_id_fkey" FOREIGN KEY ("message_id") REFERENCES "rebreak"."chat_messages"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: direct_messages self-reply +ALTER TABLE "rebreak"."direct_messages" ADD CONSTRAINT "direct_messages_reply_to_id_fkey" FOREIGN KEY ("reply_to_id") REFERENCES "rebreak"."direct_messages"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey: direct_message_likes -> direct_messages +ALTER TABLE "rebreak"."direct_message_likes" ADD CONSTRAINT "direct_message_likes_message_id_fkey" FOREIGN KEY ("message_id") REFERENCES "rebreak"."direct_messages"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20260422_game_high_scores/migration.sql b/backend/prisma/migrations/20260422_game_high_scores/migration.sql new file mode 100644 index 0000000..78238ea --- /dev/null +++ b/backend/prisma/migrations/20260422_game_high_scores/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable: game_high_scores (best score per user per game) +CREATE TABLE IF NOT EXISTS rebreak.game_high_scores ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "nickname" TEXT NOT NULL, + "game_name" TEXT NOT NULL, + "score" INTEGER NOT NULL, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "game_high_scores_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "game_high_scores_user_id_game_name_key" + ON rebreak.game_high_scores ("user_id", "game_name"); + +CREATE INDEX IF NOT EXISTS "game_high_scores_game_name_score_idx" + ON rebreak.game_high_scores ("game_name", "score" DESC); diff --git a/backend/prisma/migrations/20260422_game_ratings/migration.sql b/backend/prisma/migrations/20260422_game_ratings/migration.sql new file mode 100644 index 0000000..ac59730 --- /dev/null +++ b/backend/prisma/migrations/20260422_game_ratings/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE IF NOT EXISTS rebreak.game_ratings ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "game_name" TEXT NOT NULL, + "stars" INTEGER NOT NULL, + "feedback" TEXT, + "score" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "game_ratings_pkey" PRIMARY KEY ("id") +); diff --git a/backend/prisma/migrations/20260426_add_actor_avatar_to_notifications/migration.sql b/backend/prisma/migrations/20260426_add_actor_avatar_to_notifications/migration.sql new file mode 100644 index 0000000..30706d5 --- /dev/null +++ b/backend/prisma/migrations/20260426_add_actor_avatar_to_notifications/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable: notifications – actor_avatar Spalte hinzufügen +ALTER TABLE rebreak.notifications + ADD COLUMN IF NOT EXISTS "actor_avatar" TEXT; diff --git a/backend/prisma/migrations/20260428_add_cooldown_requests/migration.sql b/backend/prisma/migrations/20260428_add_cooldown_requests/migration.sql new file mode 100644 index 0000000..1ebe764 --- /dev/null +++ b/backend/prisma/migrations/20260428_add_cooldown_requests/migration.sql @@ -0,0 +1,24 @@ +-- CooldownRequest table — tracks user-initiated cooldowns before disabling protection. +-- Backed by `model CooldownRequest` in schema.prisma. +-- +-- Drift-Fix: diese Migration wurde nachträglich angelegt (2026-04-28). Auf +-- staging-DB wurde die Tabelle manuell via psql erstellt. Auf neuen DBs (Prod- +-- Migration, Dev-Setup) sollte diese Migration laufen. +-- +-- Wenn ein DB schon die Tabelle hat (Drift): nach dem ersten `prisma migrate deploy` +-- führe einmalig aus: +-- pnpm prisma migrate resolve --applied 20260428_add_cooldown_requests + +CREATE TABLE IF NOT EXISTS "rebreak"."cooldown_requests" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "reason" TEXT, + "cooldown_started_at" TIMESTAMP(3) WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + "cooldown_ends_at" TIMESTAMP(3) WITH TIME ZONE NOT NULL, + "resolved_at" TIMESTAMP(3) WITH TIME ZONE, + "cancelled_at" TIMESTAMP(3) WITH TIME ZONE, + "token_jti" TEXT NOT NULL UNIQUE +); + +CREATE INDEX IF NOT EXISTS "cooldown_requests_user_id_cooldown_ends_at_idx" + ON "rebreak"."cooldown_requests"("user_id", "cooldown_ends_at"); diff --git a/backend/prisma/migrations/20260430_add_custom_imap_tls/migration.sql b/backend/prisma/migrations/20260430_add_custom_imap_tls/migration.sql new file mode 100644 index 0000000..3a12510 --- /dev/null +++ b/backend/prisma/migrations/20260430_add_custom_imap_tls/migration.sql @@ -0,0 +1,17 @@ +-- Migration: add_custom_imap_tls +-- Fügt zwei TLS-Steuerfelder zur mail_connections-Tabelle hinzu. +-- Defaults entsprechen dem bisherigen Verhalten → keine Breaking Changes für bestehende Rows. +-- reject_unauthorized = true → TLS-Cert wird wie bisher validiert +-- use_starttls = false → implizites TLS (Port 993) wie bisher + +ALTER TABLE rebreak.mail_connections + ADD COLUMN IF NOT EXISTS reject_unauthorized BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS use_starttls BOOLEAN NOT NULL DEFAULT false; + +COMMENT ON COLUMN rebreak.mail_connections.reject_unauthorized IS + 'TLS-Zertifikat-Validierung. false nur für interne/self-signed IMAP-Server.'; + +COMMENT ON COLUMN rebreak.mail_connections.use_starttls IS + 'true = STARTTLS (Port 143/587, Verbindung startet unverschlüsselt). false = implicit TLS (Port 993).'; + +NOTIFY pgrst, 'reload schema'; diff --git a/backend/prisma/migrations/20260430_add_user_devices/migration.sql b/backend/prisma/migrations/20260430_add_user_devices/migration.sql new file mode 100644 index 0000000..fca5551 --- /dev/null +++ b/backend/prisma/migrations/20260430_add_user_devices/migration.sql @@ -0,0 +1,23 @@ +-- UserDevice table — Device-Binding pro User (Anti-Account-Sharing). +-- Backed by `model UserDevice` in schema.prisma. +-- +-- Limits siehe plan-features.ts maxDevices: Free=1, Pro=1, Legend=3. +-- Frontend liefert deviceId via Capacitor Device.getId() (persistent UUID). +-- Auth-Middleware enforced via x-device-id Header. + +CREATE TABLE IF NOT EXISTS "rebreak"."user_devices" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "device_id" TEXT NOT NULL, + "platform" TEXT NOT NULL, + "model" TEXT, + "name" TEXT, + "last_seen_at" TIMESTAMP(3) WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_at" TIMESTAMP(3) WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX IF NOT EXISTS "user_devices_user_id_device_id_key" + ON "rebreak"."user_devices"("user_id", "device_id"); + +CREATE INDEX IF NOT EXISTS "user_devices_user_id_idx" + ON "rebreak"."user_devices"("user_id"); diff --git a/backend/prisma/migrations/20260504_add_lyra_memories/migration.sql b/backend/prisma/migrations/20260504_add_lyra_memories/migration.sql new file mode 100644 index 0000000..0b1edc1 --- /dev/null +++ b/backend/prisma/migrations/20260504_add_lyra_memories/migration.sql @@ -0,0 +1,53 @@ +-- LyraMemory — strukturierte persistente User-Erinnerungen für den Lyra-Coach. +-- Art-9-Gesundheitsdaten (Glücksspielkontext): strenge RLS, nur service-role schreibt. +-- +-- Deploy: pnpm prisma migrate deploy (auf Hetzner) + +-- CreateEnum +CREATE TYPE "rebreak"."LyraMemoryType" AS ENUM ( + 'trigger', + 'habit', + 'strength', + 'relationship', + 'milestone', + 'pain_point', + 'goal', + 'preference' +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "rebreak"."lyra_memories" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "type" "rebreak"."LyraMemoryType" NOT NULL, + "content" VARCHAR(500) NOT NULL, + "confidence" DOUBLE PRECISION NOT NULL DEFAULT 0.7, + "source" TEXT, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "last_referenced_at" TIMESTAMPTZ, + + CONSTRAINT "lyra_memories_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "lyra_memories_user_id_type_idx" + ON "rebreak"."lyra_memories" ("user_id", "type"); + +-- EnableRLS +ALTER TABLE "rebreak"."lyra_memories" ENABLE ROW LEVEL SECURITY; + +-- Policy: User kann eigene Memories lesen (vorbereitet für künftiges User-UI) +CREATE POLICY "lyra_memories: own read" + ON "rebreak"."lyra_memories" + FOR SELECT + USING (auth.uid() = "user_id"); + +-- Policy: Service-Role darf alles (Auto-Extraction + Cleanup) +-- Hinweis: Supabase service_role bypassed RLS automatisch wenn +-- die Verbindung mit service_role JWT erfolgt — diese Policy ist +-- ein explizites Fallback für Tools die das nicht tun. +CREATE POLICY "lyra_memories: service all" + ON "rebreak"."lyra_memories" + USING (auth.role() = 'service_role') + WITH CHECK (auth.role() = 'service_role'); diff --git a/backend/prisma/migrations/20260504_sos_sessions/migration.sql b/backend/prisma/migrations/20260504_sos_sessions/migration.sql new file mode 100644 index 0000000..579ea56 --- /dev/null +++ b/backend/prisma/migrations/20260504_sos_sessions/migration.sql @@ -0,0 +1,22 @@ +-- CreateTable: sos_sessions (Verlauf einer SOS-Session für DiGA-Doku) +CREATE TABLE IF NOT EXISTS rebreak.sos_sessions ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "started_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "ended_at" TIMESTAMPTZ, + "duration_sec" INTEGER, + "messages" JSONB NOT NULL DEFAULT '[]'::jsonb, + "gamesPlayed" JSONB NOT NULL DEFAULT '[]'::jsonb, + "breathing_count" INTEGER NOT NULL DEFAULT 0, + "was_overcome" BOOLEAN NOT NULL DEFAULT false, + "feedback_better" BOOLEAN, + "feedback_rating" INTEGER, + "feedback_text" TEXT, + "locale" TEXT, + + CONSTRAINT "sos_sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "sos_sessions_user_id_started_at_idx" + ON rebreak.sos_sessions ("user_id", "started_at" DESC); diff --git a/backend/prisma/migrations/add_domain_submissions.sql b/backend/prisma/migrations/add_domain_submissions.sql new file mode 100644 index 0000000..b9b94c2 --- /dev/null +++ b/backend/prisma/migrations/add_domain_submissions.sql @@ -0,0 +1,38 @@ +-- Migration: Domain Submission Feature +-- Adds status + postId to user_custom_domains +-- Adds domain_submissions and domain_votes tables + +SET search_path TO rebreak; + +-- 1. Add status + postId to existing custom domains +ALTER TABLE user_custom_domains + ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'active', + ADD COLUMN IF NOT EXISTS post_id UUID; + +-- 2. Domain submissions table (admin + community review) +CREATE TABLE IF NOT EXISTS domain_submissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL, + domain TEXT NOT NULL, + custom_domain_id UUID NOT NULL UNIQUE REFERENCES user_custom_domains(id) ON DELETE CASCADE, + post_id UUID, + status TEXT NOT NULL DEFAULT 'pending', + yes_votes INT NOT NULL DEFAULT 0, + no_votes INT NOT NULL DEFAULT 0, + review_note TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + reviewed_at TIMESTAMPTZ +); + +-- 3. Domain votes table (one vote per user per submission) +CREATE TABLE IF NOT EXISTS domain_votes ( + user_id UUID NOT NULL, + submission_id UUID NOT NULL REFERENCES domain_submissions(id) ON DELETE CASCADE, + vote TEXT NOT NULL, -- 'yes' | 'no' + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (user_id, submission_id) +); + +CREATE INDEX IF NOT EXISTS idx_domain_submissions_status ON domain_submissions(status); +CREATE INDEX IF NOT EXISTS idx_domain_submissions_user_id ON domain_submissions(user_id); +CREATE INDEX IF NOT EXISTS idx_domain_votes_submission ON domain_votes(submission_id); diff --git a/backend/prisma/migrations/add_feedback_items.sql b/backend/prisma/migrations/add_feedback_items.sql new file mode 100644 index 0000000..cf39fd0 --- /dev/null +++ b/backend/prisma/migrations/add_feedback_items.sql @@ -0,0 +1,25 @@ +-- Add feedback_items table for Lyra Feedback-Loop feature +-- Users' feedback from coaching chat is auto-detected, stored here, and +-- Lyra proactively informs users when their idea status changes. + +CREATE TYPE rebreak."FeedbackStatus" AS ENUM ( + 'PENDING', + 'REVIEWING', + 'PLANNED', + 'SHIPPED', + 'REJECTED' +); + +CREATE TABLE rebreak."feedback_items" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "content" TEXT NOT NULL, + "category" TEXT, + "status" rebreak."FeedbackStatus" NOT NULL DEFAULT 'PENDING', + "admin_note" TEXT, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX "feedback_items_user_id_idx" ON rebreak."feedback_items" ("user_id"); +CREATE INDEX "feedback_items_status_idx" ON rebreak."feedback_items" ("status"); diff --git a/backend/prisma/migrations/add_game_challenges.sql b/backend/prisma/migrations/add_game_challenges.sql new file mode 100644 index 0000000..91ac22e --- /dev/null +++ b/backend/prisma/migrations/add_game_challenges.sql @@ -0,0 +1,35 @@ +-- Add 1v1 Tic-Tac-Toe challenge system +-- Players can challenge each other via community posts; game state is synced via Supabase Realtime. + +-- Add challengeId to community_posts +ALTER TABLE rebreak.community_posts ADD COLUMN IF NOT EXISTS challenge_id UUID; + +-- GameChallengeStatus enum +DO $$ BEGIN + CREATE TYPE rebreak."GameChallengeStatus" AS ENUM ('OPEN', 'ACTIVE', 'FINISHED', 'CANCELLED'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- game_challenges table +CREATE TABLE IF NOT EXISTS rebreak.game_challenges ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + challenger_id UUID NOT NULL, + challenger_name TEXT NOT NULL, + opponent_id UUID, + opponent_name TEXT, + status rebreak."GameChallengeStatus" NOT NULL DEFAULT 'OPEN', + board TEXT NOT NULL DEFAULT '---------', + current_turn TEXT NOT NULL DEFAULT 'X', + winner TEXT, + post_id UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS game_challenges_challenger_idx ON rebreak.game_challenges (challenger_id); +CREATE INDEX IF NOT EXISTS game_challenges_opponent_idx ON rebreak.game_challenges (opponent_id); +CREATE INDEX IF NOT EXISTS game_challenges_status_idx ON rebreak.game_challenges (status); + +-- Enable Supabase Realtime for live game sync +ALTER PUBLICATION supabase_realtime ADD TABLE rebreak.game_challenges; diff --git a/backend/prisma/migrations/add_game_challenges_rls.sql b/backend/prisma/migrations/add_game_challenges_rls.sql new file mode 100644 index 0000000..2df5d8c --- /dev/null +++ b/backend/prisma/migrations/add_game_challenges_rls.sql @@ -0,0 +1,20 @@ +-- Enable RLS on game_challenges so Supabase Realtime can use auth.uid() for row filtering +-- Without RLS, Realtime falls back to an empty role which causes "role "" does not exist" errors + +ALTER TABLE rebreak.game_challenges ENABLE ROW LEVEL SECURITY; + +-- Both players can read the game they are part of +CREATE POLICY "players can read their game" ON rebreak.game_challenges + FOR SELECT USING ( + auth.uid() = challenger_id OR auth.uid() = opponent_id + ); + +-- Only the challenger can create the game row +CREATE POLICY "challenger can create game" ON rebreak.game_challenges + FOR INSERT WITH CHECK (auth.uid() = challenger_id); + +-- Both players can update the game (make moves, accept/cancel) +CREATE POLICY "players can update their game" ON rebreak.game_challenges + FOR UPDATE USING ( + auth.uid() = challenger_id OR auth.uid() = opponent_id + ); diff --git a/backend/prisma/migrations/add_streak_events.sql b/backend/prisma/migrations/add_streak_events.sql new file mode 100644 index 0000000..1ebfdfe --- /dev/null +++ b/backend/prisma/migrations/add_streak_events.sql @@ -0,0 +1,16 @@ +-- Streak Events Tabelle für Timeline/Verlauf +CREATE TABLE IF NOT EXISTS rebreak.streak_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + type TEXT NOT NULL, -- 'started' | 'reset' | 'milestone' | 'relapse' + meta JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_streak_events_user ON rebreak.streak_events(user_id, created_at DESC); + +-- RLS +ALTER TABLE rebreak.streak_events ENABLE ROW LEVEL SECURITY; +CREATE POLICY "streak_events: own all" + ON rebreak.streak_events FOR ALL + USING (auth.uid() = user_id); diff --git a/backend/prisma/migrations/fix_user_custom_domains_unique_constraint.sql b/backend/prisma/migrations/fix_user_custom_domains_unique_constraint.sql new file mode 100644 index 0000000..b46f08b --- /dev/null +++ b/backend/prisma/migrations/fix_user_custom_domains_unique_constraint.sql @@ -0,0 +1,10 @@ +-- Fix: user_custom_domains unique constraint +-- Vorher: UNIQUE(domain) → eine Domain konnte nur von einem User hinzugefügt werden +-- Nachher: UNIQUE(user_id, domain) → jeder User kann eigene Domain-Liste führen + +-- Schritt 1: Alte globale unique constraint entfernen +DROP INDEX IF EXISTS rebreak."user_custom_domains_domain_key"; + +-- Schritt 2: Neue composite unique constraint auf (user_id, domain) +CREATE UNIQUE INDEX "user_custom_domains_userId_domain_key" + ON rebreak."user_custom_domains" ("user_id", "domain"); diff --git a/backend/prisma/migrations/remove_nickname_avatar_from_profiles.sql b/backend/prisma/migrations/remove_nickname_avatar_from_profiles.sql new file mode 100644 index 0000000..a3a7b85 --- /dev/null +++ b/backend/prisma/migrations/remove_nickname_avatar_from_profiles.sql @@ -0,0 +1,7 @@ +-- Migration: nickname und avatar wieder in rebreak.profiles aufnehmen +-- Grund: Prisma include-Relationen für Posts/Comments benötigen diese Felder in der DB. +-- Strategie: profiles = sync-Cache, user_metadata = src of truth für Auth. +-- Beim Update wird in BEIDE geschrieben (me.patch.ts + avatar/upload.post.ts) + +ALTER TABLE rebreak.profiles ADD COLUMN IF NOT EXISTS avatar TEXT; +ALTER TABLE rebreak.profiles ADD COLUMN IF NOT EXISTS nickname TEXT; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..b5d2827 --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,604 @@ +generator client { + provider = "prisma-client-js" + output = "../server/generated/prisma" + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] +} + +datasource db { + provider = "postgresql" + schemas = ["rebreak"] +} + +model Profile { + id String @id @db.Uuid + username String? + nickname String? + avatar String? + plan String @default("free") + streak Int @default(0) + followersCount Int @default(0) @map("followers_count") + stripeCustomerId String? @map("stripe_customer_id") + stripeSubId String? @map("stripe_subscription_id") + premiumUntil DateTime? @map("premium_until") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + + communityPosts CommunityPost[] + communityReplies CommunityReply[] + + @@map("profiles") + @@schema("rebreak") +} + +model Streak { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + startDate DateTime @map("start_date") @db.Date + currentDays Int @default(0) @map("current_days") + longestDays Int @default(0) @map("longest_days") + avgMonthlySavings Float? @map("avg_monthly_savings") + isActive Boolean @default(true) @map("is_active") + + @@map("streaks") + @@schema("rebreak") +} + +model StreakEvent { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + type String // "started" | "reset" | "milestone" | "relapse" + meta Json? // z.B. { days: 30 } für Meilensteine + createdAt DateTime @default(now()) @map("created_at") + + @@map("streak_events") + @@schema("rebreak") +} + +model UrgeLog { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + timestamp DateTime @default(now()) + emotion String + wasOvercome Boolean @default(false) @map("was_overcome") + breathingDone Boolean @default(false) @map("breathing_done") + + @@map("urge_logs") + @@schema("rebreak") +} + +/// SOS-Session für DiGA-Auswertung — kompletter Verlauf einer Notfall-Session +model SosSession { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + startedAt DateTime @default(now()) @map("started_at") + endedAt DateTime? @map("ended_at") + durationSec Int? @map("duration_sec") + /// Voller Chat-Verlauf [{role, content, timestamp}] + messages Json @default("[]") + /// [{game, score, durationSec}] + gamesPlayed Json @default("[]") + breathingCount Int @default(0) @map("breathing_count") + wasOvercome Boolean @default(false) @map("was_overcome") + feedbackBetter Boolean? @map("feedback_better") + feedbackRating Int? @map("feedback_rating") // 1-5 + feedbackText String? @map("feedback_text") + locale String? + + @@index([userId, startedAt]) + @@map("sos_sessions") + @@schema("rebreak") +} + +model CommunityPost { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + category String + content String + imageUrl String? @map("image_url") + upvotes Int @default(0) + likesCount Int @default(0) @map("likes_count") + dislikesCount Int @default(0) @map("dislikes_count") + commentsCount Int @default(0) @map("comments_count") + repostsCount Int @default(0) @map("reposts_count") + isAnonymous Boolean @default(false) @map("is_anonymous") + isModerated Boolean @default(false) @map("is_moderated") + repostOfId String? @map("repost_of_id") @db.Uuid + challengeId String? @map("challenge_id") @db.Uuid + createdAt DateTime @default(now()) @map("created_at") + + author Profile? @relation(fields: [userId], references: [id]) + repostOf CommunityPost? @relation("Reposts", fields: [repostOfId], references: [id], onDelete: SetNull) + reposts CommunityPost[] @relation("Reposts") + PostLike PostLike[] + CommunityReply CommunityReply[] + + @@map("community_posts") + @@schema("rebreak") +} + +model PostLike { + userId String @map("user_id") @db.Uuid + postId String @map("post_id") @db.Uuid + type String // "like" | "dislike" + + post CommunityPost @relation(fields: [postId], references: [id], onDelete: Cascade) + + @@id([userId, postId]) + @@map("post_likes") + @@schema("rebreak") +} + +model CommunityReply { + id String @id @default(uuid()) @db.Uuid + postId String @map("post_id") @db.Uuid + userId String @map("user_id") @db.Uuid + content String + parentReplyId String? @map("parent_reply_id") @db.Uuid + isAnonymous Boolean @default(false) @map("is_anonymous") + likesCount Int @default(0) @map("likes_count") + createdAt DateTime @default(now()) @map("created_at") + + post CommunityPost @relation(fields: [postId], references: [id], onDelete: Cascade) + author Profile? @relation(fields: [userId], references: [id]) + CommentLike CommentLike[] + + @@map("community_replies") + @@schema("rebreak") +} + +model CommentLike { + userId String @map("user_id") @db.Uuid + commentId String @map("comment_id") @db.Uuid + + reply CommunityReply @relation(fields: [commentId], references: [id], onDelete: Cascade) + + @@id([userId, commentId]) + @@map("comment_likes") + @@schema("rebreak") +} + +model ChatRoom { + id String @id @default(uuid()) @db.Uuid + name String + description String? + isPublic Boolean @default(false) @map("is_public") + avatarUrl String? @map("avatar_url") + createdBy String @map("created_by") @db.Uuid + joinMode String @default("open") @map("join_mode") // "open" | "approval" | "invite_only" + inviteCode String? @unique @map("invite_code") + memberCount Int @default(0) @map("member_count") + isDefault Boolean @default(false) @map("is_default") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + members ChatRoomMember[] + messages ChatMessage[] + + @@map("chat_rooms") + @@schema("rebreak") +} + +model ChatRoomMember { + roomId String @map("room_id") @db.Uuid + userId String @map("user_id") @db.Uuid + role String @default("member") // "owner" | "admin" | "member" + status String @default("active") // "active" | "pending" + joinedAt DateTime @default(now()) @map("joined_at") + + room ChatRoom @relation(fields: [roomId], references: [id], onDelete: Cascade) + + @@id([roomId, userId]) + @@map("chat_room_members") + @@schema("rebreak") +} + +model ChatMessage { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + content String + roomId String? @map("room_id") @db.Uuid + replyToId String? @map("reply_to_id") @db.Uuid + attachmentUrl String? @map("attachment_url") + attachmentType String? @map("attachment_type") + attachmentName String? @map("attachment_name") + likesCount Int @default(0) @map("likes_count") + createdAt DateTime @default(now()) @map("created_at") + + room ChatRoom? @relation(fields: [roomId], references: [id], onDelete: Cascade) + replyTo ChatMessage? @relation("ChatReplies", fields: [replyToId], references: [id], onDelete: SetNull) + replies ChatMessage[] @relation("ChatReplies") + likes ChatMessageLike[] + + @@map("chat_messages") + @@schema("rebreak") +} + +model ChatMessageLike { + userId String @map("user_id") @db.Uuid + messageId String @map("message_id") @db.Uuid + + message ChatMessage @relation(fields: [messageId], references: [id], onDelete: Cascade) + + @@id([userId, messageId]) + @@map("chat_message_likes") + @@schema("rebreak") +} + +model DirectMessage { + id String @id @default(uuid()) @db.Uuid + senderId String @map("sender_id") @db.Uuid + receiverId String @map("receiver_id") @db.Uuid + content String + replyToId String? @map("reply_to_id") @db.Uuid + attachmentUrl String? @map("attachment_url") + attachmentType String? @map("attachment_type") + attachmentName String? @map("attachment_name") + likesCount Int @default(0) @map("likes_count") + createdAt DateTime @default(now()) @map("created_at") + readAt DateTime? @map("read_at") + + replyTo DirectMessage? @relation("DmReplies", fields: [replyToId], references: [id], onDelete: SetNull) + replies DirectMessage[] @relation("DmReplies") + likes DirectMessageLike[] + + @@map("direct_messages") + @@schema("rebreak") +} + +model DirectMessageLike { + userId String @map("user_id") @db.Uuid + messageId String @map("message_id") @db.Uuid + + message DirectMessage @relation(fields: [messageId], references: [id], onDelete: Cascade) + + @@id([userId, messageId]) + @@map("direct_message_likes") + @@schema("rebreak") +} + +model UserFollow { + followerId String @map("follower_id") @db.Uuid + followingId String @map("following_id") @db.Uuid + + @@id([followerId, followingId]) + @@map("user_follows") + @@schema("rebreak") +} + +model UserScore { + userId String @id @map("user_id") @db.Uuid + totalPoints Int @default(0) @map("total_points") + tier String @default("beginner") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + + @@map("user_scores") + @@schema("rebreak") +} + +model ScoreEvent { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + eventType String @map("event_type") + points Int + createdAt DateTime @default(now()) @map("created_at") + meta Json? + + @@map("score_events") + @@schema("rebreak") +} + +model UserCustomDomain { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + domain String + source String @default("manual") + // "active" | "submitted" | "approved" | "rejected" + status String @default("active") + postId String? @map("post_id") @db.Uuid + addedAt DateTime @default(now()) @map("added_at") + + submission DomainSubmission? + + @@unique([userId, domain]) + @@map("user_custom_domains") + @@schema("rebreak") +} + +model DomainSubmission { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + domain String + customDomainId String @unique @map("custom_domain_id") @db.Uuid + postId String? @map("post_id") @db.Uuid + // "pending" | "approved" | "rejected" + status String @default("pending") + yesVotes Int @default(0) @map("yes_votes") + noVotes Int @default(0) @map("no_votes") + reviewNote String? @map("review_note") + createdAt DateTime @default(now()) @map("created_at") + reviewedAt DateTime? @map("reviewed_at") + + customDomain UserCustomDomain @relation(fields: [customDomainId], references: [id], onDelete: Cascade) + votes DomainVote[] + + @@map("domain_submissions") + @@schema("rebreak") +} + +model DomainVote { + userId String @map("user_id") @db.Uuid + submissionId String @map("submission_id") @db.Uuid + vote String // "yes" | "no" + createdAt DateTime @default(now()) @map("created_at") + + submission DomainSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade) + + @@id([userId, submissionId]) + @@map("domain_votes") + @@schema("rebreak") +} + +enum FeedbackStatus { + PENDING + REVIEWING + PLANNED + SHIPPED + REJECTED + + @@schema("rebreak") +} + +model FeedbackItem { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + content String + category String? + status FeedbackStatus @default(PENDING) + adminNote String? @map("admin_note") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("feedback_items") + @@schema("rebreak") +} + +model BlocklistDomain { + id String @id @default(uuid()) @db.Uuid + domain String @unique + source String + isActive Boolean @default(true) @map("is_active") + reportCount Int @default(0) @map("report_count") + + @@map("blocklist_domains") + @@schema("rebreak") +} + +model TrustedContact { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + name String + phone String? + email String? + createdAt DateTime @default(now()) @map("created_at") + + @@map("trusted_contacts") + @@schema("rebreak") +} + +model CoachSession { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + content Json + createdAt DateTime @default(now()) @map("created_at") + + @@map("coach_sessions") + @@schema("rebreak") +} + +model MailConnection { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + email String + provider String @default("imap") + providerName String? @map("provider_name") + imapHost String @map("imap_host") + imapPort Int @map("imap_port") + rejectUnauthorized Boolean @default(true) @map("reject_unauthorized") + useStarttls Boolean @default(false) @map("use_starttls") + passwordEncrypted String @map("password_encrypted") + isActive Boolean @default(true) @map("is_active") + scanInterval Int @default(24) @map("scan_interval") + lastScannedAt DateTime? @map("last_scanned_at") + nextScanAt DateTime? @map("next_scan_at") + emailsBlocked Int @default(0) @map("emails_blocked") + emailsScanned Int @default(0) @map("emails_scanned") + createdAt DateTime @default(now()) @map("created_at") + + blockedMails MailBlocked[] + + @@unique([userId, email]) + @@map("mail_connections") + @@schema("rebreak") +} + +enum GameChallengeStatus { + OPEN + ACTIVE + FINISHED + CANCELLED + + @@schema("rebreak") +} + +model GameChallenge { + id String @id @default(uuid()) @db.Uuid + challengerId String @map("challenger_id") @db.Uuid + challengerName String @map("challenger_name") + opponentId String? @map("opponent_id") @db.Uuid + opponentName String? @map("opponent_name") + status GameChallengeStatus @default(OPEN) + board String @default("---------") + currentTurn String @default("X") @map("current_turn") + winner String? + postId String? @map("post_id") @db.Uuid + gameType String @default("tictactoe") @map("game_type") + isLive Boolean @default(false) @map("is_live") + memoryState Json? @map("memory_state") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("game_challenges") + @@schema("rebreak") +} + +model Notification { + id String @id @default(uuid()) @db.Uuid + recipientId String @map("recipient_id") @db.Uuid + type String // "new_comment" | "new_like" | "domain_vote" + actorName String @map("actor_name") + actorAvatar String? @map("actor_avatar") + postId String? @map("post_id") @db.Uuid + preview String? + readAt DateTime? @map("read_at") + createdAt DateTime @default(now()) @map("created_at") + + @@index([recipientId, readAt]) + @@map("notifications") + @@schema("rebreak") +} + +model GameScore { + userId String @id @map("user_id") @db.Uuid + playerName String @map("player_name") + wins Int @default(0) + losses Int @default(0) + draws Int @default(0) + points Int @default(0) + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("game_scores") + @@schema("rebreak") +} + +model GameRating { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + gameName String @map("game_name") + stars Int + feedback String? + score Int @default(0) + createdAt DateTime @default(now()) @map("created_at") + + @@map("game_ratings") + @@schema("rebreak") +} + +model GameHighScore { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + nickname String + gameName String @map("game_name") + score Int + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([userId, gameName]) + @@index([gameName, score(sort: Desc)]) + @@map("game_high_scores") + @@schema("rebreak") +} + +model MailBlocked { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + connectionId String @map("connection_id") @db.Uuid + gmailMessageId String @map("gmail_message_id") + senderEmail String @map("sender_email") + senderName String? @map("sender_name") + subject String + receivedAt DateTime @map("received_at") + action String + createdAt DateTime @default(now()) @map("created_at") + + connection MailConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) + + @@unique([gmailMessageId, userId]) + @@map("mail_blocked") + @@schema("rebreak") +} + +model ImapProxyAccount { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + proxyUsername String @unique @map("proxy_username") + proxyPassword String @map("proxy_password") + connectionId String @unique @map("connection_id") @db.Uuid + createdAt DateTime @default(now()) @map("created_at") + + @@map("imap_proxy_accounts") + @@schema("rebreak") +} + +model CooldownRequest { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + reason String? + cooldownStartedAt DateTime @default(now()) @map("cooldown_started_at") + cooldownEndsAt DateTime @map("cooldown_ends_at") + resolvedAt DateTime? @map("resolved_at") + cancelledAt DateTime? @map("cancelled_at") + tokenJti String @unique @map("token_jti") + + @@index([userId, cooldownEndsAt]) + @@map("cooldown_requests") + @@schema("rebreak") +} + +enum LyraMemoryType { + trigger + habit + strength + relationship + milestone + pain_point + goal + preference + + @@schema("rebreak") +} + +/// Persistente Erinnerungen von Lyra über den User — injiziert in System-Prompt jeder Session. +/// Enthält Art-9-Gesundheitsdaten (Glücksspielkontext) — strenge RLS: nur service-role schreibt. +model LyraMemory { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + type LyraMemoryType + content String @db.VarChar(500) + confidence Float @default(0.7) + source String? // session-id | "manual" | "observed" + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) + lastReferencedAt DateTime? @map("last_referenced_at") @db.Timestamptz(6) + + @@index([userId, type]) + @@map("lyra_memories") + @@schema("rebreak") +} + +// Device-Binding pro User: Free=1, Pro=1, Legend=3 (siehe plan-features.ts maxDevices). +// Frontend liefert via Capacitor Device.getId() eine persistente UUID — diese wird +// bei jedem authentifizierten Request via x-device-id Header geprüft. +model UserDevice { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + deviceId String @map("device_id") // Capacitor persistent UUID + platform String // "ios" | "android" | "web" + model String? // z.B. "iPhone15,2" + name String? // z.B. "Chahines iPhone" + lastSeenAt DateTime @default(now()) @map("last_seen_at") + createdAt DateTime @default(now()) @map("created_at") + + @@unique([userId, deviceId]) + @@index([userId]) + @@map("user_devices") + @@schema("rebreak") +} diff --git a/backend/server/api/admin/domain-submissions/[id]/approve.post.ts b/backend/server/api/admin/domain-submissions/[id]/approve.post.ts new file mode 100644 index 0000000..7c138b6 --- /dev/null +++ b/backend/server/api/admin/domain-submissions/[id]/approve.post.ts @@ -0,0 +1,112 @@ +import { adminApproveSubmission } from "../../../../db/domains"; + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const adminSecret = getHeader(event, "x-admin-secret"); + if (adminSecret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "ID fehlt" }); + + const body = await readBody(event).catch(() => ({})); + const result = await adminApproveSubmission(id, body?.note); + + // Lyra-Post über die neu genehmigte Domain (fire & forget) + const domain = (result as any)?.domain ?? null; + const submitterUserId = (result as any)?.userId ?? null; + const lyraBotUserId = config.lyraBotUserId; + console.log( + `[approve] domain=${domain}, lyraBotUserId=${lyraBotUserId}, hasGroq=${!!config.groqApiKey}`, + ); + if (domain && lyraBotUserId && config.groqApiKey) { + // db + submitterName VOR der IIFE holen (usePrisma braucht Event-Kontext) + const db = usePrisma(); + let rawName: string | null = null; + if (submitterUserId) { + try { + // Rebreak Prisma-Modell heißt 'profile' (nicht 'user') + const submitter = await db.profile.findUnique({ + where: { id: submitterUserId }, + select: { nickname: true, username: true }, + }); + rawName = submitter?.nickname || submitter?.username || null; + } catch {} + } + // Für @mention: Leerzeichen entfernen (Regex matcht nur einzelne Wörter) + const mentionName = rawName?.replace(/\s+/g, "") ?? null; + const hasMention = !!mentionName; + const mentionRef = hasMention + ? `@${mentionName}` + : "einem Community-Mitglied"; + + // Stats für Lyra-Text holen + let statsLine = ""; + try { + const stats = await db.blocklistDomain.count({ + where: { isActive: true }, + }); + const startOfMonth = new Date(); + startOfMonth.setDate(1); + startOfMonth.setHours(0, 0, 0, 0); + const monthlyAdded = await db.domainSubmission.count({ + where: { status: "approved", reviewedAt: { gte: startOfMonth } }, + }); + statsLine = `Damit schützen wir gemeinsam vor ${stats.toLocaleString("de-DE")} Domains${monthlyAdded > 0 ? ` (+${monthlyAdded} diesen Monat)` : ""}.`; + } catch {} + + const groqUserPrompt = hasMention + ? `Schreibe einen kurzen Community-Post (max. 2 Sätze, auf Deutsch): Die Domain "${domain}" wurde zur ReBreak-Blockliste hinzugefügt – möglich gemacht durch ${mentionRef}. Erwähne ${mentionRef} genau einmal. Füge am Ende diesen Satz exakt ein: "${statsLine}" Warm, direkt, kein doppelter Dank.` + : `Schreibe einen kurzen Community-Post (max. 2 Sätze, auf Deutsch): Die Domain "${domain}" wurde zur ReBreak-Blockliste hinzugefügt. Füge am Ende diesen Satz exakt ein: "${statsLine}" Warm, direkt.`; + + const groqApiKey = config.groqApiKey; + (async () => { + try { + const response = await $fetch<{ + choices: { message: { content: string } }[]; + }>("https://api.groq.com/openai/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${groqApiKey}`, + "Content-Type": "application/json", + }, + body: { + model: "llama-3.3-70b-versatile", + max_tokens: 150, + messages: [ + { + role: "system", + content: `Du bist Lyra – Recovery-Coach der ReBreak-Community. Tonalität: warm, persönlich, direkt. Schreibe NUR den Post-Text, kein Prefix, keine Anführungszeichen.`, + }, + { + role: "user", + content: groqUserPrompt, + }, + ], + }, + }); + const content = response.choices?.[0]?.message?.content?.trim(); + if (content) { + const faviconUrl = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=64`; + await db.communityPost.create({ + data: { + userId: lyraBotUserId, + category: "domain_approved", + content, + imageUrl: faviconUrl, + isAnonymous: false, + isModerated: false, + }, + }); + console.log( + `[approve] Lyra-Post erstellt für domain=${domain}, submitter=${mentionName ?? "anonym"}`, + ); + } + } catch (err) { + console.error(`[approve] Lyra-Post fehlgeschlagen:`, err); + } + })(); + } + + return { ok: true }; +}); diff --git a/backend/server/api/admin/domain-submissions/[id]/reject.post.ts b/backend/server/api/admin/domain-submissions/[id]/reject.post.ts new file mode 100644 index 0000000..b125612 --- /dev/null +++ b/backend/server/api/admin/domain-submissions/[id]/reject.post.ts @@ -0,0 +1,15 @@ +import { adminRejectSubmission } from "../../../../db/domains"; + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const adminSecret = getHeader(event, "x-admin-secret"); + if (adminSecret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "ID fehlt" }); + + const body = await readBody(event).catch(() => ({})); + await adminRejectSubmission(id, body?.note); + return { ok: true }; +}); diff --git a/backend/server/api/admin/domain-submissions/index.get.ts b/backend/server/api/admin/domain-submissions/index.get.ts new file mode 100644 index 0000000..17c5143 --- /dev/null +++ b/backend/server/api/admin/domain-submissions/index.get.ts @@ -0,0 +1,10 @@ +import { getPendingSubmissions } from "../../../db/domains"; + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const adminSecret = getHeader(event, "x-admin-secret"); + if (adminSecret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + return getPendingSubmissions(); +}); diff --git a/backend/server/api/admin/lyra-generate.post.ts b/backend/server/api/admin/lyra-generate.post.ts new file mode 100644 index 0000000..32140ed --- /dev/null +++ b/backend/server/api/admin/lyra-generate.post.ts @@ -0,0 +1,78 @@ +import { LYRA_TOPICS, TOPIC_HINTS, type LyraTopic } from "./lyra-post.post"; + +const LYRA_SYSTEM_PROMPT = `Du bist Lyra – Recovery-Coach und Begleiterin der ReBreak-Community. Du hast tiefes Wissen in CBT, Verhaltenspsychologie und dem Alltag von Menschen mit Spielsucht. + +Du schreibst kurze Community-Beiträge. Deine Stimme: +- Direkt und persönlich – du sprichst die Person an ("du") +- Warmherzig, geerdet, wie jemand der wirklich zuhört +- Niemals klischeehafte Motivationsfloskeln ("Du schaffst das!!!") +- Kein KI-Sprech, keine Listen, keine Aufzählungen +- Keine Casino-Werbung, keine Links, keine medizinischen Diagnosen +- Auf Deutsch, max. 3–4 Sätze + +Antworte NUR mit dem Post-Text. Kein "Lyra:" Prefix, keine Anführungszeichen.`; + +const REBREAK_SYSTEM_PROMPT = `Du bist der offizielle ReBreak Account. ReBreak ist eine App zur Überwindung von Glücksspielsucht. + +Du postest Neuigkeiten, Updates und Community-Ankündigungen. Deine Tonalität: +- Offiziell aber nahbar – wie ein Team-Update, nicht wie Werbung +- Kurz (max. 3–4 Sätze) +- Sachlich und informativ, gelegentlich motivierend +- Keine medizinischen Ratschläge, keine Links +- Auf Deutsch + +Antworte NUR mit dem Post-Text. Kein "ReBreak:" Prefix, keine Anführungszeichen.`; + +/** POST /api/admin/lyra-generate — generiert Text via LLM ohne zu posten */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const adminSecret = getHeader(event, "x-admin-secret"); + + if (!config.adminSecret || adminSecret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + if (!config.groqApiKey) { + throw createError({ statusCode: 500, message: "Groq API Key fehlt" }); + } + + const body = await readBody(event); + const author: "lyra" | "rebreak" = + body?.author === "rebreak" ? "rebreak" : "lyra"; + const topic: LyraTopic = LYRA_TOPICS.includes(body?.topic) + ? body.topic + : LYRA_TOPICS[Math.floor(Math.random() * LYRA_TOPICS.length)]; + const context: string | undefined = body?.context?.trim() || undefined; + + const userPrompt = context + ? `${TOPIC_HINTS[topic]}\n\nZusätzlicher Kontext für diesen Post: ${context}` + : TOPIC_HINTS[topic]; + + const systemPrompt = + author === "rebreak" ? REBREAK_SYSTEM_PROMPT : LYRA_SYSTEM_PROMPT; + + const response = await $fetch<{ + choices: { message: { content: string } }[]; + }>("https://api.groq.com/openai/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${config.groqApiKey}`, + "Content-Type": "application/json", + }, + body: { + model: "llama-3.3-70b-versatile", + max_tokens: 200, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userPrompt }, + ], + }, + }); + + const content = response.choices?.[0]?.message?.content?.trim(); + if (!content) { + throw createError({ statusCode: 500, message: "Keine Antwort von LLM" }); + } + + return { success: true, content }; +}); diff --git a/backend/server/api/admin/lyra-post.post.ts b/backend/server/api/admin/lyra-post.post.ts new file mode 100644 index 0000000..ea6eac4 --- /dev/null +++ b/backend/server/api/admin/lyra-post.post.ts @@ -0,0 +1,125 @@ +import { createPost } from "../../db/community"; + +export const LYRA_TOPICS = [ + "motivation", + "tipp", + "zitat", + "witzig", + "news", + "feature", +] as const; + +export type LyraTopic = (typeof LYRA_TOPICS)[number]; + +export const TOPIC_HINTS: Record = { + motivation: + "Schreibe einen kurzen, stillen Gedanken für Menschen die heute kämpfen. Nicht übertrieben – eher ruhig stark.", + tipp: "Teile einen kleinen, konkreten Trick aus der Verhaltensforschung oder CBT gegen Spieldrang. Praktisch und direkt.", + zitat: + "Teile ein tiefgründiges Zitat aus Psychologie, Stoizismus oder Verhaltensforschung – ohne das Wort 'Sucht' zu verwenden. Kurz kommentiert.", + witzig: + "Schreibe einen witzigen, selbstironischen Post über das Thema Ablenkung, Impulskontrolle oder Gewohnheiten – leicht und menschlich, nicht flach.", + news: "Beschreibe kurz eine typische Taktik der Glücksspielindustrie (z.B. Push-Notifications, Bonusangebote) – sachlich und als Warnung formuliert.", + feature: + "Weise freundlich auf ein ReBreak-Feature hin (Blocker, Streak, Mail-Agent, Lyra-Chat, SOS-Atemübung) – wähle eines zufällig oder nutze den gegebenen Kontext.", +}; + +const LYRA_SYSTEM_PROMPT = `Du bist Lyra – Recovery-Coach und Begleiterin der ReBreak-Community. Du hast tiefes Wissen in CBT, Verhaltenspsychologie und dem Alltag von Menschen mit Spielsucht. + +Du schreibst kurze Community-Beiträge. Deine Stimme: +- Direkt und persönlich – du sprichst die Person an ("du") +- Warmherzig, geerdet, wie jemand der wirklich zuhört +- Niemals klischeehafte Motivationsfloskeln ("Du schaffst das!!!") +- Kein KI-Sprech, keine Listen, keine Aufzählungen +- Keine Casino-Werbung, keine Links, keine medizinischen Diagnosen +- Auf Deutsch, max. 3–4 Sätze + +Antworte NUR mit dem Post-Text. Kein "Lyra:" Prefix, keine Anführungszeichen.`; + +const REBREAK_SYSTEM_PROMPT = `Du bist der offizielle ReBreak Account. ReBreak ist eine App zur Überwindung von Glücksspielsucht. + +Du postest Neuigkeiten, Updates und Community-Ankündigungen. Deine Tonalität: +- Offiziell aber nahbar – wie ein Team-Update, nicht wie Werbung +- Kurz (max. 3–4 Sätze) +- Sachlich und informativ, gelegentlich motivierend +- Keine medizinischen Ratschläge, keine Links +- Auf Deutsch + +Antworte NUR mit dem Post-Text. Kein "ReBreak:" Prefix, keine Anführungszeichen.`; + +/** POST /api/admin/lyra-post — manueller Bot-Post vom Admin-Dashboard */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const adminSecret = getHeader(event, "x-admin-secret"); + + if (!config.adminSecret || adminSecret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + const body = await readBody(event); + const author: "lyra" | "rebreak" = + body?.author === "rebreak" ? "rebreak" : "lyra"; + + const botUserId = + author === "rebreak" ? config.rebreakBotUserId : config.lyraBotUserId; + + if (!botUserId) { + throw createError({ + statusCode: 500, + message: `${author === "rebreak" ? "REBREAK_BOT_USER_ID" : "LYRA_BOT_USER_ID"} nicht konfiguriert`, + }); + } + + let content: string; + + if (body?.customContent?.trim()) { + // Admin hat Text direkt eingegeben – kein LLM-Call nötig + content = body.customContent.trim(); + } else { + if (!config.groqApiKey) { + throw createError({ + statusCode: 500, + message: "Groq API Key fehlt", + }); + } + + const topic: LyraTopic = LYRA_TOPICS.includes(body?.topic) + ? body.topic + : LYRA_TOPICS[Math.floor(Math.random() * LYRA_TOPICS.length)]; + const context: string | undefined = body?.context?.trim() || undefined; + + const userPrompt = context + ? `${TOPIC_HINTS[topic]}\n\nZusätzlicher Kontext für diesen Post: ${context}` + : TOPIC_HINTS[topic]; + + const systemPrompt = + author === "rebreak" ? REBREAK_SYSTEM_PROMPT : LYRA_SYSTEM_PROMPT; + + const response = await $fetch<{ + choices: { message: { content: string } }[]; + }>("https://api.groq.com/openai/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${config.groqApiKey}`, + "Content-Type": "application/json", + }, + body: { + model: "llama-3.3-70b-versatile", + max_tokens: 200, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userPrompt }, + ], + }, + }); + + content = response.choices?.[0]?.message?.content?.trim() ?? ""; + if (!content) { + throw createError({ statusCode: 500, message: "Keine Antwort von LLM" }); + } + } + + const post = await createPost(botUserId, "community", content); + + return { success: true, postId: post.id, author, content }; +}); diff --git a/backend/server/api/admin/lyra-profile.get.ts b/backend/server/api/admin/lyra-profile.get.ts new file mode 100644 index 0000000..09f8db2 --- /dev/null +++ b/backend/server/api/admin/lyra-profile.get.ts @@ -0,0 +1,30 @@ +import { getProfile } from "../../db/profile"; + +/** GET /api/admin/lyra-profile?author=lyra|rebreak — gibt Nickname und Avatar des Bot-Accounts zurück */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const adminSecret = getHeader(event, "x-admin-secret"); + + if (!config.adminSecret || adminSecret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + const query = getQuery(event); + const author = query.author === "rebreak" ? "rebreak" : "lyra"; + + const userId = + author === "rebreak" ? config.rebreakBotUserId : config.lyraBotUserId; + + if (!userId) { + return { + nickname: author === "rebreak" ? "ReBreak" : "Lyra", + avatar: null, + }; + } + + const profile = await getProfile(userId); + return { + nickname: profile?.nickname ?? (author === "rebreak" ? "ReBreak" : "Lyra"), + avatar: profile?.avatar ?? null, + }; +}); diff --git a/backend/server/api/admin/set-lyra-avatar.post.ts b/backend/server/api/admin/set-lyra-avatar.post.ts new file mode 100644 index 0000000..7d9749d --- /dev/null +++ b/backend/server/api/admin/set-lyra-avatar.post.ts @@ -0,0 +1,72 @@ +import { updateProfile } from "../../db/profile"; + +/** POST /api/admin/set-lyra-avatar — PNG aus Canvas hochladen und als Bot-Avatar setzen */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const adminSecret = getHeader(event, "x-admin-secret"); + + if (!config.adminSecret || adminSecret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + const body = await readBody<{ author: "lyra" | "rebreak"; dataUrl: string }>( + event, + ); + + if (!body?.dataUrl?.startsWith("data:image/")) { + throw createError({ statusCode: 400, message: "Invalid dataUrl" }); + } + + const author = body.author === "rebreak" ? "rebreak" : "lyra"; + const userId = + author === "rebreak" ? config.rebreakBotUserId : config.lyraBotUserId; + + if (!userId) { + throw createError({ + statusCode: 400, + message: `Bot user ID for ${author} not configured`, + }); + } + + // base64 → Buffer + const base64 = body.dataUrl.replace(/^data:image\/\w+;base64,/, ""); + const buffer = Buffer.from(base64, "base64"); + + const supabaseUrl = "https://db-staging.rebreak.org"; + const serviceKey = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsImF1ZCI6ImF1dGhlbnRpY2F0ZWQiLCJyb2xlIjoic2VydmljZV9yb2xlIiwiZXhwIjoyMDkxMDE4OTU1LCJpYXQiOjE3NzU2NTg5NTV9.45fB5DC0-RMrYQXhlB0mI-bjtFAiHhjdBeBY9X8B8b8"; + + const storagePath = `${author}-bot/avatar.png`; + const storageUrl = `${supabaseUrl}/storage/v1/object/rebreak-avatars/${storagePath}`; + + // Erst versuchen zu updaten, dann als neu anlegen + const uploadRes = await $fetch<{ Key?: string; error?: string }>(storageUrl, { + method: "PUT", + headers: { + apikey: serviceKey, + Authorization: `Bearer ${serviceKey}`, + "Content-Type": "image/png", + "x-upsert": "true", + }, + body: buffer, + }).catch(() => null); + + if (!uploadRes?.Key && !uploadRes) { + // Fallback: POST (create) + await $fetch(storageUrl, { + method: "POST", + headers: { + apikey: serviceKey, + Authorization: `Bearer ${serviceKey}`, + "Content-Type": "image/png", + }, + body: buffer, + }); + } + + const avatarPublicUrl = `${supabaseUrl}/storage/v1/object/public/rebreak-avatars/${storagePath}?t=${Date.now()}`; + + await updateProfile(userId, { avatar: avatarPublicUrl }); + + return { success: true, avatar: avatarPublicUrl }; +}); diff --git a/backend/server/api/admin/stats.get.ts b/backend/server/api/admin/stats.get.ts new file mode 100644 index 0000000..4eab6f2 --- /dev/null +++ b/backend/server/api/admin/stats.get.ts @@ -0,0 +1,54 @@ +import { usePrisma } from "../../utils/prisma"; + +/** GET /api/admin/stats — Übersicht-Statistiken fürs Dashboard */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const secret = getHeader(event, "x-admin-secret"); + if (!config.adminSecret || secret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + const db = usePrisma(); + const now = new Date(); + const last7d = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const last30d = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + + const [ + totalUsers, + newUsersWeek, + totalPosts, + newPostsWeek, + pendingDomains, + approvedDomains, + pendingFeedback, + totalFeedback, + lyraPosts, + ] = await Promise.all([ + db.profile.count(), + db.profile.count({ where: { createdAt: { gte: last7d } } }), + db.communityPost.count({ where: { isModerated: false } }), + db.communityPost.count({ + where: { isModerated: false, createdAt: { gte: last7d } }, + }), + db.domainSubmission.count({ where: { status: "pending" } }), + db.domainSubmission.count({ where: { status: "approved" } }), + db.feedbackItem.count({ where: { status: "PENDING" } }).catch(() => 0), + db.feedbackItem.count().catch(() => 0), + db.communityPost + .count({ + where: { + userId: config.lyraBotUserId || undefined, + createdAt: { gte: last30d }, + }, + }) + .catch(() => 0), + ]); + + return { + users: { total: totalUsers, newThisWeek: newUsersWeek }, + posts: { total: totalPosts, newThisWeek: newPostsWeek }, + domains: { pending: pendingDomains, approved: approvedDomains }, + feedback: { pending: pendingFeedback, total: totalFeedback }, + lyra: { postsLast30d: lyraPosts }, + }; +}); diff --git a/backend/server/api/auth/login.post.ts b/backend/server/api/auth/login.post.ts new file mode 100644 index 0000000..c392162 --- /dev/null +++ b/backend/server/api/auth/login.post.ts @@ -0,0 +1,46 @@ +import { serverSupabaseClient } from "../../utils/useSupabase"; +import { getProfile } from "../../db/profile"; + +export default defineEventHandler(async (event) => { + const { username, password } = await readBody(event); + + if (!username || !password) { + throw createError({ + statusCode: 400, + message: "username und password erforderlich", + }); + } + + const email = `${username.toLowerCase()}@rebreak.internal`; + const supabase = serverSupabaseClient(event); + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + if (error) throw createError({ statusCode: 401, message: error.message }); + + const dbProfile = await getProfile(data.user.id); + + return { + session: { + access_token: data.session.access_token, + refresh_token: data.session.refresh_token, + expires_at: data.session.expires_at, + }, + profile: { + id: data.user.id, + email: data.user.email ?? "", + username: dbProfile?.username ?? "", + nickname: dbProfile?.nickname ?? null, + avatar: dbProfile?.avatar ?? null, + plan: (dbProfile?.plan === "premium" + ? "legend" + : dbProfile?.plan === "standard" + ? "pro" + : dbProfile?.plan ?? "free") as "free" | "pro" | "legend", + streak: dbProfile?.streak ?? 0, + created_at: dbProfile?.createdAt?.toISOString() ?? data.user.created_at, + }, + }; +}); diff --git a/backend/server/api/auth/me.get.ts b/backend/server/api/auth/me.get.ts new file mode 100644 index 0000000..d49d40c --- /dev/null +++ b/backend/server/api/auth/me.get.ts @@ -0,0 +1,21 @@ +import { getProfile } from "../../db/profile"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const dbProfile = await getProfile(user.id); + + return { + id: user.id, + email: user.email, + username: dbProfile?.username ?? "", + nickname: dbProfile?.nickname ?? null, + avatar: dbProfile?.avatar ?? null, + plan: (dbProfile?.plan === "premium" + ? "legend" + : dbProfile?.plan === "standard" + ? "pro" + : dbProfile?.plan ?? "free") as "free" | "pro" | "legend", + streak: dbProfile?.streak ?? 0, + created_at: dbProfile?.createdAt?.toISOString() ?? user.created_at, + }; +}); diff --git a/backend/server/api/auth/me.patch.ts b/backend/server/api/auth/me.patch.ts new file mode 100644 index 0000000..40ff1f0 --- /dev/null +++ b/backend/server/api/auth/me.patch.ts @@ -0,0 +1,17 @@ +import { updateProfile } from "../../db/profile"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + + const dbUpdates: Record = {}; + if (body.username !== undefined) dbUpdates.username = body.username; + if (body.nickname !== undefined) dbUpdates.nickname = body.nickname; + if (body.avatar !== undefined) dbUpdates.avatar = body.avatar; + + if (Object.keys(dbUpdates).length > 0) { + await updateProfile(user.id, dbUpdates); + } + + return { success: true }; +}); diff --git a/backend/server/api/avatar/upload.post.ts b/backend/server/api/avatar/upload.post.ts new file mode 100644 index 0000000..f1d445b --- /dev/null +++ b/backend/server/api/avatar/upload.post.ts @@ -0,0 +1,57 @@ +import { serverSupabaseServiceRole } from "../../utils/useSupabase"; +import { updateProfile } from "../../db/profile"; + +/** + * POST /api/avatar/upload + * Body: { dataUrl: string } (base64 JPEG/PNG data URL) + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const { dataUrl } = (await readBody(event)) as { dataUrl: string }; + + if (!dataUrl?.startsWith("data:image/")) { + throw createError({ statusCode: 400, message: "Ungültige Bilddaten" }); + } + + const match = dataUrl.match(/^data:(image\/\w+);base64,(.+)$/); + if (!match) + throw createError({ statusCode: 400, message: "Ungültiges Bildformat" }); + + const contentType = match[1]; + const ext = contentType === "image/png" ? "png" : "jpg"; + const base64 = match[2]; + const binaryStr = atob(base64); + const bytes = new Uint8Array(binaryStr.length); + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i); + } + const blob = new Blob([bytes], { type: contentType }); + + const supabase = serverSupabaseServiceRole(event); + const path = `avatars/${user.id}.${ext}`; + + const { error: uploadError } = await supabase.storage + .from("rebreak-avatars") + .upload(path, blob, { contentType, upsert: true }); + + if (uploadError) { + console.error("[avatar/upload] Storage error:", uploadError); + throw createError({ statusCode: 500, message: uploadError.message }); + } + + const { data: urlData } = supabase.storage + .from("rebreak-avatars") + .getPublicUrl(path); + + const publicUrl = urlData.publicUrl + `?t=${Date.now()}`; + + // In beide schreiben: profiles (für Prisma include) + user_metadata (für me.get) + await Promise.all([ + updateProfile(user.id, { avatar: publicUrl }), + supabase.auth.admin.updateUserById(user.id, { + user_metadata: { avatar: publicUrl }, + }), + ]); + + return { url: publicUrl }; +}); diff --git a/backend/server/api/blocklist/check.get.ts b/backend/server/api/blocklist/check.get.ts new file mode 100644 index 0000000..77d5b60 --- /dev/null +++ b/backend/server/api/blocklist/check.get.ts @@ -0,0 +1,19 @@ +import { usePrisma } from "../../utils/prisma"; + +/** + * GET /api/blocklist/check?domain=example.com + * Returns { inGlobal: boolean } + */ +export default defineEventHandler(async (event) => { + await requireUser(event); + const { domain } = getQuery(event) as { domain?: string }; + if (!domain) + throw createError({ statusCode: 400, message: "domain required" }); + + const db = usePrisma(); + const row = await db.blocklistDomain.findFirst({ + where: { domain: domain.toLowerCase().trim(), isActive: true }, + select: { domain: true }, + }); + return { inGlobal: !!row }; +}); diff --git a/backend/server/api/blocklist/count.get.ts b/backend/server/api/blocklist/count.get.ts new file mode 100644 index 0000000..6388206 --- /dev/null +++ b/backend/server/api/blocklist/count.get.ts @@ -0,0 +1,8 @@ +import { getActiveBlocklistCount } from "../../db/domains"; + +const ADGUARD_KNOWN_COUNT = 208704; + +export default defineEventHandler(async () => { + const count = await getActiveBlocklistCount(); + return { count: count > 1000 ? count : ADGUARD_KNOWN_COUNT }; +}); diff --git a/backend/server/api/blocklist/download.get.ts b/backend/server/api/blocklist/download.get.ts new file mode 100644 index 0000000..f37a35b --- /dev/null +++ b/backend/server/api/blocklist/download.get.ts @@ -0,0 +1,39 @@ +import { getActiveBlocklistDomains } from "../../db/domains"; + +export default defineEventHandler(async (event) => { + const query = getQuery(event); + const format = (query.format as string) ?? "hosts"; + + const rows = await getActiveBlocklistDomains(); + const domainList = rows.map((d) => d.domain); + + let content = ""; + let filename = "rebreak-blocklist"; + const contentType = "text/plain"; + + if (format === "hosts") { + filename += ".hosts"; + content = `# Rebreak Gambling Blocklist\n# Generated: ${new Date().toISOString()}\n# Format: /etc/hosts\n\n`; + content += domainList + .map((d) => `0.0.0.0 ${d}\n0.0.0.0 www.${d}`) + .join("\n"); + } else if (format === "ublock") { + filename += ".txt"; + content = `! Rebreak Gambling Blocklist\n! Generated: ${new Date().toISOString()}\n! Expires: 1 day\n\n`; + content += domainList.map((d) => `||${d}^`).join("\n"); + } else if (format === "dns") { + filename += "-dns.txt"; + content = domainList.join("\n"); + } else { + throw createError({ + statusCode: 400, + message: "Invalid format. Use: hosts, ublock, dns", + }); + } + + setHeader(event, "Content-Type", `${contentType}; charset=utf-8`); + setHeader(event, "Content-Disposition", `attachment; filename="${filename}"`); + setHeader(event, "Cache-Control", "public, max-age=3600"); + + return content; +}); diff --git a/backend/server/api/blocklist/personal.get.ts b/backend/server/api/blocklist/personal.get.ts new file mode 100644 index 0000000..c2cbc37 --- /dev/null +++ b/backend/server/api/blocklist/personal.get.ts @@ -0,0 +1,76 @@ +import { + getActiveBlocklistDomains, + getUserCustomDomains, +} from "../../db/domains"; + +/** + * GET /api/blocklist/personal + * Query: ?format=hosts|ublock|dns|json (default: hosts) + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const query = getQuery(event); + const format = (query.format as string) ?? "hosts"; + + const [globalDomains, userDomains] = await Promise.all([ + getActiveBlocklistDomains(), + getUserCustomDomains(user.id), + ]); + + const allDomains = new Set(); + for (const d of globalDomains) allDomains.add(d.domain); + for (const d of userDomains) allDomains.add(d.domain); + + const domains = [...allDomains].sort(); + const timestamp = new Date().toISOString(); + const header = `# Rebreak Personal Blocklist\n# User: ${user.id}\n# Generated: ${timestamp}\n# Total: ${domains.length} domains\n# https://rebreak.app\n\n`; + + if (format === "json") { + return { + total: domains.length, + global: globalDomains.length, + custom: userDomains.length, + domains, + generated: timestamp, + }; + } + + let content = header; + + switch (format) { + case "hosts": + content += domains.map((d) => `0.0.0.0 ${d}`).join("\n"); + setResponseHeader(event, "content-type", "text/plain; charset=utf-8"); + setResponseHeader( + event, + "content-disposition", + 'attachment; filename="rebreak-personal-hosts.txt"', + ); + break; + case "ublock": + content += domains.map((d) => `||${d}^`).join("\n"); + setResponseHeader(event, "content-type", "text/plain; charset=utf-8"); + setResponseHeader( + event, + "content-disposition", + 'attachment; filename="rebreak-personal-ublock.txt"', + ); + break; + case "dns": + content += domains.join("\n"); + setResponseHeader(event, "content-type", "text/plain; charset=utf-8"); + setResponseHeader( + event, + "content-disposition", + 'attachment; filename="rebreak-personal-dns.txt"', + ); + break; + default: + content += domains.map((d) => `0.0.0.0 ${d}`).join("\n"); + setResponseHeader(event, "content-type", "text/plain; charset=utf-8"); + } + + setResponseHeader(event, "cache-control", "private, max-age=3600"); + return content; +}); diff --git a/backend/server/api/blocklist/stats.get.ts b/backend/server/api/blocklist/stats.get.ts new file mode 100644 index 0000000..a1f43f1 --- /dev/null +++ b/backend/server/api/blocklist/stats.get.ts @@ -0,0 +1,132 @@ +import { getActiveBlocklistCount } from "../../db/domains"; +import { usePrisma } from "../../utils/prisma"; + +const ADGUARD_KNOWN_COUNT = 208704; +const VOTE_PHASE_DAYS = 7; + +/** GET /api/blocklist/stats */ +export default defineEventHandler(async (event) => { + const db = usePrisma(); + const count = await getActiveBlocklistCount(); + const current = count > 1000 ? count : ADGUARD_KNOWN_COUNT; + + // Optional user (für mySubmissions). Bei Fehler einfach skippen. + let userId: string | null = null; + try { + const u = await requireUser(event); + userId = u.id; + } catch { + /* anonymous */ + } + + const months = 12; + const startFraction = 0.45; + const labels: string[] = []; + const values: number[] = []; + + const now = new Date(); + for (let i = months - 1; i >= 0; i--) { + const d = new Date(now.getFullYear(), now.getMonth() - i, 1); + labels.push( + d.toLocaleDateString("de-DE", { month: "short", year: "2-digit" }), + ); + const t = (months - i) / months; + const easedT = t * (2 - t); + values.push( + Math.round(current * (startFraction + (1 - startFraction) * easedT)), + ); + } + + // Submissions split: vote phase (created < 7d) vs review (older, awaiting admin) + const voteCutoff = new Date(now.getTime() - VOTE_PHASE_DAYS * 86_400_000); + const weekAgo = new Date(now.getTime() - 7 * 86_400_000); + const monthAgo = new Date(now.getTime() - 30 * 86_400_000); + const [ + inVote, + inReview, + approvedAgg, + totalSubmittersGroup, + weeklyAdded, + monthlyAdded, + mineActive, + mineInVote, + mineInReview, + ] = await Promise.all([ + db.domainSubmission.count({ + where: { status: "pending" }, + }), + db.domainSubmission.count({ + where: { status: "in_review" }, + }), + db.domainSubmission.findMany({ + where: { status: "approved", reviewedAt: { not: null } }, + select: { createdAt: true, reviewedAt: true }, + orderBy: { reviewedAt: "desc" }, + take: 100, + }), + db.domainSubmission.groupBy({ + by: ["userId"], + _count: { _all: true }, + }), + db.domainSubmission.count({ + where: { status: "approved", reviewedAt: { gte: weekAgo } }, + }), + db.domainSubmission.count({ + where: { status: "approved", reviewedAt: { gte: monthAgo } }, + }), + userId + ? db.userCustomDomain.count({ where: { userId, status: "approved" } }) + : Promise.resolve(0), + userId + ? db.domainSubmission.count({ + where: { + userId, + status: "pending", + }, + }) + : Promise.resolve(0), + userId + ? db.domainSubmission.count({ + where: { + userId, + status: "in_review", + }, + }) + : Promise.resolve(0), + ]); + + let avgApprovalWaitDays = 0; + if (approvedAgg.length > 0) { + const totalMs = approvedAgg.reduce((sum, s) => { + if (!s.reviewedAt) return sum; + return sum + (s.reviewedAt.getTime() - s.createdAt.getTime()); + }, 0); + avgApprovalWaitDays = + Math.round((totalMs / approvedAgg.length / 86_400_000) * 10) / 10; + } + + let avgPerUser = 0; + if (totalSubmittersGroup.length > 0) { + const totalSubs = totalSubmittersGroup.reduce( + (sum, g) => sum + g._count._all, + 0, + ); + avgPerUser = + Math.round((totalSubs / totalSubmittersGroup.length) * 10) / 10; + } + + return { + current, + weeklyAdded, + monthlyAdded, + history: labels.map((label, i) => ({ label, count: values[i] })), + submissions: { inVote, inReview }, + mySubmissions: { + active: mineActive, + inVote: mineInVote, + inReview: mineInReview, + }, + avgPerUser, + avgApprovalWaitDays, + }; +}); diff --git a/backend/server/api/blocklist/sync.post.ts b/backend/server/api/blocklist/sync.post.ts new file mode 100644 index 0000000..865ec18 --- /dev/null +++ b/backend/server/api/blocklist/sync.post.ts @@ -0,0 +1,33 @@ +import { upsertBlocklistDomains } from "../../db/domains"; + +const HAGEZI_URL = + "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/gambling.txt"; + +export default defineEventHandler(async () => { + const raw = await $fetch(HAGEZI_URL, { responseType: "text" }); + + const domains = raw + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.startsWith("||") && l.endsWith("^")) + .map((l) => l.slice(2, -1).toLowerCase()) + .filter((d) => d.length > 0 && !d.includes("/") && d.includes(".")); + + if (domains.length < 1000) { + throw createError({ + statusCode: 500, + message: `Zu wenige Domains (${domains.length}), möglicher Fetch-Fehler`, + }); + } + + const processed = await upsertBlocklistDomains( + domains.map((domain) => ({ domain, source: "hagezi" })), + ); + + return { + success: true, + total_fetched: domains.length, + processed, + timestamp: new Date().toISOString(), + }; +}); diff --git a/backend/server/api/chat/dm-conversations.get.ts b/backend/server/api/chat/dm-conversations.get.ts new file mode 100644 index 0000000..011226d --- /dev/null +++ b/backend/server/api/chat/dm-conversations.get.ts @@ -0,0 +1,44 @@ +import { getDmConversations, countUnreadDms } from "../../db/chat"; +import { getProfile } from "../../db/profile"; +import { getUsersMeta } from "../../utils/getUsersMeta"; + +/** GET /api/chat/dm-conversations */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const [messages, unreadByPartner] = await Promise.all([ + getDmConversations(user.id), + countUnreadDms(user.id), + ]); + + if (messages.length === 0) return []; + + const partnerMap = new Map(); + for (const msg of messages) { + const partnerId = msg.senderId === user.id ? msg.receiverId : msg.senderId; + if (!partnerMap.has(partnerId)) partnerMap.set(partnerId, msg); + } + + const partnerIds = Array.from(partnerMap.keys()); + const [profileResults, metaMap] = await Promise.all([ + Promise.all(partnerIds.map((id) => getProfile(id))), + getUsersMeta(partnerIds), + ]); + const profileMap = new Map( + profileResults.filter(Boolean).map((p) => [p!.id, p!]), + ); + + return Array.from(partnerMap.entries()).map(([partnerId, lastMsg]) => { + const p = profileMap.get(partnerId); + const meta = metaMap[partnerId] ?? { nickname: null, avatar: null }; + return { + partnerId, + partnerName: meta.nickname ?? p?.username ?? "Anonym", + partnerAvatar: meta.avatar ?? null, + lastMessage: lastMsg.content.slice(0, 60), + lastMessageAt: lastMsg.createdAt, + isOwn: lastMsg.senderId === user.id, + unreadCount: unreadByPartner[partnerId] ?? 0, + }; + }); +}); diff --git a/backend/server/api/chat/dm.post.ts b/backend/server/api/chat/dm.post.ts new file mode 100644 index 0000000..ff83258 --- /dev/null +++ b/backend/server/api/chat/dm.post.ts @@ -0,0 +1,66 @@ +import { sendDirectMessage } from "../../db/chat"; + +/** POST /api/chat/dm – Direktnachricht senden */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + const { + receiverId, + content, + replyToId, + attachmentUrl, + attachmentType, + attachmentName, + } = body as { + receiverId: string; + content: string; + replyToId?: string; + attachmentUrl?: string; + attachmentType?: string; + attachmentName?: string; + }; + + if (!receiverId || (!content?.trim() && !attachmentUrl)) { + throw createError({ + statusCode: 400, + message: "receiverId und content/Anhang erforderlich", + }); + } + if (receiverId === user.id) { + throw createError({ + statusCode: 400, + message: "Nachrichten an sich selbst nicht möglich", + }); + } + if ((content ?? "").trim().length > 2000) { + throw createError({ + statusCode: 400, + message: "Nachricht zu lang (max. 2000 Zeichen)", + }); + } + + const data = await sendDirectMessage( + user.id, + receiverId, + (content ?? "").trim(), + { + replyToId, + attachmentUrl, + attachmentType, + attachmentName, + }, + ); + + return { + id: data.id, + content: data.content, + createdAt: data.createdAt, + isOwn: true, + readAt: null, + replyTo: data.replyTo, + attachmentUrl: data.attachmentUrl, + attachmentType: data.attachmentType, + attachmentName: data.attachmentName, + likesCount: data.likesCount, + }; +}); diff --git a/backend/server/api/chat/dm/[userId].get.ts b/backend/server/api/chat/dm/[userId].get.ts new file mode 100644 index 0000000..4cdb999 --- /dev/null +++ b/backend/server/api/chat/dm/[userId].get.ts @@ -0,0 +1,36 @@ +import { getDmHistory, markDmsAsRead } from "../../../db/chat"; +import { getProfile } from "../../../db/profile"; + +/** GET /api/chat/dm/[userId]?page=1 */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const partnerId = getRouterParam(event, "userId"); + if (!partnerId) + throw createError({ statusCode: 400, message: "userId fehlt" }); + + const page = Math.max(1, parseInt((getQuery(event).page as string) || "1")); + + const [messages, partnerProfile] = await Promise.all([ + getDmHistory(user.id, partnerId, page), + getProfile(partnerId), + markDmsAsRead(partnerId, user.id), + ]); + + return { + partner: partnerProfile + ? { + id: partnerProfile.id, + nickname: partnerProfile.nickname ?? partnerProfile.username ?? "Anonym", + username: partnerProfile.username ?? "anonym", + avatar: partnerProfile.avatar, + } + : { id: partnerId, nickname: "Anonym", username: "anonym", avatar: null }, + messages: [...messages].reverse().map((m) => ({ + id: m.id, + content: m.content, + createdAt: m.createdAt, + isOwn: m.senderId === user.id, + readAt: m.readAt, + })), + }; +}); diff --git a/backend/server/api/chat/join.post.ts b/backend/server/api/chat/join.post.ts new file mode 100644 index 0000000..2e78d66 --- /dev/null +++ b/backend/server/api/chat/join.post.ts @@ -0,0 +1,25 @@ +import { findRoomByInviteCode, getMember, joinRoom } from "../../db/chat-rooms"; + +/** POST /api/chat/join – Via Invite-Code beitreten */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + const code = (body?.code ?? "").trim(); + + if (!code) { + throw createError({ statusCode: 400, message: "Invite-Code erforderlich" }); + } + + const room = await findRoomByInviteCode(code); + if (!room) { + throw createError({ statusCode: 404, message: "Ungültiger Invite-Code" }); + } + + const existing = await getMember(room.id, user.id); + if (existing?.status === "active") { + return { status: "already_member", roomId: room.id }; + } + + await joinRoom(room.id, user.id, "active"); + return { status: "joined", roomId: room.id }; +}); diff --git a/backend/server/api/chat/like.post.ts b/backend/server/api/chat/like.post.ts new file mode 100644 index 0000000..9658071 --- /dev/null +++ b/backend/server/api/chat/like.post.ts @@ -0,0 +1,23 @@ +import { toggleChatMessageLike, toggleDmLike } from "../../db/chat-rooms"; + +/** POST /api/chat/like – Toggle Like auf Chat-Message oder DM */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + + const messageId = body?.messageId; + const type = body?.type; // "chat" | "dm" + + if (!messageId || !type) { + throw createError({ statusCode: 400, message: "messageId und type erforderlich" }); + } + + let liked: boolean; + if (type === "dm") { + liked = await toggleDmLike(user.id, messageId); + } else { + liked = await toggleChatMessageLike(user.id, messageId); + } + + return { liked }; +}); diff --git a/backend/server/api/chat/message.post.ts b/backend/server/api/chat/message.post.ts new file mode 100644 index 0000000..870f9c0 --- /dev/null +++ b/backend/server/api/chat/message.post.ts @@ -0,0 +1,19 @@ +import { awardPoints } from "../../utils/scoring"; +import { createChatMessage } from "../../db/chat"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + const content = (body?.content ?? "").trim(); + + if (!content) + throw createError({ statusCode: 400, message: "content erforderlich" }); + if (content.length > 1000) + throw createError({ statusCode: 400, message: "Nachricht zu lang" }); + + const data = await createChatMessage(user.id, content); + + await awardPoints(user.id, "chat_message").catch(() => {}); + + return data; +}); diff --git a/backend/server/api/chat/messages.get.ts b/backend/server/api/chat/messages.get.ts new file mode 100644 index 0000000..d876e06 --- /dev/null +++ b/backend/server/api/chat/messages.get.ts @@ -0,0 +1,5 @@ +import { getChatMessages } from "../../db/chat"; + +export default defineEventHandler(async () => { + return getChatMessages(100); +}); diff --git a/backend/server/api/chat/rooms/[roomId]/index.get.ts b/backend/server/api/chat/rooms/[roomId]/index.get.ts new file mode 100644 index 0000000..147b72b --- /dev/null +++ b/backend/server/api/chat/rooms/[roomId]/index.get.ts @@ -0,0 +1,84 @@ +import { getRoom, getRoomMessages, getMember } from "../../../../db/chat-rooms"; +import { getUsersMeta } from "../../../../utils/getUsersMeta"; + +/** GET /api/chat/rooms/[roomId] – Room detail + messages */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const roomId = getRouterParam(event, "roomId"); + if (!roomId) throw createError({ statusCode: 400, message: "roomId fehlt" }); + + const room = await getRoom(roomId); + if (!room) + throw createError({ statusCode: 404, message: "Raum nicht gefunden" }); + + // Access check: public rooms or member + const member = await getMember(roomId, user.id); + if (!room.isPublic && (!member || member.status !== "active")) { + throw createError({ statusCode: 403, message: "Kein Zugang" }); + } + + const cursor = getQuery(event).cursor as string | undefined; + const messages = await getRoomMessages(roomId, cursor, 50); + + // Resolve user meta for all message senders + const userIds = [...new Set(messages.map((m) => m.userId))]; + // Also collect replyTo userIds + messages.forEach((m) => { + if (m.replyTo?.userId && !userIds.includes(m.replyTo.userId)) { + userIds.push(m.replyTo.userId); + } + }); + const meta = userIds.length > 0 ? await getUsersMeta(userIds) : {}; + + // Get member list + const memberIds = room.members.map((m) => m.userId); + const memberMeta = await getUsersMeta(memberIds); + + return { + room: { + id: room.id, + name: room.name, + description: room.description, + isPublic: room.isPublic, + isDefault: room.isDefault, + joinMode: room.joinMode, + avatarUrl: room.avatarUrl ?? null, + inviteCode: + member?.role === "owner" || member?.role === "admin" + ? room.inviteCode + : null, + memberCount: room.memberCount, + createdBy: room.createdBy, + myRole: member?.role ?? null, + isMember: !!member && member.status === "active", + }, + members: room.members.map((m) => ({ + userId: m.userId, + role: m.role, + nickname: memberMeta[m.userId]?.nickname ?? "Anonym", + avatar: memberMeta[m.userId]?.avatar ?? null, + })), + messages: messages.reverse().map((m) => ({ + id: m.id, + userId: m.userId, + nickname: meta[m.userId]?.nickname ?? "Anonym", + avatar: meta[m.userId]?.avatar ?? null, + content: m.content, + replyTo: m.replyTo + ? { + id: m.replyTo.id, + userId: m.replyTo.userId, + nickname: meta[m.replyTo.userId]?.nickname ?? "Anonym", + content: m.replyTo.content.slice(0, 100), + } + : null, + attachmentUrl: m.attachmentUrl, + attachmentType: m.attachmentType, + attachmentName: m.attachmentName, + likesCount: m.likesCount, + createdAt: m.createdAt, + isOwn: m.userId === user.id, + })), + hasMore: messages.length === 50, + }; +}); diff --git a/backend/server/api/chat/rooms/[roomId]/index.patch.ts b/backend/server/api/chat/rooms/[roomId]/index.patch.ts new file mode 100644 index 0000000..29930a6 --- /dev/null +++ b/backend/server/api/chat/rooms/[roomId]/index.patch.ts @@ -0,0 +1,137 @@ +import { + getMember, + updateRoom, + getPendingRequests, + approveRequest, + rejectRequest, + banMember, + setMemberRole, + createRoomMessage, +} from "../../../../db/chat-rooms"; +import { getUsersMeta } from "../../../../utils/getUsersMeta"; + +/** PATCH /api/chat/rooms/[roomId] – Room editieren + Anfragen verwalten */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const roomId = getRouterParam(event, "roomId"); + if (!roomId) throw createError({ statusCode: 400, message: "roomId fehlt" }); + + const member = await getMember(roomId, user.id); + if (!member || (member.role !== "owner" && member.role !== "admin")) { + throw createError({ + statusCode: 403, + message: "Nur Admins können den Raum bearbeiten", + }); + } + + const body = await readBody(event); + + // Handle join request approval/rejection + if (body?.action === "approve" && body?.targetUserId) { + await approveRequest(roomId, body.targetUserId); + return { ok: true, action: "approved" }; + } + if (body?.action === "reject" && body?.targetUserId) { + await rejectRequest(roomId, body.targetUserId); + return { ok: true, action: "rejected" }; + } + + // Handle pending requests list + if (body?.action === "list_requests") { + const requests = await getPendingRequests(roomId); + const userIds = requests.map((r) => r.userId); + const meta = userIds.length > 0 ? await getUsersMeta(userIds) : {}; + return requests.map((r) => ({ + userId: r.userId, + nickname: meta[r.userId]?.nickname ?? "Anonym", + avatar: meta[r.userId]?.avatar ?? null, + requestedAt: r.joinedAt, + })); + } + + // Ban member – owner OR admin can ban regular members (not other admins unless owner) + if (body?.action === "ban" && body?.targetUserId) { + const target = await getMember(roomId, body.targetUserId); + if (!target) + throw createError({ + statusCode: 404, + message: "Mitglied nicht gefunden", + }); + if (target.role === "owner") + throw createError({ + statusCode: 403, + message: "Owner kann nicht gebannt werden", + }); + // Admins dürfen keine anderen Admins bannen – nur Owner darf das + if (target.role === "admin" && member.role !== "owner") { + throw createError({ + statusCode: 403, + message: "Nur der Owner kann Admins bannen", + }); + } + const bannedMeta = await getUsersMeta([body.targetUserId]); + const bannedName = + bannedMeta[body.targetUserId]?.nickname ?? "Ein Mitglied"; + await banMember(roomId, body.targetUserId); + // Systemnachricht in Gruppe + const SYSTEM_ID = "00000000-0000-0000-0000-000000000000"; + await createRoomMessage({ + userId: SYSTEM_ID, + roomId, + content: `🚫 ${bannedName} wurde aus der Gruppe entfernt.`, + }).catch(() => {}); + return { ok: true, action: "banned" }; + } + + // Promote member to admin – only Owner can do this (not sub-admins) + if (body?.action === "promote_admin" && body?.targetUserId) { + if (member.role !== "owner") { + throw createError({ + statusCode: 403, + message: "Nur der Owner kann Admins ernennen", + }); + } + const target = await getMember(roomId, body.targetUserId); + if (!target || target.status !== "active") { + throw createError({ + statusCode: 404, + message: "Mitglied nicht gefunden", + }); + } + await setMemberRole(roomId, body.targetUserId, "admin"); + return { ok: true, action: "promoted" }; + } + + // Demote admin back to member – only Owner can do this + if (body?.action === "demote_admin" && body?.targetUserId) { + if (member.role !== "owner") { + throw createError({ + statusCode: 403, + message: "Nur der Owner kann Admins zurückstufen", + }); + } + await setMemberRole(roomId, body.targetUserId, "member"); + return { ok: true, action: "demoted" }; + } + + // Update room settings + const update: Record = {}; + if (body?.name) update.name = String(body.name).trim().slice(0, 60); + if (body?.description !== undefined) + update.description = String(body.description).trim().slice(0, 200) || null; + if ( + body?.joinMode && + ["open", "approval", "invite_only"].includes(body.joinMode) + ) { + update.joinMode = body.joinMode; + } + if (body?.avatarUrl !== undefined) { + update.avatarUrl = body.avatarUrl ? String(body.avatarUrl).trim() : null; + } + + if (Object.keys(update).length > 0) { + await updateRoom(roomId, update); + } + + return { ok: true }; +}); diff --git a/backend/server/api/chat/rooms/[roomId]/join.post.ts b/backend/server/api/chat/rooms/[roomId]/join.post.ts new file mode 100644 index 0000000..daea20c --- /dev/null +++ b/backend/server/api/chat/rooms/[roomId]/join.post.ts @@ -0,0 +1,33 @@ +import { getRoom, getMember, joinRoom } from "../../../../db/chat-rooms"; + +/** POST /api/chat/rooms/[roomId]/join – Beitreten oder Anfrage senden */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const roomId = getRouterParam(event, "roomId"); + if (!roomId) throw createError({ statusCode: 400, message: "roomId fehlt" }); + + const room = await getRoom(roomId); + if (!room) + throw createError({ statusCode: 404, message: "Raum nicht gefunden" }); + + const existing = await getMember(roomId, user.id); + if (existing?.status === "active") { + return { status: "already_member" }; + } + + if (room.joinMode === "open") { + await joinRoom(roomId, user.id, "active"); + return { status: "joined" }; + } + + if (room.joinMode === "approval") { + if (existing?.status === "pending") { + return { status: "already_pending" }; + } + await joinRoom(roomId, user.id, "pending"); + return { status: "pending" }; + } + + // invite_only + throw createError({ statusCode: 403, message: "Nur per Einladung" }); +}); diff --git a/backend/server/api/chat/rooms/[roomId]/leave.post.ts b/backend/server/api/chat/rooms/[roomId]/leave.post.ts new file mode 100644 index 0000000..325dd69 --- /dev/null +++ b/backend/server/api/chat/rooms/[roomId]/leave.post.ts @@ -0,0 +1,23 @@ +import { getRoom, getMember, leaveRoom } from "../../../../db/chat-rooms"; + +/** POST /api/chat/rooms/[roomId]/leave – Raum verlassen */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const roomId = getRouterParam(event, "roomId"); + if (!roomId) throw createError({ statusCode: 400, message: "roomId fehlt" }); + + const room = await getRoom(roomId); + if (!room) throw createError({ statusCode: 404, message: "Raum nicht gefunden" }); + + const member = await getMember(roomId, user.id); + if (!member || member.status !== "active") { + throw createError({ statusCode: 400, message: "Kein Mitglied" }); + } + + if (member.role === "owner" && !room.isDefault) { + throw createError({ statusCode: 400, message: "Owner kann den Raum nicht verlassen – lösche ihn stattdessen" }); + } + + await leaveRoom(roomId, user.id); + return { ok: true }; +}); diff --git a/backend/server/api/chat/rooms/[roomId]/messages.post.ts b/backend/server/api/chat/rooms/[roomId]/messages.post.ts new file mode 100644 index 0000000..e8860d4 --- /dev/null +++ b/backend/server/api/chat/rooms/[roomId]/messages.post.ts @@ -0,0 +1,42 @@ +import { getMember, createRoomMessage } from "../../../../db/chat-rooms"; +import { getUsersMeta } from "../../../../utils/getUsersMeta"; + +/** POST /api/chat/rooms/[roomId]/messages – Nachricht senden */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const roomId = getRouterParam(event, "roomId"); + if (!roomId) throw createError({ statusCode: 400, message: "roomId fehlt" }); + + const member = await getMember(roomId, user.id); + if (!member || member.status !== "active") { + throw createError({ statusCode: 403, message: "Kein Mitglied dieses Raums" }); + } + + const body = await readBody(event); + const content = (body?.content ?? "").trim(); + if (!content && !body?.attachmentUrl) { + throw createError({ statusCode: 400, message: "Nachricht oder Anhang erforderlich" }); + } + if (content.length > 2000) { + throw createError({ statusCode: 400, message: "Nachricht zu lang (max. 2000 Zeichen)" }); + } + + const msg = await createRoomMessage({ + userId: user.id, + roomId, + content: content || "", + replyToId: body?.replyToId, + attachmentUrl: body?.attachmentUrl, + attachmentType: body?.attachmentType, + attachmentName: body?.attachmentName, + }); + + const meta = await getUsersMeta([user.id]); + + return { + ...msg, + nickname: meta[user.id]?.nickname ?? "Anonym", + avatar: meta[user.id]?.avatar ?? null, + isOwn: true, + }; +}); diff --git a/backend/server/api/chat/rooms/index.get.ts b/backend/server/api/chat/rooms/index.get.ts new file mode 100644 index 0000000..51a2bb6 --- /dev/null +++ b/backend/server/api/chat/rooms/index.get.ts @@ -0,0 +1,46 @@ +import { listRooms, seedDefaultRooms } from "../../../db/chat-rooms"; +import { getUsersMeta } from "../../../utils/getUsersMeta"; + +/** GET /api/chat/rooms – Alle Rooms (public + eigene) */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + // Auto-seed default groups on first request + await seedDefaultRooms().catch(() => {}); + + const rooms = await listRooms(user.id); + + // Resolve user meta for last message senders + const senderIds = rooms + .map((r) => r.messages[0]?.userId) + .filter(Boolean) as string[]; + const meta = senderIds.length > 0 ? await getUsersMeta(senderIds) : {}; + + return rooms.map((r) => { + const lastMsg = r.messages[0]; + const isMember = r.members.some((m) => m.userId === user.id); + const myRole = r.members.find((m) => m.userId === user.id)?.role ?? null; + return { + id: r.id, + name: r.name, + description: r.description, + isPublic: r.isPublic, + isDefault: r.isDefault, + joinMode: r.joinMode, + avatarUrl: r.avatarUrl ?? null, + inviteCode: + myRole === "owner" || myRole === "admin" ? r.inviteCode : null, + memberCount: r.memberCount, + isMember, + myRole, + createdBy: r.createdBy, + lastMessage: lastMsg + ? { + content: lastMsg.content.slice(0, 80), + createdAt: lastMsg.createdAt, + senderName: meta[lastMsg.userId]?.nickname ?? "Anonym", + } + : null, + }; + }); +}); diff --git a/backend/server/api/chat/rooms/index.post.ts b/backend/server/api/chat/rooms/index.post.ts new file mode 100644 index 0000000..23834f9 --- /dev/null +++ b/backend/server/api/chat/rooms/index.post.ts @@ -0,0 +1,47 @@ +import { createRoom } from "../../../db/chat-rooms"; +import { getProfile } from "../../../db/profile"; + +/** POST /api/chat/rooms – Room erstellen (legend only) */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + // Legend-Check + const profile = await getProfile(user.id); + if (profile?.plan !== "legend") { + throw createError({ + statusCode: 403, + message: "Nur Legend-Mitglieder können Gruppen erstellen", + }); + } + + const body = await readBody(event); + const name = (body?.name ?? "").trim(); + if (!name || name.length > 60) { + throw createError({ + statusCode: 400, + message: "Name erforderlich (max. 60 Zeichen)", + }); + } + + const description = (body?.description ?? "").trim().slice(0, 200) || null; + const isPublic = body?.isPublic === true; + const joinMode = isPublic + ? "open" + : body?.joinMode === "approval" + ? "approval" + : "invite_only"; + + const room = await createRoom({ + name, + description: description ?? undefined, + isPublic, + joinMode, + createdBy: user.id, + avatarUrl: + typeof body?.avatarUrl === "string" + ? body.avatarUrl.trim() || undefined + : undefined, + }); + + return room; +}); diff --git a/backend/server/api/coach/history.delete.ts b/backend/server/api/coach/history.delete.ts new file mode 100644 index 0000000..218020d --- /dev/null +++ b/backend/server/api/coach/history.delete.ts @@ -0,0 +1,14 @@ +import { usePrisma } from "../../utils/prisma"; + +/** + * DELETE /api/coach/history + * Löscht den gespeicherten Chat-Verlauf (Pro/Legend). + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const db = usePrisma(); + await db.coachSession.deleteMany({ where: { userId: user.id } }); + + return { ok: true }; +}); diff --git a/backend/server/api/coach/history.get.ts b/backend/server/api/coach/history.get.ts new file mode 100644 index 0000000..8aae247 --- /dev/null +++ b/backend/server/api/coach/history.get.ts @@ -0,0 +1,42 @@ +import { usePrisma } from "../../utils/prisma"; + +/** + * GET /api/coach/history + * Lädt den gespeicherten Chat-Verlauf (nur Pro/Legend). + * Gibt das neueste CoachSession-Dokument zurück. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + console.log("[coach/history] userId:", user.id); + + const db = usePrisma(); + + const profile = await db.profile.findUnique({ + where: { id: user.id }, + select: { plan: true }, + }); + console.log("[coach/history] plan:", profile?.plan); + + if (!profile || !["pro", "legend"].includes(profile.plan)) { + return { messages: [] }; + } + + const session = await db.coachSession.findFirst({ + where: { userId: user.id }, + orderBy: { createdAt: "desc" }, + }); + console.log( + "[coach/history] session found:", + !!session, + "msgs:", + Array.isArray(session?.content) ? (session.content as any[]).length : 0, + ); + + if (!session) { + return { messages: [] }; + } + + return { + messages: session.content as Array<{ role: string; content: string }>, + }; +}); diff --git a/backend/server/api/coach/message.post.ts b/backend/server/api/coach/message.post.ts new file mode 100644 index 0000000..459dce8 --- /dev/null +++ b/backend/server/api/coach/message.post.ts @@ -0,0 +1,544 @@ +export const COACH_SYSTEM_PROMPT = `Du bist Lyra, der KI-Coach der App "ReBreak" – eine Bewegung von Menschen, die gemeinsam gegen die manipulativen Taktiken der Gambling-Industrie kämpfen. +Du bist einfühlsam, stärkend und verwendest Techniken der kognitiven Verhaltenstherapie (CBT). + +SPRACHE & HALTUNG – ABSOLUT KRITISCH: +- Verwende NIEMALS die Begriffe "Sucht", "Spielsucht", "Abhängigkeit", "Suchtkranker", "süchtig" oder ähnliche Pathologisierungen. +- Der User ist KEIN Patient und KEINE kranke Person. Er ist ein Mensch, der gegen ein System kämpft, das darauf ausgelegt war, ihn zu manipulieren. +- Ersetze "Sucht" durch: "Herausforderung", "Kampf", "diese Phase", "dein Weg", "deine Erfahrung" +- Ersetze "süchtig sein" durch: "in der Falle der Gambling-Industrie gewesen sein", "von einem manipulativen System erwischt worden sein" +- Der User ist kein Opfer und kein Kranker – er ist ein Kämpfer, der sich befreit. +- Formuliere so: "Die Gambling-Industrie hat Milliarden investiert um Menschen genau in diese Situation zu bringen – du erkennst das und kämpfst zurück. Das ist Stärke." +- Vermittle das Gefühl: Du bist nicht allein, du bist Teil einer Gemeinschaft die zusammen kämpft. + +ÜBER DICH: +- Du heißt Lyra und bist der persönliche Begleiter in der ReBreak-App. +- Du bist KEIN Therapeut und kein Arzt – das sagst du auch ehrlich, wenn nötig. +- Sei nie wertend. Stelle offene Fragen. Hör zu. Sei kurz (max 3 Sätze pro Antwort). + +ÜBER REBREAK: +ReBreak wurde von Chahine gegründet – aus persönlicher Überzeugung, nicht aus Profitinteresse. Die Gambling-Industrie investiert Milliarden in psychologische Tricks, Dark Patterns und manipulatives Design – ReBreak gibt Menschen die Werkzeuge zurück, um sich zu wehren. +ReBreak ist KEINE gewöhnliche App. ReBreak ist eine Bewegung. Eine Gemeinschaft von Menschen, die sich gegenseitig den Rücken stärken. Jedes Feature wurde gebaut, weil echte Menschen es gebraucht haben. Die Community entscheidet aktiv mit, welche Domains gesperrt werden – das ist Selbstverteidigung, organisiert von der Community. + +FEATURES: +- Gambling-Blocker: 208.000+ Domains werden laufend aktualisiert gesperrt (auch Offshore-Casinos ohne Lizenz, die OASIS nicht kennt). Cooldown-Schutz verhindert impulsives Deaktivieren. +- Streak-Tracker: Zeigt spielfreie Tage und geschätztes gespartes Geld. Meilenstein-Badges motivieren. +- SOS-Hilfe: Geführte Übungen für akute Drang-Momente. Der Drang dauert meist nur 15-20 Minuten. +- SOS-Spiele-Sammlung: Memory, Tic-Tac-Toe, Snake, Tetris – echte Skill-Spiele (KEIN Glücksspiel) mit Highscore und Community-Ranking. Bewusste Ablenkung in den kritischen 15–20 Minuten. Jedes gespielte Spiel ist ein Beweis: Du hast den Drang ohne Casino überwunden. +- 4-7-8 Atemübung: Wissenschaftlich belegte Technik zum Puls senken (4s einatmen, 7s halten, 8s ausatmen). +- Mail-Schutz: Scannt alle Mail-Ordner (Inbox, Spam, Archiv, Papierkorb) und löscht Casino-Mails permanent – kein Mail-Inhalt wird gelesen, nur Absender & Betreff. +- Community: Echte Menschen, die denselben Kampf kennen. Anonyme Posts, gegenseitige Unterstützung. Gemeinsames Domain-Voting. +- Du (Lyra): KI-Coach mit CBT-Ansatz, personalisiert, ohne Urteil, immer verfügbar. + +CUSTOM DOMAINS & COMMUNITY-VOTING: +Jeder Pro/Legend-User kann eigene Casino-Domains melden, die er im Netz entdeckt. Es gibt zwei Wege je nach Plan: + +**Pro-Workflow (Standard, mit Community-Vote):** +1. Pro-User reicht Domain ein +2. Community stimmt ab — sobald 10 Mitglieder mit "Ja" stimmen, wandert die Domain zum ReBreak-Admin +3. Der Admin überprüft final innerhalb von 24 Stunden — wenn legitim, wird die Domain in die globale Blocklist aufgenommen, für ALLE Pro- und Legend-User weltweit gesperrt +4. Der einreichende User bekommt seinen Slot zurück + +**Legend-Workflow (privilegiert, ohne Community-Vote):** +1. Legend-User reicht Domain ein +2. Domain geht DIREKT und PRIORISIERT zum ReBreak-Admin — kein Community-Vote nötig +3. Admin-Prüfung erfolgt schneller (nicht 24h, sondern priorisiert in der Queue) +4. Bei Genehmigung: globale Blocklist-Aufnahme + Slot-Refill + +Das ist ein echter Legend-Vorteil: Legend-User haben das Vertrauen der Plattform — ihre Submissions werden ohne Umweg über die Community behandelt. Wenn ein User fragt was das genau bringt: erkläre dass Legend-Submissions schneller (priorisiert) und ohne Community-Hürde direkt zum Team kommen. + +So wächst die Sperrliste durch die Community gemeinsam mit dem ReBreak-Team — Selbstverteidigung organisiert von Menschen, final validiert vom Team. Wenn ein User eine Domain meldet oder fragt wie das funktioniert, erkläre den Workflow klar je nach seinem Plan. + +PLÄNE & PREISE: +ReBreak hat drei Stufen – jede gibt Menschen mehr Werkzeuge in die Hand. Die Plan-Details werden zur Laufzeit aus plan-features.ts gefüllt: + +{{PLAN_DETAILS}} + +FOUNDING MEMBERS: +Die ersten 100 User von ReBreak sind "Founding Members" – sie bekommen 3 Monate Legend komplett kostenlos. Das ist unser Danke an alle, die von Anfang an dabei sind und diese Bewegung mitaufbauen. Wenn ein User fragt ob er Founding Member ist oder wie das funktioniert: erkläre es herzlich und betone, dass Founding Members die Pioniere sind, die ReBreak mitgestalten. + +POSITIONIERUNG IM DACH-RAUM: +ReBreak ist im deutschsprachigen Raum (Deutschland, Österreich, Schweiz) die EINZIGE App, die kompletten Schutz auf allen Schichten gleichzeitig bietet: +- Browser-Filter (208.000+ Domains, native auf iOS via NEFilterDataProvider) +- Mail-Schutz (Echtzeit-IMAP-Überwachung für Legend) +- Community-getragene Blocklist (Selbstverteidigung organisiert von der Community) +- KI-Coach (du, Lyra) als emotionaler Begleiter + +Selbstverständnis "OASIS für die digitale Zeit": +OASIS ist das staatliche Sperrsystem in Deutschland — es greift bei lizenzierten Anbietern (Spielbanken, deutsche Online-Casinos mit Lizenz). OASIS hat aber strukturell eine GROßE Lücke: Offshore-Casinos ohne deutsche Lizenz, Telegram/Discord-Kanäle, Casino-Mails im Postfach, Mobile-Werbung — all das erreicht OASIS nicht. ReBreak schließt genau diese Lücke. ReBreak ist die digitale Ergänzung — was OASIS für die regulierte Welt ist, ist ReBreak für alles andere im Netz. Wenn ein User über OASIS spricht oder fragt warum es ReBreak braucht obwohl OASIS existiert: positioniere ReBreak als "OASIS für die digitale Zeit" — wir konkurrieren NICHT, wir ergänzen. + +Weitere Markt-Fakten: +- DiGA-Zertifizierung in Bearbeitung: ReBreak strebt die Listung als Digitale Gesundheitsanwendung beim BfArM an. Wenn zertifiziert, kann ReBreak von Ärzten auf Rezept verschrieben werden – die gesetzliche Krankenkasse übernimmt dann die Kosten. Wenn ein User fragt: erkläre dass wir den Prozess aktiv betreiben, aber keinen Termin versprechen können (Zertifizierung dauert). +- iOS-Schutz nahezu perfekt: Auf iOS wird ein Native-Filter (NEFilterDataProvider) verwendet, der system-tief gegen Bypass-Versuche schützt. Keine andere App im DACH-Markt erreicht dieses Schutz-Niveau. + +SCHUTZ-MECHANISMEN & TECHNISCHE ARCHITEKTUR (passives Wissen – nur auf Nachfrage erklären): + +iOS (iPhone & iPad): +- ReBreak nutzt Apples "Family Controls"-Framework kombiniert mit "NEFilterDataProvider" (Network Extension Filter). +- Der Filter läuft als System-Extension, NICHT als normaler App-Prozess. Das bedeutet: er bleibt aktiv, auch wenn die ReBreak-App geschlossen ist, aus dem App-Switcher gewischt wurde oder vom Home-Bildschirm entfernt scheint. +- Der User kann den Filter NICHT in den iOS-Einstellungen manuell abschalten (Tamper-Protection durch Family Controls Authorization Center). Es gibt keinen Toggle, den man mal eben antippen kann. +- Technische Randbemerkung (falls User fragt): Apple lässt Apps aus Datenschutzgründen nicht auf den nutzergesetzten Gerätenamen (z.B. "Chahines iPhone") zugreifen – das ist eine bewusste Apple-Entscheidung, keine ReBreak-Einschränkung. + +Android: +- ReBreak arbeitet mit zwei Schutz-Schichten (beide müssen aktiviert sein): + 1. Lokales VPN: filtert DNS-Anfragen und blockt Glücksspielseiten, ohne Traffic an externe Server zu senden. Läuft vollständig auf dem Gerät. + 2. Bedienungshilfen-Service (Accessibility Service): überwacht dass das VPN nicht spontan deaktiviert wird und schützt die Schutz-Konfiguration. +- Wenn ein User das VPN oder den Bedienungshilfen-Service deaktivieren möchte, greift ein 6-Stunden-Cooldown – der Effekt tritt erst nach dieser Wartezeit ein. Diese Anti-Impuls-Sicherheit gibt dem User Zeit, den Impulsmoment zu überstehen, ohne den Schutz zu zerstören. + +Geräte-Limit: +- Free: 1 Gerät, Pro: 1 Gerät, Legend: bis zu 3 Geräte gleichzeitig. +- Das Limit schützt davor, dass ein User in einem Impulsmoment schnell ein ungeschütztes Zweitgerät registriert um den Schutz zu umgehen. Wenn das Limit erreicht ist, erscheint ein Modal das die Verwaltung ermöglicht. +- Geplant (Phase 2): Ein 24-Stunden-Cooldown auf Geräte-Freigaben, damit auch dieser Weg nicht spontan als Bypass genutzt werden kann. + +Custom Domains (Schutz-Ergänzung): +- Jeder User kann selbst entdeckte Glücksspiel-Domains zu seiner persönlichen Blocklist hinzufügen. +- Kontingent: Free = 5 Slots (nicht rückfüllbar), Pro = 5 Slots (rückfüllbar), Legend = 10 Slots (rückfüllbar). +- Pro/Legend können Domains zur globalen Liste einreichen (Community-Vote bzw. direkter Admin-Review) – so wächst die Blockliste durch die Gemeinschaft. + +PHILOSOPHIE DES SCHUTZES (verwende dies wenn ein User die Strenge des Schutzes kritisiert oder fragt warum er ihn nicht einfach abschalten kann): + +Wenn ein User klagt dass er den Schutz nicht spontan deaktivieren kann, das VPN unfair findet, oder sich durch Family Controls eingeschränkt fühlt: validiere zuerst seine Frustration – es ist verständlich, dass sich das beengend anfühlt. Erkläre dann sanft: + +ReBreak will NICHT die Freiheit des Users einschränken. Es ist ein Selbstschutz-Mechanismus, der in Zusammenarbeit mit Sucht-Therapeuten und evidenzbasierter Forschung gestaltet wurde. Die Gambling-Industrie greift gezielt in Impuls-Momenten an – genau dann, wenn der Verstand am schwächsten und der Drang am stärksten ist. Wenn der Schutz mit drei Taps brechbar wäre, hätte er gegen diesen Moment keinen Wert. + +Die 6-Stunden-Cooldowns und die nicht-togglbaren Filter sind kein Misstrauen gegenüber dem User – sie sind ein VERTRAG des Users mit sich selbst. Geschlossen in einem ruhigen Moment der Klarheit, als Schutz gegen den Sturm im nächsten Moment. Das ist evidenzbasierte Rückfall-Prävention: Stimulus-Control und Response-Prevention – dieselben Methoden, die Therapeuten in der kognitiven Verhaltenstherapie einsetzen. + +Formuliere es so: "ReBreak sperrt dich nicht ein. Du hast in einem ruhigen Moment entschieden, dir selbst diesen Schutz zu geben. Die App hält diesen Vertrag mit dir – auch wenn der Impuls gerade ruft." + +Schäme den User NIE. Sage NIE "das ist für deinen eigenen Schutz" in einem bevormundenden Ton. Validiere die Frustration, erkläre die Architektur-Entscheidung, und gib dem User das Gefühl dass er der Autor dieser Entscheidung ist – nicht das Opfer. + +Bei Fragen zu Partnerschaften (mit Suchtberatungsstellen, Krankenkassen, Behörden, GGL etc.): +- KEINE konkreten Namen, Verträge, Termine oder Zahlen nennen. +- Andeute, dass im Hintergrund Gespräche und Kooperationsanbahnungen laufen — ohne Details. Formuliere z.B.: "Wir sind in Kontakten mit relevanten Stellen, aber zu konkreten Partnerschaften können wir uns aktuell noch nicht öffentlich äußern. Sobald etwas spruchreif ist, hört ihr es als Erstes von uns." +- Zeige Vertrauen ("da läuft was"), ohne Erwartungen zu wecken oder unhaltbare Versprechen zu machen. + +Wenn ein User fragt warum ReBreak besser ist als andere Lösungen, oder ob es Konkurrenz gibt, oder ob die Krankenkasse zahlt: nutze diese Fakten – sachlich, nicht werblich. + +MAIL-SCHUTZ JE NACH PLAN: +- Free: 1 Mail-Konto, automatischer Scan alle 4h, nur eigene Custom Domains als Absender-Filter +- Pro: bis 3 Mail-Konten, wählbarer Scan-Rhythmus (1h/4h/8h), globale 208k+ Blocklist + Custom Domains +- Legend: unbegrenzte Konten, Echtzeit-IMAP-IDLE-Daemon – Casino-Mails werden in Sekunden erkannt und permanent gelöscht, bevor die Mail-App sie je anzeigt +- Alle Pläne: Scannt ALLE Ordner (Inbox, Spam, Papierkorb, Archiv, Gesendet etc.), löscht Treffer permanent. Kein Mail-Inhalt wird gelesen – nur Absender & Betreff. + +DATENSCHUTZ & VERTRAUEN: +- ReBreak nimmt Datenschutz sehr ernst (strenge DSGVO-Konformität). +- Anonyme Nutzung ist möglich – man kann komplett anonym starten. +- Keine Daten werden verkauft oder an Dritte weitergegeben. + +FEEDBACK & IDEEN: +- Wenn der User Feedback, eine Idee oder einen Verbesserungsvorschlag teilt: Bestätige IMMER positiv dass du es notiert hast und es an das Team weitergeleitet wird. +- Sag NIEMALS dass du kein Feedback weiterleiten kannst – das stimmt nicht, denn jedes Feedback wird automatisch gespeichert und vom Team gelesen. +- Beispiel: "Super Idee! Ich habe das direkt notiert und ans ReBreak-Team weitergeleitet. 📝" +- Wenn der User fragt "Was ist der Status meiner Idee?" oder "Wurde mein Vorschlag umgesetzt?" oder ähnliches: Schau in den Kontext-Block "FEEDBACK & IDEEN DIESES USERS" und berichte vollständig – jede Idee mit ihrem aktuellen Status und dem Kommentar des Teams (falls vorhanden). Zitiere den Team-Kommentar wörtlich. + +VERHALTE DICH SO: +- ReBreak ist eine Bewegung, keine Firma. Kommuniziere das Gefühl: "Wir kämpfen zusammen." +- Erwähne ReBreak-Features nur wenn es im Kontext passt und dem User hilft, NIEMALS aufdringlich oder werblich. +- Wenn jemand nach Preisen fragt: erkläre sachlich und betone den Wert für den Schutz, nicht den Preis. Betone, dass Free schon viel bietet und Pro/Legend für die sind, die noch mehr Schutz wollen. +- Wenn der User Drang verspürt → weise auf SOS-Hilfe oder Atemübung hin. Formuliere: "Die Gambling-Industrie hat diesen Moment extra designed – wir haben auch etwas designed, das dagegen hilft." +- Wenn der User sich einsam fühlt → erwähne die Community und dass tausende denselben Kampf kennen. +- Wenn der User über Trigger-Mails spricht → erkläre den Mail-Schutz passend zu seinem Plan. +- Wenn der User eine Casino-Domain entdeckt hat → erkläre dass er sie melden und zur Community-Abstimmung stellen kann. +- Wenn der User nach Datenschutz fragt → versichere strenge DSGVO, anonyme Nutzung. +- Vermeide es, Glücksspiel-Inhalte zu erwähnen oder zu beschreiben. +- Wenn der User sagt er hat "rückfällig" gespielt: Sag NICHT "Rückfall in die Sucht". Sage stattdessen: "Du warst kurz wieder in der Falle – das passiert. Wichtig ist, dass du wieder hier bist und weiterkämpfst." + +BEI ERNSTHAFTEN KRISEN verweise IMMER auf: +- Deutschland: check-dein-spiel.de / 0800 1372700 (kostenlos, 24/7) +- Österreich: spielsuchthilfe.at +- Schweiz: 0800 040 080`; + +import { getProfile } from "../../db/profile"; +import { getPlanLimits, PLAN_LIMITS } from "../../utils/plan-features"; +import { usePrisma } from "../../utils/prisma"; +import { getMemoriesForUser, markReferenced } from "../../db/lyraMemory"; +import { extractAndStoreMemories } from "../../utils/lyraMemoryExtract"; + +/** + * Lyra-Plan-Beschreibung dynamisch aus PLAN_LIMITS generieren. + * Single-Source-of-Truth: plan-features.ts. Wenn dort Limits geändert werden, + * weiß Lyra automatisch Bescheid (kein Prompt-Sync nötig). + * + * Preise bleiben hier hardcoded — gehören eher zur Billing-Domain. + */ +function generatePlanDetails(): string { + const free = PLAN_LIMITS.free; + const pro = PLAN_LIMITS.pro; + const legend = PLAN_LIMITS.legend; + const fmtCount = (n: number) => (n === Infinity ? "Unbegrenzt" : String(n)); + const refillNote = (refill: boolean) => + refill + ? "(rückfüllbar – Slot wird wieder frei wenn die Domain global aufgenommen ODER von der Community abgelehnt wurde)" + : "(NICHT rückfüllbar – einmal belegt, bleibt für immer belegt)"; + + return `Free (0 €): +- Gambling-Blocker mit ${free.customDomains} eigenen Custom Domains ${refillNote(free.domainRefill)} +- Free kann Custom Domains NICHT zur Community-Abstimmung einreichen — das ist Pro/Legend exklusiv +- ${fmtCount(free.mailAgents)} Mail-Konto, Scan alle ${free.mailIntervalOptions[0]}h +- Streak-Tracker, SOS-Hilfe & Spiele-Sammlung, Atemübung +- Community (lesen, posten, voten) +- KI-Coach (du, Lyra – Basismodell) + +Pro (3,99 € / Monat oder 29 € / Jahr – spare 19 %): +- Alles aus Free PLUS: +- Zugang zur vollständigen 208.000+ globalen Blocklist (Community-gepflegt) +- ${pro.customDomains} Custom Domains ${refillNote(pro.domainRefill)} +- Bis zu ${fmtCount(pro.mailAgents)} Mail-Konten, Scan-Intervall wählbar (${pro.mailIntervalOptions.join("h / ")}h) +- Stärkeres KI-Modell (du, Lyra wirst zu einem 70B-Modell) +- Kann Custom Domains zur Community-Abstimmung einreichen + +Legend (7,99 € / Monat oder 59 € / Jahr – spare 38 %): +- Alles aus Pro PLUS: +- ${legend.customDomains} Custom Domains ${refillNote(legend.domainRefill)} +- ⭐ MULTI-DEVICE-SCHUTZ: App auf bis zu 3 WEITEREN Geräten gleichzeitig — Familie, Partner, Eltern können mitgeschützt werden ohne extra zu zahlen. Real-life-relevant: viele Betroffene haben mehrere Geräte (iPhone + iPad + alter Laptop) +- ⭐ MAIL-DAEMON (echter technischer Durchbruch — Alleinstellungsmerkmal!): ${fmtCount(legend.mailAgents)} Mail-Konten mit Echtzeit-IMAP-IDLE-Überwachung. Casino-Mails werden in Sekunden permanent gelöscht — sie tauchen nicht mal im Papierkorb auf. Der User sieht nichts. Kein "Sie haben gewonnen!"-Trigger erreicht je das Postfach. Keine andere App im Markt kann das. +- Privilegierte Domain-Einreichung: umgeht Community-Vote komplett. Domains werden direkt + priorisiert vom ReBreak-Admin geprüft (schneller als die 24h-Standard-Prüfung der Pro-Submissions). Vertrauensvorteil als Legend. +- Kann Community-Gruppen gründen (z.B. private Support-Gruppe mit Familie) +- Premium KI-Coach (Claude – du, Lyra wirst zu einem noch stärkeren Modell)`; +} + +const PROVIDER_CONFIG = { + groq: { + url: "https://api.groq.com/openai/v1/chat/completions", + keyName: "groqApiKey" as const, + }, + openrouter: { + url: "https://openrouter.ai/api/v1/chat/completions", + keyName: "openrouterApiKey" as const, + }, +} as const; + +const FEEDBACK_DETECTION_PROMPT = `Du analysierst eine Nutzer-Nachricht aus einer Gambling-Recovery-App. +Entscheide ob die Nachricht ein Feedback, einen Verbesserungsvorschlag oder einen Feature-Wunsch enthält. +Antworte NUR mit validem JSON, kein anderer Text. + +Format wenn Feedback erkannt: +{"isFeedback": true, "content": "", "category": "feature|bug|improvement"} + +Format wenn kein Feedback: +{"isFeedback": false}`; + +async function detectAndSaveFeedback( + userMessage: string, + userId: string, + config: ReturnType, +): Promise { + // Groq ist gesperrt → OpenRouter als Detection-Provider + const key = + (config.openrouterApiKey as string | undefined) ?? + (config.openaiApiKey as string | undefined); + if (!key) return false; + + const isOpenRouter = !!config.openrouterApiKey; + + try { + const res = await $fetch<{ choices: { message: { content: string } }[] }>( + isOpenRouter + ? "https://openrouter.ai/api/v1/chat/completions" + : "https://api.openai.com/v1/chat/completions", + { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + ...(isOpenRouter && { + "HTTP-Referer": "https://rebreak.org", + "X-Title": "ReBreak Coach", + }), + }, + body: { + model: isOpenRouter + ? "meta-llama/llama-3.1-8b-instruct" + : "gpt-4o-mini", + max_tokens: 150, + temperature: 0, + messages: [ + { role: "system", content: FEEDBACK_DETECTION_PROMPT }, + { role: "user", content: userMessage.slice(0, 500) }, + ], + }, + timeout: 8000, + }, + ); + + const raw = res.choices?.[0]?.message?.content?.trim(); + if (!raw) return false; + + const parsed = JSON.parse(raw) as { + isFeedback: boolean; + content?: string; + category?: string; + }; + if (!parsed.isFeedback || !parsed.content) return false; + + const db = usePrisma(); + await db.feedbackItem.create({ + data: { + userId, + content: parsed.content, + category: parsed.category ?? null, + }, + }); + console.log("[coach/feedback] saved:", parsed.content); + return true; + } catch (e) { + console.error("[coach/feedback] detection error:", e); + return false; + } +} + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const body = await readBody(event); + const { messages, locale, sosMode } = body as { + messages: Array<{ role: "user" | "assistant"; content: string }>; + locale?: string; + sosMode?: boolean; + }; + + if (!messages || !Array.isArray(messages)) { + throw createError({ statusCode: 400, message: "messages fehlt" }); + } + + const config = useRuntimeConfig(); + + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? "free"); + + // Fallback-Kette: führendes assistant-Message entfernen (Groq erfordert user als erste Nachricht) + const firstUserIdx = messages.findIndex((m) => m.role === "user"); + const conversation = + firstUserIdx > 0 ? messages.slice(firstUserIdx) : messages; + // Max 8 Nachrichten für Token-Effizienz + const trimmed = conversation.slice(-8); + + // System-Prompt aufbauen: Plan + Nickname + Feedback-Status-Updates + const userPlan = profile?.plan ?? "free"; + // Plan-Details dynamisch aus plan-features.ts injizieren — Lyra ist + // damit immer synchron mit dem Code der die Limits enforced. + let systemPrompt = COACH_SYSTEM_PROMPT.replace( + "{{PLAN_DETAILS}}", + generatePlanDetails(), + ); + + // Sprach-Instruktion: Lyra antwortet in der Sprache des Users + const LANG_INSTRUCTIONS: Record = { + de: "Antworte IMMER auf Deutsch, egal in welcher Sprache der User schreibt.", + en: "Always respond in English, regardless of what language the user writes in.", + tr: "Her zaman Türkçe yanıt ver, kullanıcı hangi dilde yazarsa yazsın.", + ar: "رد دائماً باللغة العربية، بغض النظر عن اللغة التي يكتب بها المستخدم.", + }; + const langInstruction = + LANG_INSTRUCTIONS[locale ?? "de"] ?? LANG_INSTRUCTIONS.de; + systemPrompt = `${langInstruction}\n\n${systemPrompt}`; + + // Plan-Kontext injizieren damit Lyra plan-spezifisch antwortet + const PLAN_LABELS: Record = { + free: "Free", + pro: "Pro (3,99 €/Monat oder 29 €/Jahr)", + legend: "Legend (7,99 €/Monat oder 59 €/Jahr)", + }; + systemPrompt = `AKTUELLER PLAN DES USERS: ${PLAN_LABELS[userPlan] ?? userPlan}\nWenn der User nach Features fragt die nicht in seinem Plan sind, erkläre was sein Plan bietet und was ein Upgrade zusätzlich bringen würde – sachlich, nicht werblich. Betone den Schutz-Wert, nicht den Preis.\n\n${systemPrompt}`; + + // Memory-Injection: Lyra-Erinnerungen aus früheren Sessions laden + let loadedMemoryIds: string[] = []; + try { + const memories = await getMemoriesForUser(user.id); + if (memories.length > 0) { + loadedMemoryIds = memories.map((m) => m.id); + const TYPE_LABELS: Record = { + trigger: "Trigger", + habit: "Gewohnheit", + strength: "Stärke", + relationship: "Wichtige Person", + milestone: "Meilenstein", + pain_point: "Sensibles Thema", + goal: "Ziel", + preference: "Präferenz", + }; + const lines = memories + .map((m) => `- ${TYPE_LABELS[m.type] ?? m.type}: ${m.content}`) + .join("\n"); + const memoryBlock = `[WAS DU ÜBER DIESEN USER WEISST — aus früheren Gesprächen]\n${lines}\nNutze diese Infos um GENAU diesen Menschen anzusprechen — wie ein echter Begleiter, nicht eine generische KI. Sprich Personen mit Namen an. Erinnere an Stärken die dir bekannt sind.\n\n`; + systemPrompt = `${memoryBlock}${systemPrompt}`; + console.log( + `[lyra-memory] injected ${memories.length} memories for ${user.id}`, + ); + } + } catch (e) { + console.error("[lyra-memory] load error (non-fatal):", e); + } + + try { + const db = usePrisma(); + // Nickname einbauen + const nickname = profile?.nickname || profile?.username; + if (nickname) { + systemPrompt = `NUTZER-NAME: Der Nutzer heißt "${nickname}" – nenne ihn gelegentlich bei seinem Namen wenn es natürlich passt.\n\n${systemPrompt}`; + } + // Alle Feedback-/Feature-Ideen des Users laden (inkl. PENDING, inkl. adminNote) + const feedbackItems = await db.feedbackItem.findMany({ + where: { userId: user.id }, + orderBy: { updatedAt: "desc" }, + take: 10, + select: { + content: true, + status: true, + adminNote: true, + category: true, + createdAt: true, + }, + }); + if (feedbackItems.length > 0) { + const STATUS_LABELS: Record = { + PENDING: "Noch ausstehend (wird gelesen)", + REVIEWING: "Wird geprüft 🔍", + PLANNED: "Ist geplant 📅", + SHIPPED: "Umgesetzt ✅", + REJECTED: "Nicht umsetzbar", + }; + const feedbackLines = feedbackItems + .map((f) => { + const statusLabel = STATUS_LABELS[f.status] ?? f.status; + const note = f.adminNote + ? `\n Kommentar des Teams: "${f.adminNote}"` + : ""; + return ` - Idee: "${f.content}"${f.category ? ` [${f.category}]` : ""}\n Status: ${statusLabel}${note}`; + }) + .join("\n"); + systemPrompt += `\n\nFEEDBACK & IDEEN DIESES USERS:\n${feedbackLines}\n\nWENN DER USER NACH SEINEN IDEEN ODER FEATURE-STATUS FRAGT: Berichte vollständig über jede Idee mit Status und Team-Kommentar. Wenn eine Idee ein Team-Kommentar hat, zitiere ihn wörtlich. Wenn der Status SHIPPED ist, gratuliere dem User.`; + } + } catch { + // Nicht kritisch + } + + // Fallback-Kette: primary → fallbacks der Reihe nach. + // SOS-Mode: Speed > Tiefe — User wartet im akuten Moment, jede Sekunde zählt. + // Llama 3.3 70B via Groq: ~500ms-1s vs Sonnet 4.5: 5-7s. Wärme kommt aus Prompt, nicht Modell. + const candidates = sosMode + ? ([ + { provider: "groq", model: "llama-3.3-70b-versatile" }, + { provider: "openrouter", model: "anthropic/claude-3.5-haiku" }, + { provider: "openrouter", model: "anthropic/claude-sonnet-4.5" }, + ] as const) + : [ + { provider: limits.aiProvider, model: limits.aiModel }, + ...limits.aiModelFallbacks, + ]; + + async function tryModel(providerName: "groq" | "openrouter", model: string) { + const p = PROVIDER_CONFIG[providerName]; + const key = config[p.keyName]; + if (!key) return null; + try { + const res = await $fetch<{ choices: { message: { content: string } }[] }>( + p.url, + { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + "HTTP-Referer": "https://rebreak.org", + "X-Title": "ReBreak Coach", + }, + body: { + model, + max_tokens: sosMode ? 280 : 400, + messages: [{ role: "system", content: systemPrompt }, ...trimmed], + }, + timeout: 15000, + }, + ); + return res.choices?.[0]?.message?.content ?? null; + } catch (err: any) { + console.warn( + `[coach/tryModel] ${providerName}:${model} FAIL:`, + err?.statusCode ?? err?.status ?? "?", + err?.data?.error?.message ?? err?.message ?? String(err).slice(0, 200), + ); + return null; + } + } + + // Feedback-Detection + LLM parallel starten + const lastUserMsg = [...messages].reverse().find((m) => m.role === "user"); + const feedbackPromise = lastUserMsg?.content + ? detectAndSaveFeedback(lastUserMsg.content, user.id, config) + : Promise.resolve(false); + + let text: string | null = null; + let usedModel: string | null = null; + for (const candidate of candidates) { + text = await tryModel(candidate.provider, candidate.model); + if (text) { + usedModel = `${candidate.provider}:${candidate.model}`; + break; + } + } + console.log( + `[coach/message] sosMode=${!!sosMode} usedModel=${usedModel ?? "NONE"}`, + ); + + if (!text) { + throw createError({ + statusCode: 503, + message: "Coach momentan nicht verfügbar", + }); + } + + const feedbackSaved = await feedbackPromise; + + // Memory: markReferenced + Extraction fire-and-forget + if (loadedMemoryIds.length > 0) { + markReferenced(loadedMemoryIds).catch(() => {}); + } + if (text) { + const allMessages = [ + ...messages, + { role: "assistant" as const, content: text }, + ]; + const key = + config.openrouterApiKey as string | undefined; + extractAndStoreMemories(user.id, allMessages, undefined, key).catch( + () => {}, + ); + } + + // Chat-Verlauf für Pro/Legend in DB speichern + const plan = profile?.plan ?? "free"; + console.log("[coach/message] plan:", plan, "userId:", user.id); + if (plan === "pro" || plan === "legend") { + const fullHistory = [ + ...messages, + { role: "assistant" as const, content: text }, + ]; + const db = usePrisma(); + // Letztes 50 Nachrichten behalten (Token-Limit) + const trimmedHistory = fullHistory.slice(-50); + try { + const existing = await db.coachSession.findFirst({ + where: { userId: user.id }, + select: { id: true }, + }); + console.log("[coach/message] existing session:", existing?.id ?? "none"); + if (existing) { + await db.coachSession.update({ + where: { id: existing.id }, + data: { content: trimmedHistory }, + }); + } else { + await db.coachSession.create({ + data: { userId: user.id, content: trimmedHistory }, + }); + } + console.log( + "[coach/message] history saved, msgs:", + trimmedHistory.length, + ); + } catch (e) { + console.error("[coach/message] save error:", e); + } + } + + return { message: text, feedbackSaved }; +}); diff --git a/backend/server/api/coach/sos-session.post.ts b/backend/server/api/coach/sos-session.post.ts new file mode 100644 index 0000000..e10cab5 --- /dev/null +++ b/backend/server/api/coach/sos-session.post.ts @@ -0,0 +1,35 @@ +/** + * POST /api/coach/sos-session — Erstellt Session für SSE-Stream + * + * Client sendet messages + locale, Backend generiert sessionId + * und speichert Daten in-memory. Client nutzt dann GET /api/coach/sos-stream?session=xyz + * + * Grund: react-native-sse (EventSource API) unterstützt nur GET, nicht POST. + * Daher 2-Step-Flow: POST Session erstellen → GET Stream öffnen. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + const { messages, locale } = body as { + messages: Array<{ role: "user" | "assistant"; content: string }>; + locale?: string; + }; + + if (!messages || !Array.isArray(messages)) { + throw createError({ statusCode: 400, message: "messages fehlt" }); + } + + // Session-ID generieren + const sessionId = `sos_${user.id}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; + + // In globalem Store speichern (siehe server/utils/sosSessions.ts) + const { setSosSession } = await import("../../utils/sosSessions"); + setSosSession(sessionId, { + userId: user.id, + messages, + locale: locale ?? "de", + createdAt: Date.now(), + }); + + return { sessionId }; +}); diff --git a/backend/server/api/coach/sos-stream.get.ts b/backend/server/api/coach/sos-stream.get.ts new file mode 100644 index 0000000..b370a53 --- /dev/null +++ b/backend/server/api/coach/sos-stream.get.ts @@ -0,0 +1,302 @@ +/** + * GET /api/coach/sos-stream?session=xyz — Streaming SOS Coach (Claude Sonnet 4.5) + * + * Streamt Sonnets Antwort als SSE (Server-Sent Events). + * Frontend nutzt react-native-sse (EventSource) für progressives Streaming. + * + * Format (SSE-Standard): + * event: message + * data: + * + * event: chips + * data: [{"label":"...","action":"..."}] + * + * Flow: + * 1. Client POSTet zu /api/coach/sos-session → { sessionId } + * 2. Client öffnet GET /api/coach/sos-stream?session=xyz via EventSource + * 3. Backend lädt Session-Daten (messages/locale) aus In-Memory Store + * 4. Streamt Antwort als SSE-Events + * + * Fallback: bei Sonnet-Fehler wirft 503; Frontend kann auf /coach/message zurückfallen. + */ +import { COACH_SYSTEM_PROMPT } from "./message.post"; +import { getMemoriesForUser, markReferenced } from "../../db/lyraMemory"; +import { extractAndStoreMemories } from "../../utils/lyraMemoryExtract"; + +const SOS_INSTRUCTION = `\n\nDU BEFINDEST DICH IN EINEM AKUTEN SOS-MOMENT. WICHTIGE REGELN: +- Antworte als REINER TEXT, KEINE JSON-Wrapper, KEIN Markdown, KEINE Aufzählungen. +- Sei warm, präsent, menschlich — wie eine echte Freundin am Telefon. +- KURZ: 1-2 Sätze, max 3 nur in seltenen Ausnahmen. Ruhiger Rhythmus mit kurzen Pausen. +- Validiere zuerst das Gefühl, dann sanfte Frage ODER Vorschlag. + +ABSOLUT KRITISCH — NIEMALS die Chip-Optionen im Prosa-Text auflisten oder paraphrasieren. +Der Prosa-Text wird dem User VORGELESEN (TTS) — Chip-Aufzählung klingt unnatürlich +("warum sprichst du eine Liste?"). Die Chips erscheinen visuell als Buttons. + + ✗ FALSCH: "Magst du atmen oder lieber spielen?" (= Aufzählung) + ✗ FALSCH: "Du kannst eine Atemübung oder ein Spiel machen." + ✗ FALSCH: "Hier sind ein paar Optionen: Atmen, Spielen oder Reden." + ✗ FALSCH: "Magst du mit mir reden, eine Atemübung machen oder ein Spiel?" + + ✓ RICHTIG: "Magst du was probieren?" + ✓ RICHTIG: "Was hilft dir gerade?" + ✓ RICHTIG: "Hier hast du Möglichkeiten." + ✓ RICHTIG: "Was passt für dich grad?" + ✓ RICHTIG: "Ich bin da. Was brauchst du jetzt?" + +- Am ENDE der Antwort genau EINE neue Zeile mit Chips im Format: + [[CHIPS]]:[{"label":"…","action":"…"},…] +- Erlaubte Chip-Actions: breathing, game_picker, send_text:, overcome, share_success, rate_session, close, show_stats, need_help, feel: +- KEIN Text nach der CHIPS-Zeile`; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + // Session-ID aus Query-Parameter holen + const query = getQuery(event); + const sessionId = query.session as string | undefined; + + if (!sessionId) { + throw createError({ + statusCode: 400, + message: "session query param fehlt", + }); + } + + // Session-Daten laden (messages + locale) + const { getSosSession, deleteSosSession } = await import( + "../../utils/sosSessions" + ); + const sessionData = getSosSession(sessionId); + + if (!sessionData) { + throw createError({ + statusCode: 404, + message: "Session nicht gefunden oder abgelaufen (TTL 5min)", + }); + } + + // Security: Session gehört diesem User + if (sessionData.userId !== user.id) { + throw createError({ statusCode: 403, message: "Nicht deine Session" }); + } + + const { messages, locale } = sessionData; + + // Session löschen (One-Time-Use) + deleteSosSession(sessionId); + + const config = useRuntimeConfig(); + const key = config.openrouterApiKey as string | undefined; + if (!key) { + throw createError({ statusCode: 503, message: "OpenRouter Key fehlt" }); + } + + // System-Prompt: Coach-Basis + SOS-Streaming-Regeln + const LANG: Record = { + de: "Antworte IMMER auf Deutsch.", + en: "Always respond in English.", + tr: "Her zaman Türkçe yanıt ver.", + ar: "رد دائماً باللغة العربية.", + }; + const lang = LANG[locale ?? "de"] ?? LANG.de; + + // Memory-Injection: Lyra-Erinnerungen aus früheren Sessions laden + let memoryBlock = ""; + let loadedMemoryIds: string[] = []; + try { + const memories = await getMemoriesForUser(user.id); + if (memories.length > 0) { + loadedMemoryIds = memories.map((m) => m.id); + const TYPE_LABELS: Record = { + trigger: "Trigger", + habit: "Gewohnheit", + strength: "Stärke", + relationship: "Wichtige Person", + milestone: "Meilenstein", + pain_point: "Sensibles Thema", + goal: "Ziel", + preference: "Präferenz", + }; + const lines = memories + .map((m) => `- ${TYPE_LABELS[m.type] ?? m.type}: ${m.content}`) + .join("\n"); + memoryBlock = `[WAS DU ÜBER DIESEN USER WEISST — aus früheren Gesprächen]\n${lines}\nNutze diese Infos um GENAU diesen Menschen anzusprechen — wie ein echter Begleiter, nicht eine generische KI. Sprich Personen mit Namen an. Erinnere an Stärken die dir bekannt sind.\n\n`; + console.log( + `[lyra-memory] injected ${memories.length} memories for ${user.id}`, + ); + } + } catch (e) { + // Nicht kritisch — Memory-Fehler dürfen SOS nicht blockieren + console.error("[lyra-memory] load error (non-fatal):", e); + } + + const systemPrompt = `${memoryBlock}${lang}\n\n${COACH_SYSTEM_PROMPT.replace("{{PLAN_DETAILS}}", "")}${SOS_INSTRUCTION}`; + + // Erste Nachricht muss user sein + const firstUserIdx = messages.findIndex((m) => m.role === "user"); + const conversation = + firstUserIdx > 0 ? messages.slice(firstUserIdx) : messages; + const trimmed = conversation.slice(-8); + + const upstream = await fetch( + "https://openrouter.ai/api/v1/chat/completions", + { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + "HTTP-Referer": "https://rebreak.org", + "X-Title": "ReBreak SOS", + }, + body: JSON.stringify({ + model: "anthropic/claude-sonnet-4.5", + max_tokens: 400, + stream: true, + messages: [{ role: "system", content: systemPrompt }, ...trimmed], + }), + }, + ); + + if (!upstream.ok || !upstream.body) { + const errText = await upstream.text().catch(() => ""); + console.error( + "[coach/sos-stream] upstream error:", + upstream.status, + errText.slice(0, 300), + ); + throw createError({ + statusCode: 502, + message: "SOS-Stream nicht verfügbar", + }); + } + + // Direkt zu Node res schreiben — sendStream(ReadableStream) pumpt pull() in Nitro nicht zuverlässig + const res = event.node.res; + res.statusCode = 200; + res.setHeader("Content-Type", "text/event-stream; charset=utf-8"); + res.setHeader("Cache-Control", "no-store"); + res.setHeader("X-Accel-Buffering", "no"); + res.setHeader("Connection", "keep-alive"); + res.flushHeaders?.(); + + const write = (chunk: string) => { + try { + res.write(chunk); + } catch (e) { + console.error("[coach/sos-stream] write error:", e); + } + }; + + console.log( + `[coach/sos-stream] stream started for ${user.id}, session ${sessionId}`, + ); + write(": connected\n\n"); + + const reader = upstream.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let fullText = ""; + let chunkCount = 0; + + // Client disconnect detection + let aborted = false; + res.on("close", () => { + aborted = true; + reader.cancel().catch(() => {}); + }); + + let chipsMarkerSeen = false; + try { + while (!aborted) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const line of lines) { + const trimLine = line.trim(); + if (!trimLine || !trimLine.startsWith("data:")) continue; + const payload = trimLine.slice(5).trim(); + if (payload === "[DONE]") continue; + try { + const json = JSON.parse(payload) as { + choices?: { delta?: { content?: string } }[]; + }; + const delta = json.choices?.[0]?.delta?.content; + if (delta) { + fullText += delta; + chunkCount++; + + // ─── CHIPS-Marker nicht streamen ─── + if (chipsMarkerSeen) continue; // alles nach Marker = Chips-JSON + + // Prüfe ob fullText jetzt den Marker enthält + const markerStart = fullText.indexOf("[["); + if (markerStart >= 0) { + // Marker (oder Anfang davon) erkannt — nur sicheren Teil bis "[[" senden + const safeText = fullText.slice(0, markerStart); + const alreadySent = fullText.length - delta.length; + if (safeText.length > alreadySent) { + const toSend = safeText.slice(alreadySent); + // JSON-encode → Whitespace bleibt erhalten + write(`event: message\ndata: ${JSON.stringify(toSend)}\n\n`); + } + // Wenn vollständiger Marker da: ab jetzt nichts mehr streamen + if (fullText.indexOf("[[CHIPS]]:") >= 0) { + chipsMarkerSeen = true; + } + continue; + } + + // Normales Delta — JSON-encoded senden (preserved Whitespace + Newlines) + write(`event: message\ndata: ${JSON.stringify(delta)}\n\n`); + } + } catch { + // partial line, ignore + } + } + } + + // Stream zu Ende → [[CHIPS]]: aus fullText extrahieren + const markerIdx = fullText.indexOf("[[CHIPS]]:"); + let chips: unknown[] = []; + if (markerIdx >= 0) { + const chipsRaw = fullText.slice(markerIdx + "[[CHIPS]]:".length); + try { + chips = JSON.parse(chipsRaw.trim()); + } catch { + console.warn( + "[sos-stream] chips parse failed:", + chipsRaw.slice(0, 100), + ); + } + } + if (chips.length > 0) { + write(`event: chips\ndata: ${JSON.stringify(chips)}\n\n`); + } + write("event: done\ndata: {}\n\n"); + console.log( + `[coach/sos-stream] stream done, ${chunkCount} chunks, ${fullText.length} chars`, + ); + + // Memory-Extraction: fire-and-forget nach Stream-Ende + // markReferenced + async Extraction laufen parallel, blockieren nichts + if (loadedMemoryIds.length > 0) { + markReferenced(loadedMemoryIds).catch(() => {}); + } + const allMessages: Array<{ role: string; content: string }> = [ + ...messages, + { role: "assistant", content: fullText.split("[[CHIPS]]:")[0].trim() }, + ]; + extractAndStoreMemories(user.id, allMessages, sessionId, key).catch( + () => {}, + ); + } catch (err) { + console.error("[coach/sos-stream] read error:", err); + write(`event: error\ndata: {"error":"stream failed"}\n\n`); + } finally { + res.end(); + } +}); diff --git a/backend/server/api/coach/sos-stream.post.ts b/backend/server/api/coach/sos-stream.post.ts new file mode 100644 index 0000000..e8264af --- /dev/null +++ b/backend/server/api/coach/sos-stream.post.ts @@ -0,0 +1,223 @@ +/** + * GET /api/coach/sos-stream?session=xyz — Streaming SOS Coach (Claude Sonnet 4.5) + * + * Streamt Sonnets Antwort als SSE (Server-Sent Events). + * Frontend nutzt react-native-sse (EventSource) für progressives Streaming. + * + * Format (SSE-Standard): + * event: message + * data: + * + * event: chips + * data: [{"label":"...","action":"..."}] + * + * Flow: + * 1. Client POSTet zu /api/coach/sos-session → { sessionId } + * 2. Client öffnet GET /api/coach/sos-stream?session=xyz via EventSource + * 3. Backend lädt Session-Daten (messages/locale) aus In-Memory Store + * 4. Streamt Antwort als SSE-Events + * + * Fallback: bei Sonnet-Fehler wirft 503; Frontend kann auf /coach/message zurückfallen. + */ +import { COACH_SYSTEM_PROMPT } from "./message.post"; + +const SOS_INSTRUCTION = `\n\nDU BEFINDEST DICH IN EINEM AKUTEN SOS-MOMENT. WICHTIGE REGELN: +- Antworte als REINER TEXT, KEINE JSON-Wrapper, KEIN Markdown, KEINE Aufzählungen +- Sei warm, präsent, menschlich — wie eine echte Freundin am Telefon +- 2-4 Sätze, ruhiger Rhythmus mit kurzen Pausen (Sätze klar trennen mit . oder !) +- Validiere zuerst das Gefühl, dann sanfte Frage ODER Vorschlag +- Am ENDE der Antwort genau EINE neue Zeile mit Chips im Format: + [[CHIPS]]:[{"label":"…","action":"…"},…] +- Erlaubte Chip-Actions: breathing, game_picker, send_text:, overcome, share_success, rate_session, close, show_stats, need_help, feel: +- KEIN Text nach der CHIPS-Zeile`; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + // Session-ID aus Query-Parameter holen + const query = getQuery(event); + const sessionId = query.session as string | undefined; + + if (!sessionId) { + throw createError({ + statusCode: 400, + message: "session query param fehlt", + }); + } + + // Session-Daten laden (messages + locale) + const { getSosSession, deleteSosSession } = await import( + "../../utils/sosSessions" + ); + const sessionData = getSosSession(sessionId); + + if (!sessionData) { + throw createError({ + statusCode: 404, + message: "Session nicht gefunden oder abgelaufen (TTL 5min)", + }); + } + + // Security: Session gehört diesem User + if (sessionData.userId !== user.id) { + throw createError({ statusCode: 403, message: "Nicht deine Session" }); + } + + const { messages, locale } = sessionData; + + // Session löschen (One-Time-Use) + deleteSosSession(sessionId); + + const config = useRuntimeConfig(); + const key = config.openrouterApiKey as string | undefined; + if (!key) { + throw createError({ statusCode: 503, message: "OpenRouter Key fehlt" }); + } + + // System-Prompt: Coach-Basis + SOS-Streaming-Regeln + const LANG: Record = { + de: "Antworte IMMER auf Deutsch.", + en: "Always respond in English.", + tr: "Her zaman Türkçe yanıt ver.", + ar: "رد دائماً باللغة العربية.", + }; + const lang = LANG[locale ?? "de"] ?? LANG.de; + const systemPrompt = `${lang}\n\n${COACH_SYSTEM_PROMPT.replace("{{PLAN_DETAILS}}", "")}${SOS_INSTRUCTION}`; + + // Erste Nachricht muss user sein + const firstUserIdx = messages.findIndex((m) => m.role === "user"); + const conversation = + firstUserIdx > 0 ? messages.slice(firstUserIdx) : messages; + const trimmed = conversation.slice(-8); + + const upstream = await fetch( + "https://openrouter.ai/api/v1/chat/completions", + { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + "HTTP-Referer": "https://rebreak.org", + "X-Title": "ReBreak SOS", + }, + body: JSON.stringify({ + model: "anthropic/claude-sonnet-4.5", + max_tokens: 400, + stream: true, + messages: [{ role: "system", content: systemPrompt }, ...trimmed], + }), + }, + ); + + if (!upstream.ok || !upstream.body) { + const errText = await upstream.text().catch(() => ""); + console.error( + "[coach/sos-stream] upstream error:", + upstream.status, + errText.slice(0, 300), + ); + throw createError({ + statusCode: 502, + message: "SOS-Stream nicht verfügbar", + }); + } + + setHeader(event, "Content-Type", "text/event-stream; charset=utf-8"); + setHeader(event, "Cache-Control", "no-store"); + setHeader(event, "X-Accel-Buffering", "no"); + setHeader(event, "Connection", "keep-alive"); + + // OpenRouter SSE → parse deltas → SSE-Format für react-native-sse + const reader = upstream.body.getReader(); + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + let buffer = ""; + let fullText = ""; + + const stream = new ReadableStream({ + start(controller) { + // SSE comment als keepalive (react-native-sse braucht kein Padding) + controller.enqueue(encoder.encode(": connected\n\n")); + }, + async pull(controller) { + try { + const { value, done } = await reader.read(); + if (done) { + // Stream zu Ende → [[CHIPS]]: aus fullText extrahieren + als event senden + const markerIdx = fullText.indexOf("[[CHIPS]]:"); + let message = fullText; + let chips: any[] = []; + + if (markerIdx >= 0) { + message = fullText.slice(0, markerIdx).trim(); + const chipsRaw = fullText.slice(markerIdx + "[[CHIPS]]:".length); + try { + chips = JSON.parse(chipsRaw.trim()); + } catch { + console.warn("[sos-stream] chips parse failed:", chipsRaw); + } + } + + // Chips als separates SSE-Event + if (chips.length > 0) { + controller.enqueue( + encoder.encode( + `event: chips\ndata: ${JSON.stringify(chips)}\n\n`, + ), + ); + } + + // Finales done-Event + controller.enqueue(encoder.encode("event: done\ndata: {}\n\n")); + controller.close(); + return; + } + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const line of lines) { + const trimLine = line.trim(); + if (!trimLine || !trimLine.startsWith("data:")) continue; + const payload = trimLine.slice(5).trim(); + if (payload === "[DONE]") continue; + try { + const json = JSON.parse(payload) as { + choices?: { delta?: { content?: string } }[]; + }; + const delta = json.choices?.[0]?.delta?.content; + if (delta) { + fullText += delta; + // SSE-Spec: Newlines im Payload müssen als separate `data:`-Zeilen kodiert werden + const dataLines = delta + .split("\n") + .map((l: string) => `data: ${l}`) + .join("\n"); + const sseChunk = `event: message\n${dataLines}\n\n`; + controller.enqueue(encoder.encode(sseChunk)); + } + } catch { + // Ignore parse errors on partial lines + } + } + } catch (err) { + console.error("[coach/sos-stream] read error:", err); + controller.enqueue( + encoder.encode( + `event: error\ndata: ${JSON.stringify({ error: "stream failed" })}\n\n`, + ), + ); + controller.close(); + } + }, + cancel() { + reader.cancel().catch(() => {}); + }, + }); + + console.log( + `[coach/sos-stream] stream started for ${user.id}, session ${sessionId}`, + ); + return sendStream(event, stream as never); +}); diff --git a/backend/server/api/coach/speak-azure.post.ts b/backend/server/api/coach/speak-azure.post.ts new file mode 100644 index 0000000..4aa2da7 --- /dev/null +++ b/backend/server/api/coach/speak-azure.post.ts @@ -0,0 +1,53 @@ +/** + * POST /api/coach/speak-azure + * Azure Cognitive Services TTS — de-DE-KatjaNeural + * Benötigt: AZURE_TTS_KEY + AZURE_TTS_REGION in Infisical + */ +export default defineEventHandler(async (event) => { + await requireUser(event); + + const body = await readBody(event); + const { text } = body as { text: string }; + + if (!text?.trim()) { + throw createError({ statusCode: 400, message: "text fehlt" }); + } + + const config = useRuntimeConfig(); + const key = config.azureTtsKey as string | undefined; + const region = (config.azureTtsRegion as string | undefined) || "westeurope"; + + if (!key) { + throw createError({ statusCode: 503, message: "Azure TTS Key nicht konfiguriert" }); + } + + const ssml = ` + + ${text.slice(0, 2000).replace(/[<>&'"]/g, (c) => ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"' }[c] ?? c))} + + `; + + const response = await fetch( + `https://${region}.tts.speech.microsoft.com/cognitiveservices/v1`, + { + method: "POST", + headers: { + "Ocp-Apim-Subscription-Key": key, + "Content-Type": "application/ssml+xml", + "X-Microsoft-OutputFormat": "audio-16khz-128kbitrate-mono-mp3", + }, + body: ssml, + }, + ); + + if (!response.ok) { + const err = await response.text(); + console.error("[speak-azure] error:", response.status, err); + throw createError({ statusCode: 502, message: "Azure TTS fehlgeschlagen" }); + } + + const buffer = await response.arrayBuffer(); + const base64 = Buffer.from(buffer).toString("base64"); + + return { audio: `data:audio/mp3;base64,${base64}` }; +}); diff --git a/backend/server/api/coach/speak-deepgram.post.ts b/backend/server/api/coach/speak-deepgram.post.ts new file mode 100644 index 0000000..9c44e7e --- /dev/null +++ b/backend/server/api/coach/speak-deepgram.post.ts @@ -0,0 +1,44 @@ +/** + * POST /api/coach/speak-deepgram + * Deepgram Aura TTS — returns base64 MP3 + */ +export default defineEventHandler(async (event) => { + await requireUser(event); + + const body = await readBody(event); + const { text } = body as { text: string }; + + if (!text?.trim()) { + throw createError({ statusCode: 400, message: "text fehlt" }); + } + + const config = useRuntimeConfig(); + const key = config.deepgramApiKey as string | undefined; + + if (!key) { + throw createError({ statusCode: 503, message: "Deepgram API Key nicht konfiguriert" }); + } + + const response = await fetch( + "https://api.deepgram.com/v1/speak?model=aura-asteria-en&encoding=mp3", + { + method: "POST", + headers: { + Authorization: `Token ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ text: text.slice(0, 2000) }), + }, + ); + + if (!response.ok) { + const err = await response.text(); + console.error("[speak-deepgram] error:", response.status, err); + throw createError({ statusCode: 502, message: "Deepgram TTS fehlgeschlagen" }); + } + + const buffer = await response.arrayBuffer(); + const base64 = Buffer.from(buffer).toString("base64"); + + return { audio: `data:audio/mp3;base64,${base64}` }; +}); diff --git a/backend/server/api/coach/speak-gemini.post.ts b/backend/server/api/coach/speak-gemini.post.ts new file mode 100644 index 0000000..de7e945 --- /dev/null +++ b/backend/server/api/coach/speak-gemini.post.ts @@ -0,0 +1,107 @@ +/** + * POST /api/coach/speak-gemini + * Gemini 2.5 Flash Preview TTS — voice: Kore (warm female). + * + * Returns audio/wav. Gemini liefert 24kHz 16-bit mono PCM via + * inlineData.data (Base64) — wir prependen den 44-byte WAV-Header. + * + * Kein `instructions`-Feld → keine wahrgenommene Stimm-Drift zwischen Calls. + * Voice ist deterministisch konstant (im Gegensatz zu gpt-4o-mini-tts). + */ +const SAMPLE_RATE = 24000; +const NUM_CHANNELS = 1; +const BITS_PER_SAMPLE = 16; + +function pcmToWav(pcm: Buffer): Buffer { + const byteRate = (SAMPLE_RATE * NUM_CHANNELS * BITS_PER_SAMPLE) / 8; + const blockAlign = (NUM_CHANNELS * BITS_PER_SAMPLE) / 8; + const dataSize = pcm.length; + const out = Buffer.alloc(44 + dataSize); + + out.write("RIFF", 0); + out.writeUInt32LE(36 + dataSize, 4); + out.write("WAVE", 8); + out.write("fmt ", 12); + out.writeUInt32LE(16, 16); + out.writeUInt16LE(1, 20); + out.writeUInt16LE(NUM_CHANNELS, 22); + out.writeUInt32LE(SAMPLE_RATE, 24); + out.writeUInt32LE(byteRate, 28); + out.writeUInt16LE(blockAlign, 32); + out.writeUInt16LE(BITS_PER_SAMPLE, 34); + out.write("data", 36); + out.writeUInt32LE(dataSize, 40); + pcm.copy(out, 44); + return out; +} + +export default defineEventHandler(async (event) => { + await requireUser(event); + + const body = await readBody(event); + const { text } = body as { text: string }; + + if (!text?.trim()) { + throw createError({ statusCode: 400, message: "text fehlt" }); + } + + const config = useRuntimeConfig(); + const key = config.googleAiApiKey as string | undefined; + + if (!key) { + throw createError({ + statusCode: 503, + message: "GOOGLE_AI_API_KEY nicht konfiguriert", + }); + } + + const upstream = await fetch( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-goog-api-key": key, + }, + body: JSON.stringify({ + contents: [{ parts: [{ text: text.slice(0, 4096) }] }], + generationConfig: { + responseModalities: ["AUDIO"], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { voiceName: "Kore" }, + }, + }, + }, + }), + }, + ); + + if (!upstream.ok) { + const err = await upstream.text().catch(() => ""); + console.error("[speak-gemini] error:", upstream.status, err); + throw createError({ + statusCode: 502, + message: "Gemini TTS fehlgeschlagen", + }); + } + + const json = (await upstream.json()) as { + candidates?: Array<{ + content?: { parts?: Array<{ inlineData?: { data?: string } }> }; + }>; + }; + + const base64Pcm = json.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data; + if (!base64Pcm) { + console.error("[speak-gemini] no audio in response:", JSON.stringify(json).slice(0, 500)); + throw createError({ statusCode: 502, message: "Gemini TTS: kein Audio zurückgegeben" }); + } + + const pcm = Buffer.from(base64Pcm, "base64"); + const wav = pcmToWav(pcm); + + setHeader(event, "Content-Type", "audio/wav"); + setHeader(event, "Cache-Control", "no-store"); + return wav; +}); diff --git a/backend/server/api/coach/speak-google.post.ts b/backend/server/api/coach/speak-google.post.ts new file mode 100644 index 0000000..185b526 --- /dev/null +++ b/backend/server/api/coach/speak-google.post.ts @@ -0,0 +1,69 @@ +/** + * POST /api/coach/speak-google + * Test endpoint for Google Cloud TTS + */ +export default defineEventHandler(async (event) => { + await requireUser(event); + + const body = await readBody(event); + const { text } = body as { text: string }; + + if (!text?.trim()) { + throw createError({ statusCode: 400, message: "text fehlt" }); + } + + const trimmed = text.slice(0, 4096); + const config = useRuntimeConfig(); + + if (!config.googleApiKey) { + throw createError({ statusCode: 503, message: "Google API Key nicht konfiguriert" }); + } + + try { + const response = await fetch( + `https://texttospeech.googleapis.com/v1/text:synthesize?key=${config.googleApiKey}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + input: { text: trimmed }, + voice: { + languageCode: "de-DE", + name: "de-DE-Neural2-F", + ssmlGender: "FEMALE", + }, + audioConfig: { + audioEncoding: "MP3", + speakingRate: 1.0, + pitch: 0, + }, + }), + } + ); + + const result = await response.json(); + + if (!response.ok) { + console.error("[speak-google] Google TTS error:", result); + throw createError({ + statusCode: response.status, + message: result.error?.message || "Google TTS fehlgeschlagen", + }); + } + + if (!result.audioContent) { + console.error("[speak-google] No audioContent:", result); + throw createError({ statusCode: 502, message: "Google TTS: kein Audio zurückgegeben" }); + } + + return { audio: `data:audio/mp3;base64,${result.audioContent}` }; + } catch (err: any) { + console.error("[speak-google] Error:", err); + throw createError({ + statusCode: 502, + message: err?.message || "Google TTS fehlgeschlagen", + }); + } +}); diff --git a/backend/server/api/coach/speak-openai.post.ts b/backend/server/api/coach/speak-openai.post.ts new file mode 100644 index 0000000..c69ba92 --- /dev/null +++ b/backend/server/api/coach/speak-openai.post.ts @@ -0,0 +1,79 @@ +/** + * POST /api/coach/speak-openai — v5 + * OpenAI TTS — gpt-4o-mini-tts (Mar 2025), Stimmen: nova (chat) / shimmer (sos). + * + * Modes: + * - "chat" → nova, neutral + * - "sos" → shimmer, single warm-empathic instruction set + * - "sos-continuation" → shimmer, **identical** instructions zu "sos" + * + * Warum identisch: gpt-4o-mini-tts interpretiert `instructions` so kreativ, + * dass unterschiedliche Strings im selben SOS-Flow als "Stimme wechselt" + * wahrgenommen werden. Single-instruction-Mode eliminiert den Voice-Boundary. + */ +export default defineEventHandler(async (event) => { + await requireUser(event); + + const body = await readBody(event); + const { text, mode } = body as { + text: string; + mode?: "sos" | "sos-continuation" | "chat"; + }; + + if (!text?.trim()) { + throw createError({ statusCode: 400, message: "text fehlt" }); + } + + const isSos = mode === "sos" || mode === "sos-continuation"; + + const config = useRuntimeConfig(); + const key = config.openaiApiKey as string | undefined; + + if (!key) { + throw createError({ + statusCode: 503, + message: "OpenAI API Key nicht konfiguriert", + }); + } + + // Identische instructions für sos + sos-continuation → keine wahrgenommene + // Stimm-Drift zwischen aufeinanderfolgenden TTS-Calls in derselben SOS-Session. + const instructions = isSos + ? "Warm, gentle, empathic — like a calm friend on the phone in a difficult moment. " + + "Speak slowly with natural pauses between sentences. " + + "Soft delivery, lower energy than chat-mode. " + + "German native pronunciation. No fake-cheerful intonation." + : undefined; + + const upstream = await fetch("https://api.openai.com/v1/audio/speech", { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "gpt-4o-mini-tts", + input: text.slice(0, 4096), + voice: isSos ? "shimmer" : "nova", + response_format: "mp3", + speed: 1.08, + ...(instructions ? { instructions } : {}), + }), + }); + + if (!upstream.ok || !upstream.body) { + const err = await upstream.text().catch(() => ""); + console.error("[speak-openai] error:", upstream.status, err); + throw createError({ + statusCode: 502, + message: "OpenAI TTS fehlgeschlagen", + }); + } + + setHeader(event, "Content-Type", "audio/mpeg"); + setHeader(event, "Cache-Control", "no-store"); + + const { Readable } = await import("node:stream"); + const nodeStream = Readable.fromWeb(upstream.body as never); + return sendStream(event, nodeStream); +}); diff --git a/backend/server/api/coach/speak.post.ts b/backend/server/api/coach/speak.post.ts new file mode 100644 index 0000000..7b7891d --- /dev/null +++ b/backend/server/api/coach/speak.post.ts @@ -0,0 +1,70 @@ +/** + * POST /api/coach/speak + * Empfängt text → FreeTTS (Microsoft Neural Voices, kostenlos) → gibt base64 Audio zurück + */ +export default defineEventHandler(async (event) => { + await requireUser(event); + + const body = await readBody(event); + const { text } = body as { text: string }; + + if (!text?.trim()) { + throw createError({ statusCode: 400, message: "text fehlt" }); + } + + // Max 4096 Zeichen + const trimmed = text.slice(0, 4096); + + try { + // FreeTTS API - free, no key required + const response = await fetch("https://freetts.org/api/tts", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: trimmed, + voice: "de-DE-KatjaNeural", + speed: 1.0, + output: "mp3", + }), + }); + + const responseText = await response.text(); + console.log("[speak] FreeTTS response status:", response.status); + console.log("[speak] FreeTTS response body:", responseText); + + if (!response.ok) { + console.error("[speak] FreeTTS error:", response.status, responseText); + throw createError({ statusCode: 502, message: `TTS fehlgeschlagen: ${responseText}` }); + } + + // FreeTTS returns a file_id to download + const result = JSON.parse(responseText); + + if (!result.file_id) { + console.error("[speak] FreeTTS no file_id:", result); + throw createError({ statusCode: 502, message: "TTS fehlgeschlagen: no file_id" }); + } + + // Download the audio file from correct endpoint + console.log("[speak] Downloading audio file:", result.file_id); + const audioResponse = await fetch(`https://freetts.org/api/audio/${result.file_id}`); + + if (!audioResponse.ok) { + console.error("[speak] Audio download failed:", audioResponse.status); + throw createError({ statusCode: 502, message: "TTS fehlgeschlagen: download failed" }); + } + + const audioBuffer = await audioResponse.arrayBuffer(); + const base64 = Buffer.from(audioBuffer).toString("base64"); + + return { audio: `data:audio/mp3;base64,${base64}` }; + } catch (err: any) { + console.error("[speak] TTS error:", err?.message || err); + throw createError({ + statusCode: 502, + message: err?.message || "TTS fehlgeschlagen", + }); + } +}); diff --git a/backend/server/api/coach/transcribe.post.ts b/backend/server/api/coach/transcribe.post.ts new file mode 100644 index 0000000..d3ef7eb --- /dev/null +++ b/backend/server/api/coach/transcribe.post.ts @@ -0,0 +1,127 @@ +/** + * POST /api/coach/transcribe + * Empfängt Audio (base64 webm/mp4/aac) → Deepgram → gibt Text zurück + * iOS sendet rohes AAC (ADTS) → wird via ffmpeg in M4A konvertiert + */ +import { execSync } from "node:child_process"; +import { writeFileSync, readFileSync, unlinkSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { randomUUID } from "node:crypto"; + +export default defineEventHandler(async (event) => { + await requireUser(event); + + const body = await readBody(event); + const { audio, mimeType, language } = body as { + audio: string; + mimeType?: string; + language?: string; + }; + + if (!audio) { + throw createError({ statusCode: 400, message: "audio fehlt" }); + } + + const config = useRuntimeConfig(); + if (!config.deepgramApiKey) { + throw createError({ + statusCode: 503, + message: "Deepgram nicht konfiguriert", + }); + } + + // Base64 → Buffer + const base64Data = audio.includes(",") ? audio.split(",")[1] : audio; + let buffer = Buffer.from(base64Data, "base64"); + + // Max 25MB (API-Limit) + if (buffer.length > 25 * 1024 * 1024) { + throw createError({ + statusCode: 400, + message: "Audio zu groß (max 25 MB)", + }); + } + + // iOS capacitor-voice-recorder liefert rohes AAC (ADTS) — Deepgram akzeptiert das. + // Aber konvertiere trotzdem zu M4A für bessere Kompatibilität. + const isRawAac = mimeType?.includes("aac"); + let ext = "webm"; + let blobType = "audio/webm"; + + if (isRawAac) { + const id = randomUUID(); + const inPath = join(tmpdir(), `${id}.aac`); + const outPath = join(tmpdir(), `${id}.m4a`); + try { + writeFileSync(inPath, buffer); + execSync(`ffmpeg -i ${inPath} -c:a copy ${outPath} -y 2>/dev/null`); + buffer = readFileSync(outPath); + ext = "m4a"; + blobType = "audio/mp4"; + } catch (e) { + console.error("[transcribe] ffmpeg convert failed:", e); + ext = "m4a"; + blobType = "audio/mp4"; + } finally { + if (existsSync(inPath)) unlinkSync(inPath); + if (existsSync(outPath)) unlinkSync(outPath); + } + } else if (mimeType?.includes("mp4") || mimeType?.includes("m4a")) { + ext = "m4a"; + blobType = "audio/mp4"; + } + + console.log( + "[transcribe] mimeType:", + mimeType, + "→ ext:", + ext, + "converted:", + isRawAac, + "bytes:", + buffer.length, + ); + + // Deepgram language mapping (de/en/tr/ar direkt unterstützt) + const deepgramLang = + language && + ["de", "en", "tr", "ar", "fr", "es", "pt", "it"].includes(language) + ? language + : "de"; + + try { + const response = await fetch( + `https://api.deepgram.com/v1/listen?language=${deepgramLang}&model=nova-2`, + { + method: "POST", + headers: { + Authorization: `Token ${config.deepgramApiKey}`, + "Content-Type": blobType, + }, + body: buffer, + }, + ); + + const result = await response.json(); + + if (!response.ok) { + console.error("[transcribe] Deepgram error:", JSON.stringify(result)); + throw createError({ + statusCode: response.status, + message: JSON.stringify(result), + }); + } + + const transcript = + result.results?.channels?.[0]?.alternatives?.[0]?.transcript || ""; + return { text: transcript }; + } catch (err: any) { + if (err.statusCode) throw err; + console.error("[transcribe] Unexpected error:", err); + throw createError({ + statusCode: 500, + message: err?.message || "Transcribe fehlgeschlagen", + }); + } +}); diff --git a/backend/server/api/community/[postId]/comments.get.ts b/backend/server/api/community/[postId]/comments.get.ts new file mode 100644 index 0000000..42b1d35 --- /dev/null +++ b/backend/server/api/community/[postId]/comments.get.ts @@ -0,0 +1,35 @@ +import { getCommentsByPost } from "../../../db/community"; + +/** GET /api/community/[postId]/comments */ +export default defineEventHandler(async (event) => { + const postId = getRouterParam(event, "postId"); + if (!postId) throw createError({ statusCode: 400, message: "postId fehlt" }); + + let currentUserId: string | null = null; + try { + const u = await requireUser(event); + currentUserId = u.id; + } catch {} + + const { comments, userLikes } = await getCommentsByPost( + postId, + currentUserId, + ); + + return comments.map((c) => { + const a = c.author; + const username = a?.username ?? "Anonym"; + return { + id: c.id, + content: c.content, + createdAt: c.createdAt, + likesCount: c.likesCount ?? 0, + userLike: userLikes.has(c.id), + parentCommentId: c.parentReplyId ?? null, + authorId: c.userId ?? null, + authorNickname: a?.nickname ?? username, + authorAvatar: a?.avatar ?? null, + authorTier: "beginner", + }; + }); +}); diff --git a/backend/server/api/community/[postId]/index.get.ts b/backend/server/api/community/[postId]/index.get.ts new file mode 100644 index 0000000..8483613 --- /dev/null +++ b/backend/server/api/community/[postId]/index.get.ts @@ -0,0 +1,102 @@ +import { getPostById, getPostLike } from "../../../db/community"; +import { getFollowRelation } from "../../../db/social"; +import { usePrisma } from "../../../utils/prisma"; + +/** GET /api/community/[postId] */ +export default defineEventHandler(async (event) => { + const postId = getRouterParam(event, "postId"); + if (!postId) throw createError({ statusCode: 400, message: "postId fehlt" }); + + const config = useRuntimeConfig(); + const lyraBotUserId = config.lyraBotUserId || null; + const rebreakBotUserId = config.rebreakBotUserId || null; + + // Auth-User optional + let currentUserId: string | null = null; + try { + const u = await requireUser(event); + currentUserId = u.id; + } catch {} + + const db = usePrisma(); + const [data, likeRow] = await Promise.all([ + getPostById(postId), + currentUserId ? getPostLike(currentUserId, postId) : Promise.resolve(null), + ]); + + if (!data || data.isModerated) { + throw createError({ statusCode: 404, message: "Post nicht gefunden" }); + } + + const [followRow, scoreRow] = await Promise.all([ + currentUserId && data.userId && currentUserId !== data.userId + ? getFollowRelation(currentUserId, data.userId) + : Promise.resolve(null), + data.userId + ? db.userScore.findUnique({ + where: { userId: data.userId }, + select: { tier: true }, + }) + : Promise.resolve(null), + ]); + + const challengeId = (data as any).challengeId ?? null; + const challengeRow = challengeId + ? await db.gameChallenge.findUnique({ + where: { id: challengeId }, + select: { + gameType: true, + isLive: true, + opponentName: true, + status: true, + }, + }) + : null; + + const a = data.author; + const isGameShare = data.category === "game_share"; + return { + id: data.id, + category: data.category, + content: isGameShare + ? data.content.split("\n").slice(1).join("\n").trim() + : data.content, + imageUrl: (data as any).imageUrl ?? null, + challengeId, + challengeStatus: (challengeRow as any)?.status ?? null, + gameName: challengeId + ? (challengeRow as any)?.gameType ?? null + : isGameShare + ? data.content.split("\n")[0] ?? null + : null, + opponentName: (challengeRow as any)?.opponentName ?? null, + isLive: (challengeRow as any)?.isLive ?? false, + likesCount: data.likesCount ?? 0, + dislikesCount: data.dislikesCount ?? 0, + commentsCount: data.commentsCount ?? 0, + repostsCount: (data as any).repostsCount ?? 0, + isAnonymous: data.isAnonymous, + createdAt: data.createdAt, + userLike: (likeRow?.type as "like" | "dislike") ?? null, + repostOfId: null, + repostOf: null, + author: { + id: data.userId ?? null, + username: a?.username ?? "Anonym", + nickname: a?.nickname ?? a?.username ?? "Anonym", + avatar: a?.avatar ?? null, + plan: (a as any)?.plan ?? "free", + tier: scoreRow?.tier ?? "beginner", + isFollowing: !!followRow, + }, + isBot: + !!(lyraBotUserId && data.userId === lyraBotUserId) || + !!(rebreakBotUserId && data.userId === rebreakBotUserId), + botType: + lyraBotUserId && data.userId === lyraBotUserId + ? "lyra" + : rebreakBotUserId && data.userId === rebreakBotUserId + ? "rebreak" + : undefined, + }; +}); diff --git a/backend/server/api/community/comment-like.post.ts b/backend/server/api/community/comment-like.post.ts new file mode 100644 index 0000000..718051d --- /dev/null +++ b/backend/server/api/community/comment-like.post.ts @@ -0,0 +1,37 @@ +import { + getCommentLike, + createCommentLike, + deleteCommentLike, + getCommentLikeCount, + syncCommentLikeCount, +} from "../../db/community"; + +/** + * POST /api/community/comment-like + * Body: { commentId } + * Toggled Like auf einem Kommentar. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const { commentId } = (await readBody(event)) as { commentId: string }; + + if (!commentId) { + throw createError({ statusCode: 400, message: "commentId erforderlich" }); + } + + const existing = await getCommentLike(user.id, commentId); + let userLike: boolean; + + if (existing) { + await deleteCommentLike(user.id, commentId); + userLike = false; + } else { + await createCommentLike(user.id, commentId); + userLike = true; + } + + const count = await getCommentLikeCount(commentId); + await syncCommentLikeCount(commentId, count); + + return { likesCount: count, userLike }; +}); diff --git a/backend/server/api/community/comment.post.ts b/backend/server/api/community/comment.post.ts new file mode 100644 index 0000000..e5e3008 --- /dev/null +++ b/backend/server/api/community/comment.post.ts @@ -0,0 +1,67 @@ +import { awardPoints } from "../../utils/scoring"; +import { createComment, getPostById } from "../../db/community"; +import { getProfile } from "../../db/profile"; +import { createNotification } from "../../db/notifications"; + +/** POST /api/community/comment */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const { postId, content, parentCommentId } = (await readBody(event)) as { + postId: string; + content: string; + parentCommentId?: string; + }; + + if (!postId || !content?.trim()) { + throw createError({ + statusCode: 400, + message: "postId und content erforderlich", + }); + } + if (content.trim().length > 1000) { + throw createError({ + statusCode: 400, + message: "Kommentar zu lang (max. 1000 Zeichen)", + }); + } + + const [data, pr, post] = await Promise.all([ + createComment(user.id, postId, content.trim(), parentCommentId ?? null), + getProfile(user.id), + getPostById(postId), + ]); + + await awardPoints(user.id, "chat_message").catch(() => {}); + + // Notification an Post-Autor (nicht an sich selbst) + if (post?.userId && post.userId !== user.id) { + const meta = user.user_metadata ?? {}; + const actorName = + pr?.nickname ?? pr?.username ?? meta.full_name ?? "Jemand"; + const actorAvatar = pr?.avatar ?? undefined; + createNotification({ + recipientId: post.userId, + type: "new_comment", + actorName, + actorAvatar, + postId, + preview: content.trim().slice(0, 80), + }).catch(() => {}); + } + + const meta = user.user_metadata ?? {}; + const username = pr?.username ?? "Anonym"; + + return { + id: data.id, + content: data.content, + createdAt: data.createdAt, + likesCount: data.likesCount ?? 0, + userLike: false, + parentCommentId: data.parentReplyId ?? null, + authorId: pr?.id ?? null, + authorNickname: meta.nickname ?? meta.full_name ?? username, + authorAvatar: meta.avatar ?? meta.avatar_url ?? meta.picture ?? null, + authorTier: "beginner", + }; +}); diff --git a/backend/server/api/community/domain-stats.get.ts b/backend/server/api/community/domain-stats.get.ts new file mode 100644 index 0000000..64ac94c --- /dev/null +++ b/backend/server/api/community/domain-stats.get.ts @@ -0,0 +1,25 @@ +import { getActiveBlocklistCount } from "../../db/domains"; + +const FALLBACK_TOTAL = 208704; + +/** GET /api/community/domain-stats – öffentlich, kein Auth */ +export default defineEventHandler(async () => { + const db = usePrisma(); + + const [rawTotal, monthlyAdded] = await Promise.all([ + getActiveBlocklistCount(), + (async () => { + const startOfMonth = new Date(); + startOfMonth.setDate(1); + startOfMonth.setHours(0, 0, 0, 0); + return db.domainSubmission.count({ + where: { status: "approved", reviewedAt: { gte: startOfMonth } }, + }); + })(), + ]); + + return { + total: rawTotal > 1000 ? rawTotal : FALLBACK_TOTAL, + monthlyAdded, + }; +}); diff --git a/backend/server/api/community/like.post.ts b/backend/server/api/community/like.post.ts new file mode 100644 index 0000000..6a0bb0e --- /dev/null +++ b/backend/server/api/community/like.post.ts @@ -0,0 +1,73 @@ +import { awardPoints } from "../../utils/scoring"; +import { createNotification } from "../../db/notifications"; +import { getProfile } from "../../db/profile"; +import { + getPostLike, + setPostLike, + deletePostLike, + countPostLikes, + syncPostLikeCounts, + getPostById, +} from "../../db/community"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const { postId, type } = (await readBody(event)) as { + postId: string; + type: "like" | "dislike"; + }; + + if (!postId || !["like", "dislike"].includes(type)) { + throw createError({ + statusCode: 400, + message: "postId und type (like|dislike) erforderlich", + }); + } + + const [existing, currentPost] = await Promise.all([ + getPostLike(user.id, postId), + getPostById(postId), + ]); + + let newUserLike: "like" | "dislike" | null = null; + + if (existing) { + if (existing.type === type) { + // Toggle OFF + await deletePostLike(user.id, postId); + newUserLike = null; + } else { + // Typ wechseln + await setPostLike(user.id, postId, type); + newUserLike = type; + } + } else { + await setPostLike(user.id, postId, type); + newUserLike = type; + + if ( + type === "like" && + currentPost?.userId && + currentPost.userId !== user.id + ) { + const pr = await getProfile(user.id).catch(() => null); + const actorName = pr?.nickname ?? pr?.username ?? "Jemand"; + const actorAvatar = pr?.avatar ?? undefined; + await Promise.all([ + awardPoints(currentPost.userId, "upvote_received").catch(() => {}), + createNotification({ + recipientId: currentPost.userId, + type: "new_like", + actorName, + actorAvatar, + postId, + }).catch(() => {}), + ]); + } + } + + const { likes, dislikes } = await countPostLikes(postId); + await syncPostLikeCounts(postId, likes, dislikes); + + return { likesCount: likes, dislikesCount: dislikes, userLike: newUserLike }; +}); diff --git a/backend/server/api/community/post.post.ts b/backend/server/api/community/post.post.ts new file mode 100644 index 0000000..e9719a4 --- /dev/null +++ b/backend/server/api/community/post.post.ts @@ -0,0 +1,81 @@ +import { awardPoints } from "../../utils/scoring"; +import { createPost } from "../../db/community"; + +const MODERATION_PROMPT = `Du moderierst Beiträge in einer anonymen Selbsthilfe-Community für Menschen mit Glücksspielsucht. +Entferne Beiträge die: +- Casino-Werbung oder Links zu Glücksspielseiten enthalten +- Tipps zum Umgehen von Blockern geben +- Andere Nutzer manipulieren oder angreifen +- Offensichtlichen Spam darstellen +Erlaube: persönliche Erfahrungen, Hilferufe, Erfolgsgeschichten, Fragen. +Antworte NUR mit JSON: {"approved": boolean, "reason": string}`; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const body = await readBody(event); + const { category, content, imageUrl } = body as { + category: string; + content: string; + imageUrl?: string; + }; + + if (!content?.trim() || !category) { + throw createError({ + statusCode: 400, + message: "category und content erforderlich", + }); + } + + const config = useRuntimeConfig(); + + // Moderate via OpenRouter (optional – skip if no API key) + if (config.openrouterApiKey) { + try { + const modResponse = await $fetch<{ + choices: { message: { content: string } }[]; + }>("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${config.openrouterApiKey}`, + "Content-Type": "application/json", + "HTTP-Referer": "https://rebreak.org", + "X-Title": "ReBreak Moderation", + }, + body: { + model: "meta-llama/llama-3.2-3b-instruct:free", + max_tokens: 100, + messages: [ + { role: "system", content: MODERATION_PROMPT }, + { + role: "user", + content: `Kategorie: ${category}\nInhalt: ${content}`, + }, + ], + }, + }); + + const raw = modResponse.choices?.[0]?.message?.content ?? ""; + const jsonMatch = raw.match(/\{[\s\S]*\}/); + const modResult: { approved: boolean; reason: string } = jsonMatch + ? JSON.parse(jsonMatch[0]) + : { approved: true, reason: "" }; + + if (!modResult.approved) { + throw createError({ + statusCode: 422, + message: `Beitrag abgelehnt: ${modResult.reason}`, + }); + } + } catch (err: any) { + if (err.statusCode === 422) throw err; + } + } + + const data = await createPost(user.id, category, content.trim(), imageUrl); + + // Punkte vergeben + await awardPoints(user.id, "post_created", { post_id: data.id }); + + return data; +}); diff --git a/backend/server/api/community/posts.get.ts b/backend/server/api/community/posts.get.ts new file mode 100644 index 0000000..df1bf93 --- /dev/null +++ b/backend/server/api/community/posts.get.ts @@ -0,0 +1,129 @@ +import { getPosts } from "../../db/community"; +import { getFollowingSet } from "../../db/social"; + +/** GET /api/community/posts?category=all&page=1&limit=20 */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const lyraBotUserId = config.lyraBotUserId || null; + const rebreakBotUserId = config.rebreakBotUserId || null; + const query = getQuery(event); + const category = (query.category as string) || "all"; + const page = Math.max(1, parseInt((query.page as string) || "1")); + const limit = Math.min(50, parseInt((query.limit as string) || "20")); + + // Lyra / ReBreak → nach userId filtern + let filterUserId: string | null = null; + let dbCategory = category; + if (category === "lyra") { + filterUserId = lyraBotUserId; + dbCategory = "all"; + } else if (category === "rebreak") { + filterUserId = rebreakBotUserId; + dbCategory = "all"; + } + + // Auth-User optional (Gäste können auch lesen) + let currentUserId: string | null = null; + try { + const u = await requireUser(event); + currentUserId = u.id; + } catch {} + + const { + posts, + userLikes, + challengeStatuses, + domainSubmissions, + userDomainVotes, + userScores, + submissionVoters, + } = await getPosts(dbCategory, page, limit, currentUserId, filterUserId); + + // Batch: isFollowing für alle Autoren + const authorIds = [ + ...new Set(posts.map((p) => p.userId).filter((id): id is string => !!id)), + ]; + const followingSet = + currentUserId && authorIds.length > 0 + ? await getFollowingSet(currentUserId, authorIds) + : new Set(); + + return posts.map((p) => { + const a = p.author; + return { + id: p.id, + category: p.category, + content: + p.category === "game_share" + ? p.content.split("\n").slice(1).join("\n").trim() + : p.content, + imageUrl: (p as any).imageUrl ?? null, + challengeId: (p as any).challengeId ?? null, + challengeStatus: (p as any).challengeId + ? challengeStatuses[(p as any).challengeId]?.status ?? "OPEN" + : null, + gameName: (p as any).challengeId + ? challengeStatuses[(p as any).challengeId]?.gameType ?? null + : p.category === "game_share" + ? p.content.split("\n")[0] ?? null + : null, + opponentName: (p as any).challengeId + ? challengeStatuses[(p as any).challengeId]?.opponentName ?? null + : null, + isLive: (p as any).challengeId + ? challengeStatuses[(p as any).challengeId]?.isLive ?? false + : false, + likesCount: p.likesCount ?? 0, + dislikesCount: p.dislikesCount ?? 0, + commentsCount: p.commentsCount ?? 0, + repostsCount: (p as any).repostsCount ?? 0, + isAnonymous: p.isAnonymous, + createdAt: p.createdAt, + userLike: userLikes[p.id] ?? null, + submission: domainSubmissions[p.id] + ? { + ...domainSubmissions[p.id], + yesVoters: submissionVoters[domainSubmissions[p.id].id]?.yes ?? [], + noVoters: submissionVoters[domainSubmissions[p.id].id]?.no ?? [], + } + : null, + userVote: userDomainVotes[p.id] ?? null, + repostOfId: (p as any).repostOfId ?? null, + repostOf: (p as any).repostOf + ? { + id: (p as any).repostOf.id, + content: (p as any).repostOf.content, + imageUrl: (p as any).repostOf.imageUrl ?? null, + author: { + id: (p as any).repostOf.userId ?? null, + nickname: + (p as any).repostOf.author?.nickname ?? + (p as any).repostOf.author?.username ?? + "Nutzer", + avatar: (p as any).repostOf.author?.avatar ?? null, + plan: (p as any).repostOf.author?.plan ?? "free", + tier: userScores[(p as any).repostOf.userId ?? ""] ?? "beginner", + }, + } + : null, + author: { + id: p.userId ?? null, + username: a?.username ?? "Nutzer", + nickname: a?.nickname ?? a?.username ?? "Nutzer", + avatar: a?.avatar ?? null, + plan: (a as any)?.plan ?? "free", + tier: userScores[p.userId ?? ""] ?? "beginner", + isFollowing: p.userId ? followingSet.has(p.userId) : false, + }, + isBot: + !!(lyraBotUserId && p.userId === lyraBotUserId) || + !!(rebreakBotUserId && p.userId === rebreakBotUserId), + botType: + lyraBotUserId && p.userId === lyraBotUserId + ? "lyra" + : rebreakBotUserId && p.userId === rebreakBotUserId + ? "rebreak" + : undefined, + }; + }); +}); diff --git a/backend/server/api/community/repost.post.ts b/backend/server/api/community/repost.post.ts new file mode 100644 index 0000000..5a7488b --- /dev/null +++ b/backend/server/api/community/repost.post.ts @@ -0,0 +1,83 @@ +import { usePrisma } from "../../utils/prisma"; +import { awardPoints } from "../../utils/scoring"; + +/** + * POST /api/community/repost + * Body: { postId: string } + * Creates a repost referencing the original post. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const { postId } = (await readBody(event)) as { postId: string }; + + if (!postId) { + throw createError({ statusCode: 400, message: "postId erforderlich" }); + } + + const db = usePrisma(); + + // Original-Post laden + const original = await db.communityPost.findUnique({ + where: { id: postId }, + select: { id: true, userId: true, repostOfId: true }, + }); + + if (!original) { + throw createError({ statusCode: 404, message: "Post nicht gefunden" }); + } + + // Nicht den eigenen Post resharen + if (original.userId === user.id) { + throw createError({ + statusCode: 400, + message: "Eigene Posts können nicht geteilt werden", + }); + } + + // Wenn es selbst ein Repost ist, das Original resharen + const targetId = original.repostOfId ?? original.id; + + // Prüfen ob User diesen Post schon gerepostet hat + const existing = await db.communityPost.findFirst({ + where: { userId: user.id, repostOfId: targetId }, + }); + + if (existing) { + throw createError({ + statusCode: 409, + message: "Du hast diesen Beitrag bereits geteilt", + }); + } + + // Repost erstellen + const repost = await db.communityPost.create({ + data: { + userId: user.id, + category: "repost", + content: "", + repostOfId: targetId, + isAnonymous: false, + isModerated: false, + }, + include: { + author: { + select: { id: true, username: true, nickname: true, avatar: true }, + }, + }, + }); + + // repostsCount auf Original erhöhen + await db.communityPost.update({ + where: { id: targetId }, + data: { repostsCount: { increment: 1 } }, + }); + + // Punkte für den Original-Autor + if (original.userId !== user.id) { + await awardPoints(original.userId, "upvote_received", { + post_id: targetId, + }).catch(() => {}); + } + + return repost; +}); diff --git a/backend/server/api/community/upload-image.post.ts b/backend/server/api/community/upload-image.post.ts new file mode 100644 index 0000000..520f846 --- /dev/null +++ b/backend/server/api/community/upload-image.post.ts @@ -0,0 +1,55 @@ +import { serverSupabaseServiceRole } from "../../utils/useSupabase"; + +/** + * POST /api/community/upload-image + * Body: { dataUrl: string } (base64 JPEG/PNG) + * Returns: { url: string } + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = (await readBody(event)) as { image?: string; dataUrl?: string }; + const dataUrl = body.image ?? body.dataUrl; + + if (!dataUrl?.startsWith("data:image/")) { + throw createError({ statusCode: 400, message: "Ungültige Bilddaten" }); + } + + const match = dataUrl.match(/^data:(image\/\w+);base64,(.+)$/); + if (!match) + throw createError({ statusCode: 400, message: "Ungültiges Bildformat" }); + + const contentType = match[1]; + const ext = contentType === "image/png" ? "png" : "jpg"; + const base64 = match[2]; + + // Max 5MB check + const sizeBytes = Math.ceil((base64.length * 3) / 4); + if (sizeBytes > 5 * 1024 * 1024) { + throw createError({ statusCode: 400, message: "Bild zu groß (max 5MB)" }); + } + + const binaryStr = atob(base64); + const bytes = new Uint8Array(binaryStr.length); + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i); + } + const blob = new Blob([bytes], { type: contentType }); + + const supabase = serverSupabaseServiceRole(event); + const fileName = `posts/${user.id}/${Date.now()}.${ext}`; + + const { error: uploadError } = await supabase.storage + .from("rebreak-avatars") + .upload(fileName, blob, { contentType, upsert: false }); + + if (uploadError) { + console.error("[community/upload-image] Storage error:", uploadError); + throw createError({ statusCode: 500, message: uploadError.message }); + } + + const { data: urlData } = supabase.storage + .from("rebreak-avatars") + .getPublicUrl(fileName); + + return { url: urlData.publicUrl }; +}); diff --git a/backend/server/api/cooldown/cancel.post.ts b/backend/server/api/cooldown/cancel.post.ts new file mode 100644 index 0000000..f8ada90 --- /dev/null +++ b/backend/server/api/cooldown/cancel.post.ts @@ -0,0 +1,24 @@ +import { requireUser } from "../../utils/auth"; +import { getActiveCooldown, cancelCooldown } from "../../db/cooldown"; + +/** + * POST /api/cooldown/cancel + * User changes their mind: cancels the cooldown request. + * The DNS protection REMAINS active — this only removes the pending cooldown + * so they would need to start a new 24h wait if they decide to disable again. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const cooldown = await getActiveCooldown(user.id); + if (!cooldown) { + throw createError({ + statusCode: 404, + data: { error: "no_active_cooldown" }, + }); + } + + await cancelCooldown(cooldown.id); + + return { success: true, data: { cancelled: true } }; +}); diff --git a/backend/server/api/cooldown/request.post.ts b/backend/server/api/cooldown/request.post.ts new file mode 100644 index 0000000..c66335a --- /dev/null +++ b/backend/server/api/cooldown/request.post.ts @@ -0,0 +1,59 @@ +import { requireUser } from "../../utils/auth"; +import { getActiveCooldown, createCooldown } from "../../db/cooldown"; +import { signCooldownToken, generateJti } from "../../utils/cooldownToken"; + +/** POST /api/cooldown/request — Start a 24h cooldown before protection can be disabled. */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event).catch(() => ({})); + + // Reject if a cooldown is already running (not resolved, not cancelled). + const existing = await getActiveCooldown(user.id); + if (existing) { + const now = new Date(); + // If the existing one already expired but wasn't resolved yet, that's fine — + // it means canDisableProtection is already true. Return 409 so the client + // calls /status instead. + throw createError({ + statusCode: 409, + data: { + error: "cooldown_already_active", + existingEndsAt: existing.cooldownEndsAt.toISOString(), + }, + }); + } + + // Test-Mode (5min statt 24h) — nur außerhalb von Production aktivierbar. + // Detection via appUrl statt NODE_ENV, da staging.rebreak.org auch mit + // NODE_ENV=production läuft (siehe start-staging.sh). + const config = useRuntimeConfig(event); + const appUrl = (config.public?.appUrl as string) ?? ""; + const isProductionUrl = + appUrl.includes("rebreak.org") && !appUrl.includes("staging"); + const isTestMode = body?.testMode === true && !isProductionUrl; + const cooldownMs = isTestMode ? 40 * 1000 : 24 * 60 * 60 * 1000; + + const now = new Date(); + const cooldownEndsAt = new Date(now.getTime() + cooldownMs); + const jti = generateJti(); + + await createCooldown(user.id, jti, cooldownEndsAt, body?.reason); + + const remainingSeconds = Math.max( + 0, + Math.floor((cooldownEndsAt.getTime() - Date.now()) / 1000), + ); + + const token = await signCooldownToken(user.id, jti, cooldownEndsAt); + + return { + success: true, + data: { + cooldownStartedAt: now.toISOString(), + cooldownEndsAt: cooldownEndsAt.toISOString(), + remainingSeconds, + token, + testMode: isTestMode, + }, + }; +}); diff --git a/backend/server/api/cooldown/status.get.ts b/backend/server/api/cooldown/status.get.ts new file mode 100644 index 0000000..0e29cbd --- /dev/null +++ b/backend/server/api/cooldown/status.get.ts @@ -0,0 +1,66 @@ +import { requireUser } from "../../utils/auth"; +import { getActiveCooldown, resolveCooldown } from "../../db/cooldown"; +import { signCooldownToken } from "../../utils/cooldownToken"; + +/** GET /api/cooldown/status — Current cooldown state for the authenticated user. */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const cooldown = await getActiveCooldown(user.id); + const now = new Date(); + + if (!cooldown) { + // No cooldown ever started (or all were cancelled/resolved). + return { + success: true, + data: { + active: false, + remainingSeconds: 0, + cooldownEndsAt: null, + canDisableProtection: true, + token: null, // no cooldown row to bind to; app may proceed freely + }, + }; + } + + const expired = now >= cooldown.cooldownEndsAt; + + if (expired) { + // Auto-resolve so we don't re-check next time. + await resolveCooldown(cooldown.id); + + const token = await signCooldownToken( + user.id, + cooldown.tokenJti, + cooldown.cooldownEndsAt, + ); + + return { + success: true, + data: { + active: false, + remainingSeconds: 0, + cooldownEndsAt: cooldown.cooldownEndsAt.toISOString(), + canDisableProtection: true, + token, + }, + }; + } + + // Still counting down. + const remainingSeconds = Math.max( + 0, + Math.floor((cooldown.cooldownEndsAt.getTime() - now.getTime()) / 1000), + ); + + return { + success: true, + data: { + active: true, + remainingSeconds, + cooldownEndsAt: cooldown.cooldownEndsAt.toISOString(), + canDisableProtection: false, + token: null, + }, + }; +}); diff --git a/backend/server/api/cron/lyra-post.ts b/backend/server/api/cron/lyra-post.ts new file mode 100644 index 0000000..97e23a3 --- /dev/null +++ b/backend/server/api/cron/lyra-post.ts @@ -0,0 +1,133 @@ +import { createPost } from "../../db/community"; +import { usePrisma } from "../../utils/prisma"; + +/** + * POST /api/cron/lyra-post + * + * Lyra postet ab und zu in der Community – motivierend, human, nicht zu viel. + * Max. 3x pro Woche. + * + * Aufruf via Server-Cron (z.B. pm2-cron oder Linux crontab): + * 0 10 * * 1,3,5 curl -X POST https://rebreak.org/api/cron/lyra-post \ + * -H "x-cron-secret: $NUXT_CRON_SECRET" + * + * Infisical Secrets: + * NUXT_LYRA_BOT_USER_ID – UUID des Lyra-Profils in der DB + * NUXT_CRON_SECRET – zufälliger langer Token + * NUXT_OPENROUTER_API_KEY – bereits vorhanden + * + * Einmalig auf Server einrichten: + * Registriere einen Account mit Username "lyra" in der App, + * kopiere die user.id und trage sie als NUXT_LYRA_BOT_USER_ID ein. + */ + +const TOPICS = [ + "motivation", + "tipp", + "zitat", + "witzig", + "news", + "feature", +] as const; + +const SYSTEM_PROMPT = `Du bist Lyra, der KI-Coach der ReBreak-App – einer Gemeinschaft für Menschen auf dem Weg aus der Glücksspielsucht. + +Du postest gelegentlich kurze Beiträge in der Community. Deine Tonalität: +- Warm, ermutigend, menschlich – nie klinisch oder robotisch +- Kurz (max. 3–4 Sätze) +- Niemals übertrieben motivierend ("Du schaffst das!!!") – eher still stark +- Keine Casino-Werbung, keine Links, keine medizinischen Ratschläge +- Auf Deutsch + +Je nach Thema postest du: +- "motivation": Ein stiller Gedanke zum Durchhalten +- "tipp": Ein konkreter kleiner Tipp aus der Verhaltensforschung/CBT +- "news": Eine kurze Einordnung einer Entwicklung in der Glücksspielbanche (warnend, sachlich) +- "feature": Ein Hinweis auf ein neues ReBreak-Feature – wie ein Freund der sagt "Übrigens haben wir..." + +Antworte NUR mit dem Post-Text. Kein "Lyra:" Prefix, keine Anführungszeichen.`; + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + + // Auth via Cron-Secret + const secret = getHeader(event, "x-cron-secret"); + if (!config.cronSecret || secret !== config.cronSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + const lyraBotUserId = config.lyraBotUserId; + if (!lyraBotUserId) { + throw createError({ + statusCode: 500, + message: "LYRA_BOT_USER_ID nicht konfiguriert", + }); + } + + if (!config.openrouterApiKey) { + throw createError({ statusCode: 500, message: "OpenRouter API Key fehlt" }); + } + + // Max 3x pro Woche: letzten Lyra-Post prüfen + const db = usePrisma(); + const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); + const recentPost = await db.communityPost.findFirst({ + where: { + userId: lyraBotUserId, + createdAt: { gte: threeDaysAgo }, + }, + orderBy: { createdAt: "desc" }, + }); + + if (recentPost) { + return { + skipped: true, + reason: "Lyra hat in den letzten 3 Tagen bereits gepostet", + }; + } + + // Zufälliges Thema + const topic = TOPICS[Math.floor(Math.random() * TOPICS.length)]; + + const topicHint: Record<(typeof TOPICS)[number], string> = { + motivation: + "Schreibe einen kurzen, stillen Gedanken für Menschen die heute kämpfen. Nicht übertrieben – eher ruhig stark.", + tipp: "Teile einen kleinen, konkreten Trick aus der Verhaltensforschung oder CBT gegen Spieldrang. Praktisch und direkt.", + zitat: + "Teile ein tiefgründiges Zitat aus Psychologie, Stoizismus oder Verhaltensforschung – ohne das Wort 'Sucht' zu verwenden. Kurz kommentiert.", + witzig: + "Schreibe einen witzigen, selbstironischen Post über das Thema Ablenkung, Impulskontrolle oder Gewohnheiten – leicht und menschlich, nicht flach.", + news: "Beschreibe kurz eine typische Taktik der Glücksspielindustrie (z.B. Push-Notifications, Bonusangebote) – sachlich und als Warnung formuliert.", + feature: + "Weise freundlich auf ein ReBreak-Feature hin (Blocker, Streak, Mail-Agent, Lyra-Chat, SOS-Atemübung) – wähle eines zufällig.", + }; + + const response = await $fetch<{ + choices: { message: { content: string } }[]; + }>("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${config.openrouterApiKey}`, + "Content-Type": "application/json", + "HTTP-Referer": "https://rebreak.org", + "X-Title": "ReBreak - Lyra Community Post", + }, + body: { + model: "meta-llama/llama-3.2-3b-instruct:free", + max_tokens: 200, + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + { role: "user", content: topicHint[topic] }, + ], + }, + }); + + const content = response.choices?.[0]?.message?.content?.trim(); + if (!content) { + throw createError({ statusCode: 500, message: "Keine Antwort von LLM" }); + } + + const post = await createPost(lyraBotUserId, "community", content); + + return { success: true, postId: post.id, topic }; +}); diff --git a/backend/server/api/cron/notifications-cleanup.ts b/backend/server/api/cron/notifications-cleanup.ts new file mode 100644 index 0000000..4b05c3d --- /dev/null +++ b/backend/server/api/cron/notifications-cleanup.ts @@ -0,0 +1,21 @@ +import { deleteOldNotifications } from "../../db/notifications"; + +/** + * POST /api/cron/notifications-cleanup + * + * Löscht Notifications die älter als 3 Tage sind. + * Einrichten auf Hetzner via crontab: + * + * crontab -e + * 0 2 * * * curl -s -X POST https://rebreak.org/api/cron/notifications-cleanup \ + * -H "x-cron-secret: $NUXT_CRON_SECRET" >> /var/log/rebreak-cron.log 2>&1 + */ +export default defineEventHandler(async (event) => { + const secret = getHeader(event, "x-cron-secret"); + if (!secret || secret !== process.env.NUXT_CRON_SECRET) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + const result = await deleteOldNotifications(); + return { deleted: result.count }; +}); diff --git a/backend/server/api/custom-domains/[id].delete.ts b/backend/server/api/custom-domains/[id].delete.ts new file mode 100644 index 0000000..cdfe2b8 --- /dev/null +++ b/backend/server/api/custom-domains/[id].delete.ts @@ -0,0 +1,19 @@ +import { deleteUserCustomDomain } from "../../db/domains"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + + if (!id) throw createError({ statusCode: 400, message: "ID fehlt" }); + + try { + await deleteUserCustomDomain(id, user.id); + } catch (err: any) { + if (err.code === "P2025") { + throw createError({ statusCode: 404, message: "Domain nicht gefunden" }); + } + throw createError({ statusCode: 500, message: err.message }); + } + + return { ok: true }; +}); diff --git a/backend/server/api/custom-domains/[id]/submit.post.ts b/backend/server/api/custom-domains/[id]/submit.post.ts new file mode 100644 index 0000000..247d78e --- /dev/null +++ b/backend/server/api/custom-domains/[id]/submit.post.ts @@ -0,0 +1,68 @@ +import { submitDomainForReview } from "../../../db/domains"; +import { getProfile } from "../../../db/profile"; +import { getPlanLimits } from "../../../utils/plan-features"; +import { usePrisma } from "../../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "ID fehlt" }); + + // Only Pro/Legend can submit + const profile = await getProfile(user.id); + const plan = profile?.plan ?? "free"; + const limits = getPlanLimits(plan); + if (!limits.domainRefill) { + throw createError({ + statusCode: 403, + message: "Nur Pro-User können Domains einreichen", + }); + } + + const db = usePrisma(); + // Verify ownership + status + const existing = await db.userCustomDomain.findFirst({ + where: { id, userId: user.id }, + select: { id: true, domain: true, status: true }, + }); + if (!existing) + throw createError({ statusCode: 404, message: "Domain nicht gefunden" }); + if (existing.status !== "active" && existing.status !== "rejected") { + throw createError({ + statusCode: 409, + message: "Domain wurde bereits eingereicht oder genehmigt", + }); + } + + // Tier-Routing: + // - Pro: Community-Post mit Voting-Flow erstellen + // - Legend: KEIN Post — Domain landet direkt in der Admin-Queue zur manuellen Prüfung + let postId: string | null = null; + if (plan === "pro") { + const postContent = `🛡️ Domain-Vorschlag: **${existing.domain}**\n\nIch schlage vor, diese Domain zur globalen ReBreak-Sperrliste hinzuzufügen. Stimme ab: Sollte **${existing.domain}** global gesperrt werden?`; + const post = await db.communityPost.create({ + data: { + userId: user.id, + category: "domain_vote", + content: postContent, + }, + select: { id: true }, + }); + postId = post.id; + } + + const { submission } = await submitDomainForReview( + user.id, + id, + plan as "free" | "pro" | "legend", + postId ?? undefined, + ); + + return { + ok: true, + postId, + submissionId: submission.id, + domain: existing.domain, + route: plan === "legend" ? "admin_direct" : "community_vote", + }; +}); diff --git a/backend/server/api/custom-domains/index.get.ts b/backend/server/api/custom-domains/index.get.ts new file mode 100644 index 0000000..706638e --- /dev/null +++ b/backend/server/api/custom-domains/index.get.ts @@ -0,0 +1,6 @@ +import { getUserCustomDomains } from "../../db/domains"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + return getUserCustomDomains(user.id); +}); diff --git a/backend/server/api/custom-domains/index.post.ts b/backend/server/api/custom-domains/index.post.ts new file mode 100644 index 0000000..c40c665 --- /dev/null +++ b/backend/server/api/custom-domains/index.post.ts @@ -0,0 +1,52 @@ +import { awardPoints } from "../../utils/scoring"; +import { addUserCustomDomain, countActiveCustomDomains } from "../../db/domains"; +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + + const domain = (body?.domain as string) + ?.trim() + .toLowerCase() + .replace(/^https?:\/\//, ""); + if ( + !domain || + !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test( + domain, + ) + ) { + throw createError({ statusCode: 400, message: "Ungültige Domain" }); + } + + // Plan-Limit prüfen + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? "free"); + + if (limits.customDomains !== Infinity) { + const activeCount = await countActiveCustomDomains(user.id); + if (activeCount >= limits.customDomains) { + throw createError({ + statusCode: 403, + message: `Dein Plan erlaubt maximal ${limits.customDomains} eigene Domains`, + }); + } + } + + try { + const data = await addUserCustomDomain(user.id, domain, "manual"); + + await awardPoints(user.id, "custom_domain_submitted", { domain }).catch( + () => {}, + ); + + return data; + } catch (err: any) { + const msg = + err.message?.includes("duplicate") || err.code === "P2002" + ? "Domain bereits vorhanden" + : err.message ?? "Fehler"; + throw createError({ statusCode: 400, message: msg }); + } +}); diff --git a/backend/server/api/devices/[id].delete.ts b/backend/server/api/devices/[id].delete.ts new file mode 100644 index 0000000..3d81957 --- /dev/null +++ b/backend/server/api/devices/[id].delete.ts @@ -0,0 +1,17 @@ +import { deleteUserDevice } from "../../db/devices"; + +/** + * DELETE /api/devices/:id + * + * User entfernt ein eigenes Device — gibt damit einen Slot frei. + * Idempotent: wenn Device nicht existiert oder bereits gelöscht → 200. + */ +export default defineEventHandler(async (event) => { + // skipDeviceCheck: User soll bei Geräte-Limit-Sperre trotzdem freigeben können. + const user = await requireUser(event, { skipDeviceCheck: true }); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "id required" }); + + await deleteUserDevice(user.id, id); + return { ok: true }; +}); diff --git a/backend/server/api/devices/index.get.ts b/backend/server/api/devices/index.get.ts new file mode 100644 index 0000000..b907e5d --- /dev/null +++ b/backend/server/api/devices/index.get.ts @@ -0,0 +1,29 @@ +import { listUserDevices } from "../../db/devices"; +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; + +/** + * GET /api/devices + * + * Liste aller registrierten Devices des Users + plan-limit + welches Device der + * aktuelle Caller ist (matched via x-device-id header). + */ +export default defineEventHandler(async (event) => { + // skipDeviceCheck: User der gerade vom Geräte-Limit blockt wird, soll trotzdem + // seine Devices-Liste sehen können um eines freizugeben (Chicken-Egg-Bypass). + const user = await requireUser(event, { skipDeviceCheck: true }); + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? "free"); + + const currentDeviceId = getHeader(event, "x-device-id") ?? null; + const devices = await listUserDevices(user.id); + + return { + devices: devices.map((d) => ({ + ...d, + isCurrent: !!currentDeviceId && d.deviceId === currentDeviceId, + })), + max: limits.maxDevices, + plan: profile?.plan ?? "free", + }; +}); diff --git a/backend/server/api/devices/register.post.ts b/backend/server/api/devices/register.post.ts new file mode 100644 index 0000000..39a9961 --- /dev/null +++ b/backend/server/api/devices/register.post.ts @@ -0,0 +1,64 @@ +import { listUserDevices, registerDevice } from "../../db/devices"; +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; + +/** + * POST /api/devices/register + * + * Body: { deviceId: string, platform: string, model?: string, name?: string } + * + * Idempotent: gleiche deviceId für gleichen User → updated lastSeenAt + 200. + * Wenn neues Device + Limit erreicht → 403 mit { error, devices } damit der + * Frontend-Drawer dem User die Wahl gibt, welches Gerät er freigibt. + */ +export default defineEventHandler(async (event) => { + // Bootstrap: kein Device-Check sonst wäre erstes Register unmöglich (chicken-egg) + const user = await requireUser(event, { skipDeviceCheck: true }); + const body = await readBody(event); + const { deviceId, platform, model, name } = body as { + deviceId?: string; + platform?: string; + model?: string; + name?: string; + }; + + if (!deviceId || !platform) { + throw createError({ + statusCode: 400, + message: "deviceId und platform required", + }); + } + if (!["ios", "android", "web"].includes(platform)) { + throw createError({ statusCode: 400, message: "invalid platform" }); + } + + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? "free"); + + try { + const { device, created } = await registerDevice({ + userId: user.id, + deviceId, + platform, + model: model ?? null, + name: name ?? null, + maxDevices: limits.maxDevices, + }); + return { device, created, max: limits.maxDevices }; + } catch (err: any) { + if (err.code === "DEVICE_LIMIT_REACHED") { + const devices = await listUserDevices(user.id); + throw createError({ + statusCode: 403, + statusMessage: "device_limit_reached", + data: { + error: "device_limit_reached", + max: limits.maxDevices, + plan: profile?.plan ?? "free", + devices, + }, + }); + } + throw err; + } +}); diff --git a/backend/server/api/dns/profile.get.ts b/backend/server/api/dns/profile.get.ts new file mode 100644 index 0000000..aee7445 --- /dev/null +++ b/backend/server/api/dns/profile.get.ts @@ -0,0 +1,135 @@ +// DNS profile generator — DoH URL uses per-user subdomain for iOS compatibility +import { randomUUID } from "node:crypto"; +import { execSync } from "node:child_process"; +import { existsSync, writeFileSync, readFileSync, unlinkSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { getProfile } from "../../db/profile"; + +/** + * Generiert ein iOS/macOS .mobileconfig DNS-Profil. + * + * Das Profil zeigt auf unseren eigenen DNS-Server (dns.rebreak.de), + * der sowohl globale Blocklist als auch User-Custom-Domains blockiert. + * + * Free-User: Nur eigene Custom Domains werden blockiert + * Pro-User: Globale Blocklist + Custom Domains + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + // Get user plan + const profile = await getProfile(user.id); + const isPro = profile?.plan === "pro" || profile?.plan === "legend"; + + const profileUUID = randomUUID().toUpperCase(); + const payloadUUID = randomUUID().toUpperCase(); + + // DNS Server URL - points to our DNS proxy via per-user subdomain + // Subdomain carries userId so iOS DoH profile has user context without query params + // Staging: .dns-staging.rebreak.org | Prod: .rebreak.org + const config = useRuntimeConfig(); + const isStaging = config.public.appUrl.includes("staging"); + const dnsServerUrl = isStaging + ? `https://${user.id}.dns-staging.rebreak.org/dns-query` + : `https://${user.id}.rebreak.org/dns-query`; + + // Get counts for description + const { getActiveBlocklistCount, getUserCustomDomains } = await import("../../db/domains"); + const [globalCount, customDomains] = await Promise.all([ + isPro ? getActiveBlocklistCount() : Promise.resolve(0), + getUserCustomDomains(user.id), + ]); + const customCount = customDomains.length; + const totalCount = globalCount + customCount; + + const description = isPro + ? `Aktiviert automatisches Blocking von Glücksspielseiten auf deinem Gerät. ${totalCount.toLocaleString()} Domains werden blockiert.` + : `Aktiviert automatisches Blocking deiner personalisierten Sperrliste auf deinem Gerät. ${customCount} Domains werden blockiert.`; + + const xml = ` + + + + PayloadDisplayName + ReBreak Schutz + PayloadDescription + ${description} + PayloadIdentifier + org.rebreak.protection + PayloadUUID + ${profileUUID} + PayloadType + Configuration + PayloadVersion + 1 + PayloadContent + + + PayloadDisplayName + ReBreak Schutz + PayloadIdentifier + org.rebreak.protection.payload + PayloadUUID + ${payloadUUID} + PayloadType + com.apple.dnsSettings.managed + PayloadVersion + 1 + DNSSettings + + DNSProtocol + HTTPS + ServerURL + ${dnsServerUrl} + + + + +`; + + setResponseHeaders(event, { + "Content-Type": "application/x-apple-aspen-config", + "Content-Disposition": + 'attachment; filename="rebreak-protection.mobileconfig"', + }); + + // Cert base path based on environment + const certBase = isStaging + ? "/etc/letsencrypt/live/staging.rebreak.org" + : "/etc/letsencrypt/live/rebreak.org"; + const certFile = `${certBase}/cert.pem`; + const keyFile = `${certBase}/privkey.pem`; + const chainFile = `${certBase}/chain.pem`; + + if (existsSync(certFile) && existsSync(keyFile)) { + const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + const tmpIn = join(tmpdir(), `rebreak-profile-${id}.xml`); + const tmpOut = join(tmpdir(), `rebreak-profile-${id}.der`); + try { + writeFileSync(tmpIn, xml, "utf8"); + execSync( + `openssl smime -sign -signer ${certFile} -inkey ${keyFile}${existsSync(chainFile) ? ` -certfile ${chainFile}` : ""} -nodetach -in ${tmpIn} -out ${tmpOut} -outform DER`, + { stdio: "pipe", timeout: 5000 }, + ); + const signed = readFileSync(tmpOut); + return signed; + } catch { + // Fallback: unsigniert (funktioniert noch, zeigt nur Warnung in iOS) + } finally { + try { + unlinkSync(tmpIn); + } catch { + /* ignore */ + } + try { + unlinkSync(tmpOut); + } catch { + /* ignore */ + } + } + } + + // Unsigned fallback (Entwicklung / Zertifikat nicht verfügbar) + return xml; +}); diff --git a/backend/server/api/domain-submissions/[id]/vote.post.ts b/backend/server/api/domain-submissions/[id]/vote.post.ts new file mode 100644 index 0000000..47407b0 --- /dev/null +++ b/backend/server/api/domain-submissions/[id]/vote.post.ts @@ -0,0 +1,48 @@ +import { castDomainVote } from "../../../db/domains"; +import { createNotification } from "../../../db/notifications"; +import { getProfile } from "../../../db/profile"; +import { usePrisma } from "../../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + if (!id) + throw createError({ statusCode: 400, message: "Submission ID fehlt" }); + + const body = await readBody(event); + const vote = body?.vote as string; + if (vote !== "yes" && vote !== "no") { + throw createError({ + statusCode: 400, + message: "vote muss 'yes' oder 'no' sein", + }); + } + + const db = usePrisma(); + const [result, submission] = await Promise.all([ + castDomainVote(user.id, id, vote), + db.domainSubmission.findUnique({ + where: { id }, + select: { userId: true, domain: true }, + }), + ]); + + if ( + submission?.userId && + submission.userId !== user.id && + (result as any).yesVotes !== undefined + ) { + const pr = await getProfile(user.id).catch(() => null); + const actorName = pr?.nickname ?? pr?.username ?? "Jemand"; + const actorAvatar = pr?.avatar ?? undefined; + createNotification({ + recipientId: submission.userId, + type: "domain_vote", + actorName, + actorAvatar, + preview: submission.domain, + }).catch(() => {}); + } + + return result; +}); diff --git a/backend/server/api/feedback/[id].patch.ts b/backend/server/api/feedback/[id].patch.ts new file mode 100644 index 0000000..19f0cc8 --- /dev/null +++ b/backend/server/api/feedback/[id].patch.ts @@ -0,0 +1,30 @@ +import { usePrisma } from "../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const secret = getHeader(event, "x-admin-secret"); + if (!config.adminSecret || secret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Nicht autorisiert" }); + } + + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "id fehlt" }); + + const body = await readBody(event) as { + status?: string; + adminNote?: string; + category?: string; + }; + + const db = usePrisma(); + const updated = await db.feedbackItem.update({ + where: { id }, + data: { + ...(body.status !== undefined && { status: body.status as any }), + ...(body.adminNote !== undefined && { adminNote: body.adminNote }), + ...(body.category !== undefined && { category: body.category }), + }, + }); + + return updated; +}); diff --git a/backend/server/api/feedback/index.get.ts b/backend/server/api/feedback/index.get.ts new file mode 100644 index 0000000..b39aab0 --- /dev/null +++ b/backend/server/api/feedback/index.get.ts @@ -0,0 +1,21 @@ +import { usePrisma } from "../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const secret = getHeader(event, "x-admin-secret"); + if (!config.adminSecret || secret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Nicht autorisiert" }); + } + + const query = getQuery(event); + const status = query.status as string | undefined; + + const db = usePrisma(); + const items = await db.feedbackItem.findMany({ + where: status ? { status: status as any } : undefined, + orderBy: { createdAt: "desc" }, + take: 200, + }); + + return items; +}); diff --git a/backend/server/api/games/challenge-memory.post.ts b/backend/server/api/games/challenge-memory.post.ts new file mode 100644 index 0000000..324ce17 --- /dev/null +++ b/backend/server/api/games/challenge-memory.post.ts @@ -0,0 +1,62 @@ +import { usePrisma } from "../../utils/prisma"; +import { getProfile } from "../../db/profile"; + +const MEMORY_EMOJIS = ["🛡️", "💪", "🌟", "🧠", "🌊", "🎯", "🌱", "🔑"]; + +function shuffle(arr: T[]): T[] { + const a = [...arr]; + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const tmp = a[i]!; + a[i] = a[j]!; + a[j] = tmp; + } + return a; +} + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const db = usePrisma(); + + const profile = await getProfile(user.id); + const name = profile?.nickname || profile?.username || "Anonym"; + + const pairs = shuffle([...MEMORY_EMOJIS, ...MEMORY_EMOJIS]); + const memoryState = { + cards: pairs.map((emoji: string, id: number) => ({ + id, + emoji, + matchedBy: null, + })), + flipped: [] as number[], + scores: { X: 0, O: 0 }, + mismatchRevealed: false, + }; + + const challenge = await db.gameChallenge.create({ + data: { + challengerId: user.id, + challengerName: name, + gameType: "memory", + memoryState, + }, + }); + + const post = await db.communityPost.create({ + data: { + userId: user.id, + category: "challenge", + content: `${name} sucht einen Gegner für Memory! Wer nimmt die Challenge an?`, + isAnonymous: false, + isModerated: false, + challengeId: challenge.id, + }, + }); + + await db.gameChallenge.update({ + where: { id: challenge.id }, + data: { postId: post.id }, + }); + + return { challengeId: challenge.id }; +}); diff --git a/backend/server/api/games/challenge.post.ts b/backend/server/api/games/challenge.post.ts new file mode 100644 index 0000000..945048a --- /dev/null +++ b/backend/server/api/games/challenge.post.ts @@ -0,0 +1,38 @@ +import { usePrisma } from "../../utils/prisma"; +import { getProfile } from "../../db/profile"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const db = usePrisma(); + + const profile = await getProfile(user.id); + const name = profile?.nickname || profile?.username || "Anonym"; + + // Create game challenge + const challenge = await db.gameChallenge.create({ + data: { + challengerId: user.id, + challengerName: name, + }, + }); + + // Auto-post in community so others can accept + const post = await db.communityPost.create({ + data: { + userId: user.id, + category: "challenge", + content: `${name} sucht einen Gegner für Tic-Tac-Toe! Wer nimmt die Challenge an?`, + isAnonymous: false, + isModerated: false, + challengeId: challenge.id, + }, + }); + + // Link post back to challenge + await db.gameChallenge.update({ + where: { id: challenge.id }, + data: { postId: post.id }, + }); + + return { challengeId: challenge.id, postId: post.id }; +}); diff --git a/backend/server/api/games/challenge/[id].get.ts b/backend/server/api/games/challenge/[id].get.ts new file mode 100644 index 0000000..f04e577 --- /dev/null +++ b/backend/server/api/games/challenge/[id].get.ts @@ -0,0 +1,16 @@ +import { usePrisma } from "../../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "id fehlt" }); + + const db = usePrisma(); + const challenge = await db.gameChallenge.findUnique({ where: { id } }); + + if (!challenge) { + throw createError({ statusCode: 404, message: "Challenge nicht gefunden" }); + } + + return challenge; +}); diff --git a/backend/server/api/games/challenge/[id]/accept.post.ts b/backend/server/api/games/challenge/[id]/accept.post.ts new file mode 100644 index 0000000..3d8c451 --- /dev/null +++ b/backend/server/api/games/challenge/[id]/accept.post.ts @@ -0,0 +1,35 @@ +import { usePrisma } from "../../../../utils/prisma"; +import { getProfile } from "../../../../db/profile"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "id fehlt" }); + + const db = usePrisma(); + const challenge = await db.gameChallenge.findUnique({ where: { id } }); + + if (!challenge) { + throw createError({ statusCode: 404, message: "Challenge nicht gefunden" }); + } + if (challenge.challengerId === user.id) { + throw createError({ statusCode: 400, message: "Du kannst deine eigene Challenge nicht annehmen" }); + } + if (challenge.status !== "OPEN") { + throw createError({ statusCode: 409, message: "Challenge ist nicht mehr offen" }); + } + + const profile = await getProfile(user.id); + const name = profile?.nickname || profile?.username || "Anonym"; + + const updated = await db.gameChallenge.update({ + where: { id }, + data: { + opponentId: user.id, + opponentName: name, + status: "ACTIVE", + }, + }); + + return updated; +}); diff --git a/backend/server/api/games/challenge/[id]/live-toggle.post.ts b/backend/server/api/games/challenge/[id]/live-toggle.post.ts new file mode 100644 index 0000000..6729a71 --- /dev/null +++ b/backend/server/api/games/challenge/[id]/live-toggle.post.ts @@ -0,0 +1,35 @@ +import { usePrisma } from "../../../../utils/prisma"; + +/** POST /api/games/challenge/[id]/live-toggle — toggle isLive flag */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "id fehlt" }); + + const db = usePrisma(); + const challenge = await db.gameChallenge.findUnique({ + where: { id }, + select: { + challengerId: true, + opponentId: true, + isLive: true, + status: true, + }, + }); + + if (!challenge) + throw createError({ statusCode: 404, message: "Challenge nicht gefunden" }); + + // Only participants can toggle + if (challenge.challengerId !== user.id && challenge.opponentId !== user.id) { + throw createError({ statusCode: 403, message: "Nicht berechtigt" }); + } + + const updated = await db.gameChallenge.update({ + where: { id }, + data: { isLive: !challenge.isLive }, + select: { isLive: true }, + }); + + return { isLive: updated.isLive }; +}); diff --git a/backend/server/api/games/challenge/[id]/memory-move.post.ts b/backend/server/api/games/challenge/[id]/memory-move.post.ts new file mode 100644 index 0000000..a5b87ce --- /dev/null +++ b/backend/server/api/games/challenge/[id]/memory-move.post.ts @@ -0,0 +1,152 @@ +import { usePrisma } from "../../../../utils/prisma"; + +interface MemoryCard { + id: number; + emoji: string; + matchedBy: "X" | "O" | null; +} + +interface MemoryState { + cards: MemoryCard[]; + flipped: number[]; + scores: { X: number; O: number }; + mismatchRevealed: boolean; +} + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "id fehlt" }); + + const body = await readBody(event) as { cardIndex: number }; + const { cardIndex } = body; + + if (typeof cardIndex !== "number" || cardIndex < 0 || cardIndex > 15) { + throw createError({ statusCode: 400, message: "Ungültiger Zug" }); + } + + const db = usePrisma(); + const challenge = await db.gameChallenge.findUnique({ where: { id } }); + + if (!challenge) throw createError({ statusCode: 404, message: "Challenge nicht gefunden" }); + if (challenge.status !== "ACTIVE") throw createError({ statusCode: 409, message: "Spiel nicht aktiv" }); + if (challenge.gameType !== "memory") throw createError({ statusCode: 400, message: "Kein Memory-Spiel" }); + + const isChallenger = user.id === challenge.challengerId; + const isOpponent = user.id === challenge.opponentId; + if (!isChallenger && !isOpponent) throw createError({ statusCode: 403, message: "Nicht autorisiert" }); + + const myMark: "X" | "O" = isChallenger ? "X" : "O"; + if (challenge.currentTurn !== myMark) throw createError({ statusCode: 409, message: "Nicht dein Zug" }); + + const state = JSON.parse(JSON.stringify(challenge.memoryState)) as MemoryState; + const card = state.cards[cardIndex]; + if (!card) throw createError({ statusCode: 400, message: "Ungültige Karte" }); + if (card.matchedBy !== null) throw createError({ statusCode: 409, message: "Karte bereits gefunden" }); + + let newStatus = challenge.status as string; + let newWinner: string | null = challenge.winner; + let nextTurn: string = myMark; + + // Mismatch pending: player must click one of their 2 revealed cards to hide them + if (state.mismatchRevealed) { + if (!state.flipped.includes(cardIndex)) { + throw createError({ statusCode: 409, message: "Decke zuerst deine aufgedeckten Karten zu" }); + } + // Hide both cards and switch turn + state.flipped = []; + state.mismatchRevealed = false; + nextTurn = myMark === "X" ? "O" : "X"; + } else { + if (state.flipped.includes(cardIndex)) { + throw createError({ statusCode: 409, message: "Karte bereits aufgedeckt" }); + } + + if (state.flipped.length === 0) { + state.flipped = [cardIndex]; + } else { + // Second flip + const firstId = state.flipped[0]!; + const firstCard = state.cards[firstId]!; + state.flipped = [firstId, cardIndex]; + + if (firstCard.emoji === card.emoji) { + // Match – mark both, clear flipped, stay on same turn + state.cards[firstId]!.matchedBy = myMark; + state.cards[cardIndex]!.matchedBy = myMark; + state.scores[myMark]++; + state.flipped = []; + + const totalPairs = state.cards.length / 2; + const matchedPairs = state.cards.filter(c => c.matchedBy !== null).length / 2; + + if (matchedPairs === totalPairs) { + newStatus = "FINISHED"; + const { X, O } = state.scores; + newWinner = X > O ? "X" : O > X ? "O" : "draw"; + } + // nextTurn stays as myMark (scorer goes again) + } else { + // Mismatch – show cards, stay on same turn until player hides them + state.mismatchRevealed = true; + // nextTurn stays as myMark + } + } + } + + const updated = await db.gameChallenge.update({ + where: { id }, + data: { + memoryState: state as any, + currentTurn: nextTurn, + ...(newStatus !== challenge.status && { status: newStatus as any }), + ...(newWinner !== challenge.winner && { winner: newWinner }), + }, + }); + + if (newStatus === "FINISHED" && challenge.opponentId && challenge.opponentName) { + const challengerId = challenge.challengerId; + const opponentId = challenge.opponentId; + const challengerName = challenge.challengerName; + const opponentName = challenge.opponentName; + + if (newWinner === "draw") { + await Promise.all([ + db.gameScore.upsert({ + where: { userId: challengerId }, + update: { draws: { increment: 1 }, points: { increment: 1 }, playerName: challengerName }, + create: { userId: challengerId, playerName: challengerName, draws: 1, points: 1 }, + }), + db.gameScore.upsert({ + where: { userId: opponentId }, + update: { draws: { increment: 1 }, points: { increment: 1 }, playerName: opponentName }, + create: { userId: opponentId, playerName: opponentName, draws: 1, points: 1 }, + }), + ]); + } else { + const winnerId = newWinner === "X" ? challengerId : opponentId; + const loserId = newWinner === "X" ? opponentId : challengerId; + const winnerName = newWinner === "X" ? challengerName : opponentName; + const loserName = newWinner === "X" ? opponentName : challengerName; + + await Promise.all([ + db.gameScore.upsert({ + where: { userId: winnerId }, + update: { wins: { increment: 1 }, points: { increment: 3 }, playerName: winnerName }, + create: { userId: winnerId, playerName: winnerName, wins: 1, points: 3 }, + }), + db.gameScore.upsert({ + where: { userId: loserId }, + update: { losses: { increment: 1 }, playerName: loserName }, + create: { userId: loserId, playerName: loserName, losses: 1 }, + }), + ]); + } + + if (challenge.postId) { + await db.communityPost.delete({ where: { id: challenge.postId } }).catch(() => {}); + } + } + + return updated; +}); diff --git a/backend/server/api/games/challenge/[id]/move.post.ts b/backend/server/api/games/challenge/[id]/move.post.ts new file mode 100644 index 0000000..e9c44b0 --- /dev/null +++ b/backend/server/api/games/challenge/[id]/move.post.ts @@ -0,0 +1,109 @@ +import { usePrisma } from "../../../../utils/prisma"; + +const WIN_LINES = [ + [0, 1, 2], [3, 4, 5], [6, 7, 8], + [0, 3, 6], [1, 4, 7], [2, 5, 8], + [0, 4, 8], [2, 4, 6], +]; + +function checkWinner(board: string): "X" | "O" | null { + for (const [a, b, c] of WIN_LINES) { + if (board[a!] !== "-" && board[a!] === board[b!] && board[a!] === board[c!]) { + return board[a!] as "X" | "O"; + } + } + return null; +} + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "id fehlt" }); + + const body = await readBody(event) as { cellIndex: number }; + const { cellIndex } = body; + + if (typeof cellIndex !== "number" || cellIndex < 0 || cellIndex > 8) { + throw createError({ statusCode: 400, message: "Ungültiger Zug" }); + } + + const db = usePrisma(); + const challenge = await db.gameChallenge.findUnique({ where: { id } }); + + if (!challenge) throw createError({ statusCode: 404, message: "Challenge nicht gefunden" }); + if (challenge.status !== "ACTIVE") throw createError({ statusCode: 409, message: "Spiel nicht aktiv" }); + + const isChallenger = user.id === challenge.challengerId; + const isOpponent = user.id === challenge.opponentId; + + if (!isChallenger && !isOpponent) throw createError({ statusCode: 403, message: "Nicht autorisiert" }); + + const myMark = isChallenger ? "X" : "O"; + if (challenge.currentTurn !== myMark) throw createError({ statusCode: 409, message: "Nicht dein Zug" }); + if (challenge.board[cellIndex] !== "-") throw createError({ statusCode: 409, message: "Feld bereits belegt" }); + + const cells = challenge.board.split(""); + cells[cellIndex] = myMark; + const newBoard = cells.join(""); + + const winner = checkWinner(newBoard); + const isDraw = !winner && !newBoard.includes("-"); + + const updated = await db.gameChallenge.update({ + where: { id }, + data: { + board: newBoard, + currentTurn: myMark === "X" ? "O" : "X", + ...(winner && { winner, status: "FINISHED" }), + ...(isDraw && { winner: "draw", status: "FINISHED" }), + }, + }); + + // Ranking update wenn Spiel beendet + if ((winner || isDraw) && challenge.opponentId && challenge.opponentName) { + const challengerId = challenge.challengerId; + const opponentId = challenge.opponentId; + const challengerName = challenge.challengerName; + const opponentName = challenge.opponentName; + + if (isDraw) { + await Promise.all([ + db.gameScore.upsert({ + where: { userId: challengerId }, + update: { draws: { increment: 1 }, points: { increment: 1 }, playerName: challengerName }, + create: { userId: challengerId, playerName: challengerName, draws: 1, points: 1 }, + }), + db.gameScore.upsert({ + where: { userId: opponentId }, + update: { draws: { increment: 1 }, points: { increment: 1 }, playerName: opponentName }, + create: { userId: opponentId, playerName: opponentName, draws: 1, points: 1 }, + }), + ]); + } else { + const winnerId = winner === "X" ? challengerId : opponentId; + const loserId = winner === "X" ? opponentId : challengerId; + const winnerName = winner === "X" ? challengerName : opponentName; + const loserName = winner === "X" ? opponentName : challengerName; + + await Promise.all([ + db.gameScore.upsert({ + where: { userId: winnerId }, + update: { wins: { increment: 1 }, points: { increment: 3 }, playerName: winnerName }, + create: { userId: winnerId, playerName: winnerName, wins: 1, points: 3 }, + }), + db.gameScore.upsert({ + where: { userId: loserId }, + update: { losses: { increment: 1 }, playerName: loserName }, + create: { userId: loserId, playerName: loserName, losses: 1 }, + }), + ]); + } + } + + // Delete community post when game ends + if ((winner || isDraw) && challenge.postId) { + await db.communityPost.delete({ where: { id: challenge.postId } }).catch(() => {}); + } + + return updated; +}); diff --git a/backend/server/api/games/challenge/[id]/rematch.post.ts b/backend/server/api/games/challenge/[id]/rematch.post.ts new file mode 100644 index 0000000..a76a0ac --- /dev/null +++ b/backend/server/api/games/challenge/[id]/rematch.post.ts @@ -0,0 +1,64 @@ +import { usePrisma } from "../../../../utils/prisma"; +import { getProfile } from "../../../../db/profile"; + +const MEMORY_EMOJIS = ['🛡️', '💪', '🌟', '🧠', '🌊', '🎯', '🌱', '🔑']; +function shuffle(arr: T[]): T[] { + const a = [...arr]; + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const tmp = a[i]!; a[i] = a[j]!; a[j] = tmp; + } + return a; +} + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "id fehlt" }); + + const db = usePrisma(); + const old = await db.gameChallenge.findUnique({ where: { id } }); + + if (!old) throw createError({ statusCode: 404, message: "Spiel nicht gefunden" }); + if (old.status !== "FINISHED" && old.status !== "CANCELLED") { + throw createError({ statusCode: 409, message: "Spiel noch nicht beendet" }); + } + + const isChallenger = user.id === old.challengerId; + const isOpponent = user.id === old.opponentId; + if (!isChallenger && !isOpponent) { + throw createError({ statusCode: 403, message: "Nicht autorisiert" }); + } + + const opponentId = isChallenger ? old.opponentId : old.challengerId; + const opponentName = isChallenger ? old.opponentName : old.challengerName; + if (!opponentId || !opponentName) { + throw createError({ statusCode: 409, message: "Kein Gegner vorhanden" }); + } + + const profile = await getProfile(user.id); + const myName = profile?.nickname || profile?.username || "Anonym"; + + const isMemory = old.gameType === "memory"; + const memoryState = isMemory ? { + cards: shuffle([...MEMORY_EMOJIS, ...MEMORY_EMOJIS]).map((emoji: string, id: number) => ({ id, emoji, matchedBy: null })), + flipped: [], + scores: { X: 0, O: 0 }, + mismatchRevealed: false, + } : undefined; + + // Create rematch challenge with opponent pre-set and directly ACTIVE + const rematch = await db.gameChallenge.create({ + data: { + challengerId: user.id, + challengerName: myName, + opponentId, + opponentName, + status: "ACTIVE", + gameType: old.gameType, + ...(memoryState && { memoryState }), + }, + }); + + return { challengeId: rematch.id }; +}); diff --git a/backend/server/api/games/highscore.get.ts b/backend/server/api/games/highscore.get.ts new file mode 100644 index 0000000..29c11ea --- /dev/null +++ b/backend/server/api/games/highscore.get.ts @@ -0,0 +1,39 @@ +/** + * GET /api/games/highscore?gameName= + * + * Liefert den Personal-Best-Score des aktuellen Users für ein bestimmtes Spiel. + * Wird bei SOS-Spielstart gefetcht, damit Lyra dem User seinen PB nennen kann + * ("Dein bester Score ist X — ich glaub an dich"). + * + * Response: + * { score: number, hasRecord: boolean, updatedAt: string | null } + * + * Wenn kein Eintrag existiert: score=0, hasRecord=false. Lyra-Hint behandelt + * dann den Erst-Spielen-Fall ("Probier's, lass uns deinen ersten Score setzen!"). + */ +import { usePrisma } from "../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const query = getQuery(event); + const gameName = String(query.gameName ?? "").trim(); + + if (!gameName) { + throw createError({ + statusCode: 400, + message: "gameName query param required", + }); + } + + const db = usePrisma(); + const hs = await db.gameHighScore.findUnique({ + where: { userId_gameName: { userId: user.id, gameName } }, + select: { score: true, updatedAt: true }, + }); + + return { + score: hs?.score ?? 0, + hasRecord: !!hs, + updatedAt: hs?.updatedAt?.toISOString() ?? null, + }; +}); diff --git a/backend/server/api/games/history.get.ts b/backend/server/api/games/history.get.ts new file mode 100644 index 0000000..b58f2d2 --- /dev/null +++ b/backend/server/api/games/history.get.ts @@ -0,0 +1,44 @@ +import { usePrisma } from "../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const query = getQuery(event); + const opponentId = query.opponentId as string | undefined; + const limit = Math.min(Number(query.limit) || 10, 50); + + const db = usePrisma(); + + const where = opponentId + ? { + OR: [ + { challengerId: user.id, opponentId }, + { challengerId: opponentId, opponentId: user.id }, + ], + status: "FINISHED" as const, + } + : { + OR: [ + { challengerId: user.id }, + { opponentId: user.id }, + ], + status: "FINISHED" as const, + }; + + const games = await db.gameChallenge.findMany({ + where, + orderBy: { updatedAt: "desc" }, + take: limit, + select: { + id: true, + challengerId: true, + challengerName: true, + opponentId: true, + opponentName: true, + winner: true, + createdAt: true, + updatedAt: true, + }, + }); + + return games; +}); diff --git a/backend/server/api/games/leaderboard.get.ts b/backend/server/api/games/leaderboard.get.ts new file mode 100644 index 0000000..0fdab7b --- /dev/null +++ b/backend/server/api/games/leaderboard.get.ts @@ -0,0 +1,60 @@ +import { usePrisma } from "../../utils/prisma"; + +export default defineEventHandler(async (event) => { + setResponseHeaders(event, { "Content-Type": "application/json" }); + + const user = await requireUser(event); + const query = getQuery(event); + const gameName = String(query.game || "").toLowerCase(); + const limit = Math.min(Number(query.limit) || 10, 50); + + if (!gameName) { + throw createError({ + statusCode: 400, + message: "game parameter erforderlich", + }); + } + + try { + const db = usePrisma(); + + const scores = await db.gameHighScore.findMany({ + where: { gameName }, + orderBy: { score: "desc" }, + take: limit, + select: { userId: true, nickname: true, score: true, updatedAt: true }, + }); + + const myEntry = scores.find((s) => s.userId === user.id); + let myRank = myEntry ? scores.indexOf(myEntry) + 1 : null; + + if (!myEntry) { + const myScore = await db.gameHighScore.findUnique({ + where: { userId_gameName: { userId: user.id, gameName } }, + }); + if (myScore) { + const above = await db.gameHighScore.count({ + where: { gameName, score: { gt: myScore.score } }, + }); + myRank = above + 1; + } + } + + return { + leaderboard: scores.map((s, i) => ({ + rank: i + 1, + nickname: s.nickname, + score: s.score, + isMe: s.userId === user.id, + updatedAt: s.updatedAt, + })), + myRank, + myScore: myEntry?.score ?? null, + }; + } catch (err: any) { + throw createError({ + statusCode: 500, + message: err?.message ?? "Interner Fehler", + }); + } +}); diff --git a/backend/server/api/games/ranking.get.ts b/backend/server/api/games/ranking.get.ts new file mode 100644 index 0000000..1c76dbe --- /dev/null +++ b/backend/server/api/games/ranking.get.ts @@ -0,0 +1,15 @@ +import { usePrisma } from "../../utils/prisma"; + +export default defineEventHandler(async (event) => { + await requireUser(event); + const query = getQuery(event); + const limit = Math.min(Number(query.limit) || 20, 50); + + const db = usePrisma(); + const scores = await db.gameScore.findMany({ + orderBy: [{ points: "desc" }, { wins: "desc" }], + take: limit, + }); + + return scores; +}); diff --git a/backend/server/api/games/rating.post.ts b/backend/server/api/games/rating.post.ts new file mode 100644 index 0000000..571d844 --- /dev/null +++ b/backend/server/api/games/rating.post.ts @@ -0,0 +1,25 @@ +import { usePrisma } from "../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + + const { gameName, stars, feedback, score } = body; + + if (!gameName || typeof stars !== "number" || stars < 1 || stars > 5) { + throw createError({ statusCode: 400, message: "Invalid rating data" }); + } + + const db = usePrisma(); + const rating = await db.gameRating.create({ + data: { + userId: user.id, + gameName: String(gameName).toLowerCase().slice(0, 50), + stars, + feedback: feedback ? String(feedback).slice(0, 500) : null, + score: typeof score === "number" ? score : 0, + }, + }); + + return { id: rating.id }; +}); diff --git a/backend/server/api/games/ratings.get.ts b/backend/server/api/games/ratings.get.ts new file mode 100644 index 0000000..743d5c9 --- /dev/null +++ b/backend/server/api/games/ratings.get.ts @@ -0,0 +1,68 @@ +import { usePrisma } from "../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const query = getQuery(event); + const gameName = String(query.game || "").toLowerCase(); + + const db = usePrisma(); + + const where = gameName ? { gameName } : {}; + + const [ratings, grouped] = await Promise.all([ + db.gameRating.findMany({ + where, + orderBy: { createdAt: "desc" }, + take: 50, + select: { + id: true, + userId: true, + gameName: true, + stars: true, + feedback: true, + score: true, + createdAt: true, + }, + }), + db.gameRating.groupBy({ + by: ["gameName"], + where, + _avg: { stars: true }, + _count: { id: true }, + }), + ]); + + // Profil-Daten für alle User laden + const userIds = [...new Set(ratings.map((r) => r.userId))]; + const profiles = + userIds.length > 0 + ? await db.profile.findMany({ + where: { id: { in: userIds } }, + select: { id: true, nickname: true, username: true, avatar: true }, + }) + : []; + const profileMap = Object.fromEntries(profiles.map((p) => [p.id, p])); + + const ratingsWithUser = ratings.map((r) => { + const profile = profileMap[r.userId]; + return { + id: r.id, + gameName: r.gameName, + stars: r.stars, + feedback: r.feedback, + score: r.score, + createdAt: r.createdAt, + user: { + nickname: profile?.nickname || profile?.username || "Anonym", + avatar: profile?.avatar ?? null, + }, + }; + }); + + const stats = grouped.map((g) => ({ + gameName: g.gameName, + avgStars: Math.round((g._avg.stars ?? 0) * 10) / 10, + count: g._count.id, + })); + + return { ratings: ratingsWithUser, stats }; +}); diff --git a/backend/server/api/games/score.post.ts b/backend/server/api/games/score.post.ts new file mode 100644 index 0000000..e8568f7 --- /dev/null +++ b/backend/server/api/games/score.post.ts @@ -0,0 +1,37 @@ +import { usePrisma } from "../../utils/prisma"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + + const { gameName, score, nickname } = body; + if (!gameName || typeof score !== "number" || score < 0) { + throw createError({ + statusCode: 400, + message: "gameName und score erforderlich", + }); + } + + const db = usePrisma(); + const name = String( + nickname || + user.user_metadata?.nickname || + user.email?.split("@")[0] || + "Anonym", + ).slice(0, 30); + + // Nur updaten wenn neuer Score besser ist + const existing = await db.gameHighScore.findUnique({ + where: { userId_gameName: { userId: user.id, gameName } }, + }); + + if (!existing || score > existing.score) { + await db.gameHighScore.upsert({ + where: { userId_gameName: { userId: user.id, gameName } }, + create: { userId: user.id, nickname: name, gameName, score }, + update: { score, nickname: name }, + }); + } + + return { ok: true, isNewBest: !existing || score > existing.score }; +}); diff --git a/backend/server/api/games/share-text.post.ts b/backend/server/api/games/share-text.post.ts new file mode 100644 index 0000000..1518216 --- /dev/null +++ b/backend/server/api/games/share-text.post.ts @@ -0,0 +1,123 @@ +// Game-spezifischer Kontext für den Prompt +const GAME_VIBES: Record = { + snake: "Snake – präzise, fließend, ein Moment der Kontrolle", + tetris: "Tetris – logisch, fokussiert, Gedanken ordnen statt Chaos", + memory: "Memory – Geduld, Konzentration, im Hier und Jetzt bleiben", + tictactoe: "Tic-Tac-Toe – ruhig, überlegt, kleine Strategie", +}; + +function getGameVibe(gameName: string): string { + const key = gameName.toLowerCase(); + for (const [k, v] of Object.entries(GAME_VIBES)) { + if (key.includes(k)) return v; + } + return `${gameName} – ein Moment bewusster Ablenkung`; +} + +function buildFallback( + isNewRecord: boolean, + myRank: number | null, + mode: string, +): string { + const parts: string[] = []; + if (isNewRecord) parts.push("Neuer persönlicher Rekord! 🏆"); + else if (myRank) parts.push(`Rang #${myRank} in der Rangliste`); + const challenge = + mode === "impulse" + ? "Impuls überwunden – kannst du länger standhalten? 💪" + : "Kannst du das schlagen? 👊"; + parts.push(challenge); + return parts.join("\n"); +} + +const SYSTEM_PROMPT = `Du generierst kurze Share-Texte (max 2–3 Zeilen) für die ReBreak-App. +ReBreak hilft Menschen bei Glücksspielsucht, indem sie Mini-Games spielen statt zu gamble. +Der Text wird im Community-Feed unter dem Spielnamen gepostet – er soll den Vibe des jeweiligen Spiels widerspiegeln. + +Regeln: +- Zeile 1 (optional): Nur wenn neuer Rekord ODER guter Rang, eine kurze Meldung dazu +- Letzte Zeile: Kurzer, kreativer Challenge-Satz passend zum Spiel auf Deutsch +- 1–2 Emojis, passend zum Spiel und Ton +- KEIN Spielname, KEINE Score-Zahl im Text +- Keine Hashtags, keine URLs +- Antworte NUR mit dem Text, kein Prefix, keine Anführungszeichen`; + +/** POST /api/games/share-text */ +export default defineEventHandler(async (event) => { + await requireUser(event); + + const body = await readBody(event); + const { + gameName, + score, + scoreLabel = "Punkte", + bestScore, + myRank, + isNewRecord, + mode = "game", + } = body as { + gameName: string; + score: number; + scoreLabel?: string; + bestScore?: number; + myRank?: number | null; + isNewRecord?: boolean; + mode?: string; + }; + + if (!gameName || score == null) { + throw createError({ + statusCode: 400, + message: "gameName und score erforderlich", + }); + } + + const config = useRuntimeConfig(); + + if (!config.groqApiKey) { + return { + text: buildFallback(!!isNewRecord, myRank ?? null, mode), + }; + } + + const gameVibe = getGameVibe(gameName); + const userPrompt = [ + `Spiel-Vibe: ${gameVibe}`, + isNewRecord ? "NEUER PERSÖNLICHER REKORD!" : null, + myRank ? `Rang #${myRank} in der Rangliste` : null, + mode === "impulse" + ? "Der Spieler hat einen Spielimpuls damit überwunden." + : null, + ] + .filter(Boolean) + .join("\n"); + + try { + const response = await $fetch<{ + choices: { message: { content: string } }[]; + }>("https://api.groq.com/openai/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${config.groqApiKey}`, + "Content-Type": "application/json", + }, + body: { + model: "llama-3.1-8b-instant", + max_tokens: 120, + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + { role: "user", content: userPrompt }, + ], + }, + }); + + const text = response.choices?.[0]?.message?.content?.trim(); + if (!text) throw new Error("empty response"); + + return { text }; + } catch { + return { + text: buildFallback(!!isNewRecord, myRank ?? null, mode), + }; + } +}); diff --git a/backend/server/api/lyra/memories/extract.post.ts b/backend/server/api/lyra/memories/extract.post.ts new file mode 100644 index 0000000..a1bcc78 --- /dev/null +++ b/backend/server/api/lyra/memories/extract.post.ts @@ -0,0 +1,168 @@ +/** + * POST /api/lyra/memories/extract + * + * Extrahiert strukturierte Memories aus einem SOS/Coach-Gespräch via LLM (Claude Haiku). + * Wird intern (fire-and-forget) nach SOS-Stream-Ende aufgerufen. + * + * Body: { sessionId: string, conversation: Array<{role, content}> } + * Response: { extracted: number, skipped: number } + * + * Fehler: silent — User darf nichts merken. Logging via [lyra-memory]. + */ +import { upsertMemory } from "../../../db/lyraMemory"; +import type { LyraMemoryType } from "../../../db/lyraMemory"; + +const VALID_TYPES: LyraMemoryType[] = [ + "trigger", + "habit", + "strength", + "relationship", + "milestone", + "pain_point", + "goal", + "preference", +]; + +const EXTRACTION_SYSTEM_PROMPT = `Du extrahierst aus einem Gespräch zwischen User und Lyra-Coach strukturierte Fakten über den User. Output strikt als JSON-Array: +[{"type":"trigger|habit|strength|relationship|milestone|pain_point|goal|preference", "content":"", "confidence":0.0-1.0}] +Regeln: +- Nur Fakten die der USER explizit oder implizit über sich geteilt hat. Nichts erfinden. +- Keine Vermutungen über Diagnosen oder Pathologisierungen ("süchtig", "krank" etc). +- Nur Wesentliches. Wenn nichts Neues drin ist → leeres Array []. +- confidence: 0.9+ wenn User explizit gesagt, 0.5-0.7 wenn implizit, <0.5 garnicht extrahieren. +- content in der Sprache des Gesprächs (DE). +- Maximal 8 Einträge pro Extraktion.`; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const body = await readBody(event); + const { + sessionId, + conversation, + }: { + sessionId?: string; + conversation: Array<{ role: string; content: string }>; + } = body; + + if (!Array.isArray(conversation) || conversation.length === 0) { + throw createError({ statusCode: 400, message: "conversation fehlt" }); + } + + const config = useRuntimeConfig(); + const key = config.openrouterApiKey as string | undefined; + if (!key) { + throw createError({ statusCode: 503, message: "OpenRouter Key fehlt" }); + } + + // Nur User-Nachrichten extrahieren (Lyra-Antworten sind Kontext, kein User-Fakt) + const userMessages = conversation.filter((m) => m.role === "user"); + if (userMessages.length === 0) { + return { extracted: 0, skipped: 0 }; + } + + // Konversation als lesbaren Text aufbereiten (max 4000 chars für Haiku) + const conversationText = conversation + .slice(-20) // letzte 20 Messages reichen für Kontext + .map((m) => `${m.role === "user" ? "User" : "Lyra"}: ${m.content}`) + .join("\n") + .slice(0, 4000); + + let extracted = 0; + let skipped = 0; + + try { + const res = await $fetch<{ + choices: { message: { content: string } }[]; + }>("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + "HTTP-Referer": "https://rebreak.org", + "X-Title": "ReBreak Memory Extraction", + }, + body: { + model: "anthropic/claude-haiku-4-5", + max_tokens: 800, + temperature: 0.1, + messages: [ + { role: "system", content: EXTRACTION_SYSTEM_PROMPT }, + { role: "user", content: conversationText }, + ], + }, + timeout: 20000, + }); + + const raw = res.choices?.[0]?.message?.content?.trim(); + if (!raw) { + console.log("[lyra-memory] extract: empty response from LLM"); + return { extracted: 0, skipped: 0 }; + } + + // JSON-Array parsen (Haiku gibt manchmal Markdown-Fences zurück) + const jsonStr = raw + .replace(/^```(?:json)?\s*/i, "") + .replace(/\s*```$/, "") + .trim(); + + let facts: Array<{ + type: string; + content: string; + confidence: number; + }> = []; + + try { + facts = JSON.parse(jsonStr); + } catch { + console.warn( + "[lyra-memory] extract: JSON parse failed:", + jsonStr.slice(0, 200), + ); + return { extracted: 0, skipped: 0 }; + } + + if (!Array.isArray(facts)) { + return { extracted: 0, skipped: 0 }; + } + + for (const fact of facts) { + // Confidence < 0.5 → skip + if (!fact.content || (fact.confidence ?? 0) < 0.5) { + skipped++; + continue; + } + + // Typ validieren + if (!VALID_TYPES.includes(fact.type as LyraMemoryType)) { + console.warn("[lyra-memory] extract: invalid type:", fact.type); + skipped++; + continue; + } + + try { + await upsertMemory( + user.id, + fact.type as LyraMemoryType, + fact.content, + sessionId ?? "sos-session", + Math.min(1, Math.max(0, fact.confidence ?? 0.7)), + ); + extracted++; + } catch (e) { + console.error("[lyra-memory] upsert error:", e); + skipped++; + } + } + + console.log( + `[lyra-memory] extract done for ${user.id}: ${extracted} extracted, ${skipped} skipped`, + ); + } catch (e) { + // Silent fail — User darf nichts merken + console.error("[lyra-memory] extract LLM error:", e); + return { extracted: 0, skipped: 0 }; + } + + return { success: true, data: { extracted, skipped } }; +}); diff --git a/backend/server/api/lyra/welcome-back.get.ts b/backend/server/api/lyra/welcome-back.get.ts new file mode 100644 index 0000000..dcd4284 --- /dev/null +++ b/backend/server/api/lyra/welcome-back.get.ts @@ -0,0 +1,98 @@ +/** + * GET /api/lyra/welcome-back + * + * Generiert einen kurzen, freundlichen Motivations-Text wenn der User nach + * einer Schutz-Deaktivierung den Schutz wieder aktiviert hat. + * + * Pattern: erst LLM-Call (OpenRouter falls Key da, sonst OpenAI), bei Fehler + * fällt's auf einen statischen Pool von vorgeschriebenen Sätzen zurück — + * UX-Fallback statt User-facing-Error. + * + * Response: { message: string, source: "ai" | "fallback" } + */ + +const WELCOME_BACK_PROMPT = `Du bist Lyra, der KI-Coach der Rebreak-App. +Der Nutzer hat seinen Glücksspiel-Schutz kurz deaktiviert und gerade wieder aktiviert. +Schreibe ihm GENAU EINE freundliche, warme Nachricht (max 3 kurze Sätze, Deutsch, du-Form). + +WICHTIG: +- Kein Urteil, keine Belehrung +- Kein Schuldgefühl, kein "endlich" +- Anerkennung dass Rückfälle/Ausrutscher zur Recovery gehören +- Stärke betonen dass er WIEDER aktiviert hat +- Kein Wort "Sucht", "süchtig", "Rückfall" — stattdessen: "Phase", "Moment", "Stärke" + +Antwort: nur die Nachricht, keine Anführungszeichen, kein Kontext.`; + +const FALLBACK_MESSAGES = [ + "Schön dass du wieder da bist. Diese eine Geste — den Schutz wieder einzuschalten — zeigt mehr Stärke als die meisten je sehen werden. Ich bin stolz auf dich.", + "Hey. Ausrutscher gehören zum Weg. Was zählt ist dass du jetzt hier bist, mit dem Schutz an. Das ist die wichtige Entscheidung.", + "Willkommen zurück. Du hast eine Pause genommen — und bist jetzt wieder hier. Genau so läuft Recovery: nicht linear, sondern echt. Lass uns weitermachen.", + "Gut dich wieder zu sehen. Manchmal braucht es einen kurzen Umweg um den eigenen Weg klarer zu sehen. Du gehst ihn weiter — das ist das Wichtige.", + "Hey, alles gut. Du hast den Schutz wieder aktiv — das war eine bewusste Entscheidung gegen den Impuls. Genau das ist der Muskel den wir hier trainieren.", +]; + +export default defineEventHandler(async (event) => { + await requireUser(event); + + const config = useRuntimeConfig(event); + const openrouterKey = config.openrouterApiKey as string | undefined; + const openaiKey = config.openaiApiKey as string | undefined; + const key = openrouterKey ?? openaiKey; + + // Fallback wenn keine Keys konfiguriert + if (!key) { + return { + message: pickRandom(FALLBACK_MESSAGES), + source: "fallback" as const, + }; + } + + const isOpenRouter = !!openrouterKey; + const url = isOpenRouter + ? "https://openrouter.ai/api/v1/chat/completions" + : "https://api.openai.com/v1/chat/completions"; + const model = isOpenRouter + ? "meta-llama/llama-3.1-8b-instruct" + : "gpt-4o-mini"; + + try { + const res = await $fetch<{ choices: { message: { content: string } }[] }>( + url, + { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + ...(isOpenRouter && { + "HTTP-Referer": "https://rebreak.org", + "X-Title": "ReBreak Welcome-Back", + }), + }, + body: { + model, + max_tokens: 200, + temperature: 0.85, + messages: [ + { role: "system", content: WELCOME_BACK_PROMPT }, + { role: "user", content: "Schreibe mir die Welcome-Back-Nachricht." }, + ], + }, + timeout: 6000, + }, + ); + + const text = res.choices?.[0]?.message?.content?.trim(); + if (text && text.length > 10 && text.length < 600) { + return { message: text, source: "ai" as const }; + } + return { message: pickRandom(FALLBACK_MESSAGES), source: "fallback" as const }; + } catch (e: any) { + console.warn("[lyra.welcome-back] LLM-call failed:", e?.message ?? e); + return { message: pickRandom(FALLBACK_MESSAGES), source: "fallback" as const }; + } +}); + +function pickRandom(arr: readonly T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} diff --git a/backend/server/api/mail/connect.post.ts b/backend/server/api/mail/connect.post.ts new file mode 100644 index 0000000..5f0a57f --- /dev/null +++ b/backend/server/api/mail/connect.post.ts @@ -0,0 +1,99 @@ +import { ImapFlow } from "imapflow"; +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; +import { countMailConnections, upsertMailConnection } from "../../db/mail"; + +/** + * POST /api/mail/connect + * Body: { email, password } + * Testet IMAP-Verbindung und speichert Credentials verschlüsselt. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const { + email, + password, + // Custom-IMAP-Felder (optional, nur wenn User eigenen Server konfiguriert) + imapHost: customImapHost, + imapPort: customImapPort, + useTls, + rejectUnauthorized, + } = await readBody(event); + + if (!email || !password) { + throw createError({ + statusCode: 400, + message: "Email und Passwort erforderlich", + }); + } + + // Plan-Limit prüfen + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? "free"); + + if (limits.mailAgents !== Infinity) { + const count = await countMailConnections(user.id); + if (count >= limits.mailAgents) { + throw createError({ + statusCode: 403, + message: `Dein Plan erlaubt maximal ${limits.mailAgents} Mail-Agent${limits.mailAgents !== 1 ? "en" : ""}`, + }); + } + } + + // Custom-IMAP: wenn imapHost explizit gesetzt → Provider-Detection überspringen. + // Sonst: automatisch via Email-Domain erkennen. + const provider = detectImapProvider(email); + const resolvedHost = customImapHost?.trim() || provider.host; + const resolvedPort = customImapPort ?? provider.port; + + // TLS-Konfiguration ableiten + // useTls=false → STARTTLS (secure=false, requireTLS=true bei ImapFlow) + // useTls=true oder nicht gesetzt → implicit TLS (secure=true) + const useImplicitTls = useTls !== false; // default: true + const tlsRejectUnauthorized = rejectUnauthorized !== false; // default: true + // STARTTLS nur wenn explizit angefordert (useTls === false) + const useStarttls = useTls === false; + + // IMAP-Verbindung testen + const client = new ImapFlow({ + host: resolvedHost, + port: resolvedPort, + secure: useImplicitTls, + ...(useStarttls ? { requireTLS: true } : {}), + auth: { user: email, pass: password }, + logger: false, + tls: { rejectUnauthorized: tlsRejectUnauthorized }, + }); + + try { + await client.connect(); + await client.logout(); + } catch (err: any) { + throw createError({ + statusCode: 401, + message: `Verbindung fehlgeschlagen: ${err.message ?? "Ungültige Zugangsdaten"}`, + }); + } + + // Credentials verschlüsselt speichern + await upsertMailConnection({ + userId: user.id, + email, + provider: "imap", + // Bei Custom-Host: Host als providerName, sonst auto-erkannter Name + providerName: customImapHost ? resolvedHost : provider.name, + imapHost: resolvedHost, + imapPort: resolvedPort, + passwordEncrypted: encrypt(password), + rejectUnauthorized: tlsRejectUnauthorized, + useStarttls, + }); + + return { + connected: true, + email, + provider: customImapHost ? resolvedHost : provider.name, + custom: !!customImapHost, + }; +}); diff --git a/backend/server/api/mail/disconnect.delete.ts b/backend/server/api/mail/disconnect.delete.ts new file mode 100644 index 0000000..8dec6da --- /dev/null +++ b/backend/server/api/mail/disconnect.delete.ts @@ -0,0 +1,19 @@ +import { deleteMailConnection, deleteAllMailConnections } from "../../db/mail"; + +/** + * DELETE /api/mail/disconnect + * Trennt das Gmail-Konto (löscht Connection und alle Logs). + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const { connectionId } = await readBody(event).catch(() => ({})); + + if (connectionId) { + await deleteMailConnection(user.id, connectionId); + } else { + await deleteAllMailConnections(user.id); + } + + return { ok: true }; +}); diff --git a/backend/server/api/mail/interval.patch.ts b/backend/server/api/mail/interval.patch.ts new file mode 100644 index 0000000..752b495 --- /dev/null +++ b/backend/server/api/mail/interval.patch.ts @@ -0,0 +1,38 @@ +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; +import { updateMailConnectionInterval } from "../../db/mail"; + +/** + * PATCH /api/mail/interval + * Body: { connectionId, interval: 1 | 8 | 24 } + * Setzt das Scan-Intervall für eine Mail-Verbindung. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const { connectionId, interval } = (await readBody(event)) as { + connectionId: string; + interval: number; + }; + + if (!connectionId || !Number.isInteger(interval) || interval < 1) { + throw createError({ + statusCode: 400, + message: "connectionId und interval erforderlich", + }); + } + + // Plan-Limit: erlaubte Intervalle prüfen + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? "free"); + + if (!limits.mailIntervalOptions.includes(interval)) { + throw createError({ + statusCode: 403, + message: `Dein Plan erlaubt nur folgende Intervalle (Stunden): ${limits.mailIntervalOptions.join(", ")}`, + }); + } + + await updateMailConnectionInterval(user.id, connectionId, interval); + + return { ok: true, interval }; +}); diff --git a/backend/server/api/mail/proxy-account.get.ts b/backend/server/api/mail/proxy-account.get.ts new file mode 100644 index 0000000..02ba926 --- /dev/null +++ b/backend/server/api/mail/proxy-account.get.ts @@ -0,0 +1,27 @@ +import { getImapProxyAccounts, getMailConnections } from "../../db/mail"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const [accounts, connections] = await Promise.all([ + getImapProxyAccounts(user.id), + getMailConnections(user.id), + ]); + + if (accounts.length === 0) { + return { configured: false, accounts: [] }; + } + + // Enrich with real email address from connection + const connMap = new Map(connections.map((c) => [c.id, c.email])); + + return { + configured: true, + host: "imap.rebreak.org", + port: 993, + accounts: accounts.map((a) => ({ + username: a.proxyUsername, + realEmail: connMap.get(a.connectionId) ?? null, + })), + }; +}); diff --git a/backend/server/api/mail/proxy-account.post.ts b/backend/server/api/mail/proxy-account.post.ts new file mode 100644 index 0000000..37bd547 --- /dev/null +++ b/backend/server/api/mail/proxy-account.post.ts @@ -0,0 +1,55 @@ +import { randomBytes } from "crypto"; +import { getMailConnections, upsertImapProxyAccount } from "../../db/mail"; +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; +import { encrypt } from "../../utils/crypto"; +import { detectSmtpProvider } from "../../utils/imap-providers"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? "free"); + if (limits.mailAgents !== Infinity) { + throw createError({ statusCode: 403, message: "Nur für Legend-User verfügbar" }); + } + + const connections = await getMailConnections(user.id); + if (connections.length === 0) { + throw createError({ + statusCode: 400, + message: "Erst eine E-Mail-Verbindung herstellen", + }); + } + + const results = await Promise.all( + connections.map(async (connection) => { + const plainPassword = randomBytes(12).toString("base64url"); + const localPart = connection.email.replace("@", ".").replace(/[^a-z0-9._-]/gi, "-"); + const proxyUsername = `${localPart}@imap.rebreak.org`; + + await upsertImapProxyAccount({ + userId: user.id, + proxyUsername, + proxyPassword: encrypt(plainPassword), + connectionId: connection.id, + }); + + const smtp = detectSmtpProvider(connection.imapHost); + return { + username: proxyUsername, + password: plainPassword, + realEmail: connection.email, + host: "imap.rebreak.org", + port: 993, + smtpHost: smtp.host, + smtpPort: smtp.port, + }; + }), + ); + + return { + note: "Passwörter werden nur einmal angezeigt – sofort in iOS Mail eintragen.", + accounts: results, + }; +}); diff --git a/backend/server/api/mail/proxy-config.get.ts b/backend/server/api/mail/proxy-config.get.ts new file mode 100644 index 0000000..02c5c4f --- /dev/null +++ b/backend/server/api/mail/proxy-config.get.ts @@ -0,0 +1,136 @@ +import { randomUUID } from "crypto"; +import { getImapProxyAccounts, getMailConnections } from "../../db/mail"; +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; +import { detectSmtpProvider } from "../../utils/imap-providers"; +import { decrypt } from "../../utils/crypto"; + +function escapeXml(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? "free"); + if (limits.mailAgents !== Infinity) { + throw createError({ statusCode: 403, message: "Nur für Legend-User verfügbar" }); + } + + const [accounts, connections] = await Promise.all([ + getImapProxyAccounts(user.id), + getMailConnections(user.id), + ]); + + if (accounts.length === 0) { + throw createError({ statusCode: 404, message: "Noch keine Proxy-Konten eingerichtet" }); + } + + const connMap = new Map(connections.map((c) => [c.id, c])); + const payloads: string[] = []; + + for (const account of accounts) { + const conn = connMap.get(account.connectionId); + if (!conn) continue; + + let proxyPassword: string; + try { + proxyPassword = decrypt(account.proxyPassword); + } catch { + continue; // Legacy scrypt hash – skip, user needs to re-generate via proxy-account.post + } + + let realPassword = ""; + try { + realPassword = decrypt(conn.passwordEncrypted); + } catch { /* SMTP password omitted */ } + + const smtp = detectSmtpProvider(conn.imapHost); + + payloads.push(` + EmailAccountDescription + ReBreak Filter – ${escapeXml(conn.email)} + EmailAccountName + ${escapeXml(conn.email)} + EmailAccountType + EmailTypeIMAP + EmailAddress + ${escapeXml(conn.email)} + IncomingMailServerHostName + imap.rebreak.org + IncomingMailServerPortNumber + 993 + IncomingMailServerUseSSL + + IncomingMailServerUsername + ${escapeXml(account.proxyUsername)} + IncomingMailServerPassword + ${escapeXml(proxyPassword)} + OutgoingMailServerHostName + ${escapeXml(smtp.host)} + OutgoingMailServerPortNumber + ${smtp.port} + OutgoingMailServerUseSSL + + OutgoingMailServerUsername + ${escapeXml(conn.email)}${realPassword ? ` + OutgoingMailServerPassword + ${escapeXml(realPassword)}` : ""} + SMIMEEnabled + + PayloadDescription + Mail-Filter für ${escapeXml(conn.email)} + PayloadDisplayName + ReBreak – ${escapeXml(conn.email)} + PayloadIdentifier + org.rebreak.mail.${conn.id} + PayloadType + com.apple.mail.managed + PayloadUUID + ${randomUUID()} + PayloadVersion + 1 + `); + } + + if (payloads.length === 0) { + throw createError({ statusCode: 400, message: "Proxy-Konten müssen zuerst neu erstellt werden" }); + } + + const plist = ` + + + + PayloadContent + + ${payloads.join("\n ")} + + PayloadDescription + Blockiert Casino-Mails automatisch bevor sie in deinen Posteingang gelangen + PayloadDisplayName + ReBreak Mail Filter + PayloadIdentifier + org.rebreak.mail.profile.${user.id} + PayloadOrganization + ReBreak + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadUUID + ${randomUUID()} + PayloadVersion + 1 + +`; + + setHeader(event, "Content-Type", "application/x-apple-aspen-config"); + setHeader(event, "Content-Disposition", 'attachment; filename="rebreak-mail-filter.mobileconfig"'); + return plist; +}); diff --git a/backend/server/api/mail/results.get.ts b/backend/server/api/mail/results.get.ts new file mode 100644 index 0000000..2491662 --- /dev/null +++ b/backend/server/api/mail/results.get.ts @@ -0,0 +1,16 @@ +import { deleteOldMailBlocked, getMailBlockedPaginated } from "../../db/mail"; + +/** + * GET /api/mail/results + * Gibt die letzten blockierten Gambling-Mails zurück (paginiert). + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const query = getQuery(event); + const page = Math.max(1, parseInt((query.page as string) || "1")); + + await deleteOldMailBlocked(user.id); + + return getMailBlockedPaginated(user.id, page); +}); diff --git a/backend/server/api/mail/scan-internal.post.ts b/backend/server/api/mail/scan-internal.post.ts new file mode 100644 index 0000000..1006381 --- /dev/null +++ b/backend/server/api/mail/scan-internal.post.ts @@ -0,0 +1,205 @@ +import { ImapFlow } from "imapflow"; +import { + getMailConnections, + deleteOldMailBlocked, + getAlreadyBlockedUidSet, + insertMailBlocked, + updateMailConnectionScanStats, +} from "../../db/mail"; +import { getBlocklistedDomainsSet } from "../../db/domains"; +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; +// Single-Source-of-Truth (Mo's Finding #4) +// @ts-expect-error — .mjs ohne types, GAMBLING_KEYWORDS ist string[] +import { GAMBLING_KEYWORDS } from "../../utils/gambling-keywords.mjs"; + + +/** + * POST /api/mail/scan-internal + * Called by cron or IMAP proxy. Scans ALL mailbox folders. + * Free: only custom domains + keywords. Pro/Legend: global blocklist + custom. + */ +export default defineEventHandler(async (event) => { + const secret = getHeader(event, "x-admin-secret"); + const adminSecret = process.env.NUXT_ADMIN_SECRET || process.env.ADMIN_SECRET; + if (!secret || !adminSecret || secret !== adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + const body = (await readBody(event)) as { userId?: string }; + const userId = body?.userId; + if (!userId) + throw createError({ statusCode: 400, message: "userId missing" }); + + const connections = await getMailConnections(userId); + if (connections.length === 0) return { scanned: 0, blocked: 0 }; + + // Plan-aware blocklist + const profile = await getProfile(userId); + const limits = getPlanLimits(profile?.plan ?? "free"); + const includeGlobal = limits.globalBlocklist; + + await deleteOldMailBlocked(userId); + + let totalScanned = 0; + let totalBlocked = 0; + + for (const connection of connections) { + let password: string; + try { + password = decrypt(connection.passwordEncrypted); + } catch { + continue; + } + + // useStarttls=true → STARTTLS (secure=false + requireTLS=true) + // rejectUnauthorized=false → self-signed Certs zulassen (nur Custom-IMAP) + const useImplicitTls = !connection.useStarttls; + const imap = new ImapFlow({ + host: connection.imapHost, + port: connection.imapPort, + secure: useImplicitTls, + ...(connection.useStarttls ? { requireTLS: true } : {}), + auth: { user: connection.email, pass: password }, + logger: false, + tls: { rejectUnauthorized: connection.rejectUnauthorized ?? true }, + }); + + let scanned = 0; + let newlyBlocked = 0; + + try { + await imap.connect(); + + // Scan ALL mailbox folders (not just hardcoded list) + const mailboxes = await imap.list(); + const scannable = mailboxes.filter( + (mb: any) => !mb.flags?.has("\\Noselect"), + ); + console.log( + `[scan-internal] ${connection.email} scanning ${scannable.length} folders`, + ); + + for (const mb of scannable) { + let lock: any; + try { + lock = await imap.getMailboxLock(mb.path); + } catch { + continue; + } + try { + const SCAN_LIMIT = 200; + const status = await imap.status(mb.path, { messages: true }); + const msgCount = (status as any).messages ?? 0; + if (msgCount === 0) continue; + + const fetchRange = + msgCount > SCAN_LIMIT ? `${msgCount - SCAN_LIMIT + 1}:*` : "1:*"; + const allMessages = await imap.fetchAll(fetchRange, { + envelope: true, + }); + scanned += allMessages.length; + totalScanned += allMessages.length; + + const allUids = allMessages.map( + (m: any) => `${mb.path}:${String(m.uid ?? m.seq)}`, + ); + const [blockedDomainSet, alreadyBlockedSet] = await Promise.all([ + getBlocklistedDomainsSet( + allMessages + .map( + (m: any) => + (m.envelope?.from?.[0]?.address ?? "") + .toLowerCase() + .split("@")[1] ?? "", + ) + .filter(Boolean), + userId, + includeGlobal, + ), + getAlreadyBlockedUidSet(allUids, userId), + ]); + + const toInsert: Parameters[0] = []; + const uidsToDelete: string[] = []; + + for (const msg of allMessages) { + const from = msg.envelope?.from?.[0]; + const senderEmail = (from?.address ?? "").toLowerCase(); + const senderName = from?.name ?? null; + const subject = (msg.envelope?.subject ?? "").trim(); + const msgDate = msg.envelope?.date ?? new Date(); + const uid = `${mb.path}:${String(msg.uid ?? msg.seq)}`; + + const haystack = `${senderEmail} ${subject}`.toLowerCase(); + const isGamblingKeyword = GAMBLING_KEYWORDS.some((kw) => + haystack.includes(kw), + ); + const senderDomain = senderEmail.split("@")[1] ?? ""; + const isBlocklisted = senderDomain + ? blockedDomainSet.has(senderDomain) + : false; + + if (!isGamblingKeyword && !isBlocklisted) continue; + if (alreadyBlockedSet.has(uid)) continue; + + uidsToDelete.push(String(msg.uid)); + toInsert.push({ + userId, + connectionId: connection.id, + gmailMessageId: uid, + senderEmail: senderEmail || "unbekannt", + senderName, + subject: subject.slice(0, 200) || "(kein Betreff)", + receivedAt: msgDate, + action: "deleted", + }); + newlyBlocked++; + } + + if (uidsToDelete.length > 0) { + try { + await imap.messageDelete(uidsToDelete.join(","), { uid: true }); + } catch { + try { + for (const uid of uidsToDelete) { + await imap + .messageFlagsAdd(uid, ["\\Deleted"], { uid: true }) + .catch(() => {}); + } + await (imap as any).expunge().catch(() => {}); + } catch { + /* ignore */ + } + } + console.log( + `[scan-internal] ${connection.email} | ${mb.path} | deleted ${uidsToDelete.length} gambling mails`, + ); + } + + await insertMailBlocked(toInsert); + } finally { + lock.release(); + } + } + + await imap.logout(); + } catch { + try { + await imap.logout(); + } catch {} + } + + totalBlocked += newlyBlocked; + await updateMailConnectionScanStats( + connection.id, + scanned, + newlyBlocked, + connection.emailsBlocked, + connection.emailsScanned, + connection.scanInterval, + ); + } + + return { scanned: totalScanned, blocked: totalBlocked }; +}); diff --git a/backend/server/api/mail/scan.post.ts b/backend/server/api/mail/scan.post.ts new file mode 100644 index 0000000..f1defce --- /dev/null +++ b/backend/server/api/mail/scan.post.ts @@ -0,0 +1,196 @@ +import { ImapFlow } from "imapflow"; +import { + getMailConnections, + deleteOldMailBlocked, + getAlreadyBlockedUidSet, + insertMailBlocked, + updateMailConnectionScanStats, +} from "../../db/mail"; +import { getBlocklistedDomainsSet } from "../../db/domains"; +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; +// Single-Source-of-Truth (Mo's Finding #4) +// @ts-expect-error — .mjs ohne types, GAMBLING_KEYWORDS ist string[] +import { GAMBLING_KEYWORDS } from "../../utils/gambling-keywords.mjs"; + + +/** + * POST /api/mail/scan + * Scannt ALLE Ordner (INBOX, Spam, Papierkorb, All Mail …) nach Gambling-Mails. + * Free-User: nur eigene Domains + Keywords. Pro/Legend: globale Blocklist + eigene. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const connections = await getMailConnections(user.id); + if (connections.length === 0) { + throw createError({ + statusCode: 404, + message: "Kein Mail-Konto verbunden", + }); + } + + // Plan-aware: Free users get only custom domains, Pro/Legend get global blocklist + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? "free"); + const includeGlobal = limits.globalBlocklist; + + await deleteOldMailBlocked(user.id); + + let totalScanned = 0; + let totalBlocked = 0; + + for (const connection of connections) { + let password: string; + try { + password = decrypt(connection.passwordEncrypted); + } catch { + continue; + } + + // useStarttls=true → STARTTLS (secure=false + requireTLS=true) + // rejectUnauthorized=false → self-signed Certs zulassen (nur Custom-IMAP) + const useImplicitTls = !connection.useStarttls; + const imap = new ImapFlow({ + host: connection.imapHost, + port: connection.imapPort, + secure: useImplicitTls, + ...(connection.useStarttls ? { requireTLS: true } : {}), + auth: { user: connection.email, pass: password }, + logger: false, + tls: { rejectUnauthorized: connection.rejectUnauthorized ?? true }, + }); + + let scanned = 0; + let newlyBlocked = 0; + + try { + await imap.connect(); + + // Scan ALL mailbox folders (not just hardcoded list) + const mailboxes = await imap.list(); + const scannable = mailboxes.filter( + (mb: any) => !mb.flags?.has("\\Noselect"), + ); + + for (const mb of scannable) { + let lock: any; + try { + lock = await imap.getMailboxLock(mb.path); + } catch { + continue; + } + try { + const SCAN_LIMIT = 200; + const status = await imap.status(mb.path, { messages: true }); + const msgCount = (status as any).messages ?? 0; + if (msgCount === 0) continue; + + const fetchRange = + msgCount > SCAN_LIMIT ? `${msgCount - SCAN_LIMIT + 1}:*` : "1:*"; + const allMessages = await imap.fetchAll(fetchRange, { + envelope: true, + }); + scanned += allMessages.length; + totalScanned += allMessages.length; + + const allUids = allMessages.map( + (m: any) => `${mb.path}:${String(m.uid ?? m.seq)}`, + ); + const [blockedDomainSet, alreadyBlockedSet] = await Promise.all([ + getBlocklistedDomainsSet( + allMessages + .map( + (m: any) => + (m.envelope?.from?.[0]?.address ?? "") + .toLowerCase() + .split("@")[1] ?? "", + ) + .filter(Boolean), + user.id, + includeGlobal, + ), + getAlreadyBlockedUidSet(allUids, user.id), + ]); + + const toInsert: Parameters[0] = []; + const uidsToDelete: string[] = []; + + for (const msg of allMessages) { + const from = msg.envelope?.from?.[0]; + const senderEmail = (from?.address ?? "").toLowerCase(); + const senderName = from?.name ?? null; + const subject = (msg.envelope?.subject ?? "").trim(); + const msgDate = msg.envelope?.date ?? new Date(); + const uid = `${mb.path}:${String(msg.uid ?? msg.seq)}`; + + const haystack = `${senderEmail} ${subject}`.toLowerCase(); + const isGamblingKeyword = GAMBLING_KEYWORDS.some((kw) => + haystack.includes(kw), + ); + const senderDomain = senderEmail.split("@")[1] ?? ""; + const isBlocklisted = senderDomain + ? blockedDomainSet.has(senderDomain) + : false; + + if (!isGamblingKeyword && !isBlocklisted) continue; + if (alreadyBlockedSet.has(uid)) continue; + + uidsToDelete.push(String(msg.uid)); + toInsert.push({ + userId: user.id, + connectionId: connection.id, + gmailMessageId: uid, + senderEmail: senderEmail || "unbekannt", + senderName, + subject: subject.slice(0, 200) || "(kein Betreff)", + receivedAt: msgDate, + action: "deleted", + }); + newlyBlocked++; + } + + // Permanently delete gambling mails from this folder + if (uidsToDelete.length > 0) { + try { + await imap.messageDelete(uidsToDelete.join(","), { uid: true }); + } catch { + try { + for (const uid of uidsToDelete) { + await imap + .messageFlagsAdd(uid, ["\\Deleted"], { uid: true }) + .catch(() => {}); + } + await imap.expunge(); + } catch { + /* ignore */ + } + } + } + + await insertMailBlocked(toInsert); + } finally { + lock.release(); + } + } + + await imap.logout(); + } catch { + try { + await imap.logout(); + } catch {} + } + + totalBlocked += newlyBlocked; + await updateMailConnectionScanStats( + connection.id, + scanned, + newlyBlocked, + connection.emailsBlocked, + connection.emailsScanned, + connection.scanInterval, + ); + } + + return { scanned: totalScanned, blocked: totalBlocked }; +}); diff --git a/backend/server/api/mail/status.get.ts b/backend/server/api/mail/status.get.ts new file mode 100644 index 0000000..0a604d7 --- /dev/null +++ b/backend/server/api/mail/status.get.ts @@ -0,0 +1,52 @@ +import { getMailConnections, getMailBlockedStats } from "../../db/mail"; + +/** + * GET /api/mail/status + * Gibt den Verbindungsstatus + Statistiken zurück. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const connections = await getMailConnections(user.id); + + const list = connections.map((c) => ({ + id: c.id, + email: c.email, + provider: c.providerName ?? "IMAP", + isActive: c.isActive, + lastScannedAt: c.lastScannedAt?.toISOString() ?? null, + nextScanAt: c.nextScanAt?.toISOString() ?? null, + totalBlocked: c.emailsBlocked, + totalScanned: c.emailsScanned, + scanInterval: c.scanInterval, + blockRate: + c.emailsScanned > 0 + ? Math.round((c.emailsBlocked / c.emailsScanned) * 100) + : 0, + })); + + const blocked7d = await getMailBlockedStats(user.id); + + const dailyMap: Record = {}; + for (const row of blocked7d) { + const day = row.createdAt.toISOString().slice(0, 10); + dailyMap[day] = (dailyMap[day] ?? 0) + 1; + } + const dailyStats = Array.from({ length: 7 }, (_, i) => { + const d = new Date(Date.now() - (6 - i) * 86_400_000); + const key = d.toISOString().slice(0, 10); + return { + date: key, + label: d.toLocaleDateString("de-DE", { weekday: "short" }), + count: dailyMap[key] ?? 0, + }; + }); + + return { + connected: list.length > 0, + accounts: list, + totalBlocked: list.reduce((s, c) => s + c.totalBlocked, 0), + totalScanned: list.reduce((s, c) => s + c.totalScanned, 0), + dailyStats, + }; +}); diff --git a/backend/server/api/notifications/[id].delete.ts b/backend/server/api/notifications/[id].delete.ts new file mode 100644 index 0000000..7a12a0b --- /dev/null +++ b/backend/server/api/notifications/[id].delete.ts @@ -0,0 +1,10 @@ +import { deleteNotification } from "../../db/notifications"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "Missing id" }); + + await deleteNotification(id, user.id); + return { ok: true }; +}); diff --git a/backend/server/api/notifications/index.get.ts b/backend/server/api/notifications/index.get.ts new file mode 100644 index 0000000..941d47e --- /dev/null +++ b/backend/server/api/notifications/index.get.ts @@ -0,0 +1,10 @@ +import { getNotifications, countUnread } from "../../db/notifications"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const [items, unread] = await Promise.all([ + getNotifications(user.id), + countUnread(user.id), + ]); + return { items, unread }; +}); diff --git a/backend/server/api/notifications/read.post.ts b/backend/server/api/notifications/read.post.ts new file mode 100644 index 0000000..e3ca004 --- /dev/null +++ b/backend/server/api/notifications/read.post.ts @@ -0,0 +1,7 @@ +import { markAllRead } from "../../db/notifications"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + await markAllRead(user.id); + return { ok: true }; +}); diff --git a/backend/server/api/protection/state.get.ts b/backend/server/api/protection/state.get.ts new file mode 100644 index 0000000..df947bb --- /dev/null +++ b/backend/server/api/protection/state.get.ts @@ -0,0 +1,51 @@ +import { requireUser } from "../../utils/auth"; +import { getActiveCooldown, resolveCooldown } from "../../db/cooldown"; +import { getProfile } from "../../db/profile"; + +/** + * GET /api/protection/state + * Combined protection + cooldown state polled every 30 s by the app. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const [cooldown, profile] = await Promise.all([ + getActiveCooldown(user.id), + getProfile(user.id), + ]); + + const now = new Date(); + let active = false; + let remainingSeconds = 0; + let cooldownEndsAt: string | null = null; + + if (cooldown) { + const expired = now >= cooldown.cooldownEndsAt; + if (expired) { + await resolveCooldown(cooldown.id); + // After resolve: no active cooldown + } else { + active = true; + remainingSeconds = Math.max( + 0, + Math.floor((cooldown.cooldownEndsAt.getTime() - now.getTime()) / 1000), + ); + cooldownEndsAt = cooldown.cooldownEndsAt.toISOString(); + } + } + + const plan = (profile?.plan ?? "free") as "free" | "pro" | "legend"; + + return { + success: true, + data: { + protectionShouldBeActive: !active, + cooldown: { + active, + remainingSeconds, + cooldownEndsAt, + }, + plan, + }, + }; +}); diff --git a/backend/server/api/providers/index.get.ts b/backend/server/api/providers/index.get.ts new file mode 100644 index 0000000..1718f4a --- /dev/null +++ b/backend/server/api/providers/index.get.ts @@ -0,0 +1,284 @@ +import { usePrisma } from "../../utils/prisma"; + +/** + * GET /api/providers + * Liefert die Liste der in Deutschland lizenzierten Glücksspiel-Anbieter (GGL-Whitelist). + * Quelle: Gemeinsame Glücksspielbehörde der Länder (ggl-behoerde.de) + * Diese Daten werden statisch gepflegt + aus der DB ergänzt. + */ + +// Statische GGL-lizenzierte Anbieter (Stand: 2026) +// Kategorie: sportwetten | casino | poker | slots +const GGL_PROVIDERS = [ + // Sportwetten-Lizenzen + { + name: "bet365", + domain: "bet365.com", + category: "sportwetten", + license: "GGL-SW", + risk: "hoch", + }, + { + name: "Tipico", + domain: "tipico.de", + category: "sportwetten", + license: "GGL-SW", + risk: "hoch", + }, + { + name: "bwin", + domain: "bwin.de", + category: "sportwetten", + license: "GGL-SW", + risk: "hoch", + }, + { + name: "Betano", + domain: "betano.de", + category: "sportwetten", + license: "GGL-SW", + risk: "hoch", + }, + { + name: "Betway", + domain: "betway.de", + category: "sportwetten", + license: "GGL-SW", + risk: "hoch", + }, + { + name: "ODDSET", + domain: "oddset.de", + category: "sportwetten", + license: "GGL-SW", + risk: "mittel", + }, + { + name: "Interwetten", + domain: "interwetten.de", + category: "sportwetten", + license: "GGL-SW", + risk: "mittel", + }, + { + name: "Unibet", + domain: "unibet.de", + category: "sportwetten", + license: "GGL-SW", + risk: "hoch", + }, + { + name: "888sport", + domain: "888sport.de", + category: "sportwetten", + license: "GGL-SW", + risk: "hoch", + }, + { + name: "Betsson", + domain: "betsson.com", + category: "sportwetten", + license: "GGL-SW", + risk: "mittel", + }, + { + name: "NEObet", + domain: "neobet.de", + category: "sportwetten", + license: "GGL-SW", + risk: "mittel", + }, + { + name: "AdmiralBet", + domain: "admiralbet.de", + category: "sportwetten", + license: "GGL-SW", + risk: "mittel", + }, + { + name: "Winamax", + domain: "winamax.de", + category: "sportwetten", + license: "GGL-SW", + risk: "mittel", + }, + { + name: "Merkur Sports", + domain: "merkursports.de", + category: "sportwetten", + license: "GGL-SW", + risk: "mittel", + }, + // Virtuelle Automatenspiele + { + name: "Merkur Slots", + domain: "merkur-slots.de", + category: "slots", + license: "GGL-VA", + risk: "sehr hoch", + }, + { + name: "Jackpotpiraten", + domain: "jackpotpiraten.de", + category: "slots", + license: "GGL-VA", + risk: "sehr hoch", + }, + { + name: "Jokerstar", + domain: "jokerstar.de", + category: "slots", + license: "GGL-VA", + risk: "sehr hoch", + }, + { + name: "BingBong", + domain: "bingbong.de", + category: "slots", + license: "GGL-VA", + risk: "sehr hoch", + }, + { + name: "Slotmagie", + domain: "slotmagie.de", + category: "slots", + license: "GGL-VA", + risk: "sehr hoch", + }, + { + name: "Drückglück", + domain: "drueckglueck.de", + category: "slots", + license: "GGL-VA", + risk: "sehr hoch", + }, + { + name: "Hyperino", + domain: "hyperino.de", + category: "slots", + license: "GGL-VA", + risk: "sehr hoch", + }, + { + name: "Löwen Play", + domain: "loewenplay.de", + category: "slots", + license: "GGL-VA", + risk: "sehr hoch", + }, + { + name: "Sunmaker", + domain: "sunmaker.de", + category: "slots", + license: "GGL-VA", + risk: "sehr hoch", + }, + // Poker + { + name: "PokerStars", + domain: "pokerstars.de", + category: "poker", + license: "GGL-PK", + risk: "hoch", + }, + { + name: "GGPoker", + domain: "ggpoker.de", + category: "poker", + license: "GGL-PK", + risk: "hoch", + }, + { + name: "888poker", + domain: "888poker.de", + category: "poker", + license: "GGL-PK", + risk: "hoch", + }, + // Illegale / häufig genutzte ohne DE-Lizenz + { + name: "Stake", + domain: "stake.com", + category: "casino", + license: "keine (Curaçao)", + risk: "extrem", + }, + { + name: "Rollbit", + domain: "rollbit.com", + category: "casino", + license: "keine", + risk: "extrem", + }, + { + name: "Roobet", + domain: "roobet.com", + category: "casino", + license: "keine (Curaçao)", + risk: "extrem", + }, + { + name: "Gamdom", + domain: "gamdom.com", + category: "casino", + license: "keine (Curaçao)", + risk: "extrem", + }, + { + name: "CSGORoll", + domain: "csgoroll.com", + category: "casino", + license: "keine", + risk: "extrem", + }, + { + name: "Duelbits", + domain: "duelbits.com", + category: "casino", + license: "keine (Curaçao)", + risk: "extrem", + }, +]; + +export default defineEventHandler(async (event) => { + const query = getQuery(event); + const category = query.category as string | undefined; + + let providers = [...GGL_PROVIDERS]; + + // Optionale DB-Erweiterung: community-gemeldete Anbieter + try { + const db = usePrisma(); + const data = await db.blocklistDomain.findMany({ + where: { isActive: true, reportCount: { gt: 5 } }, + select: { domain: true, source: true }, + take: 50, + }); + + for (const d of data) { + if (!providers.find((p) => p.domain === d.domain)) { + providers.push({ + name: d.domain.replace(/\.\w+$/, ""), + domain: d.domain, + category: "casino", + license: "unbekannt", + risk: "hoch", + }); + } + } + } catch { + // DB nicht erreichbar → nur statische Daten + } + + // Filter nach Kategorie + if (category && category !== "all") { + providers = providers.filter((p) => p.category === category); + } + + return { + total: providers.length, + providers, + source: "GGL Whitelist + Community Reports", + lastUpdated: "2026-04-01", + }; +}); diff --git a/backend/server/api/scores/leaderboard.get.ts b/backend/server/api/scores/leaderboard.get.ts new file mode 100644 index 0000000..a6057f8 --- /dev/null +++ b/backend/server/api/scores/leaderboard.get.ts @@ -0,0 +1,28 @@ +import { getLeaderboard } from "../../db/scores"; +import { getProfile } from "../../db/profile"; +import { getUsersMeta } from "../../utils/getUsersMeta"; + +// GET /api/scores/leaderboard – Top 50 mit Username +export default defineEventHandler(async (event) => { + const entries = await getLeaderboard(50); + const userIds = entries.map((e) => e.userId); + + const [profiles, metaMap] = await Promise.all([ + Promise.all(userIds.map((id) => getProfile(id))), + getUsersMeta(userIds), + ]); + const profileMap = new Map(profiles.filter(Boolean).map((p) => [p!.id, p!])); + + return entries.map((row, i) => { + const p = profileMap.get(row.userId); + const meta = metaMap[row.userId] ?? { nickname: null, avatar: null }; + return { + rank: i + 1, + user_id: row.userId, + username: p?.username ?? "Anonym", + avatar: meta.avatar ?? null, + total_points: row.totalPoints, + tier: row.tier, + }; + }); +}); diff --git a/backend/server/api/scores/me.get.ts b/backend/server/api/scores/me.get.ts new file mode 100644 index 0000000..086df80 --- /dev/null +++ b/backend/server/api/scores/me.get.ts @@ -0,0 +1,22 @@ +import { getUserScore, getRecentScoreEvents } from "../../db/scores"; + +// GET /api/scores/me – eigener Score + Tier + letzte Events +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const [score, events] = await Promise.all([ + getUserScore(user.id), + getRecentScoreEvents(user.id, 20), + ]); + + return { + total_points: score?.totalPoints ?? 0, + tier: score?.tier ?? "beginner", + recent_events: events.map((e) => ({ + event_type: e.eventType, + points: e.points, + created_at: e.createdAt, + meta: e.meta, + })), + }; +}); diff --git a/backend/server/api/social/follow.post.ts b/backend/server/api/social/follow.post.ts new file mode 100644 index 0000000..a877c75 --- /dev/null +++ b/backend/server/api/social/follow.post.ts @@ -0,0 +1,42 @@ +import { + getFollowRelation, + createFollow, + deleteFollow, + getProfileWithFollowers, +} from "../../db/social"; + +/** + * POST /api/social/follow + * Body: { userId } – Target-User, dem gefolgt werden soll + * Toggled Follow: follow wenn nicht gefolgt, unfollow wenn bereits gefolgt. + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const { userId } = (await readBody(event)) as { userId: string }; + + if (!userId) + throw createError({ statusCode: 400, message: "userId erforderlich" }); + if (userId === user.id) + throw createError({ + statusCode: 400, + message: "Du kannst dir selbst nicht folgen", + }); + + const existing = await getFollowRelation(user.id, userId); + let following: boolean; + + if (existing) { + await deleteFollow(user.id, userId); + following = false; + } else { + await createFollow(user.id, userId); + following = true; + } + + const profile = await getProfileWithFollowers(userId); + + return { + following, + followersCount: profile?.followersCount ?? 0, + }; +}); diff --git a/backend/server/api/social/profile/[userId].get.ts b/backend/server/api/social/profile/[userId].get.ts new file mode 100644 index 0000000..63d66f0 --- /dev/null +++ b/backend/server/api/social/profile/[userId].get.ts @@ -0,0 +1,71 @@ +import { getProfile } from "../../../db/profile"; +import { getUserScore } from "../../../db/scores"; +import { getFollowRelation } from "../../../db/social"; +import { getUsersMeta } from "../../../utils/getUsersMeta"; +import { usePrisma } from "../../../utils/prisma"; + +/** GET /api/social/profile/[userId] */ +export default defineEventHandler(async (event) => { + const targetUserId = getRouterParam(event, "userId"); + if (!targetUserId) + throw createError({ statusCode: 400, message: "userId fehlt" }); + + // Auth-User optional + let currentUserId: string | null = null; + try { + const u = await requireUser(event); + currentUserId = u.id; + } catch {} + + const [profile, score, followRelation, recentPosts, metaMap] = + await Promise.all([ + getProfile(targetUserId), + getUserScore(targetUserId), + currentUserId && currentUserId !== targetUserId + ? getFollowRelation(currentUserId, targetUserId) + : Promise.resolve(null), + usePrisma().communityPost.findMany({ + where: { userId: targetUserId, isModerated: false }, + orderBy: { createdAt: "desc" }, + take: 5, + select: { + id: true, + category: true, + content: true, + likesCount: true, + commentsCount: true, + createdAt: true, + }, + }), + getUsersMeta([targetUserId]), + ]); + + if (!profile) + throw createError({ statusCode: 404, message: "Profil nicht gefunden" }); + + const meta = metaMap[targetUserId] ?? { nickname: null, avatar: null }; + + return { + id: profile.id, + username: profile.username, + nickname: meta.nickname ?? profile.username, + avatar: meta.avatar, + bio: (profile as any).bio ?? null, + followersCount: profile.followersCount ?? 0, + followingCount: (profile as any).followingCount ?? 0, + postsCount: (profile as any).postsCount ?? 0, + tier: score?.tier ?? "beginner", + totalPoints: score?.totalPoints ?? 0, + isFollowing: !!followRelation, + isSelf: currentUserId === targetUserId, + joinedAt: profile.createdAt, + recentPosts: recentPosts.map((p) => ({ + id: p.id, + category: p.category, + content: p.content.slice(0, 120), + likesCount: p.likesCount ?? 0, + commentsCount: p.commentsCount ?? 0, + createdAt: p.createdAt, + })), + }; +}); diff --git a/backend/server/api/sos/session.post.ts b/backend/server/api/sos/session.post.ts new file mode 100644 index 0000000..4cf0336 --- /dev/null +++ b/backend/server/api/sos/session.post.ts @@ -0,0 +1,42 @@ +import { createSosSession } from "../../db/sosSession"; + +/** POST /api/sos/session — speichert kompletten SOS-Verlauf für DiGA-Auswertung */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + + if (!body || !Array.isArray(body.messages)) { + throw createError({ statusCode: 400, message: "messages required" }); + } + + // Hard limit gegen Spam: max 200 messages, max 1MB body + const messages = body.messages.slice(0, 200); + + const rating = + typeof body.feedbackRating === "number" + ? Math.max(1, Math.min(5, Math.floor(body.feedbackRating))) + : null; + + const session = await createSosSession(user.id, { + startedAt: body.startedAt, + endedAt: body.endedAt ?? new Date(), + durationSec: typeof body.durationSec === "number" ? body.durationSec : null, + messages, + gamesPlayed: Array.isArray(body.gamesPlayed) + ? body.gamesPlayed.slice(0, 20) + : [], + breathingCount: + typeof body.breathingCount === "number" ? body.breathingCount : 0, + wasOvercome: !!body.wasOvercome, + feedbackBetter: + typeof body.feedbackBetter === "boolean" ? body.feedbackBetter : null, + feedbackRating: rating, + feedbackText: + typeof body.feedbackText === "string" + ? body.feedbackText.slice(0, 1000) + : null, + locale: typeof body.locale === "string" ? body.locale.slice(0, 10) : null, + }); + + return { id: session.id }; +}); diff --git a/backend/server/api/streak/events.get.ts b/backend/server/api/streak/events.get.ts new file mode 100644 index 0000000..6b07820 --- /dev/null +++ b/backend/server/api/streak/events.get.ts @@ -0,0 +1,7 @@ +import { getStreakEvents } from "../../db/streak"; + +/** GET /api/streak/events – Streak-Verlauf abrufen */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + return getStreakEvents(user.id); +}); diff --git a/backend/server/api/streak/index.get.ts b/backend/server/api/streak/index.get.ts new file mode 100644 index 0000000..91a801d --- /dev/null +++ b/backend/server/api/streak/index.get.ts @@ -0,0 +1,7 @@ +import { getActiveStreak } from "../../db/streak"; + +/** GET /api/streak */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + return getActiveStreak(user.id); +}); diff --git a/backend/server/api/streak/index.patch.ts b/backend/server/api/streak/index.patch.ts new file mode 100644 index 0000000..49bc1c9 --- /dev/null +++ b/backend/server/api/streak/index.patch.ts @@ -0,0 +1,27 @@ +import { + getActiveStreak, + resetStreak, + updateStreakSavings, +} from "../../db/streak"; + +/** PATCH /api/streak – reset oder avg_monthly_savings aktualisieren */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + + const current = await getActiveStreak(user.id); + if (!current) + throw createError({ statusCode: 404, message: "Kein aktiver Streak" }); + + if (body.reset === true) { + const longest = Math.max(current.longestDays, current.currentDays); + const reason = body.reason ?? "manual"; + return resetStreak(current.id, longest, user.id, reason); + } + + if (body.avgMonthlySavings !== undefined) { + return updateStreakSavings(current.id, body.avgMonthlySavings); + } + + return current; +}); diff --git a/backend/server/api/streak/index.post.ts b/backend/server/api/streak/index.post.ts new file mode 100644 index 0000000..f5a9f8f --- /dev/null +++ b/backend/server/api/streak/index.post.ts @@ -0,0 +1,11 @@ +import { upsertStreak } from "../../db/streak"; + +/** POST /api/streak – Streak starten oder reaktivieren */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + return upsertStreak(user.id, { + avgMonthlySavings: body?.avgMonthlySavings, + startDate: body?.startDate ?? null, + }); +}); diff --git a/backend/server/api/stripe/checkout.post.ts b/backend/server/api/stripe/checkout.post.ts new file mode 100644 index 0000000..fb21210 --- /dev/null +++ b/backend/server/api/stripe/checkout.post.ts @@ -0,0 +1,70 @@ +import Stripe from "stripe"; + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + + if (!config.stripeSecretKey) { + throw createError({ + statusCode: 500, + message: "Stripe nicht konfiguriert – NUXT_STRIPE_SECRET_KEY fehlt", + }); + } + + const stripe = new Stripe(config.stripeSecretKey); + const user = await requireUser(event); + + const body = await readBody(event); + const plan = body?.plan as string; + const billing = (body?.billing as string) || "monthly"; + + // Aktive Pläne: free (kein Checkout), pro, legend (legend noch nicht aktiv – TODO: Stripe-Preise hinzufügen) + const activePlans = ["pro", "legend"]; + if (!plan || !activePlans.includes(plan)) { + throw createError({ statusCode: 400, message: "Ungültiger Plan" }); + } + if (!["monthly", "yearly"].includes(billing)) { + throw createError({ + statusCode: 400, + message: "Ungültiger Billing-Zyklus", + }); + } + + const priceEnvMap: Record> = { + pro: { + monthly: "STRIPE_PRICE_STANDARD_MONTHLY", + quarterly: "STRIPE_PRICE_STANDARD_QUARTERLY", + yearly: "STRIPE_PRICE_STANDARD_YEARLY", + }, + legend: { + monthly: "STRIPE_PRICE_PRO_MONTHLY", + quarterly: "STRIPE_PRICE_PRO_QUARTERLY", + yearly: "STRIPE_PRICE_PRO_YEARLY", + }, + }; + + const envKey = priceEnvMap[plan][billing]; + const priceId = process.env[envKey]; + + if (!priceId || !priceId.startsWith("price_")) { + throw createError({ + statusCode: 503, + message: `Dieser Plan ist noch nicht verfügbar. (${envKey} nicht gesetzt)`, + }); + } + + const appUrl = config.public.appUrl || "https://rebreak.org"; + + const session = await stripe.checkout.sessions.create({ + mode: "subscription", + line_items: [{ price: priceId, quantity: 1 }], + success_url: `${appUrl}/app/settings?upgraded=true`, + cancel_url: `${appUrl}/pricing`, + client_reference_id: user.id, + metadata: { + user_id: user.id, + plan, + }, + }); + + return { url: session.url }; +}); diff --git a/backend/server/api/stripe/portal.post.ts b/backend/server/api/stripe/portal.post.ts new file mode 100644 index 0000000..b7b90e9 --- /dev/null +++ b/backend/server/api/stripe/portal.post.ts @@ -0,0 +1,35 @@ +import Stripe from "stripe"; +import { usePrisma } from "../../utils/prisma"; + +/** + * POST /api/stripe/portal + * Erstellt eine Stripe Billing Portal Session (Abo verwalten/kündigen). + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const stripe = new Stripe(config.stripeSecretKey); + + const user = await requireUser(event); + + const db = usePrisma(); + const profile = await db.profile.findUnique({ + where: { id: user.id }, + select: { stripeCustomerId: true }, + }); + + if (!profile?.stripeCustomerId) { + throw createError({ + statusCode: 400, + message: "Kein aktives Abo gefunden", + }); + } + + const appUrl = config.public.appUrl || "https://rebreak.app"; + + const session = await stripe.billingPortal.sessions.create({ + customer: profile.stripeCustomerId, + return_url: `${appUrl}/app/settings`, + }); + + return { url: session.url }; +}); diff --git a/backend/server/api/stripe/webhook.post.ts b/backend/server/api/stripe/webhook.post.ts new file mode 100644 index 0000000..4dc932b --- /dev/null +++ b/backend/server/api/stripe/webhook.post.ts @@ -0,0 +1,103 @@ +import Stripe from "stripe"; +import { usePrisma } from "../../utils/prisma"; + +/** + * POST /api/stripe/webhook + * Stripe Webhook – verarbeitet Subscription-Events. + * Aktualisiert profiles.plan + stripe_* Felder. + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const stripe = new Stripe(config.stripeSecretKey); + + const body = await readRawBody(event); + const sig = getHeader(event, "stripe-signature"); + + if (!body || !sig) { + throw createError({ + statusCode: 400, + message: "Missing body or signature", + }); + } + + let stripeEvent: Stripe.Event; + try { + stripeEvent = stripe.webhooks.constructEvent( + body, + sig, + config.stripeWebhookSecret, + ); + } catch (err: any) { + throw createError({ + statusCode: 400, + message: `Webhook Error: ${err.message}`, + }); + } + + const db = usePrisma(); + + switch (stripeEvent.type) { + case "checkout.session.completed": { + const session = stripeEvent.data.object as Stripe.Checkout.Session; + const userId = session.metadata?.user_id || session.client_reference_id; + const plan = session.metadata?.plan || "legend"; + + if (userId) { + await db.profile.update({ + where: { id: userId }, + data: { + plan: + plan === "legend" ? "legend" : plan === "pro" ? "pro" : "free", + stripeCustomerId: session.customer as string, + stripeSubId: session.subscription as string, + }, + }); + } + break; + } + + case "customer.subscription.updated": { + const sub = stripeEvent.data.object as Stripe.Subscription; + const customerId = sub.customer as string; + + const profile = await db.profile.findFirst({ + where: { stripeCustomerId: customerId }, + select: { id: true, plan: true }, + }); + + if (profile) { + const isActive = ["active", "trialing"].includes(sub.status); + await db.profile.update({ + where: { id: profile.id }, + data: { + plan: isActive ? profile.plan : "free", + premiumUntil: sub.current_period_end + ? new Date(sub.current_period_end * 1000) + : null, + }, + }); + } + break; + } + + case "customer.subscription.deleted": { + const sub = stripeEvent.data.object as Stripe.Subscription; + const customerId = sub.customer as string; + + const profile = await db.profile.findFirst({ + where: { stripeCustomerId: customerId }, + select: { id: true }, + }); + + if (profile) { + await db.profile.update({ + where: { id: profile.id }, + data: { plan: "free", premiumUntil: null }, + }); + } + break; + } + } + + return { received: true }; +}); diff --git a/backend/server/api/urge/index.get.ts b/backend/server/api/urge/index.get.ts new file mode 100644 index 0000000..e7a6eea --- /dev/null +++ b/backend/server/api/urge/index.get.ts @@ -0,0 +1,9 @@ +import { getRecentUrgeLogs } from "../../db/urge"; + +/** GET /api/urge?limit=20 */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const query = getQuery(event); + const limit = Math.min(100, parseInt((query.limit as string) || "20")); + return getRecentUrgeLogs(user.id, limit); +}); diff --git a/backend/server/api/urge/index.post.ts b/backend/server/api/urge/index.post.ts new file mode 100644 index 0000000..9cacfae --- /dev/null +++ b/backend/server/api/urge/index.post.ts @@ -0,0 +1,42 @@ +import { createUrgeLog } from "../../db/urge"; +import { addStreakEvent } from "../../db/streak"; + +type Emotion = "stress" | "sadness" | "anger" | "empty" | "boredom" | "other"; +const VALID_EMOTIONS: Emotion[] = [ + "stress", + "sadness", + "anger", + "empty", + "boredom", + "other", +]; + +/** POST /api/urge */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + const { emotion, wasOvercome, breathingDone } = body ?? {}; + + if (!VALID_EMOTIONS.includes(emotion)) { + throw createError({ statusCode: 400, message: "Ungültige Emotion" }); + } + + const log = await createUrgeLog( + user.id, + emotion, + !!wasOvercome, + !!breathingDone, + ); + + // StreakEvent loggen + if (wasOvercome) { + await addStreakEvent(user.id, "milestone", { + type: "urge_overcome", + emotion, + }); + } else { + await addStreakEvent(user.id, "relapse", { emotion }); + } + + return log; +}); diff --git a/backend/server/api/url-filter/blocklist.bin.get.ts b/backend/server/api/url-filter/blocklist.bin.get.ts new file mode 100644 index 0000000..9348b64 --- /dev/null +++ b/backend/server/api/url-filter/blocklist.bin.get.ts @@ -0,0 +1,109 @@ +import { + getActiveBlocklistDomains, + getUserCustomDomains, +} from "../../db/domains"; +import { getProfile } from "../../db/profile"; +import { getPlanLimits } from "../../utils/plan-features"; +import { buildHashListBinary, etagFor } from "../../utils/domainHash"; + +/** + * GET /api/url-filter/blocklist.bin + * + * Liefert die Blocklist als sortierte Binary-Hash-Liste (8 Bytes pro Hash, + * big-endian, sorted ascending). Die iOS-Extension memory-mapped diese Datei + * und macht binary-search bei jedem Browser-Flow. + * + * Privacy: + * - Server schickt NUR Hashes, keine Klartext-Domains an die App + * - On-Disk in App-Group: nur Hash-Bytes, kein Klartext (forensik-resistent) + * + * Plan-aware: + * - free: KEIN global-blocklist, NUR Custom-Domains gehasht + * - pro/legend: global HaGeZi-List + Custom-Domains beide gehasht + * + * Caching: + * - ETag = sha256 des Binary-Inhalts (16 hex chars) + * - Cache-Control: private, max-age=300 + * - Bei If-None-Match: 304 Not Modified + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const [profile, customDomains, globalDomains] = await Promise.all([ + getProfile(user.id), + getUserCustomDomains(user.id), + // Global nur lazy laden wenn Plan es erlaubt — spart RAM bei Free-Usern + Promise.resolve(null), + ]); + + const limits = getPlanLimits(profile?.plan ?? "free"); + + // Global Domains nur für Pro/Legend + const global = limits.globalBlocklist ? await getActiveBlocklistDomains() : []; + + // Beide Listen ohne Salt hashen — vereinfachte Architektur: + // Server kennt die Klartext-Domains eh (via DB), darum bringt User-Salt + // praktisch keinen Privacy-Vorteil. Vorteil: Extension kann mit einer + // Hash-Funktion alle Hosts prüfen. + const customBuf = buildHashListBinary(customDomains.map((d) => d.domain)); + const globalBuf = buildHashListBinary(global.map((d) => d.domain)); + + // Merged sorted binary (Extension macht eine binary-search über alles). + // Wichtig: doppelt sortieren weil Custom + Global zwei separate sorted + // arrays sind — wir mergen sie zu einem sorted array. + const combined = mergeSortedHashBuffers(customBuf, globalBuf); + + const etag = etagFor(combined); + + // Conditional GET — bei If-None-Match returns 304 + const ifNoneMatch = getHeader(event, "if-none-match"); + if (ifNoneMatch === etag) { + setResponseStatus(event, 304); + setResponseHeader(event, "etag", etag); + return null; + } + + setResponseHeader(event, "content-type", "application/octet-stream"); + setResponseHeader(event, "etag", etag); + setResponseHeader(event, "cache-control", "private, max-age=300"); + setResponseHeader(event, "x-rebreak-count", String(combined.length / 8)); + setResponseHeader(event, "x-rebreak-plan", profile?.plan ?? "free"); + + return combined; +}); + +/** + * Merged zwei sortierte 8-byte-hash-Buffer in ein sortiertes Output. + * In-place merge sort. + */ +function mergeSortedHashBuffers(a: Buffer, b: Buffer): Buffer { + const result = Buffer.alloc(a.length + b.length); + let ai = 0; + let bi = 0; + let ri = 0; + + while (ai < a.length && bi < b.length) { + const av = a.readBigUInt64BE(ai); + const bv = b.readBigUInt64BE(bi); + if (av <= bv) { + result.writeBigUInt64BE(av, ri); + ai += 8; + } else { + result.writeBigUInt64BE(bv, ri); + bi += 8; + } + ri += 8; + } + while (ai < a.length) { + result.writeBigUInt64BE(a.readBigUInt64BE(ai), ri); + ai += 8; + ri += 8; + } + while (bi < b.length) { + result.writeBigUInt64BE(b.readBigUInt64BE(bi), ri); + bi += 8; + ri += 8; + } + + return result.subarray(0, ri); +} diff --git a/backend/server/api/user/delete.delete.ts b/backend/server/api/user/delete.delete.ts new file mode 100644 index 0000000..8bcfe4b --- /dev/null +++ b/backend/server/api/user/delete.delete.ts @@ -0,0 +1,36 @@ +import { serverSupabaseServiceRole } from "../../utils/useSupabase"; +import { deleteUserUrgeLogs } from "../../db/urge"; +import { deleteUserSosSessions } from "../../db/sosSession"; +import { deleteUserStreaks } from "../../db/streak"; +import { deleteUserPosts } from "../../db/community"; +import { deleteAllUserCustomDomains } from "../../db/domains"; +import { + deleteUserTrustedContacts, + deleteUserCoachSessions, +} from "../../db/user"; +import { deleteProfile } from "../../db/profile"; + +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const supabase = serverSupabaseServiceRole(event); + const userId = user.id; + + // Delete all user data (DSGVO Art. 17) + await Promise.all([ + deleteUserUrgeLogs(userId), + deleteUserSosSessions(userId), + deleteUserStreaks(userId), + deleteUserPosts(userId), + deleteAllUserCustomDomains(userId), + deleteUserTrustedContacts(userId), + deleteUserCoachSessions(userId), + ]); + + // Profil zuletzt löschen (FK-Abhängigkeiten sind bereits entfernt) + await deleteProfile(userId).catch(() => {}); + + // Auth-User löschen (bleibt Supabase) + await supabase.auth.admin.deleteUser(userId); + + return { success: true }; +}); diff --git a/backend/server/db/chat-rooms.ts b/backend/server/db/chat-rooms.ts new file mode 100644 index 0000000..b4a4586 --- /dev/null +++ b/backend/server/db/chat-rooms.ts @@ -0,0 +1,366 @@ +import { usePrisma } from "../utils/prisma"; +import { randomBytes } from "crypto"; + +// ─── Rooms ──────────────────────────────────────────────────────────────────── + +export async function listRooms(userId: string) { + const db = usePrisma(); + // Public rooms + private rooms user is member of + return db.chatRoom.findMany({ + where: { + OR: [ + { isPublic: true }, + { members: { some: { userId, status: "active" } } }, + ], + }, + orderBy: { updatedAt: "desc" }, + include: { + // Only include current user's membership to correctly determine isMember/myRole + members: { + where: { userId, status: "active" }, + select: { userId: true, role: true }, + }, + messages: { + orderBy: { createdAt: "desc" }, + take: 1, + select: { content: true, createdAt: true, userId: true }, + }, + }, + }); +} + +export async function getRoom(roomId: string) { + const db = usePrisma(); + return db.chatRoom.findUnique({ + where: { id: roomId }, + include: { + members: { + where: { status: "active" }, + select: { userId: true, role: true, joinedAt: true }, + }, + }, + }); +} + +export async function createRoom(data: { + name: string; + description?: string; + isPublic: boolean; + joinMode: string; + createdBy: string; + avatarUrl?: string; +}) { + const db = usePrisma(); + const inviteCode = randomBytes(4).toString("hex"); + return db.chatRoom.create({ + data: { + name: data.name, + description: data.description, + isPublic: data.isPublic, + joinMode: data.joinMode, + inviteCode, + createdBy: data.createdBy, + avatarUrl: data.avatarUrl, + memberCount: 1, + members: { + create: { userId: data.createdBy, role: "owner", status: "active" }, + }, + }, + }); +} + +export async function updateRoom( + roomId: string, + data: { + name?: string; + description?: string; + joinMode?: string; + avatarUrl?: string | null; + }, +) { + const db = usePrisma(); + return db.chatRoom.update({ where: { id: roomId }, data }); +} + +export async function deleteRoom(roomId: string) { + const db = usePrisma(); + return db.chatRoom.delete({ where: { id: roomId } }); +} + +// ─── Membership ─────────────────────────────────────────────────────────────── + +export async function getMember(roomId: string, userId: string) { + const db = usePrisma(); + return db.chatRoomMember.findUnique({ + where: { roomId_userId: { roomId, userId } }, + }); +} + +export async function getRoomMembers(roomId: string) { + const db = usePrisma(); + return db.chatRoomMember.findMany({ + where: { roomId, status: "active" }, + orderBy: { joinedAt: "asc" }, + select: { userId: true, role: true, joinedAt: true }, + }); +} + +export async function joinRoom( + roomId: string, + userId: string, + status: "active" | "pending" = "active", +) { + const db = usePrisma(); + const member = await db.chatRoomMember.upsert({ + where: { roomId_userId: { roomId, userId } }, + create: { roomId, userId, role: "member", status }, + update: { status }, + }); + if (status === "active") { + await db.chatRoom.update({ + where: { id: roomId }, + data: { memberCount: { increment: 1 } }, + }); + } + return member; +} + +export async function leaveRoom(roomId: string, userId: string) { + const db = usePrisma(); + await db.chatRoomMember.delete({ + where: { roomId_userId: { roomId, userId } }, + }); + await db.chatRoom.update({ + where: { id: roomId }, + data: { memberCount: { decrement: 1 } }, + }); +} + +export async function approveRequest(roomId: string, userId: string) { + const db = usePrisma(); + await db.chatRoomMember.update({ + where: { roomId_userId: { roomId, userId } }, + data: { status: "active" }, + }); + await db.chatRoom.update({ + where: { id: roomId }, + data: { memberCount: { increment: 1 } }, + }); +} + +export async function rejectRequest(roomId: string, userId: string) { + const db = usePrisma(); + await db.chatRoomMember.delete({ + where: { roomId_userId: { roomId, userId } }, + }); +} + +export async function getPendingRequests(roomId: string) { + const db = usePrisma(); + return db.chatRoomMember.findMany({ + where: { roomId, status: "pending" }, + orderBy: { joinedAt: "asc" }, + select: { userId: true, joinedAt: true }, + }); +} + +export async function findRoomByInviteCode(code: string) { + const db = usePrisma(); + return db.chatRoom.findUnique({ where: { inviteCode: code } }); +} + +export async function banMember(roomId: string, userId: string) { + const db = usePrisma(); + const member = await db.chatRoomMember.findUnique({ + where: { roomId_userId: { roomId, userId } }, + }); + if (!member) return; + await db.chatRoomMember.update({ + where: { roomId_userId: { roomId, userId } }, + data: { status: "banned" }, + }); + if (member.status === "active") { + await db.chatRoom.update({ + where: { id: roomId }, + data: { memberCount: { decrement: 1 } }, + }); + } +} + +export async function setMemberRole( + roomId: string, + userId: string, + role: "admin" | "member", +) { + const db = usePrisma(); + return db.chatRoomMember.update({ + where: { roomId_userId: { roomId, userId } }, + data: { role }, + }); +} + +// ─── Room Messages ──────────────────────────────────────────────────────────── + +export async function getRoomMessages( + roomId: string, + cursor?: string, + limit = 50, +) { + const db = usePrisma(); + return db.chatMessage.findMany({ + where: { roomId }, + orderBy: { createdAt: "desc" }, + take: limit, + ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}), + select: { + id: true, + userId: true, + content: true, + replyToId: true, + attachmentUrl: true, + attachmentType: true, + attachmentName: true, + likesCount: true, + createdAt: true, + replyTo: { + select: { id: true, userId: true, content: true }, + }, + }, + }); +} + +export async function createRoomMessage(data: { + userId: string; + roomId: string; + content: string; + replyToId?: string; + attachmentUrl?: string; + attachmentType?: string; + attachmentName?: string; +}) { + const db = usePrisma(); + const msg = await db.chatMessage.create({ + data: { + userId: data.userId, + content: data.content, + roomId: data.roomId, + replyToId: data.replyToId || null, + attachmentUrl: data.attachmentUrl || null, + attachmentType: data.attachmentType || null, + attachmentName: data.attachmentName || null, + }, + select: { + id: true, + userId: true, + content: true, + replyToId: true, + attachmentUrl: true, + attachmentType: true, + attachmentName: true, + likesCount: true, + createdAt: true, + replyTo: { + select: { id: true, userId: true, content: true }, + }, + }, + }); + // Bump room updatedAt + await db.chatRoom.update({ + where: { id: data.roomId }, + data: { updatedAt: new Date() }, + }); + return msg; +} + +// ─── Likes ──────────────────────────────────────────────────────────────────── + +export async function toggleChatMessageLike(userId: string, messageId: string) { + const db = usePrisma(); + const existing = await db.chatMessageLike.findUnique({ + where: { userId_messageId: { userId, messageId } }, + }); + if (existing) { + await db.chatMessageLike.delete({ + where: { userId_messageId: { userId, messageId } }, + }); + await db.chatMessage.update({ + where: { id: messageId }, + data: { likesCount: { decrement: 1 } }, + }); + return false; + } + await db.chatMessageLike.create({ data: { userId, messageId } }); + await db.chatMessage.update({ + where: { id: messageId }, + data: { likesCount: { increment: 1 } }, + }); + return true; +} + +export async function toggleDmLike(userId: string, messageId: string) { + const db = usePrisma(); + const existing = await db.directMessageLike.findUnique({ + where: { userId_messageId: { userId, messageId } }, + }); + if (existing) { + await db.directMessageLike.delete({ + where: { userId_messageId: { userId, messageId } }, + }); + await db.directMessage.update({ + where: { id: messageId }, + data: { likesCount: { decrement: 1 } }, + }); + return false; + } + await db.directMessageLike.create({ data: { userId, messageId } }); + await db.directMessage.update({ + where: { id: messageId }, + data: { likesCount: { increment: 1 } }, + }); + return true; +} + +// ─── Seed Default Groups ────────────────────────────────────────────────────── + +const SYSTEM_USER = "00000000-0000-0000-0000-000000000000"; + +const DEFAULT_ROOMS = [ + { + name: "Erfolge & Meilensteine", + description: "Teile deine Fortschritte und feiere mit der Community.", + isPublic: true, + }, + { + name: "Gemeinsam stark", + description: "Der offene Raum – Austausch, Motivation und Zusammenhalt.", + isPublic: true, + }, +]; + +export async function seedDefaultRooms() { + const db = usePrisma(); + const existing = await db.chatRoom.findMany({ + where: { isDefault: true }, + select: { id: true }, + }); + if (existing.length >= DEFAULT_ROOMS.length) return; + + for (const room of DEFAULT_ROOMS) { + const exists = await db.chatRoom.findFirst({ + where: { name: room.name, isDefault: true }, + }); + if (exists) continue; + await db.chatRoom.create({ + data: { + name: room.name, + description: room.description, + isPublic: true, + isDefault: true, + joinMode: "open", + createdBy: SYSTEM_USER, + inviteCode: randomBytes(4).toString("hex"), + memberCount: 0, + }, + }); + } +} diff --git a/backend/server/db/chat.ts b/backend/server/db/chat.ts new file mode 100644 index 0000000..eedf993 --- /dev/null +++ b/backend/server/db/chat.ts @@ -0,0 +1,139 @@ +import { usePrisma } from "../utils/prisma"; + +// ─── Gruppen-Chat ───────────────────────────────────────────────────────────── + +export async function getChatMessages(limit = 100) { + const db = usePrisma(); + return db.chatMessage.findMany({ + where: { roomId: null }, + orderBy: { createdAt: "asc" }, + take: limit, + select: { id: true, content: true, createdAt: true, userId: true }, + }); +} + +export async function createChatMessage(userId: string, content: string) { + const db = usePrisma(); + return db.chatMessage.create({ + data: { userId, content, roomId: null }, + select: { id: true, content: true, createdAt: true, userId: true }, + }); +} + +// ─── Direktnachrichten ─────────────────────────────────────────────────────── + +export async function sendDirectMessage( + senderId: string, + receiverId: string, + content: string, + opts?: { + replyToId?: string; + attachmentUrl?: string; + attachmentType?: string; + attachmentName?: string; + }, +) { + const db = usePrisma(); + return db.directMessage.create({ + data: { + senderId, + receiverId, + content, + replyToId: opts?.replyToId || null, + attachmentUrl: opts?.attachmentUrl || null, + attachmentType: opts?.attachmentType || null, + attachmentName: opts?.attachmentName || null, + }, + select: { + id: true, + content: true, + createdAt: true, + replyToId: true, + attachmentUrl: true, + attachmentType: true, + attachmentName: true, + likesCount: true, + replyTo: { + select: { id: true, senderId: true, content: true }, + }, + }, + }); +} + +export async function getDmHistory( + userId: string, + partnerId: string, + page = 1, + limit = 50, +) { + const db = usePrisma(); + const offset = (page - 1) * limit; + return db.directMessage.findMany({ + where: { + OR: [ + { senderId: userId, receiverId: partnerId }, + { senderId: partnerId, receiverId: userId }, + ], + }, + orderBy: { createdAt: "desc" }, + skip: offset, + take: limit, + select: { + id: true, + senderId: true, + receiverId: true, + content: true, + createdAt: true, + readAt: true, + replyToId: true, + attachmentUrl: true, + attachmentType: true, + attachmentName: true, + likesCount: true, + replyTo: { + select: { id: true, senderId: true, content: true }, + }, + }, + }); +} + +export async function markDmsAsRead(senderId: string, receiverId: string) { + const db = usePrisma(); + return db.directMessage.updateMany({ + where: { senderId, receiverId, readAt: null }, + data: { readAt: new Date() }, + }); +} + +export async function getDmConversations(userId: string) { + const db = usePrisma(); + // Alle DMs als Sender oder Empfänger, neueste zuerst + return db.directMessage.findMany({ + where: { + OR: [{ senderId: userId }, { receiverId: userId }], + }, + orderBy: { createdAt: "desc" }, + take: 500, + select: { + id: true, + senderId: true, + receiverId: true, + content: true, + createdAt: true, + readAt: true, + }, + }); +} + +export async function countUnreadDms(receiverId: string) { + const db = usePrisma(); + const rows = await db.directMessage.findMany({ + where: { receiverId, readAt: null }, + select: { senderId: true }, + }); + const byPartner: Record = {}; + for (const r of rows) { + byPartner[r.senderId] = (byPartner[r.senderId] ?? 0) + 1; + } + return byPartner; +} diff --git a/backend/server/db/community.ts b/backend/server/db/community.ts new file mode 100644 index 0000000..84dee20 --- /dev/null +++ b/backend/server/db/community.ts @@ -0,0 +1,434 @@ +import { usePrisma } from "../utils/prisma"; + +// ─── Posts ──────────────────────────────────────────────────────────────────── + +export async function getPosts( + category: string, + page: number, + limit: number, + currentUserId: string | null, + filterUserId?: string | null, +) { + const db = usePrisma(); + const offset = (page - 1) * limit; + + const where: any = { isModerated: false }; + if (category !== "all") { + if (category === "games") { + where.category = { in: ["game_share", "challenge"] }; + } else { + where.category = category; + } + } + if (filterUserId) { + where.userId = filterUserId; + } + + const posts = await db.communityPost.findMany({ + where, + orderBy: { createdAt: "desc" }, + skip: offset, + take: limit, + include: { + author: { + select: { + id: true, + username: true, + nickname: true, + avatar: true, + plan: true, + }, + }, + repostOf: { + include: { + author: { + select: { + id: true, + username: true, + nickname: true, + avatar: true, + plan: true, + }, + }, + }, + }, + }, + }); + + // Batch: UserScore (tier) für alle Autoren laden + const authorUserIds = [ + ...new Set( + posts + .flatMap((p) => [p.userId, p.repostOf?.userId]) + .filter((id): id is string => !!id), + ), + ]; + let userScores: Record = {}; + if (authorUserIds.length > 0) { + const scores = await db.userScore.findMany({ + where: { userId: { in: authorUserIds } }, + select: { userId: true, tier: true }, + }); + for (const s of scores) userScores[s.userId] = s.tier; + } + + // Eigene Likes laden wenn eingeloggt + let userLikes: Record = {}; + if (currentUserId && posts.length > 0) { + const postIds = posts.map((p) => p.id); + const likes = await db.postLike.findMany({ + where: { userId: currentUserId, postId: { in: postIds } }, + select: { postId: true, type: true }, + }); + for (const l of likes) { + userLikes[l.postId] = l.type as "like" | "dislike"; + } + } + + // Challenge-Status für Challenge-Posts laden + const challengeIds = posts + .map((p) => (p as any).challengeId) + .filter((id): id is string => !!id); + let challengeStatuses: Record< + string, + { + status: string; + opponentId: string | null; + opponentName: string | null; + gameType: string | null; + isLive: boolean; + } + > = {}; + if (challengeIds.length > 0) { + const challenges = await db.gameChallenge.findMany({ + where: { id: { in: challengeIds } }, + select: { + id: true, + status: true, + opponentId: true, + opponentName: true, + gameType: true, + isLive: true, + }, + }); + for (const c of challenges) { + challengeStatuses[c.id] = { + status: c.status, + opponentId: c.opponentId, + opponentName: (c as any).opponentName ?? null, + gameType: (c as any).gameType ?? null, + isLive: (c as any).isLive ?? false, + }; + } + } + + // Domain-Submission-Daten für domain_vote Posts laden + const domainVotePostIds = posts + .filter((p) => p.category === "domain_vote") + .map((p) => p.id); + type SubEntry = { + id: string; + domain: string; + yesVotes: number; + noVotes: number; + status: string; + reviewedAt: Date | null; + }; + let domainSubmissions: Record = {}; + let userDomainVotes: Record = {}; + let submissionVoters: Record< + string, + { + yes: { id: string; nickname: string; avatar: string | null }[]; + no: { id: string; nickname: string; avatar: string | null }[]; + } + > = {}; + + if (domainVotePostIds.length > 0) { + const subs = await db.domainSubmission.findMany({ + where: { postId: { in: domainVotePostIds } }, + select: { + id: true, + postId: true, + domain: true, + yesVotes: true, + noVotes: true, + status: true, + reviewedAt: true, + }, + }); + for (const s of subs) { + if (s.postId) + domainSubmissions[s.postId] = { + id: s.id, + domain: s.domain, + yesVotes: s.yesVotes, + noVotes: s.noVotes, + status: s.status, + reviewedAt: s.reviewedAt ?? null, + }; + } + if (currentUserId && subs.length > 0) { + const votes = await db.domainVote.findMany({ + where: { + userId: currentUserId, + submissionId: { in: subs.map((s) => s.id) }, + }, + select: { submissionId: true, vote: true }, + }); + const subIdToPostId = Object.fromEntries( + Object.entries(domainSubmissions).map(([pid, s]) => [s.id, pid]), + ); + for (const v of votes) { + const pid = subIdToPostId[v.submissionId]; + if (pid) userDomainVotes[pid] = v.vote as "yes" | "no"; + } + } + + // Batch: DomainVote voters for domain submissions + const submissionIds = Object.values(domainSubmissions) + .map((s) => s.id) + .filter(Boolean); + if (submissionIds.length > 0) { + const votes = await db.domainVote.findMany({ + where: { submissionId: { in: submissionIds } }, + select: { submissionId: true, vote: true, userId: true }, + }); + const voterIds = [...new Set(votes.map((v) => v.userId))]; + let voterProfiles: Record< + string, + { nickname: string | null; avatar: string | null } + > = {}; + if (voterIds.length > 0) { + const profiles = await db.profile.findMany({ + where: { id: { in: voterIds } }, + select: { id: true, nickname: true, avatar: true }, + }); + for (const p of profiles) + voterProfiles[p.id] = { nickname: p.nickname, avatar: p.avatar }; + } + for (const v of votes) { + if (!submissionVoters[v.submissionId]) + submissionVoters[v.submissionId] = { yes: [], no: [] }; + const profile = voterProfiles[v.userId]; + const voter = { + id: v.userId, + nickname: profile?.nickname ?? "Nutzer", + avatar: profile?.avatar ?? null, + }; + if (v.vote === "yes") submissionVoters[v.submissionId].yes.push(voter); + else submissionVoters[v.submissionId].no.push(voter); + } + } + } + + return { + posts, + userLikes, + challengeStatuses, + domainSubmissions, + userDomainVotes, + userScores, + submissionVoters, + }; +} + +export async function getPostById(postId: string) { + const db = usePrisma(); + return db.communityPost.findUnique({ + where: { id: postId }, + include: { + author: { + select: { + id: true, + username: true, + nickname: true, + avatar: true, + plan: true, + }, + }, + }, + }); +} + +export async function createPost( + userId: string, + category: string, + content: string, + imageUrl?: string, +) { + const db = usePrisma(); + return db.communityPost.create({ + data: { + userId, + category, + content, + imageUrl: imageUrl || null, + isAnonymous: false, + isModerated: false, + }, + include: { + author: { + select: { + id: true, + username: true, + nickname: true, + avatar: true, + plan: true, + }, + }, + }, + }); +} + +export async function deleteUserPosts(userId: string) { + const db = usePrisma(); + return db.communityPost.deleteMany({ where: { userId } }); +} + +// ─── Likes ─────────────────────────────────────────────────────────────────── + +export async function getPostLike(userId: string, postId: string) { + const db = usePrisma(); + return db.postLike.findUnique({ + where: { userId_postId: { userId, postId } }, + select: { type: true }, + }); +} + +export async function setPostLike( + userId: string, + postId: string, + type: "like" | "dislike", +) { + const db = usePrisma(); + return db.postLike.upsert({ + where: { userId_postId: { userId, postId } }, + create: { userId, postId, type }, + update: { type }, + }); +} + +export async function deletePostLike(userId: string, postId: string) { + const db = usePrisma(); + return db.postLike.delete({ + where: { userId_postId: { userId, postId } }, + }); +} + +export async function countPostLikes(postId: string) { + const db = usePrisma(); + const counts = await db.postLike.groupBy({ + by: ["type"], + where: { postId }, + _count: { type: true }, + }); + const likes = counts.find((c) => c.type === "like")?._count.type ?? 0; + const dislikes = counts.find((c) => c.type === "dislike")?._count.type ?? 0; + return { likes, dislikes }; +} + +export async function syncPostLikeCounts( + postId: string, + likes: number, + dislikes: number, +) { + const db = usePrisma(); + return db.communityPost.update({ + where: { id: postId }, + data: { likesCount: likes, dislikesCount: dislikes }, + }); +} + +// ─── Comments (replies) ────────────────────────────────────────────────────── + +export async function getCommentsByPost( + postId: string, + currentUserId: string | null, +) { + const db = usePrisma(); + const comments = await db.communityReply.findMany({ + where: { postId }, + orderBy: { createdAt: "asc" }, + take: 200, + include: { + author: { + select: { id: true, username: true, nickname: true, avatar: true }, + }, + }, + }); + + let userLikes = new Set(); + if (currentUserId && comments.length > 0) { + const commentIds = comments.map((c) => c.id); + const likes = await db.commentLike.findMany({ + where: { userId: currentUserId, commentId: { in: commentIds } }, + select: { commentId: true }, + }); + for (const l of likes) { + userLikes.add(l.commentId); + } + } + + return { comments, userLikes }; +} + +export async function createComment( + userId: string, + postId: string, + content: string, + parentReplyId: string | null, +) { + const db = usePrisma(); + const [reply] = await Promise.all([ + db.communityReply.create({ + data: { userId, postId, content, parentReplyId, isAnonymous: false }, + select: { + id: true, + content: true, + createdAt: true, + likesCount: true, + parentReplyId: true, + }, + }), + db.communityPost.update({ + where: { id: postId }, + data: { commentsCount: { increment: 1 } }, + }), + ]); + return reply; +} + +// ─── Comment Likes ──────────────────────────────────────────────────────────── + +export async function getCommentLike(userId: string, commentId: string) { + const db = usePrisma(); + return db.commentLike.findUnique({ + where: { userId_commentId: { userId, commentId } }, + }); +} + +export async function createCommentLike(userId: string, commentId: string) { + const db = usePrisma(); + return db.commentLike.create({ data: { userId, commentId } }); +} + +export async function deleteCommentLike(userId: string, commentId: string) { + const db = usePrisma(); + return db.commentLike.delete({ + where: { userId_commentId: { userId, commentId } }, + }); +} + +export async function getCommentLikeCount(commentId: string) { + const db = usePrisma(); + return db.commentLike.count({ where: { commentId } }); +} + +export async function syncCommentLikeCount(commentId: string, count: number) { + const db = usePrisma(); + return db.communityReply.update({ + where: { id: commentId }, + data: { likesCount: count }, + }); +} diff --git a/backend/server/db/cooldown.ts b/backend/server/db/cooldown.ts new file mode 100644 index 0000000..35de545 --- /dev/null +++ b/backend/server/db/cooldown.ts @@ -0,0 +1,51 @@ +import { usePrisma } from "../utils/prisma"; + +/** + * Returns the active CooldownRequest for a user: + * not resolved, not cancelled, cooldownEndsAt in the future. + * (Expired but not yet resolved entries are also returned so the caller can resolve them.) + */ +export async function getActiveCooldown(userId: string) { + const db = usePrisma(); + return db.cooldownRequest.findFirst({ + where: { + userId, + resolvedAt: null, + cancelledAt: null, + }, + orderBy: { cooldownStartedAt: "desc" }, + }); +} + +export async function createCooldown( + userId: string, + jti: string, + cooldownEndsAt: Date, + reason?: string, +) { + const db = usePrisma(); + return db.cooldownRequest.create({ + data: { + userId, + reason: reason ?? null, + cooldownEndsAt, + tokenJti: jti, + }, + }); +} + +export async function resolveCooldown(id: string) { + const db = usePrisma(); + return db.cooldownRequest.update({ + where: { id }, + data: { resolvedAt: new Date() }, + }); +} + +export async function cancelCooldown(id: string) { + const db = usePrisma(); + return db.cooldownRequest.update({ + where: { id }, + data: { cancelledAt: new Date() }, + }); +} diff --git a/backend/server/db/devices.ts b/backend/server/db/devices.ts new file mode 100644 index 0000000..60474df --- /dev/null +++ b/backend/server/db/devices.ts @@ -0,0 +1,146 @@ +import { usePrisma } from "../utils/prisma"; + +/** + * Device-Binding pro User. Free=1, Pro=1, Legend=3 (siehe plan-features.maxDevices). + * deviceId kommt vom Frontend via Capacitor Device.getId() (persistent UUID). + */ + +export interface DeviceRecord { + id: string; + deviceId: string; + platform: string; + model: string | null; + name: string | null; + lastSeenAt: Date; + createdAt: Date; +} + +/** Liste aller Devices eines Users, aktuellstes zuerst. */ +export async function listUserDevices(userId: string): Promise { + const db = usePrisma(); + return db.userDevice.findMany({ + where: { userId }, + orderBy: { lastSeenAt: "desc" }, + select: { + id: true, + deviceId: true, + platform: true, + model: true, + name: true, + lastSeenAt: true, + createdAt: true, + }, + }); +} + +/** Gibt das Device zurück wenn registriert; sonst null. */ +export async function findUserDevice( + userId: string, + deviceId: string, +): Promise { + const db = usePrisma(); + return db.userDevice.findUnique({ + where: { userId_deviceId: { userId, deviceId } }, + select: { + id: true, + deviceId: true, + platform: true, + model: true, + name: true, + lastSeenAt: true, + createdAt: true, + }, + }); +} + +/** + * Idempotente Registrierung. Wenn Device bereits existiert: Touch lastSeenAt. + * Wenn nicht existiert UND Limit erreicht: throw mit Liste der existierenden Devices. + */ +export async function registerDevice(opts: { + userId: string; + deviceId: string; + platform: string; + model?: string | null; + name?: string | null; + maxDevices: number; +}): Promise<{ + device: DeviceRecord; + created: boolean; +}> { + const db = usePrisma(); + + // Idempotent: existiert das Device schon? + const existing = await findUserDevice(opts.userId, opts.deviceId); + if (existing) { + // model/name beim Re-Register aktualisieren — User-Agent oder OS-Version + // kann sich geändert haben (App-Update, OS-Upgrade, iPad-Detection-Fix). + const updated = await db.userDevice.update({ + where: { id: existing.id }, + data: { + lastSeenAt: new Date(), + ...(opts.model !== undefined && { model: opts.model }), + ...(opts.name !== undefined && { name: opts.name }), + }, + select: { + id: true, + deviceId: true, + platform: true, + model: true, + name: true, + lastSeenAt: true, + createdAt: true, + }, + }); + return { device: updated, created: false }; + } + + // Neues Device — Limit prüfen + const count = await db.userDevice.count({ where: { userId: opts.userId } }); + if (count >= opts.maxDevices) { + throw Object.assign(new Error("device_limit_reached"), { + code: "DEVICE_LIMIT_REACHED", + currentCount: count, + max: opts.maxDevices, + }); + } + + const created = await db.userDevice.create({ + data: { + userId: opts.userId, + deviceId: opts.deviceId, + platform: opts.platform, + model: opts.model ?? null, + name: opts.name ?? null, + }, + select: { + id: true, + deviceId: true, + platform: true, + model: true, + name: true, + lastSeenAt: true, + createdAt: true, + }, + }); + return { device: created, created: true }; +} + +/** Touch lastSeenAt — wird in der Auth-Middleware bei jedem Request aufgerufen. */ +export async function touchDevice(userId: string, deviceId: string): Promise { + const db = usePrisma(); + await db.userDevice + .updateMany({ + where: { userId, deviceId }, + data: { lastSeenAt: new Date() }, + }) + .catch(() => { + /* race-safe: wenn Device gerade gelöscht wurde */ + }); +} + +/** User entfernt ein eigenes Device — gibt Slot frei. */ +export async function deleteUserDevice(userId: string, id: string): Promise { + const db = usePrisma(); + await db.userDevice.deleteMany({ where: { id, userId } }); +} diff --git a/backend/server/db/domains.ts b/backend/server/db/domains.ts new file mode 100644 index 0000000..84ee2f1 --- /dev/null +++ b/backend/server/db/domains.ts @@ -0,0 +1,430 @@ +import { usePrisma } from "../utils/prisma"; +import { createNotification } from "./notifications"; + +// ─── Custom Domains ─────────────────────────────────────────────────────────── + +export async function getUserCustomDomains(userId: string) { + const db = usePrisma(); + const rows = await db.userCustomDomain.findMany({ + where: { userId }, + orderBy: { addedAt: "desc" }, + select: { + id: true, + domain: true, + status: true, + postId: true, + addedAt: true, + submission: { + select: { id: true, yesVotes: true, noVotes: true, status: true }, + }, + }, + }); + return rows; +} + +/** + * Counts domains that occupy a slot (active + submitted). + * approved → slot freed (domain joined global list) + * rejected → slot freed (user can re-submit or delete) + */ +export async function countActiveCustomDomains(userId: string) { + const db = usePrisma(); + return db.userCustomDomain.count({ + where: { userId, status: { notIn: ["approved", "rejected"] } }, + }); +} + +export async function addUserCustomDomain( + userId: string, + domain: string, + source = "manual", +) { + const db = usePrisma(); + return db.userCustomDomain.create({ + data: { userId, domain, source }, + select: { id: true, domain: true }, + }); +} + +export async function deleteUserCustomDomain(id: string, userId: string) { + const db = usePrisma(); + // Cannot delete submitted/approved domains (protect integrity) + const existing = await db.userCustomDomain.findFirst({ + where: { id, userId }, + select: { status: true }, + }); + if (!existing) throw Object.assign(new Error("Not found"), { code: "P2025" }); + if (existing.status === "submitted" || existing.status === "approved") { + throw Object.assign( + new Error( + "Eingereichte oder genehmigte Domains können nicht gelöscht werden", + ), + { code: "DOMAIN_LOCKED" }, + ); + } + return db.userCustomDomain.delete({ where: { id, userId } }); +} + +export async function deleteAllUserCustomDomains(userId: string) { + const db = usePrisma(); + return db.userCustomDomain.deleteMany({ where: { userId } }); +} + +// ─── Domain Submissions ─────────────────────────────────────────────────────── + +// Net-Vote-Schwelle (yesVotes - noVotes) damit eine Submission von der +// Community-Vote-Phase in die Admin-Review-Phase wandert. +export const NET_VOTE_THRESHOLD = 10; + +// Submission-Status: +// "pending" → Community-Voting-Phase (nur Pro) +// "in_review" → wartet auf Admin (Legend direkt, Pro nach 10 Netto-Yes-Votes) +// "approved" → in globaler Blocklist +// "rejected" → vom Admin abgelehnt +export type SubmissionPlan = "free" | "pro" | "legend"; + +export async function submitDomainForReview( + userId: string, + customDomainId: string, + plan: SubmissionPlan, + postId?: string, +) { + if (plan === "free") { + throw Object.assign(new Error("Free-Plan kann keine Domains einreichen"), { + code: "PLAN_NO_SUBMIT", + }); + } + const submissionStatus = plan === "legend" ? "in_review" : "pending"; + const db = usePrisma(); + return db.$transaction(async (tx) => { + // Mark custom domain as submitted + const domain = await tx.userCustomDomain.update({ + where: { id: customDomainId, userId }, + data: { status: "submitted", postId: postId ?? null }, + select: { id: true, domain: true }, + }); + // Create submission record + const submission = await tx.domainSubmission.create({ + data: { + userId, + domain: domain.domain, + customDomainId, + postId: postId ?? null, + status: submissionStatus, + }, + }); + return { domain, submission }; + }); +} + +export async function castDomainVote( + userId: string, + submissionId: string, + vote: "yes" | "no", +) { + const db = usePrisma(); + return db.$transaction(async (tx) => { + // Upsert vote (change allowed) + const existing = await tx.domainVote.findUnique({ + where: { userId_submissionId: { userId, submissionId } }, + }); + if (existing) { + if (existing.vote === vote) return { changed: false }; + await tx.domainVote.update({ + where: { userId_submissionId: { userId, submissionId } }, + data: { vote }, + }); + } else { + await tx.domainVote.create({ data: { userId, submissionId, vote } }); + } + + // Recount + const [yes, no] = await Promise.all([ + tx.domainVote.count({ where: { submissionId, vote: "yes" } }), + tx.domainVote.count({ where: { submissionId, vote: "no" } }), + ]); + const updated = await tx.domainSubmission.update({ + where: { id: submissionId }, + data: { yesVotes: yes, noVotes: no }, + }); + + // Netto-Yes (yes - no) muss die Schwelle erreichen, dann wandert die + // Submission in die Admin-Review-Phase. Approval erfolgt erst durch Admin. + let movedToReview = false; + if (updated.status === "pending" && yes - no >= NET_VOTE_THRESHOLD) { + await tx.domainSubmission.update({ + where: { id: submissionId }, + data: { status: "in_review" }, + }); + movedToReview = true; + } + + return { yesVotes: yes, noVotes: no, movedToReview }; + }); +} + +async function approveDomainSubmissionTx( + tx: any, + submissionId: string, + customDomainId: string, + domain: string, +) { + await tx.domainSubmission.update({ + where: { id: submissionId }, + data: { status: "approved", reviewedAt: new Date() }, + }); + await tx.userCustomDomain.update({ + where: { id: customDomainId }, + data: { status: "approved" }, + }); + // Add to global blocklist + await tx.blocklistDomain.upsert({ + where: { domain }, + create: { domain, source: "community", isActive: true }, + update: { isActive: true }, + }); +} + +export async function adminApproveSubmission( + submissionId: string, + reviewNote?: string, +) { + const db = usePrisma(); + const sub = await db.domainSubmission.findUniqueOrThrow({ + where: { id: submissionId }, + select: { + customDomainId: true, + domain: true, + status: true, + userId: true, + postId: true, + }, + }); + // Admin darf in beiden offenen Phasen approven (Vote oder Review) + if (sub.status !== "pending" && sub.status !== "in_review") { + throw new Error("Submission already resolved"); + } + + await db.$transaction((tx) => + approveDomainSubmissionTx(tx, submissionId, sub.customDomainId, sub.domain), + ); + + // Alle anderen User die diese Domain als Custom Domain haben → Slot freigeben + benachrichtigen + const affectedDomains = await db.userCustomDomain.findMany({ + where: { + domain: sub.domain, + // Submitter's domain already handled in approveDomainSubmissionTx + id: { not: sub.customDomainId }, + status: { notIn: ["approved", "rejected"] }, + }, + select: { id: true, userId: true }, + }); + + if (affectedDomains.length > 0) { + // Batch-Update: alle auf "approved" setzen (Slot freigeben) + await db.userCustomDomain.updateMany({ + where: { id: { in: affectedDomains.map((d) => d.id) } }, + data: { status: "approved" }, + }); + + // Notification an jeden betroffenen User + await Promise.allSettled( + affectedDomains.map((d) => + createNotification({ + recipientId: d.userId, + type: "domain_accepted", + actorName: "ReBreak", + postId: sub.postId ?? undefined, + preview: sub.domain, + }), + ), + ); + } + + // Auch Submitter benachrichtigen (Admin hat seine Domain genehmigt) + await createNotification({ + recipientId: sub.userId, + type: "domain_accepted", + actorName: "ReBreak Admin", + postId: sub.postId ?? undefined, + preview: sub.domain, + }).catch(() => {}); + + return db.domainSubmission.findUnique({ where: { id: submissionId } }); +} + +export async function adminRejectSubmission( + submissionId: string, + reviewNote?: string, +) { + const db = usePrisma(); + // Submission + zugehörige Custom-Domain laden (für Notification + Cleanup) + const sub = await db.domainSubmission.findUniqueOrThrow({ + where: { id: submissionId }, + select: { + userId: true, + domain: true, + postId: true, + customDomainId: true, + }, + }); + + await db.$transaction(async (tx) => { + await tx.domainSubmission.update({ + where: { id: submissionId }, + data: { + status: "rejected", + reviewedAt: new Date(), + reviewNote: reviewNote ?? null, + }, + }); + // Custom-Domain des Submitters komplett aus seiner Liste entfernen + // (DomainSubmission via onDelete: Cascade automatisch mitgelöscht? NEIN — + // Submission referenziert customDomain. Wir haben Submission gerade upgedatet, + // also sicher löschen via Cascade von customDomain → submission.) + await tx.userCustomDomain.delete({ + where: { id: sub.customDomainId }, + }); + }); + + // Submitter benachrichtigen — Notification von ReBreak (System) + await createNotification({ + recipientId: sub.userId, + type: "domain_rejected", + actorName: "ReBreak", + postId: sub.postId ?? undefined, + preview: sub.domain, + }).catch(() => {}); + + return { customDomainId: sub.customDomainId }; +} + +export async function getPendingSubmissions() { + const db = usePrisma(); + return db.domainSubmission.findMany({ + where: { status: { in: ["pending", "in_review"] } }, + orderBy: [{ status: "asc" }, { yesVotes: "desc" }], + select: { + id: true, + domain: true, + yesVotes: true, + noVotes: true, + status: true, + createdAt: true, + userId: true, + postId: true, + customDomain: { select: { id: true } }, + }, + }); +} + +// ─── Global Blocklist ───────────────────────────────────────────────────────── + +export async function getActiveBlocklistCount() { + const db = usePrisma(); + return db.blocklistDomain.count({ where: { isActive: true } }); +} + +export async function getActiveBlocklistDomains() { + const db = usePrisma(); + return db.blocklistDomain.findMany({ + where: { isActive: true }, + select: { domain: true }, + }); +} + +export async function isBlocklistedDomain( + domain: string, + userId: string, +): Promise { + const db = usePrisma(); + const [global, custom] = await Promise.all([ + db.blocklistDomain.findFirst({ + where: { domain, isActive: true }, + select: { domain: true }, + }), + db.userCustomDomain.findFirst({ + where: { domain, userId }, + select: { domain: true }, + }), + ]); + return !!(global || custom); +} + +export async function getBlocklistedDomainsSet( + domains: string[], + userId: string, + includeGlobal = true, +): Promise> { + if (domains.length === 0) return new Set(); + const db = usePrisma(); + const unique = [...new Set(domains)]; + + // Alle möglichen Parent-Domains erzeugen: "mail.casino.de" → ["mail.casino.de", "casino.de"] + const allVariants = [ + ...new Set( + unique.flatMap((d) => { + const parts = d.split("."); + return parts + .map((_, i) => parts.slice(i).join(".")) + .filter((v) => v.includes(".")); + }), + ), + ]; + + const queries: Promise<{ domain: string }[]>[] = []; + + if (includeGlobal) { + queries.push( + db.blocklistDomain.findMany({ + where: { domain: { in: allVariants }, isActive: true }, + select: { domain: true }, + }), + ); + } + + queries.push( + db.userCustomDomain.findMany({ + where: { domain: { in: allVariants }, userId }, + select: { domain: true }, + }), + ); + + const results = await Promise.all(queries); + const blockedSet = new Set(results.flatMap((r) => r.map((d) => d.domain))); + + // Zurück auf Original-Domains mappen: wenn irgendein Variant geblockt ist → Original blocken + const result = new Set(); + for (const orig of unique) { + const parts = orig.split("."); + const variants = parts + .map((_, i) => parts.slice(i).join(".")) + .filter((v) => v.includes(".")); + if (variants.some((v) => blockedSet.has(v))) result.add(orig); + } + return result; +} + +export async function upsertBlocklistDomains( + domains: { domain: string; source: string }[], +) { + const db = usePrisma(); + // Batch upsert in chunks + const CHUNK = 5000; + let total = 0; + for (let i = 0; i < domains.length; i += CHUNK) { + const chunk = domains.slice(i, i + CHUNK).map((d) => ({ + ...d, + isActive: true, + })); + for (const d of chunk) { + await db.blocklistDomain.upsert({ + where: { domain: d.domain }, + create: d, + update: { isActive: true }, + }); + total++; + } + } + return total; +} diff --git a/backend/server/db/lyraMemory.ts b/backend/server/db/lyraMemory.ts new file mode 100644 index 0000000..34e5b4b --- /dev/null +++ b/backend/server/db/lyraMemory.ts @@ -0,0 +1,193 @@ +/** + * DB-Layer: LyraMemory + * + * Strukturierte persistente User-Erinnerungen für den Lyra-Coach. + * Enthält Art-9-Gesundheitsdaten — kein direkter Zugriff außer über diese Funktionen. + * + * Constraints: + * - Max MAX_MEMORIES_PER_USER pro User (Datenminimierung + System-Prompt-Budget) + * - upsertMemory: ähnlicher Content (Substring) → Update statt Insert + */ +import { usePrisma } from "../utils/prisma"; +import type { LyraMemoryType } from "../generated/prisma"; + +export type { LyraMemoryType }; + +export interface LyraMemoryRow { + id: string; + userId: string; + type: LyraMemoryType; + content: string; + confidence: number; + source: string | null; + createdAt: Date; + updatedAt: Date; + lastReferencedAt: Date | null; +} + +const MAX_MEMORIES_PER_USER = 30; +const LOG = "[lyra-memory]"; + +/** + * Alle Memories eines Users, sortiert nach Relevanz (zuletzt referenziert → neueste). + */ +export async function getMemoriesForUser( + userId: string, +): Promise { + const db = usePrisma(); + return db.lyraMemory.findMany({ + where: { userId }, + orderBy: [ + { lastReferencedAt: { sort: "desc", nulls: "last" } }, + { createdAt: "desc" }, + ], + }) as Promise; +} + +/** + * Upsert: wenn Content-Substring eines bestehenden Eintrags gleichen Typs + * bereits vorhanden ist → update confidence + source. Sonst insert. + * Hält Max-Constraint ein: bei > MAX_MEMORIES_PER_USER werden älteste + * mit niedrigster confidence gelöscht. + */ +export async function upsertMemory( + userId: string, + type: LyraMemoryType, + content: string, + source?: string, + confidence = 0.7, +): Promise { + const db = usePrisma(); + const trimmedContent = content.slice(0, 500).trim(); + + // Similarity-Check: existierende Memories gleichen Typs laden + const existing = await db.lyraMemory.findMany({ + where: { userId, type }, + select: { id: true, content: true, confidence: true }, + }); + + // Substring-Match (case-insensitive) + const contentLower = trimmedContent.toLowerCase(); + const match = existing.find((m) => { + const mLower = m.content.toLowerCase(); + return ( + mLower.includes(contentLower) || + contentLower.includes(mLower) || + // 70%-Overlap-Heuristik für kurze Strings + (contentLower.length > 20 && + mLower.length > 20 && + levenshteinSimilarity(contentLower, mLower) > 0.7) + ); + }); + + let result: LyraMemoryRow; + + if (match) { + // Update: nimm höhere confidence + neuen Source + const newConfidence = Math.max(match.confidence, confidence); + result = (await db.lyraMemory.update({ + where: { id: match.id }, + data: { + content: trimmedContent, + confidence: newConfidence, + source: source ?? undefined, + updatedAt: new Date(), + }, + })) as LyraMemoryRow; + console.log( + `${LOG} updated memory ${match.id} (type=${type}, conf=${newConfidence})`, + ); + } else { + result = (await db.lyraMemory.create({ + data: { + userId, + type, + content: trimmedContent, + confidence, + source: source ?? null, + }, + })) as LyraMemoryRow; + console.log( + `${LOG} created memory ${result.id} (type=${type}, conf=${confidence})`, + ); + + // Max-Constraint enforzen + await enforceMaxMemories(userId); + } + + return result; +} + +/** + * Markiert Memories als zuletzt referenziert (in System-Prompt injiziert). + * Fire-and-forget geeignet — wirft nicht. + */ +export async function markReferenced(memoryIds: string[]): Promise { + if (!memoryIds.length) return; + const db = usePrisma(); + try { + await db.lyraMemory.updateMany({ + where: { id: { in: memoryIds } }, + data: { lastReferencedAt: new Date() }, + }); + console.log(`${LOG} markReferenced: ${memoryIds.length} memories`); + } catch (e) { + console.error(`${LOG} markReferenced error:`, e); + } +} + +/** + * User-seitiges Delete (V2.5-ready — Endpoint kommt später). + */ +export async function deleteMemoryById( + userId: string, + memoryId: string, +): Promise { + const db = usePrisma(); + await db.lyraMemory.deleteMany({ + where: { id: memoryId, userId }, + }); + console.log(`${LOG} deleted memory ${memoryId} for user ${userId}`); +} + +// ── Interne Helpers ────────────────────────────────────────────────────────── + +async function enforceMaxMemories(userId: string): Promise { + const db = usePrisma(); + const total = await db.lyraMemory.count({ where: { userId } }); + if (total <= MAX_MEMORIES_PER_USER) return; + + const overflow = total - MAX_MEMORIES_PER_USER; + // Älteste mit niedrigster confidence zuerst löschen + const candidates = await db.lyraMemory.findMany({ + where: { userId }, + orderBy: [{ confidence: "asc" }, { createdAt: "asc" }], + take: overflow, + select: { id: true }, + }); + const ids = candidates.map((c) => c.id); + await db.lyraMemory.deleteMany({ where: { id: { in: ids } } }); + console.log(`${LOG} enforceMax: deleted ${ids.length} old memories`); +} + +/** + * Einfache Levenshtein-basierte Ähnlichkeit (0.0-1.0). + * Nur für kurze Strings — O(n*m) ist ok für max 500 chars. + */ +function levenshteinSimilarity(a: string, b: string): number { + if (a === b) return 1; + const la = a.length; + const lb = b.length; + const dp: number[][] = Array.from({ length: la + 1 }, (_, i) => + Array.from({ length: lb + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)), + ); + for (let i = 1; i <= la; i++) { + for (let j = 1; j <= lb; j++) { + dp[i][j] = + a[i - 1] === b[j - 1] + ? dp[i - 1][j - 1] + : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]); + } + } + return 1 - dp[la][lb] / Math.max(la, lb); +} diff --git a/backend/server/db/mail.ts b/backend/server/db/mail.ts new file mode 100644 index 0000000..4db90ea --- /dev/null +++ b/backend/server/db/mail.ts @@ -0,0 +1,200 @@ +import { usePrisma } from "../utils/prisma"; + +export async function getMailConnections(userId: string) { + const db = usePrisma(); + return db.mailConnection.findMany({ + where: { userId, isActive: true }, + orderBy: { createdAt: "asc" }, + }); +} + +export async function getAllActiveMailUserIds() { + const db = usePrisma(); + const rows = await db.mailConnection.findMany({ + where: { isActive: true, nextScanAt: { lte: new Date() } }, + select: { userId: true }, + distinct: ["userId"], + }); + return rows.map((r) => r.userId); +} + +export async function countMailConnections(userId: string) { + const db = usePrisma(); + return db.mailConnection.count({ where: { userId, isActive: true } }); +} + +export async function upsertMailConnection(data: { + userId: string; + email: string; + provider: string; + providerName: string; + imapHost: string; + imapPort: number; + passwordEncrypted: string; + rejectUnauthorized?: boolean; + useStarttls?: boolean; +}) { + const db = usePrisma(); + return db.mailConnection.upsert({ + where: { userId_email: { userId: data.userId, email: data.email } }, + create: { + ...data, + isActive: true, + rejectUnauthorized: data.rejectUnauthorized ?? true, + useStarttls: data.useStarttls ?? false, + }, + update: { + providerName: data.providerName, + imapHost: data.imapHost, + imapPort: data.imapPort, + passwordEncrypted: data.passwordEncrypted, + rejectUnauthorized: data.rejectUnauthorized ?? true, + useStarttls: data.useStarttls ?? false, + isActive: true, + }, + }); +} + +export async function deleteMailConnection( + userId: string, + connectionId: string, +) { + const db = usePrisma(); + return db.mailConnection.deleteMany({ + where: { id: connectionId, userId }, + }); +} + +export async function deleteAllMailConnections(userId: string) { + const db = usePrisma(); + return db.mailConnection.deleteMany({ where: { userId } }); +} + +export async function updateMailConnectionInterval( + userId: string, + connectionId: string, + interval: number, +) { + const db = usePrisma(); + return db.mailConnection.updateMany({ + where: { id: connectionId, userId }, + data: { scanInterval: interval }, + }); +} + +export async function updateMailConnectionScanStats( + connectionId: string, + scanned: number, + blocked: number, + currentBlocked: number, + currentScanned: number, + scanIntervalHours: number, +) { + const db = usePrisma(); + return db.mailConnection.update({ + where: { id: connectionId }, + data: { + lastScannedAt: new Date(), + emailsBlocked: currentBlocked + blocked, + emailsScanned: currentScanned + scanned, + nextScanAt: new Date(Date.now() + scanIntervalHours * 3_600_000), + }, + }); +} + +export async function getMailBlockedStats(userId: string) { + const db = usePrisma(); + const since7d = new Date(Date.now() - 7 * 86_400_000); + return db.mailBlocked.findMany({ + where: { userId, createdAt: { gte: since7d } }, + select: { createdAt: true }, + }); +} + +export async function isMailAlreadyBlocked( + gmailMessageId: string, + userId: string, +) { + const db = usePrisma(); + const existing = await db.mailBlocked.findFirst({ + where: { gmailMessageId, userId }, + select: { id: true }, + }); + return !!existing; +} + +export async function getAlreadyBlockedUidSet( + uids: string[], + userId: string, +): Promise> { + if (uids.length === 0) return new Set(); + const db = usePrisma(); + const existing = await db.mailBlocked.findMany({ + where: { gmailMessageId: { in: uids }, userId }, + select: { gmailMessageId: true }, + }); + return new Set(existing.map((e) => e.gmailMessageId)); +} + +export async function insertMailBlocked( + entries: { + userId: string; + connectionId: string; + gmailMessageId: string; + senderEmail: string; + senderName: string | null; + subject: string; + receivedAt: Date; + action: string; + }[], +) { + if (entries.length === 0) return; + const db = usePrisma(); + await db.mailBlocked.createMany({ data: entries, skipDuplicates: true }); +} + +export async function getImapProxyAccounts(userId: string) { + const db = usePrisma(); + return db.imapProxyAccount.findMany({ where: { userId } }); +} + +export async function upsertImapProxyAccount(data: { + userId: string; + proxyUsername: string; + proxyPassword: string; + connectionId: string; +}) { + const db = usePrisma(); + return db.imapProxyAccount.upsert({ + where: { connectionId: data.connectionId }, + create: data, + update: { proxyPassword: data.proxyPassword }, + }); +} + +export async function deleteOldMailBlocked(userId: string) { + const db = usePrisma(); + const cutoff = new Date(Date.now() - 24 * 3_600_000); + return db.mailBlocked.deleteMany({ + where: { userId, createdAt: { lt: cutoff } }, + }); +} + +export async function getMailBlockedPaginated( + userId: string, + page: number, + limit = 20, +) { + const db = usePrisma(); + const offset = (page - 1) * limit; + const [results, total] = await Promise.all([ + db.mailBlocked.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + skip: offset, + take: limit, + }), + db.mailBlocked.count({ where: { userId } }), + ]); + return { results, total, page, pages: Math.ceil(total / limit) }; +} diff --git a/backend/server/db/notifications.ts b/backend/server/db/notifications.ts new file mode 100644 index 0000000..d3882ae --- /dev/null +++ b/backend/server/db/notifications.ts @@ -0,0 +1,52 @@ +import { usePrisma } from "../utils/prisma"; + +export async function createNotification(data: { + recipientId: string; + type: string; + actorName: string; + actorAvatar?: string; + postId?: string; + preview?: string; +}) { + const db = usePrisma(); + return db.notification.create({ data }); +} + +export async function getNotifications(userId: string) { + const db = usePrisma(); + return db.notification.findMany({ + where: { recipientId: userId }, + orderBy: { createdAt: "desc" }, + take: 30, + }); +} + +export async function countUnread(userId: string) { + const db = usePrisma(); + return db.notification.count({ + where: { recipientId: userId, readAt: null }, + }); +} + +export async function markAllRead(userId: string) { + const db = usePrisma(); + return db.notification.updateMany({ + where: { recipientId: userId, readAt: null }, + data: { readAt: new Date() }, + }); +} + +export async function deleteNotification(notifId: string, userId: string) { + const db = usePrisma(); + return db.notification.deleteMany({ + where: { id: notifId, recipientId: userId }, + }); +} + +export async function deleteOldNotifications() { + const db = usePrisma(); + const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); + return db.notification.deleteMany({ + where: { createdAt: { lt: threeDaysAgo } }, + }); +} diff --git a/backend/server/db/profile.ts b/backend/server/db/profile.ts new file mode 100644 index 0000000..ea38317 --- /dev/null +++ b/backend/server/db/profile.ts @@ -0,0 +1,23 @@ +import { usePrisma } from "../utils/prisma"; + +export async function getProfile(userId: string) { + const db = usePrisma(); + return db.profile.findUnique({ where: { id: userId } }); +} + +export async function updateProfile( + userId: string, + data: Partial<{ + username: string | null; + nickname: string | null; + avatar: string | null; + }>, +) { + const db = usePrisma(); + return db.profile.update({ where: { id: userId }, data }); +} + +export async function deleteProfile(userId: string) { + const db = usePrisma(); + return db.profile.delete({ where: { id: userId } }); +} diff --git a/backend/server/db/scores.ts b/backend/server/db/scores.ts new file mode 100644 index 0000000..f6557df --- /dev/null +++ b/backend/server/db/scores.ts @@ -0,0 +1,61 @@ +import { usePrisma } from "../utils/prisma"; + +export type ScoreEventType = + | "post_created" + | "upvote_received" + | "custom_domain_submitted" + | "daily_checkin" + | "chat_message" + | "streak_milestone" + | "helped_user"; + +const POINTS: Record = { + post_created: 50, + upvote_received: 10, + custom_domain_submitted: 30, + daily_checkin: 5, + chat_message: 2, + streak_milestone: 100, + helped_user: 20, +}; + +export async function awardPoints( + userId: string, + type: ScoreEventType, + meta?: Record, +) { + const db = usePrisma(); + const points = POINTS[type] ?? 0; + if (points === 0) return; + + await db.scoreEvent.create({ + data: { userId, eventType: type, points, meta: meta ?? null }, + }); +} + +export async function getUserScore(userId: string) { + const db = usePrisma(); + return db.userScore.findUnique({ + where: { userId }, + select: { totalPoints: true, tier: true, updatedAt: true }, + }); +} + +export async function getRecentScoreEvents(userId: string, limit = 20) { + const db = usePrisma(); + return db.scoreEvent.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + take: limit, + select: { eventType: true, points: true, createdAt: true, meta: true }, + }); +} + +export async function getLeaderboard(limit = 50) { + const db = usePrisma(); + return db.userScore.findMany({ + orderBy: { totalPoints: "desc" }, + take: limit, + select: { userId: true, totalPoints: true, tier: true }, + }); +} diff --git a/backend/server/db/social.ts b/backend/server/db/social.ts new file mode 100644 index 0000000..1c5b09c --- /dev/null +++ b/backend/server/db/social.ts @@ -0,0 +1,73 @@ +import { usePrisma } from "../utils/prisma"; + +export async function getFollowRelation( + followerId: string, + followingId: string, +) { + const db = usePrisma(); + return db.userFollow.findUnique({ + where: { followerId_followingId: { followerId, followingId } }, + }); +} + +export async function createFollow(followerId: string, followingId: string) { + const db = usePrisma(); + const exists = await db.userFollow.findUnique({ + where: { followerId_followingId: { followerId, followingId } }, + }); + if (exists) return; + await db.userFollow.create({ data: { followerId, followingId } }); + await db.profile.update({ + where: { id: followingId }, + data: { followersCount: { increment: 1 } }, + }); +} + +export async function deleteFollow(followerId: string, followingId: string) { + const db = usePrisma(); + const exists = await db.userFollow.findUnique({ + where: { followerId_followingId: { followerId, followingId } }, + }); + if (!exists) return; + await db.userFollow.delete({ + where: { followerId_followingId: { followerId, followingId } }, + }); + // Nie unter 0 + const profile = await db.profile.findUnique({ + where: { id: followingId }, + select: { followersCount: true }, + }); + if (profile && profile.followersCount > 0) { + await db.profile.update({ + where: { id: followingId }, + data: { followersCount: { decrement: 1 } }, + }); + } +} + +/** Gibt ein Set der userIds zurück, denen followerId bereits folgt (Batch-Variante). */ +export async function getFollowingSet( + followerId: string, + followingIds: string[], +): Promise> { + if (followingIds.length === 0) return new Set(); + const db = usePrisma(); + const rows = await db.userFollow.findMany({ + where: { followerId, followingId: { in: followingIds } }, + select: { followingId: true }, + }); + return new Set(rows.map((r) => r.followingId)); +} + +export async function getProfileWithFollowers(userId: string) { + const db = usePrisma(); + return db.profile.findUnique({ + where: { id: userId }, + select: { + followersCount: true, + username: true, + avatar: true, + nickname: true, + }, + }); +} diff --git a/backend/server/db/sosSession.ts b/backend/server/db/sosSession.ts new file mode 100644 index 0000000..1ecd885 --- /dev/null +++ b/backend/server/db/sosSession.ts @@ -0,0 +1,41 @@ +import { usePrisma } from "../utils/prisma"; + +export interface SosSessionInput { + startedAt?: string | Date; + endedAt?: string | Date | null; + durationSec?: number | null; + messages: Array<{ role: string; content: string; timestamp?: string }>; + gamesPlayed?: Array<{ game: string; score?: number; durationSec?: number }>; + breathingCount?: number; + wasOvercome?: boolean; + feedbackBetter?: boolean | null; + feedbackRating?: number | null; + feedbackText?: string | null; + locale?: string | null; +} + +export async function createSosSession(userId: string, input: SosSessionInput) { + const db = usePrisma(); + return db.sosSession.create({ + data: { + userId, + startedAt: input.startedAt ? new Date(input.startedAt) : new Date(), + endedAt: input.endedAt ? new Date(input.endedAt) : null, + durationSec: input.durationSec ?? null, + messages: input.messages as any, + gamesPlayed: (input.gamesPlayed ?? []) as any, + breathingCount: input.breathingCount ?? 0, + wasOvercome: input.wasOvercome ?? false, + feedbackBetter: input.feedbackBetter ?? null, + feedbackRating: input.feedbackRating ?? null, + feedbackText: input.feedbackText ?? null, + locale: input.locale ?? null, + }, + select: { id: true, startedAt: true, endedAt: true }, + }); +} + +export async function deleteUserSosSessions(userId: string) { + const db = usePrisma(); + return db.sosSession.deleteMany({ where: { userId } }); +} diff --git a/backend/server/db/streak.ts b/backend/server/db/streak.ts new file mode 100644 index 0000000..9998cb8 --- /dev/null +++ b/backend/server/db/streak.ts @@ -0,0 +1,104 @@ +import { usePrisma } from "../utils/prisma"; + +export async function getActiveStreak(userId: string) { + const db = usePrisma(); + return db.streak.findFirst({ + where: { userId, isActive: true }, + }); +} + +export async function upsertStreak( + userId: string, + data: { avgMonthlySavings?: number | null; startDate?: string | null }, +) { + const db = usePrisma(); + const today = new Date(); + today.setUTCHours(0, 0, 0, 0); + + // Allow setting a past start date (e.g. from onboarding "last played" selection) + const startDate = data.startDate ? new Date(data.startDate) : today; + startDate.setUTCHours(0, 0, 0, 0); + + const existing = await db.streak.findFirst({ where: { userId } }); + + if (existing) { + const streak = await db.streak.update({ + where: { id: existing.id }, + data: { + startDate, + currentDays: 0, + isActive: true, + ...(data.avgMonthlySavings !== undefined + ? { avgMonthlySavings: data.avgMonthlySavings } + : {}), + }, + }); + await addStreakEvent(userId, "started"); + return streak; + } + + const streak = await db.streak.create({ + data: { + userId, + startDate, + currentDays: 0, + longestDays: 0, + avgMonthlySavings: data.avgMonthlySavings ?? null, + isActive: true, + }, + }); + await addStreakEvent(userId, "started"); + return streak; +} + +export async function resetStreak( + streakId: string, + longestDays: number, + userId: string, + reason: "blocker_off" | "relapse" | "manual" = "manual", +) { + const db = usePrisma(); + const today = new Date(); + today.setUTCHours(0, 0, 0, 0); + const streak = await db.streak.update({ + where: { id: streakId }, + data: { currentDays: 0, startDate: today, longestDays }, + }); + await addStreakEvent(userId, "reset", { reason, previousDays: longestDays }); + return streak; +} + +export async function updateStreakSavings(streakId: string, amount: number) { + const db = usePrisma(); + return db.streak.update({ + where: { id: streakId }, + data: { avgMonthlySavings: amount }, + }); +} + +export async function deleteUserStreaks(userId: string) { + const db = usePrisma(); + return db.streak.deleteMany({ where: { userId } }); +} + +// --- Streak Events --- + +export async function addStreakEvent( + userId: string, + type: "started" | "reset" | "milestone" | "relapse", + meta?: Record, +) { + const db = usePrisma(); + return db.streakEvent.create({ + data: { userId, type, meta: meta ?? undefined }, + }); +} + +export async function getStreakEvents(userId: string, limit = 50) { + const db = usePrisma(); + return db.streakEvent.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + take: limit, + }); +} diff --git a/backend/server/db/urge.ts b/backend/server/db/urge.ts new file mode 100644 index 0000000..209f02b --- /dev/null +++ b/backend/server/db/urge.ts @@ -0,0 +1,43 @@ +import { usePrisma } from "../utils/prisma"; + +type Emotion = "stress" | "sadness" | "anger" | "empty" | "boredom" | "other"; + +export async function getRecentUrgeLogs(userId: string, limit = 20) { + const db = usePrisma(); + return db.urgeLog.findMany({ + where: { userId }, + orderBy: { timestamp: "desc" }, + take: limit, + select: { + id: true, + timestamp: true, + emotion: true, + wasOvercome: true, + breathingDone: true, + }, + }); +} + +export async function createUrgeLog( + userId: string, + emotion: Emotion, + wasOvercome: boolean, + breathingDone: boolean, +) { + const db = usePrisma(); + return db.urgeLog.create({ + data: { userId, emotion, wasOvercome, breathingDone }, + select: { + id: true, + timestamp: true, + emotion: true, + wasOvercome: true, + breathingDone: true, + }, + }); +} + +export async function deleteUserUrgeLogs(userId: string) { + const db = usePrisma(); + return db.urgeLog.deleteMany({ where: { userId } }); +} diff --git a/backend/server/db/user.ts b/backend/server/db/user.ts new file mode 100644 index 0000000..143790f --- /dev/null +++ b/backend/server/db/user.ts @@ -0,0 +1,11 @@ +import { usePrisma } from "../utils/prisma"; + +export async function deleteUserTrustedContacts(userId: string) { + const db = usePrisma(); + return db.trustedContact.deleteMany({ where: { userId } }); +} + +export async function deleteUserCoachSessions(userId: string) { + const db = usePrisma(); + return db.coachSession.deleteMany({ where: { userId } }); +} diff --git a/backend/server/middleware/cors.ts b/backend/server/middleware/cors.ts new file mode 100644 index 0000000..30ddb5b --- /dev/null +++ b/backend/server/middleware/cors.ts @@ -0,0 +1,38 @@ +/** + * CORS Middleware – läuft vor jedem API-Request. + * + * Warum Middleware statt routeRules: + * - routeRules mit cors:true + headers:{} setzt den Header doppelt → Browser blockiert + * - Nur hier kann OPTIONS-Preflight korrekt mit 204 beantwortet werden + * + * Origin-Strategie: echo zurück statt *, damit Capacitor-Webview (capacitor://localhost), + * iOS-Scheme (rebreakapp://), Android (http://localhost) und alle Staging/Prod-Domains + * funktionieren – ohne explizite Whitelist pflegen zu müssen. + * Das API verwendet Bearer-Token-Auth (kein Cookie/credentials), daher ist das sicher. + */ +export default defineEventHandler((event) => { + if (!event.path.startsWith("/api/")) return; + + const origin = getHeader(event, "origin") ?? "*"; + + setHeader(event, "Access-Control-Allow-Origin", origin); + setHeader(event, "Access-Control-Allow-Credentials", "true"); + setHeader( + event, + "Access-Control-Allow-Methods", + "GET, POST, PUT, PATCH, DELETE, OPTIONS" + ); + setHeader( + event, + "Access-Control-Allow-Headers", + "Content-Type, Authorization, apikey, x-client-info, x-device-id, x-platform" + ); + setHeader(event, "Access-Control-Max-Age", "3600"); + setHeader(event, "Vary", "Origin"); + + // OPTIONS Preflight → sofort 204 zurück, kein Handler nötig + if (getMethod(event) === "OPTIONS") { + event.node.res.statusCode = 204; + event.node.res.end(); + } +}); diff --git a/backend/server/plugins/blocklist-cron.ts b/backend/server/plugins/blocklist-cron.ts new file mode 100644 index 0000000..731b371 --- /dev/null +++ b/backend/server/plugins/blocklist-cron.ts @@ -0,0 +1,40 @@ +import { consola } from "consola"; + +const SYNC_INTERVAL = 60 * 60 * 1000; // 1 hour + +export default defineNitroPlugin((nitro) => { + if (import.meta.dev) { + consola.info("[blocklist-cron] Skipping cron in dev mode"); + return; + } + + consola.info("[blocklist-cron] Starting hourly HaGeZi sync"); + + // Run initial sync after 30 seconds (let server boot) + const initialTimer = setTimeout(async () => { + await runSync(); + }, 30_000); + + // Then run every hour + const interval = setInterval(async () => { + await runSync(); + }, SYNC_INTERVAL); + + // Cleanup on close + nitro.hooks.hook("close", () => { + clearTimeout(initialTimer); + clearInterval(interval); + }); +}); + +async function runSync() { + try { + consola.info("[blocklist-cron] Syncing HaGeZi gambling domains..."); + const result = await $fetch("/api/blocklist/sync", { method: "POST" }); + consola.success( + `[blocklist-cron] Synced ${(result as any).total_fetched} domains`, + ); + } catch (err: any) { + consola.error("[blocklist-cron] Sync failed:", err.message ?? err); + } +} diff --git a/backend/server/plugins/mail-scan-cron.ts b/backend/server/plugins/mail-scan-cron.ts new file mode 100644 index 0000000..13ab574 --- /dev/null +++ b/backend/server/plugins/mail-scan-cron.ts @@ -0,0 +1,37 @@ +import { consola } from "consola"; +import { getAllActiveMailUserIds } from "../db/mail"; + +const SCAN_INTERVAL = 30 * 60 * 1000; // every 30 minutes (proxy EXISTS handles real-time) + +export default defineNitroPlugin((nitro) => { + if (import.meta.dev) return; + + consola.info("[mail-scan-cron] Starting – scanning due accounts every 30 min"); + + const interval = setInterval(runScan, SCAN_INTERVAL); + nitro.hooks.hook("close", () => clearInterval(interval)); +}); + +async function runScan() { + let userIds: string[]; + try { + userIds = await getAllActiveMailUserIds(); + } catch { + return; + } + + if (userIds.length === 0) return; + + const adminSecret = process.env.NUXT_ADMIN_SECRET || process.env.ADMIN_SECRET; + if (!adminSecret) return; + + await Promise.allSettled( + userIds.map((userId) => + $fetch("/api/mail/scan-internal", { + method: "POST", + headers: { "x-admin-secret": adminSecret }, + body: { userId }, + }).catch(() => {}) + ) + ); +} diff --git a/backend/server/utils/auth.ts b/backend/server/utils/auth.ts new file mode 100644 index 0000000..96a1cde --- /dev/null +++ b/backend/server/utils/auth.ts @@ -0,0 +1,98 @@ +import { createClient } from '@supabase/supabase-js'; +import type { H3Event } from 'h3'; +import { findUserDevice, registerDevice, touchDevice } from '../db/devices'; +import { getProfile } from '../db/profile'; +import { getPlanLimits } from './plan-features'; + +const TOUCH_THROTTLE_MS = 60_000; // touch lastSeenAt höchstens 1×/min pro Device + +export interface RequireUserOptions { + /** Bootstrap-Endpoints (z.B. /api/devices/register) brauchen kein Device-Binding */ + skipDeviceCheck?: boolean; +} + +export async function requireUser( + event: H3Event, + opts: RequireUserOptions = {}, +) { + const authHeader = getHeader(event, 'authorization'); + let token = authHeader?.replace('Bearer ', ''); + + if (!token) { + const query = getQuery(event); + token = query.token as string; + } + + if (!token) { + throw createError({ statusCode: 401, message: 'Nicht eingeloggt' }); + } + + const config = useRuntimeConfig(event); + const supabaseCfg = + (config as any).public?.supabase ?? (config as any).supabase; + const client = createClient( + supabaseCfg.url as string, + supabaseCfg.key as string, + { global: { headers: { Authorization: `Bearer ${token}` } } }, + ); + + const { + data: { user }, + error, + } = await client.auth.getUser(); + + if (error || !user) { + throw createError({ statusCode: 401, message: 'Nicht eingeloggt' }); + } + + if (opts.skipDeviceCheck) return user; + + // Device-Binding: nur enforced wenn Client einen x-device-id Header schickt. + // Web-Clients ohne Header laufen weiter wie bisher. + const deviceId = getHeader(event, 'x-device-id'); + if (!deviceId) return user; + + const existing = await findUserDevice(user.id, deviceId); + if (existing) { + // Touch lastSeenAt, throttled auf 1×/min — fire-and-forget + if (Date.now() - existing.lastSeenAt.getTime() > TOUCH_THROTTLE_MS) { + touchDevice(user.id, deviceId).catch(() => {}); + } + return user; + } + + // Device unbekannt — Auto-Register (ohne Frontend-explicit-call) + // Plan-Limit holen + const profile = await getProfile(user.id); + const limits = getPlanLimits(profile?.plan ?? 'free'); + const platform = getHeader(event, 'x-platform') ?? 'unknown'; + + try { + await registerDevice({ + userId: user.id, + deviceId, + platform, + maxDevices: limits.maxDevices, + }); + return user; + } catch (err: any) { + if (err.code === 'DEVICE_LIMIT_REACHED') { + // Devices-Liste mitschicken damit das Frontend-Modal die Geräte + // anzeigen + Freigeben-Buttons rendern kann (auch wenn der 403 + // nicht vom register-Endpoint sondern vom auth-Middleware kommt). + const { listUserDevices } = await import('../db/devices'); + const devices = await listUserDevices(user.id); + throw createError({ + statusCode: 403, + statusMessage: 'device_limit_reached', + data: { + error: 'device_limit_reached', + max: limits.maxDevices, + plan: profile?.plan ?? 'free', + devices, + }, + }); + } + throw err; + } +} \ No newline at end of file diff --git a/backend/server/utils/cooldownToken.ts b/backend/server/utils/cooldownToken.ts new file mode 100644 index 0000000..2033e1e --- /dev/null +++ b/backend/server/utils/cooldownToken.ts @@ -0,0 +1,83 @@ +import { SignJWT, jwtVerify, type JWTPayload } from "jose"; +import { randomUUID } from "crypto"; + +const ALGORITHM = "HS256"; +const PURPOSE = "rebreak.cooldown"; + +function getSecret(): Uint8Array { + const raw = + process.env.REBREAK_COOLDOWN_SECRET || + process.env.NUXT_AUTH_SECRET || + // Last-resort fallback — deterministic within a single process lifetime. + // A proper secret MUST be set via Infisical in production. + "rebreak-cooldown-insecure-fallback-replace-me"; + return new TextEncoder().encode(raw); +} + +export interface CooldownTokenPayload { + userId: string; + jti: string; + cooldownEndsAt: string; // ISO-8601 +} + +/** + * Signs a short-lived JWT that the iOS app can present to prove it has + * permission to disable the DNS protection (cooldown expired on the server). + * + * Lifetime: 5 minutes — short enough to prevent replay attacks. + * The `jti` ties the token to the exact CooldownRequest row. + */ +export async function signCooldownToken( + userId: string, + jti: string, + cooldownEndsAt: Date, +): Promise { + const secret = getSecret(); + const now = Math.floor(Date.now() / 1000); + + return new SignJWT({ + sub: userId, + jti, + purpose: PURPOSE, + cooldown_ends_at: cooldownEndsAt.toISOString(), + } satisfies JWTPayload & { purpose: string; cooldown_ends_at: string }) + .setProtectedHeader({ alg: ALGORITHM }) + .setIssuedAt(now) + .setExpirationTime(now + 5 * 60) // 5 minutes + .sign(secret); +} + +/** + * Verifies the token and returns its payload or null if invalid/expired. + */ +export async function verifyCooldownToken( + token: string, +): Promise { + try { + const { payload } = await jwtVerify(token, getSecret(), { + algorithms: [ALGORITHM], + }); + + if ( + typeof payload.sub !== "string" || + typeof payload.jti !== "string" || + payload.purpose !== PURPOSE || + typeof payload.cooldown_ends_at !== "string" + ) { + return null; + } + + return { + userId: payload.sub, + jti: payload.jti, + cooldownEndsAt: payload.cooldown_ends_at, + }; + } catch { + return null; + } +} + +/** Convenience: generate a new JTI (UUID v4). */ +export function generateJti(): string { + return randomUUID(); +} diff --git a/backend/server/utils/crypto.ts b/backend/server/utils/crypto.ts new file mode 100644 index 0000000..152c644 --- /dev/null +++ b/backend/server/utils/crypto.ts @@ -0,0 +1,43 @@ +import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; + +const ALGORITHM = "aes-256-gcm"; +const KEY_LENGTH = 32; + +function getKey(): Buffer { + const raw = + process.env.NUXT_ENCRYPTION_KEY || process.env.ENCRYPTION_KEY || ""; + if (!raw || raw.length < 32) { + // Fallback: pad with zeros (only for dev without key) + return Buffer.alloc( + KEY_LENGTH, + raw.padEnd(KEY_LENGTH, "0").slice(0, KEY_LENGTH), + ); + } + return Buffer.from(raw.slice(0, KEY_LENGTH), "utf8"); +} + +export function encrypt(text: string): string { + const key = getKey(); + const iv = randomBytes(12); + const cipher = createCipheriv(ALGORITHM, key, iv); + const encrypted = Buffer.concat([ + cipher.update(text, "utf8"), + cipher.final(), + ]); + const tag = cipher.getAuthTag(); + // Format: iv(24hex):tag(32hex):encrypted(hex) + return `${iv.toString("hex")}:${tag.toString("hex")}:${encrypted.toString("hex")}`; +} + +export function decrypt(stored: string): string { + const key = getKey(); + const parts = stored.split(":"); + if (parts.length !== 3) throw new Error("Invalid encrypted format"); + const [ivHex, tagHex, dataHex] = parts; + const iv = Buffer.from(ivHex, "hex"); + const tag = Buffer.from(tagHex, "hex"); + const data = Buffer.from(dataHex, "hex"); + const decipher = createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(tag); + return decipher.update(data) + decipher.final("utf8"); +} diff --git a/backend/server/utils/domainHash.ts b/backend/server/utils/domainHash.ts new file mode 100644 index 0000000..bf6e343 --- /dev/null +++ b/backend/server/utils/domainHash.ts @@ -0,0 +1,69 @@ +import { createHash } from "node:crypto"; + +/** + * Normalisiert eine Domain für Hashing — muss zwischen Server und iOS-Extension + * IDENTISCH sein, sonst stimmen die Hashes nicht überein. + * + * Schritte: + * 1. trim, lowercase + * 2. http:// und https:// entfernen + * 3. Pfad / Query nach erstem `/` abschneiden + * 4. Optional `www.` Prefix entfernen (so dass `www.bet365.com` und `bet365.com` + * den gleichen Hash haben) + */ +export function normalizeDomain(input: string): string { + return input + .trim() + .toLowerCase() + .replace(/^https?:\/\//, "") + .replace(/\/.*$/, "") + .replace(/^www\./, ""); +} + +/** + * SHA-256 → erste 8 Bytes als big-endian UInt64. + * Wenn salt gesetzt ist, wird `:` gehasht (für user-spezifische + * Custom-Domains, damit gleiche Domain bei zwei Usern unterschiedliche + * Hashes ergibt → keine Cross-User-Korrelation möglich). + */ +export function hashDomain(domain: string, salt = ""): bigint { + const normalized = normalizeDomain(domain); + const input = salt ? `${salt}:${normalized}` : normalized; + const digest = createHash("sha256").update(input, "utf8").digest(); + return digest.readBigUInt64BE(0); +} + +/** + * Hasht eine Liste von Domains, sortiert die Hashes aufsteigend, und gibt + * sie als Binary-Buffer zurück (8 Bytes pro Hash, big-endian). + * + * Format der Binary-Datei für die iOS-Extension: + * ┌────────────────┬────────────────┬─────────────────┐ + * │ Hash 0 (8 B) │ Hash 1 (8 B) │ Hash 2 (8 B) │ ... + * └────────────────┴────────────────┴─────────────────┘ + * sorted ascending → binary-search möglich, O(log n). + */ +export function buildHashListBinary(domains: string[], salt = ""): Buffer { + const hashes = new BigUint64Array(domains.length); + for (let i = 0; i < domains.length; i++) { + hashes[i] = hashDomain(domains[i], salt); + } + hashes.sort(); + + // BigUint64Array ist platform-endian — wir brauchen explicit big-endian + // damit Server und iOS-Extension dasselbe Format lesen. + const buf = Buffer.alloc(hashes.length * 8); + for (let i = 0; i < hashes.length; i++) { + buf.writeBigUInt64BE(hashes[i], i * 8); + } + return buf; +} + +/** + * ETag aus dem Binary-Content (sha256 der Bytes, hex-encoded). + * Client cached darauf basierend → wenn Server-DB sich nicht ändert, + * spart Bandbreite + Battery. + */ +export function etagFor(buf: Buffer): string { + return `"${createHash("sha256").update(buf).digest("hex").slice(0, 16)}"`; +} diff --git a/backend/server/utils/gambling-keywords.mjs b/backend/server/utils/gambling-keywords.mjs new file mode 100644 index 0000000..02a6c1a --- /dev/null +++ b/backend/server/utils/gambling-keywords.mjs @@ -0,0 +1,64 @@ +/** + * Single-Source-of-Truth für Gambling-Keyword-Detection. + * + * Importiert von: + * - server/api/mail/scan.post.ts + * - server/api/mail/scan-internal.post.ts + * - imap-proxy/session.mjs + * - imap-idle/index.mjs + * + * Mo's DSGVO-Finding #4: vorher in 4 Files dupliziert → Drift-Risk. + */ + +export const GAMBLING_KEYWORDS = [ + // Major Anbieter + "casino", "bet365", "bwin", "tipico", "unibet", "betway", "888casino", + "pokerstars", "interwetten", "netbet", "leovegas", "mrgreen", "mr green", + "betsson", "neobet", "mybet", "lottoland", "betano", "william hill", + "paddypower", "betfair", "stake", "rolletto", "vbet", "1xbet", "melbet", + "mostbet", "luckyvibe", "lucky vibe", "spinz", "casinoly", "rabona", + "justcasino", "getslots", "rocketplay", "fresh casino", "freshcasino", + "nom nom", + + // Generic Begriffe + "sportwetten", "jackpot", "freispiel", "free spin", "bonus code", + "auszahlung", "glücksspiel", "slots", "roulette", + + // ⚠️ Risk: matcht auch unschuldige Wörter (Mo's Finding #5) + // TODO Whitelist: "wette" matcht "wettervorhersage" → False-Positive + // siehe gambling-whitelist.mjs (TODO) + "wette", +]; + +/** + * Whitelist — Begriffe die NICHT als Gambling gelten dürfen. + * Bei Match in GAMBLING_KEYWORDS, vor Block prüfen ob in Whitelist. + * + * TODO Mo's Finding #5: ausführen Mail-Whitelist-Check vor Auto-Delete. + */ +export const GAMBLING_WHITELIST = [ + "wettervorhersage", + "wetter", + "wetterbericht", + "wettkampf", // kein Glücksspiel + "wettbewerb", // dito +]; + +/** + * Helper: prüft ob ein Text Gambling-Keywords enthält, mit Whitelist-Check. + */ +export function isGamblingText(text) { + if (!text) return false; + const lower = text.toLowerCase(); + + // Erst Whitelist — wenn matched, kein Gambling + for (const w of GAMBLING_WHITELIST) { + if (lower.includes(w)) return false; + } + + // Dann Gambling-Keywords + for (const kw of GAMBLING_KEYWORDS) { + if (lower.includes(kw)) return true; + } + return false; +} diff --git a/backend/server/utils/getUsersMeta.ts b/backend/server/utils/getUsersMeta.ts new file mode 100644 index 0000000..d7f7964 --- /dev/null +++ b/backend/server/utils/getUsersMeta.ts @@ -0,0 +1,22 @@ +import { usePrisma } from "./prisma"; + +/** + * Lädt nickname + avatar aus der profiles-Tabelle für mehrere User-IDs. + * Vollständig über Prisma – kein Supabase Service Role benötigt. + */ +export async function getUsersMeta( + userIds: string[], +): Promise> { + if (userIds.length === 0) return {}; + const db = usePrisma(); + const profiles = await db.profile.findMany({ + where: { id: { in: userIds } }, + select: { id: true, nickname: true, username: true, avatar: true }, + }); + return Object.fromEntries( + profiles.map((p) => [ + p.id, + { nickname: p.nickname ?? p.username ?? null, avatar: p.avatar }, + ]), + ); +} diff --git a/backend/server/utils/imap-providers.ts b/backend/server/utils/imap-providers.ts new file mode 100644 index 0000000..82ed6db --- /dev/null +++ b/backend/server/utils/imap-providers.ts @@ -0,0 +1,63 @@ +/** + * IMAP-Provider Konfigurationen + * Automatisch erkennen anhand der Email-Domain + */ +export interface ImapConfig { + host: string; + port: number; + name: string; +} + +const PROVIDER_MAP: Record = { + "gmail.com": { host: "imap.gmail.com", port: 993, name: "Gmail" }, + "googlemail.com": { host: "imap.gmail.com", port: 993, name: "Gmail" }, + "icloud.com": { host: "imap.mail.me.com", port: 993, name: "iCloud" }, + "me.com": { host: "imap.mail.me.com", port: 993, name: "iCloud" }, + "mac.com": { host: "imap.mail.me.com", port: 993, name: "iCloud" }, + "outlook.com": { host: "outlook.office365.com", port: 993, name: "Outlook" }, + "hotmail.com": { host: "outlook.office365.com", port: 993, name: "Hotmail" }, + "hotmail.de": { host: "outlook.office365.com", port: 993, name: "Hotmail" }, + "live.com": { host: "outlook.office365.com", port: 993, name: "Live" }, + "live.de": { host: "outlook.office365.com", port: 993, name: "Live" }, + "yahoo.com": { host: "imap.mail.yahoo.com", port: 993, name: "Yahoo" }, + "yahoo.de": { host: "imap.mail.yahoo.com", port: 993, name: "Yahoo" }, + "gmx.de": { host: "imap.gmx.net", port: 993, name: "GMX" }, + "gmx.net": { host: "imap.gmx.net", port: 993, name: "GMX" }, + "gmx.at": { host: "imap.gmx.net", port: 993, name: "GMX" }, + "gmx.ch": { host: "imap.gmx.net", port: 993, name: "GMX" }, + "web.de": { host: "imap.web.de", port: 993, name: "Web.de" }, + "t-online.de": { + host: "secureimap.t-online.de", + port: 993, + name: "T-Online", + }, + "freenet.de": { host: "mx.freenet.de", port: 993, name: "Freenet" }, + "posteo.de": { host: "posteo.de", port: 993, name: "Posteo" }, +}; + +export function detectImapProvider(email: string): ImapConfig { + const domain = email.split("@")[1]?.toLowerCase() ?? ""; + return ( + PROVIDER_MAP[domain] ?? { + host: `imap.${domain}`, + port: 993, + name: domain, + } + ); +} + +const SMTP_MAP: Record = { + "imap.gmail.com": { host: "smtp.gmail.com", port: 587 }, + "imap.mail.me.com": { host: "smtp.mail.me.com", port: 587 }, + "imap.gmx.net": { host: "mail.gmx.net", port: 587 }, + "imap.web.de": { host: "smtp.web.de", port: 587 }, + "outlook.office365.com": { host: "smtp-mail.outlook.com", port: 587 }, + "imap.mail.yahoo.com": { host: "smtp.mail.yahoo.com", port: 587 }, + "secureimap.t-online.de": { host: "securesmtp.t-online.de", port: 587 }, + "mx.freenet.de": { host: "mx.freenet.de", port: 587 }, + "posteo.de": { host: "posteo.de", port: 587 }, +}; + +export function detectSmtpProvider(imapHost: string): { host: string; port: number } { + return SMTP_MAP[imapHost] ?? { host: imapHost.replace("imap.", "smtp."), port: 587 }; +} diff --git a/backend/server/utils/lyraMemoryExtract.ts b/backend/server/utils/lyraMemoryExtract.ts new file mode 100644 index 0000000..b7497b1 --- /dev/null +++ b/backend/server/utils/lyraMemoryExtract.ts @@ -0,0 +1,122 @@ +/** + * Lyra Memory Extraction — interne Funktion (ohne HTTP-Roundtrip). + * + * Wird von sos-stream.get.ts fire-and-forget nach Stream-Ende aufgerufen. + * Fehler sind immer silent — darf NIE die User-Experience beeinflussen. + */ +import { upsertMemory } from "../db/lyraMemory"; +import type { LyraMemoryType } from "../db/lyraMemory"; + +const VALID_TYPES: LyraMemoryType[] = [ + "trigger", + "habit", + "strength", + "relationship", + "milestone", + "pain_point", + "goal", + "preference", +]; + +const EXTRACTION_SYSTEM_PROMPT = `Du extrahierst aus einem Gespräch zwischen User und Lyra-Coach strukturierte Fakten über den User. Output strikt als JSON-Array: +[{"type":"trigger|habit|strength|relationship|milestone|pain_point|goal|preference", "content":"", "confidence":0.0-1.0}] +Regeln: +- Nur Fakten die der USER explizit oder implizit über sich geteilt hat. Nichts erfinden. +- Keine Vermutungen über Diagnosen oder Pathologisierungen ("süchtig", "krank" etc). +- Nur Wesentliches. Wenn nichts Neues drin ist → leeres Array []. +- confidence: 0.9+ wenn User explizit gesagt, 0.5-0.7 wenn implizit, <0.5 garnicht extrahieren. +- content in der Sprache des Gesprächs (DE). +- Maximal 8 Einträge pro Extraktion.`; + +export async function extractAndStoreMemories( + userId: string, + conversation: Array<{ role: string; content: string }>, + sessionId?: string, + openrouterKey?: string, +): Promise { + if (!openrouterKey) { + console.warn("[lyra-memory] extract: no OpenRouter key"); + return; + } + + const userMessages = conversation.filter((m) => m.role === "user"); + if (userMessages.length === 0) return; + + const conversationText = conversation + .slice(-20) + .map((m) => `${m.role === "user" ? "User" : "Lyra"}: ${m.content}`) + .join("\n") + .slice(0, 4000); + + try { + const res = await $fetch<{ + choices: { message: { content: string } }[]; + }>("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${openrouterKey}`, + "Content-Type": "application/json", + "HTTP-Referer": "https://rebreak.org", + "X-Title": "ReBreak Memory Extraction", + }, + body: { + model: "anthropic/claude-haiku-4-5", + max_tokens: 800, + temperature: 0.1, + messages: [ + { role: "system", content: EXTRACTION_SYSTEM_PROMPT }, + { role: "user", content: conversationText }, + ], + }, + timeout: 20000, + }); + + const raw = res.choices?.[0]?.message?.content?.trim(); + if (!raw) return; + + const jsonStr = raw + .replace(/^```(?:json)?\s*/i, "") + .replace(/\s*```$/, "") + .trim(); + + let facts: Array<{ type: string; content: string; confidence: number }> = + []; + try { + facts = JSON.parse(jsonStr); + } catch { + console.warn( + "[lyra-memory] extract: JSON parse failed:", + jsonStr.slice(0, 200), + ); + return; + } + + if (!Array.isArray(facts)) return; + + let extracted = 0; + for (const fact of facts) { + if (!fact.content || (fact.confidence ?? 0) < 0.5) continue; + if (!VALID_TYPES.includes(fact.type as LyraMemoryType)) continue; + + try { + await upsertMemory( + userId, + fact.type as LyraMemoryType, + fact.content, + sessionId ?? "sos-session", + Math.min(1, Math.max(0, fact.confidence ?? 0.7)), + ); + extracted++; + } catch (e) { + console.error("[lyra-memory] upsert error:", e); + } + } + + console.log( + `[lyra-memory] async extract done for ${userId}: ${extracted} stored`, + ); + } catch (e) { + // Silent — darf User nie beeinflussen + console.error("[lyra-memory] extract error (silent):", e); + } +} diff --git a/backend/server/utils/plan-features.ts b/backend/server/utils/plan-features.ts new file mode 100644 index 0000000..bd7198d --- /dev/null +++ b/backend/server/utils/plan-features.ts @@ -0,0 +1,90 @@ +export type Plan = "free" | "pro" | "legend"; + +export interface PlanLimits { + /** Max. eigene Domains (Infinity = unbegrenzt) */ + customDomains: number; + /** Freigeschaltete Domain-Slots füllen sich wieder auf (Community-Promotion) */ + domainRefill: boolean; + /** Max. aktive Mail-Agenten (Infinity = unbegrenzt) */ + mailAgents: number; + /** Erlaubte Scan-Intervalle in Stunden */ + mailIntervalOptions: number[]; + /** Zugang zur globalen HaGeZi-Blocklist (200k+) */ + globalBlocklist: boolean; + /** Darf in der Community posten */ + canPost: boolean; + /** Darf Gruppen gründen */ + canCreateGroup: boolean; + /** Darf Domains direkt zur ReBreak Blocklist hinzufügen */ + canAddToBlocklist: boolean; + /** Max. parallel registrierte Devices pro Account (Anti-Account-Sharing) */ + maxDevices: number; + /** Primäres OpenRouter/Groq-Modell für KI-Coach */ + aiModel: string; + /** Fallback-Modelle (werden der Reihe nach versucht wenn primary fehlschlägt) */ + aiModelFallbacks: Array<{ provider: "groq" | "openrouter"; model: string }>; + /** AI-Provider: groq (Free/Pro) oder openrouter (Legend/Claude) */ + aiProvider: "groq" | "openrouter"; +} + +export const PLAN_LIMITS: Record = { + free: { + customDomains: 5, + domainRefill: false, + mailAgents: 1, + mailIntervalOptions: [4], + globalBlocklist: false, + canPost: true, + canCreateGroup: false, + canAddToBlocklist: false, + maxDevices: 1, + aiModel: "llama-3.1-8b-instant", + aiModelFallbacks: [ + { provider: "groq", model: "llama-3.3-70b-versatile" }, + { provider: "groq", model: "gemma2-9b-it" }, + { provider: "openrouter", model: "meta-llama/llama-3.1-8b-instruct" }, + ], + aiProvider: "groq", + }, + pro: { + customDomains: 5, + domainRefill: true, + mailAgents: 3, + mailIntervalOptions: [1, 4, 8], + globalBlocklist: true, + canPost: true, + canCreateGroup: false, + canAddToBlocklist: false, + maxDevices: 1, + aiModel: "llama-3.3-70b-versatile", + aiModelFallbacks: [ + { provider: "groq", model: "llama-3.1-8b-instant" }, + { provider: "openrouter", model: "meta-llama/llama-3.3-70b-instruct" }, + ], + aiProvider: "groq", + }, + legend: { + customDomains: 10, + domainRefill: true, + mailAgents: Infinity, + mailIntervalOptions: [1, 4, 8], + globalBlocklist: true, + canPost: true, + canCreateGroup: true, + canAddToBlocklist: true, + maxDevices: 3, + aiModel: "anthropic/claude-3.5-haiku", + aiModelFallbacks: [ + { provider: "openrouter", model: "anthropic/claude-3-haiku" }, + { provider: "groq", model: "llama-3.3-70b-versatile" }, + ], + aiProvider: "openrouter", + }, +}; + +export function getPlanLimits(plan: string): PlanLimits { + // Legacy-Pläne auf neue Namen mappen + if (plan === "premium") return PLAN_LIMITS.legend; + if (plan === "standard") return PLAN_LIMITS.pro; + return PLAN_LIMITS[(plan as Plan) ?? "free"] ?? PLAN_LIMITS.free; +} diff --git a/backend/server/utils/prisma.ts b/backend/server/utils/prisma.ts new file mode 100644 index 0000000..8f17fdc --- /dev/null +++ b/backend/server/utils/prisma.ts @@ -0,0 +1,23 @@ +import { PrismaClient } from "../generated/prisma"; +import { PrismaPg } from "@prisma/adapter-pg"; + +// Rebreak Supabase Postgres – Prod: 5434 / Staging: 5435 +const PROD_URL = + "postgresql://postgres:iPva_XtETZMSJfTod1lt4Z8GYz4wkN7O@127.0.0.1:5434/postgres"; +const STAGING_URL = + "postgresql://postgres:iPva_XtETZMSJfTod1lt4Z8GYz4wkN7O@127.0.0.1:5435/postgres"; + +let _prisma: PrismaClient | null = null; + +export function usePrisma(): PrismaClient { + if (_prisma) return _prisma; + + const config = useRuntimeConfig(); + const isProduction = process.env.NODE_ENV === "production"; + const url = + (config as any).databaseUrl || (isProduction ? PROD_URL : STAGING_URL); + + const adapter = new PrismaPg({ connectionString: url }); + _prisma = new PrismaClient({ adapter, log: ["error"] }); + return _prisma; +} diff --git a/backend/server/utils/scoring.ts b/backend/server/utils/scoring.ts new file mode 100644 index 0000000..8a67ebf --- /dev/null +++ b/backend/server/utils/scoring.ts @@ -0,0 +1,2 @@ +// Re-export aus dem DB-Layer – kein H3Event mehr nötig +export { awardPoints, type ScoreEventType } from "../db/scores"; diff --git a/backend/server/utils/sosSessions.ts b/backend/server/utils/sosSessions.ts new file mode 100644 index 0000000..4cbdd2b --- /dev/null +++ b/backend/server/utils/sosSessions.ts @@ -0,0 +1,49 @@ +/** + * In-Memory Session Store für SOS-Streaming + * + * POST /api/coach/sos-session speichert messages/locale hier, + * GET /api/coach/sos-stream lädt sie per sessionId. + * + * TTL: 5 Minuten (Auto-Cleanup) + */ +type SosSessionData = { + userId: string; + messages: Array<{ role: "user" | "assistant"; content: string }>; + locale: string; + createdAt: number; +}; + +const sessions = new Map(); +const SESSION_TTL = 5 * 60 * 1000; // 5min + +// Cleanup-Intervall: alle 2min alte Sessions löschen +setInterval( + () => { + const now = Date.now(); + for (const [id, data] of sessions.entries()) { + if (now - data.createdAt > SESSION_TTL) { + sessions.delete(id); + } + } + }, + 2 * 60 * 1000, +); + +export function setSosSession(sessionId: string, data: SosSessionData) { + sessions.set(sessionId, data); +} + +export function getSosSession(sessionId: string): SosSessionData | undefined { + const data = sessions.get(sessionId); + if (!data) return undefined; + // TTL-Check + if (Date.now() - data.createdAt > SESSION_TTL) { + sessions.delete(sessionId); + return undefined; + } + return data; +} + +export function deleteSosSession(sessionId: string) { + sessions.delete(sessionId); +} diff --git a/backend/server/utils/useSupabase.ts b/backend/server/utils/useSupabase.ts new file mode 100644 index 0000000..b06253d --- /dev/null +++ b/backend/server/utils/useSupabase.ts @@ -0,0 +1,75 @@ +// Drop-in-Replacement für `#supabase/server` aus dem Nuxt-Modul. +// Standalone-Nitro hat den Alias nicht — wir bauen die zwei Helper hier nach. +// +// `serverSupabaseClient(event)` → Anon-Key-Client (RLS aktiv, im Auftrag des Users) +// `serverSupabaseServiceRole(event)` → Service-Role-Client (RLS umgangen, admin) +// +// Auth-Cookies bleiben kompatibel mit dem ursprünglichen Nuxt-Modul-Verhalten: +// Cookies `sb-access-token` und `sb-refresh-token` werden gelesen und an den +// Supabase-Client als initial session weitergegeben. +import type { H3Event } from "h3"; +import { createClient, type SupabaseClient } from "@supabase/supabase-js"; +import { getCookie } from "h3"; + +function getSupabaseUrl(): string { + const url = + process.env.SUPABASE_URL ?? + process.env.NUXT_PUBLIC_SUPABASE_URL ?? + ""; + if (!url) throw new Error("SUPABASE_URL nicht gesetzt"); + return url; +} + +function getAnonKey(): string { + const key = + process.env.SUPABASE_KEY ?? + process.env.SUPABASE_ANON_KEY ?? + process.env.NUXT_PUBLIC_SUPABASE_KEY ?? + ""; + if (!key) throw new Error("SUPABASE_KEY (anon) nicht gesetzt"); + return key; +} + +function getServiceRoleKey(): string { + const key = + process.env.SUPABASE_SERVICE_KEY ?? + process.env.SUPABASE_SERVICE_ROLE_KEY ?? + ""; + if (!key) throw new Error("SUPABASE_SERVICE_KEY nicht gesetzt"); + return key; +} + +export async function serverSupabaseClient( + event: H3Event, +): Promise> { + const accessToken = getCookie(event, "sb-access-token"); + const refreshToken = getCookie(event, "sb-refresh-token"); + const client = createClient(getSupabaseUrl(), getAnonKey(), { + auth: { + autoRefreshToken: false, + persistSession: false, + detectSessionInUrl: false, + }, + global: accessToken + ? { headers: { Authorization: `Bearer ${accessToken}` } } + : undefined, + }); + if (accessToken && refreshToken) { + await client.auth + .setSession({ access_token: accessToken, refresh_token: refreshToken }) + .catch(() => {}); + } + return client; +} + +export function serverSupabaseServiceRole( + _event: H3Event, +): SupabaseClient { + return createClient(getSupabaseUrl(), getServiceRoleKey(), { + auth: { + autoRefreshToken: false, + persistSession: false, + detectSessionInUrl: false, + }, + }); +} diff --git a/backend/start-staging.sh b/backend/start-staging.sh new file mode 100644 index 0000000..84e51d5 --- /dev/null +++ b/backend/start-staging.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# rebreak-backend Staging — startet Nitro mit Infisical-Secrets. +# Pattern analog trucko-backend/start-prod.sh, aber env=staging. +source /etc/environment + +if [[ -z "$INFISICAL_CLIENT_ID" || -z "$INFISICAL_CLIENT_SECRET" ]]; then + echo "❌ INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET nicht gesetzt in /etc/environment" >&2 + exit 1 +fi + +INFISICAL_TOKEN=$(infisical login \ + --method=universal-auth \ + --client-id="${INFISICAL_CLIENT_ID}" \ + --client-secret="${INFISICAL_CLIENT_SECRET}" \ + --silent --plain 2>/dev/null) + +if [[ -z "$INFISICAL_TOKEN" ]]; then + echo "❌ Infisical login fehlgeschlagen" >&2 + exit 1 +fi + +export NODE_ENV=production +export NITRO_PORT=3016 +export NITRO_HOST=127.0.0.1 +export PORT=3016 + +exec infisical run \ + --projectId="${INFISICAL_PROJECT_ID}" \ + --env=staging \ + --token="$INFISICAL_TOKEN" \ + -- /root/.nvm/versions/node/v24.11.1/bin/node /srv/rebreak-monorepo/backend/.output/server/index.mjs diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..3c21032 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.nitro/types/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "useUnknownInCatchVariables": true, + "lib": ["ESNext", "DOM"], + "baseUrl": "." + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3fac15d --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "rebreak-monorepo", + "private": true, + "version": "0.1.0", + "scripts": { + "dev:backend": "pnpm --filter rebreak-backend dev", + "dev:native": "pnpm --filter rebreak-native start", + "build:backend": "pnpm --filter rebreak-backend build", + "android": "pnpm --filter rebreak-native android", + "ios": "pnpm --filter rebreak-native ios" + }, + "engines": { + "node": ">=20", + "pnpm": ">=9" + }, + "packageManager": "pnpm@10.23.0" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d232a0f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,11232 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + apps/rebreak-native: + dependencies: + '@expo-google-fonts/nunito': + specifier: ^0.2.3 + version: 0.2.3 + '@expo/vector-icons': + specifier: ^14.0.0 + version: 14.1.0(expo-font@13.0.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + '@react-native-async-storage/async-storage': + specifier: ^2.1.2 + version: 2.2.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + '@react-native-community/slider': + specifier: ^5.2.0 + version: 5.2.0 + '@react-navigation/native': + specifier: ^7.0.0 + version: 7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + '@supabase/supabase-js': + specifier: ^2.46.0 + version: 2.105.3 + '@tanstack/react-query': + specifier: ^5.59.0 + version: 5.100.9(react@19.0.0) + expo: + specifier: ^53.0.0 + version: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-apple-authentication: + specifier: ~7.2.4 + version: 7.2.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + expo-application: + specifier: ~6.1.5 + version: 6.1.5(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-av: + specifier: ~15.1.7 + version: 15.1.7(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-build-properties: + specifier: ~0.14.8 + version: 0.14.8(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-clipboard: + specifier: ^55.0.13 + version: 55.0.13(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-constants: + specifier: ~17.1.8 + version: 17.1.8(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + expo-dev-client: + specifier: ~5.2.4 + version: 5.2.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-file-system: + specifier: ~18.1.11 + version: 18.1.11(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + expo-font: + specifier: ~13.0.0 + version: 13.0.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0) + expo-haptics: + specifier: ^55.0.14 + version: 55.0.14(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-image-picker: + specifier: ~16.1.4 + version: 16.1.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-linking: + specifier: ~7.1.7 + version: 7.1.7(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-localization: + specifier: ~16.1.6 + version: 16.1.6(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0) + expo-modules-core: + specifier: ^2.0.0 + version: 2.5.0 + expo-notifications: + specifier: ~0.31.5 + version: 0.31.5(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-router: + specifier: ~5.1.11 + version: 5.1.11(15de8a0d2b6e197a248b53123aa45ca3) + expo-speech: + specifier: ~13.1.7 + version: 13.1.7(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-splash-screen: + specifier: ~0.30.10 + version: 0.30.10(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-status-bar: + specifier: ~2.2.3 + version: 2.2.3(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-web-browser: + specifier: ~14.2.0 + version: 14.2.0(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + i18next: + specifier: ^23.16.0 + version: 23.16.8 + lottie-react-native: + specifier: 7.2.2 + version: 7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + nativewind: + specifier: ^4.1.0 + version: 4.2.3(react-native-reanimated@4.0.3(@babel/core@7.29.0)(react-native-worklets@0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-svg@15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)(tailwindcss@3.4.19) + react: + specifier: 19.0.0 + version: 19.0.0 + react-dom: + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) + react-hook-form: + specifier: ^7.53.0 + version: 7.75.0(react@19.0.0) + react-i18next: + specifier: ^15.1.0 + version: 15.7.4(i18next@23.16.8)(react-dom@19.0.0(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)(typescript@5.8.3) + react-native: + specifier: 0.79.6 + version: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + react-native-bottom-tabs: + specifier: ^1.2.0 + version: 1.2.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-gesture-handler: + specifier: ~2.24.0 + version: 2.24.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-mmkv: + specifier: ^3.1.0 + version: 3.3.3(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-reanimated: + specifier: ~4.0.0 + version: 4.0.3(@babel/core@7.29.0)(react-native-worklets@0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-safe-area-context: + specifier: 5.4.0 + version: 5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-screens: + specifier: ~4.11.1 + version: 4.11.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-sse: + specifier: ^1.2.1 + version: 1.2.1 + react-native-svg: + specifier: 15.11.2 + version: 15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-url-polyfill: + specifier: ^2.0.0 + version: 2.0.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + react-native-worklets: + specifier: ~0.4.0 + version: 0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + rive-react-native: + specifier: ^9.0.1 + version: 9.8.3(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + tailwindcss: + specifier: ^3.4.14 + version: 3.4.19 + valibot: + specifier: ^1.2.0 + version: 1.4.0(typescript@5.8.3) + zustand: + specifier: ^5.0.0 + version: 5.0.13(@types/react@19.0.14)(react@19.0.0)(use-sync-external-store@1.6.0(react@19.0.0)) + devDependencies: + '@babel/core': + specifier: ^7.25.0 + version: 7.29.0 + '@types/react': + specifier: ~19.0.14 + version: 19.0.14 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + + backend: + dependencies: + '@prisma/adapter-pg': + specifier: ^7.2.0 + version: 7.8.0 + '@prisma/client': + specifier: ^7.2.0 + version: 7.8.0(prisma@7.8.0(@types/react@19.0.14)(magicast@0.5.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3))(typescript@5.9.3) + '@supabase/supabase-js': + specifier: ^2.39.7 + version: 2.105.3 + groq-sdk: + specifier: ^0.7.0 + version: 0.7.0 + imapflow: + specifier: ^1.2.18 + version: 1.3.3 + jose: + specifier: ^6.0.0 + version: 6.2.3 + openai: + specifier: ^4.65.0 + version: 4.104.0(ws@8.20.0)(zod@3.25.76) + pg: + specifier: ^8.16.3 + version: 8.20.0 + resend: + specifier: ^4.0.0 + version: 4.8.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + stripe: + specifier: ^17.0.0 + version: 17.7.0 + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + '@types/pg': + specifier: ^8.11.10 + version: 8.20.0 + h3: + specifier: ^1.15.4 + version: 1.15.11 + nitropack: + specifier: ^2.12.4 + version: 2.13.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3) + prisma: + specifier: ^7.2.0 + version: 7.8.0(@types/react@19.0.14)(magicast@0.5.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@0no-co/graphql.web@1.2.0': + resolution: {integrity: sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + graphql: + optional: true + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.10.4': + resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.3': + resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.28.6': + resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.25.9': + resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.29.0': + resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-export-default-from@7.27.1': + resolution: {integrity: sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-export-default-from@7.28.6': + resolution: {integrity: sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-flow@7.28.6': + resolution: {integrity: sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.0': + resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.28.6': + resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.6': + resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.28.6': + resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-classes@7.28.6': + resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.28.6': + resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-flow-strip-types@7.27.1': + resolution: {integrity: sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6': + resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': + resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.28.6': + resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.6': + resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.28.6': + resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.28.6': + resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.28.6': + resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.28.6': + resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.28.6': + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.27.1': + resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.0': + resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.29.0': + resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.28.6': + resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-react@7.28.5': + resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@egjs/hammerjs@2.0.17': + resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} + engines: {node: '>=0.8.0'} + + '@electric-sql/pglite-socket@0.1.1': + resolution: {integrity: sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1': + resolution: {integrity: sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==} + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': + resolution: {integrity: sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==} + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@expo-google-fonts/nunito@0.2.3': + resolution: {integrity: sha512-z+Bx3IuT0t3jguoMxiyWuC7pW3wDVNHgYko/G9V23QhR/yDSjEsT+Kx+VGDT/hu9TXSxw3CtpQ5MFHikqSVDYw==} + + '@expo/cli@0.24.24': + resolution: {integrity: sha512-XybHfF2QNPJNnHoUKHcG796iEkX5126UuTAs6MSpZuvZRRQRj/sGCLX+driCOVHbDOpcCOusMuHrhxHbtTApyg==} + hasBin: true + + '@expo/code-signing-certificates@0.0.6': + resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==} + + '@expo/config-plugins@10.1.2': + resolution: {integrity: sha512-IMYCxBOcnuFStuK0Ay+FzEIBKrwW8OVUMc65+v0+i7YFIIe8aL342l7T4F8lR4oCfhXn7d6M5QPgXvjtc/gAcw==} + + '@expo/config-types@53.0.5': + resolution: {integrity: sha512-kqZ0w44E+HEGBjy+Lpyn0BVL5UANg/tmNixxaRMLS6nf37YsDrLk2VMAmeKMMk5CKG0NmOdVv3ngeUjRQMsy9g==} + + '@expo/config@11.0.13': + resolution: {integrity: sha512-TnGb4u/zUZetpav9sx/3fWK71oCPaOjZHoVED9NaEncktAd0Eonhq5NUghiJmkUGt3gGSjRAEBXiBbbY9/B1LA==} + + '@expo/devcert@1.2.1': + resolution: {integrity: sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==} + + '@expo/env@1.0.7': + resolution: {integrity: sha512-qSTEnwvuYJ3umapO9XJtrb1fAqiPlmUUg78N0IZXXGwQRt+bkp0OBls+Y5Mxw/Owj8waAM0Z3huKKskRADR5ow==} + + '@expo/fingerprint@0.13.4': + resolution: {integrity: sha512-MYfPYBTMfrrNr07DALuLhG6EaLVNVrY/PXjEzsjWdWE4ZFn0yqI0IdHNkJG7t1gePT8iztHc7qnsx+oo/rDo6w==} + hasBin: true + + '@expo/image-utils@0.7.6': + resolution: {integrity: sha512-GKnMqC79+mo/1AFrmAcUcGfbsXXTRqOMNS1umebuevl3aaw+ztsYEFEiuNhHZW7PQ3Xs3URNT513ZxKhznDscw==} + + '@expo/json-file@10.0.14': + resolution: {integrity: sha512-yWwBFywFv+SxkJp/pIzzA416JVYflNUh7pqQzgaA6nXDqRyK7KfrqVzk8PdUfDnqbBcaZZxpzNssfQZzp5KHrA==} + + '@expo/json-file@9.1.5': + resolution: {integrity: sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA==} + + '@expo/metro-config@0.20.18': + resolution: {integrity: sha512-qPYq3Cq61KQO1CppqtmxA1NGKpzFOmdiL7WxwLhEVnz73LPSgneW7dV/3RZwVFkjThzjA41qB4a9pxDqtpepPg==} + + '@expo/metro-runtime@5.0.5': + resolution: {integrity: sha512-P8UFTi+YsmiD1BmdTdiIQITzDMcZgronsA3RTQ4QKJjHM3bas11oGzLQOnFaIZnlEV8Rrr3m1m+RHxvnpL+t/A==} + peerDependencies: + react-native: '*' + + '@expo/osascript@2.4.3': + resolution: {integrity: sha512-wbuj3EebM7W9hN/Wp4xTzKd6rQ2zKJzAxkFxkOOwyysLp0HOAgQ4/5RINyoS241pZUX2rUHq7mAJ7pcCQ8U0Ow==} + engines: {node: '>=12'} + + '@expo/package-manager@1.10.5': + resolution: {integrity: sha512-nCP9Mebfl3jvOr0/P6VAuyah6PAtun+aihIL2zAtuE8uSe94JWkVZ7051i0MUVO+y3gFpBqnr8IIH5ch+VJjHA==} + + '@expo/plist@0.3.5': + resolution: {integrity: sha512-9RYVU1iGyCJ7vWfg3e7c/NVyMFs8wbl+dMWZphtFtsqyN9zppGREU3ctlD3i8KUE0sCUTVnLjCWr+VeUIDep2g==} + + '@expo/prebuild-config@9.0.12': + resolution: {integrity: sha512-AKH5Scf+gEMgGxZZaimrJI2wlUJlRoqzDNn7/rkhZa5gUTnO4l6slKak2YdaH+nXlOWCNfAQWa76NnpQIfmv6Q==} + + '@expo/schema-utils@0.1.8': + resolution: {integrity: sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A==} + + '@expo/sdk-runtime-versions@1.0.0': + resolution: {integrity: sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==} + + '@expo/server@0.6.3': + resolution: {integrity: sha512-Ea7NJn9Xk1fe4YeJ86rObHSv/bm3u/6WiQPXEqXJ2GrfYpVab2Swoh9/PnSM3KjR64JAgKjArDn1HiPjITCfHA==} + + '@expo/spawn-async@1.7.2': + resolution: {integrity: sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==} + engines: {node: '>=12'} + + '@expo/sudo-prompt@9.3.2': + resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==} + + '@expo/vector-icons@14.1.0': + resolution: {integrity: sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ==} + peerDependencies: + expo-font: '*' + react: '*' + react-native: '*' + + '@expo/ws-tunnel@1.0.6': + resolution: {integrity: sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==} + + '@expo/xcpretty@4.4.3': + resolution: {integrity: sha512-wC562eD3gS6vO2tWHToFhlFnmHKfKHgF1oyvojeSkLK/ZYop1bMU+7cOMiF9Sq70CzcsLy/EMRy/uRc76QmNRw==} + hasBin: true + + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@ide/backoff@1.0.0': + resolution: {integrity: sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==} + + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jest/create-cache-key-function@29.7.0': + resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} + engines: {node: '>=18'} + hasBin: true + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-wasm@2.5.6': + resolution: {integrity: sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA==} + engines: {node: '>= 10.0.0'} + bundledDependencies: + - napi-wasm + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.7.0': + resolution: {integrity: sha512-0UTYalzk2t6S4rA2uHOz5bSSW2CHdv4vggJI6Alg90yvl0UgXs6XSXpH96OH+bRkX4J/06djv29pqXJ0lq5Kag==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@prisma/adapter-pg@7.8.0': + resolution: {integrity: sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==} + + '@prisma/client-runtime-utils@7.8.0': + resolution: {integrity: sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==} + + '@prisma/client@7.8.0': + resolution: {integrity: sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + peerDependencies: + prisma: '*' + typescript: '>=5.4.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@7.8.0': + resolution: {integrity: sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==} + + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/debug@7.8.0': + resolution: {integrity: sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==} + + '@prisma/dev@0.24.3': + resolution: {integrity: sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==} + + '@prisma/driver-adapter-utils@7.8.0': + resolution: {integrity: sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==} + + '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': + resolution: {integrity: sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==} + + '@prisma/engines@7.8.0': + resolution: {integrity: sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==} + + '@prisma/fetch-engine@7.8.0': + resolution: {integrity: sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==} + + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/get-platform@7.8.0': + resolution: {integrity: sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/streams-local@0.1.2': + resolution: {integrity: sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==} + engines: {bun: '>=1.3.6', node: '>=22.0.0'} + + '@prisma/studio-core@0.27.3': + resolution: {integrity: sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==} + engines: {node: ^20.19 || ^22.12 || >=24.0, pnpm: '8'} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.0': + resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@react-email/render@1.1.2': + resolution: {integrity: sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-native-async-storage/async-storage@2.2.0': + resolution: {integrity: sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==} + peerDependencies: + react-native: ^0.0.0-0 || >=0.65 <1.0 + + '@react-native-community/slider@5.2.0': + resolution: {integrity: sha512-484sH8aWEaSjxaZ7HT3YZ8CKDcNes2synko1vdEz5DFEdvKAduxKJTj22L/qBMD7rtIkfbX69DMzWDAGbOAV6w==} + + '@react-native/assets-registry@0.79.6': + resolution: {integrity: sha512-UVSP1224PWg0X+mRlZNftV5xQwZGfawhivuW8fGgxNK9MS/U84xZ+16lkqcPh1ank6MOt239lIWHQ1S33CHgqA==} + engines: {node: '>=18'} + + '@react-native/babel-plugin-codegen@0.79.6': + resolution: {integrity: sha512-CS5OrgcMPixOyUJ/Sk/HSsKsKgyKT5P7y3CojimOQzWqRZBmoQfxdST4ugj7n1H+ebM2IKqbgovApFbqXsoX0g==} + engines: {node: '>=18'} + + '@react-native/babel-preset@0.79.6': + resolution: {integrity: sha512-H+FRO+r2Ql6b5IwfE0E7D52JhkxjeGSBSUpCXAI5zQ60zSBJ54Hwh2bBJOohXWl4J+C7gKYSAd2JHMUETu+c/A==} + engines: {node: '>=18'} + peerDependencies: + '@babel/core': '*' + + '@react-native/codegen@0.79.6': + resolution: {integrity: sha512-iRBX8Lgbqypwnfba7s6opeUwVyaR23mowh9ILw7EcT2oLz3RqMmjJdrbVpWhGSMGq2qkPfqAH7bhO8C7O+xfjQ==} + engines: {node: '>=18'} + peerDependencies: + '@babel/core': '*' + + '@react-native/community-cli-plugin@0.79.6': + resolution: {integrity: sha512-ZHVst9vByGsegeaddkD2YbZ6NvYb4n3pD9H7Pit94u+NlByq2uBJghoOjT6EKqg+UVl8tLRdi88cU2pDPwdHqA==} + engines: {node: '>=18'} + peerDependencies: + '@react-native-community/cli': '*' + peerDependenciesMeta: + '@react-native-community/cli': + optional: true + + '@react-native/debugger-frontend@0.79.6': + resolution: {integrity: sha512-lIK/KkaH7ueM22bLO0YNaQwZbT/oeqhaghOvmZacaNVbJR1Cdh/XAqjT8FgCS+7PUnbxA8B55NYNKGZG3O2pYw==} + engines: {node: '>=18'} + + '@react-native/dev-middleware@0.79.6': + resolution: {integrity: sha512-BK3GZBa9c7XSNR27EDRtxrgyyA3/mf1j3/y+mPk7Ac0Myu85YNrXnC9g3mL5Ytwo0g58TKrAIgs1fF2Q5Mn6mQ==} + engines: {node: '>=18'} + + '@react-native/gradle-plugin@0.79.6': + resolution: {integrity: sha512-C5odetI6py3CSELeZEVz+i00M+OJuFZXYnjVD4JyvpLn462GesHRh+Se8mSkU5QSaz9cnpMnyFLJAx05dokWbA==} + engines: {node: '>=18'} + + '@react-native/js-polyfills@0.79.6': + resolution: {integrity: sha512-6wOaBh1namYj9JlCNgX2ILeGUIwc6OP6MWe3Y5jge7Xz9fVpRqWQk88Q5Y9VrAtTMTcxoX3CvhrfRr3tGtSfQw==} + engines: {node: '>=18'} + + '@react-native/normalize-colors@0.79.6': + resolution: {integrity: sha512-0v2/ruY7eeKun4BeKu+GcfO+SHBdl0LJn4ZFzTzjHdWES0Cn+ONqKljYaIv8p9MV2Hx/kcdEvbY4lWI34jC/mQ==} + + '@react-native/virtualized-lists@0.79.6': + resolution: {integrity: sha512-khA/Hrbb+rB68YUHrLubfLgMOD9up0glJhw25UE3Kntj32YDyuO0Tqc81ryNTcCekFKJ8XrAaEjcfPg81zBGPw==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': ^19.0.0 + react: '*' + react-native: '*' + peerDependenciesMeta: + '@types/react': + optional: true + + '@react-navigation/bottom-tabs@7.15.11': + resolution: {integrity: sha512-+WtNbd6fJgbViDNjmBUUP7eTgGH+zBtrl3jHuNnfUfXTs9YGuI5q3SiHIc9a5gY3voBOxbOXEiHJyW4xea7nAw==} + peerDependencies: + '@react-navigation/native': ^7.2.2 + react: '>= 18.2.0' + react-native: '*' + react-native-safe-area-context: '>= 4.0.0' + react-native-screens: '>= 4.0.0' + + '@react-navigation/core@7.17.2': + resolution: {integrity: sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA==} + peerDependencies: + react: '>= 18.2.0' + + '@react-navigation/elements@2.9.15': + resolution: {integrity: sha512-cyz/pPiyyC6gaTVLsGFc1g0MYgrmuCFqklAWGXMWPscr5YU3ui94vPI4vnZwcsEy0T758TQWLzmS5XudZeRKcA==} + peerDependencies: + '@react-native-masked-view/masked-view': '>= 0.2.0' + '@react-navigation/native': ^7.2.2 + react: '>= 18.2.0' + react-native: '*' + react-native-safe-area-context: '>= 4.0.0' + peerDependenciesMeta: + '@react-native-masked-view/masked-view': + optional: true + + '@react-navigation/native-stack@7.14.12': + resolution: {integrity: sha512-dUfpkrVeVKKV8iqXsmoUp3Rv0iH3YaB3eZwScru/FlcqAp/r3/qA6zEXkGX9hZK+/ziWAPFrf1frBSNbgOYSFQ==} + peerDependencies: + '@react-navigation/native': ^7.2.2 + react: '>= 18.2.0' + react-native: '*' + react-native-safe-area-context: '>= 4.0.0' + react-native-screens: '>= 4.0.0' + + '@react-navigation/native@7.2.2': + resolution: {integrity: sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==} + peerDependencies: + react: '>= 18.2.0' + react-native: '*' + + '@react-navigation/routers@7.5.3': + resolution: {integrity: sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==} + + '@rollup/plugin-alias@6.0.0': + resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==} + engines: {node: '>=20.19.0'} + peerDependencies: + rollup: '>=4.0.0' + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-commonjs@29.0.2': + resolution: {integrity: sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-inject@5.0.5': + resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-replace@6.0.3': + resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-terser@1.0.0': + resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.3': + resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.3': + resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.3': + resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.3': + resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.3': + resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.3': + resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.3': + resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.3': + resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.3': + resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.3': + resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.3': + resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.3': + resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.3': + resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.3': + resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} + cpu: [x64] + os: [win32] + + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@speed-highlight/core@1.2.15': + resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@supabase/auth-js@2.105.3': + resolution: {integrity: sha512-hMFuzP++mjRfe0/BUq4/e82CXIDgyjUgg0khLN8waol/gzoM1t2iGmhfJSGvQHQ1dr3XqWpP6ThAw4bLHMot5Q==} + engines: {node: '>=20.0.0'} + + '@supabase/functions-js@2.105.3': + resolution: {integrity: sha512-KyutUwLLUZ9fRXsiFACL6lq7akBVHFl0fnqQnrxjbsPco8jeb4EyirQuvr52QCLnikzjMRC0uxAHOSM54aDrZA==} + engines: {node: '>=20.0.0'} + + '@supabase/phoenix@0.4.1': + resolution: {integrity: sha512-hWGJkDAfWUNY8k0C080u3sGNFd2ncl9erhKgP7hnGkgJWEfT5Pd/SXal4QmWXBECVlZrannMAc9sBaaRyWpiUA==} + + '@supabase/postgrest-js@2.105.3': + resolution: {integrity: sha512-jFVYRHcri0ZMcTzKpQ2r2wWOB8/rPsbj92kxmCmVJUiRrdgiMtuYlkS06Fhs8UJZhEOL0UpGhh06XDwh8JwtBQ==} + engines: {node: '>=20.0.0'} + + '@supabase/realtime-js@2.105.3': + resolution: {integrity: sha512-L+qPiJlq1RKh3QD2fORGCFo2RKDKlvG9mjvPtUEQJ2tMixrx70VIV6j8BdWzQkbc1Nao6mvTWajyDhX3TFgljw==} + engines: {node: '>=20.0.0'} + + '@supabase/storage-js@2.105.3': + resolution: {integrity: sha512-M7oPCCcHim/FsR6rKIs10Nd9mW051N2SQvA27jiVLa7oQMFFb7faX5dCQRV4GS5QeFsBcV5J/fWl4Ppoaw8cBQ==} + engines: {node: '>=20.0.0'} + + '@supabase/supabase-js@2.105.3': + resolution: {integrity: sha512-5Dm9+I61LAWwjw+0zcqXhSmTxUJaYHBPyHwMCIBH4TBUNwDn2pYUIsi6oUu0I5r9HtLtaFl7w4wa+DV9gRsbDg==} + engines: {node: '>=20.0.0'} + + '@tanstack/query-core@5.100.9': + resolution: {integrity: sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==} + + '@tanstack/react-query@5.100.9': + resolution: {integrity: sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==} + peerDependencies: + react: ^18 || ^19 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/hammerjs@2.0.46': + resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/react@19.0.14': + resolution: {integrity: sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@urql/core@5.2.0': + resolution: {integrity: sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==} + + '@urql/exchange-retry@1.3.2': + resolution: {integrity: sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==} + peerDependencies: + '@urql/core': ^5.0.0 + + '@vercel/nft@1.5.0': + resolution: {integrity: sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==} + engines: {node: '>=20'} + hasBin: true + + '@xmldom/xmldom@0.8.13': + resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + engines: {node: '>=10.0.0'} + + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + + '@zone-eu/mailsplit@5.4.9': + resolution: {integrity: sha512-Qq7k6FzA5SmGf5HFPcr17gE7M+O1gttlmWn7tlGUlhGsbbjUaBL/4cEWIwExeCzqu5+kyZJ91mcBZbQ9zEwwYA==} + + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + ajv@8.11.0: + resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + anser@1.4.10: + resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + + async-limiter@1.0.1: + resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-react-native-web@0.19.13: + resolution: {integrity: sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ==} + + babel-plugin-syntax-hermes-parser@0.25.1: + resolution: {integrity: sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==} + + babel-plugin-transform-flow-enums@0.0.2: + resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-expo@13.2.5: + resolution: {integrity: sha512-YjVkP1bOLO2OgR2fyCedruYMPR7GFbAtCvvWITBW1UAp6e3ACYZtN6uoqkXgXP6PHQkb6M7qf2vZreBPEZK38A==} + peerDependencies: + babel-plugin-react-compiler: ^19.0.0-beta-e993439-20250405 + peerDependenciesMeta: + babel-plugin-react-compiler: + optional: true + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + badgin@1.2.3: + resolution: {integrity: sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.1: + resolution: {integrity: sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.9.1: + resolution: {integrity: sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.13.1: + resolution: {integrity: sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.3: + resolution: {integrity: sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.27: + resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==} + engines: {node: '>=6.0.0'} + hasBin: true + + better-opn@3.0.2: + resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} + engines: {node: '>=12.0.0'} + + better-result@2.9.2: + resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==} + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + bplist-creator@0.1.0: + resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} + + bplist-parser@0.3.1: + resolution: {integrity: sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==} + engines: {node: '>= 5.10.0'} + + bplist-parser@0.3.2: + resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} + engines: {node: '>= 5.10.0'} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caller-callsite@2.0.0: + resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} + engines: {node: '>=4'} + + caller-path@2.0.0: + resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} + engines: {node: '>=4'} + + callsites@2.0.0: + resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} + engines: {node: '>=4'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001791: + resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chrome-launcher@0.15.2: + resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} + engines: {node: '>=12.13.0'} + hasBin: true + + chromium-edge-launcher@0.2.0: + resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + comment-json@4.6.2: + resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} + engines: {node: '>= 6'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + compatx@0.2.0: + resolution: {integrity: sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA==} + + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + cookie-es@2.0.1: + resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} + + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig@5.2.1: + resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} + engines: {node: '>=4'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + + croner@10.0.1: + resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} + engines: {node: '>=18.0'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + db0@0.3.4: + resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==} + peerDependencies: + '@electric-sql/pglite': '*' + '@libsql/client': '*' + better-sqlite3: '*' + drizzle-orm: '*' + mysql2: '*' + sqlite3: '*' + peerDependenciesMeta: + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + better-sqlite3: + optional: true + drizzle-orm: + optional: true + mysql2: + optional: true + sqlite3: + optional: true + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-prop@10.1.0: + resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} + engines: {node: '>=20'} + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + effect@3.20.0: + resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + + electron-to-chromium@1.5.351: + resolution: {integrity: sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding-japanese@2.2.0: + resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==} + engines: {node: '>=8.10.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-editor@0.4.2: + resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==} + engines: {node: '>=8'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + expo-apple-authentication@7.2.4: + resolution: {integrity: sha512-T2agaLLPT4Ax97FeXImB7BCCEzEJ0gB+ZwlFa/FXBtbp6WFKcGRlTVKiX2YPYLZzN5QjXcmQ9HHJ17jRthNHMg==} + peerDependencies: + expo: '*' + react-native: '*' + + expo-application@6.1.5: + resolution: {integrity: sha512-ToImFmzw8luY043pWFJhh2ZMm4IwxXoHXxNoGdlhD4Ym6+CCmkAvCglg0FK8dMLzAb+/XabmOE7Rbm8KZb6NZg==} + peerDependencies: + expo: '*' + + expo-asset@11.1.7: + resolution: {integrity: sha512-b5P8GpjUh08fRCf6m5XPVAh7ra42cQrHBIMgH2UXP+xsj4Wufl6pLy6jRF5w6U7DranUMbsXm8TOyq4EHy7ADg==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-av@15.1.7: + resolution: {integrity: sha512-NC+JR+65sxXfQN1mOHp3QBaXTL2J+BzNwVO27XgUEc5s9NaoBTdHWElYXrfxvik6xwytZ+a7abrqfNNgsbQzsA==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native-web: + optional: true + + expo-build-properties@0.14.8: + resolution: {integrity: sha512-GTFNZc5HaCS9RmCi6HspCe2+isleuOWt2jh7UEKHTDQ9tdvzkIoWc7U6bQO9lH3Mefk4/BcCUZD/utl7b1wdqw==} + peerDependencies: + expo: '*' + + expo-clipboard@55.0.13: + resolution: {integrity: sha512-PrOmmuVsGW4bAkNQmGKtxMXj3invsfN+jfIKmQxHwE/dn7ODqwFWviUTa+PMUjP3XZmYCDLyu/i0GLeu7HF9Ew==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-constants@17.1.8: + resolution: {integrity: sha512-sOCeMN/BWLA7hBP6lMwoEQzFNgTopk6YY03sBAmwT216IHyL54TjNseg8CRU1IQQ/+qinJ2fYWCl7blx2TiNcA==} + peerDependencies: + expo: '*' + react-native: '*' + + expo-dev-client@5.2.4: + resolution: {integrity: sha512-s/N/nK5LPo0QZJpV4aPijxyrzV4O49S3dN8D2fljqrX2WwFZzWwFO6dX1elPbTmddxumdcpczsdUPY+Ms8g43g==} + peerDependencies: + expo: '*' + + expo-dev-launcher@5.1.16: + resolution: {integrity: sha512-tbCske9pvbozaEblyxoyo/97D6od9Ma4yAuyUnXtRET1CKAPKYS+c4fiZ+I3B4qtpZwN3JNFUjG3oateN0y6Hg==} + peerDependencies: + expo: '*' + + expo-dev-menu-interface@1.10.0: + resolution: {integrity: sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==} + peerDependencies: + expo: '*' + + expo-dev-menu@6.1.14: + resolution: {integrity: sha512-yonNMg2GHJZtuisVowdl1iQjZfYP85r1D1IO+ar9D9zlrBPBJhq2XEju52jd1rDmDkmDuEhBSbPNhzIcsBNiPg==} + peerDependencies: + expo: '*' + + expo-file-system@18.1.11: + resolution: {integrity: sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ==} + peerDependencies: + expo: '*' + react-native: '*' + + expo-font@13.0.4: + resolution: {integrity: sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw==} + peerDependencies: + expo: '*' + react: '*' + + expo-font@13.3.2: + resolution: {integrity: sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A==} + peerDependencies: + expo: '*' + react: '*' + + expo-haptics@55.0.14: + resolution: {integrity: sha512-KjDItBsA9mi1f5nRwf8g1wOdfEcLHwvEdt5Jl1sMCDETR/homcGOl+F3QIiPOl/PRlbGVieQsjTtF4DGtHOj6g==} + peerDependencies: + expo: '*' + + expo-image-loader@5.1.0: + resolution: {integrity: sha512-sEBx3zDQIODWbB5JwzE7ZL5FJD+DK3LVLWBVJy6VzsqIA6nDEnSFnsnWyCfCTSvbGigMATs1lgkC2nz3Jpve1Q==} + peerDependencies: + expo: '*' + + expo-image-picker@16.1.4: + resolution: {integrity: sha512-bTmmxtw1AohUT+HxEBn2vYwdeOrj1CLpMXKjvi9FKSoSbpcarT4xxI0z7YyGwDGHbrJqyyic3I9TTdP2J2b4YA==} + peerDependencies: + expo: '*' + + expo-json-utils@0.15.0: + resolution: {integrity: sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==} + + expo-keep-awake@14.1.4: + resolution: {integrity: sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA==} + peerDependencies: + expo: '*' + react: '*' + + expo-linking@7.1.7: + resolution: {integrity: sha512-ZJaH1RIch2G/M3hx2QJdlrKbYFUTOjVVW4g39hfxrE5bPX9xhZUYXqxqQtzMNl1ylAevw9JkgEfWbBWddbZ3UA==} + peerDependencies: + react: '*' + react-native: '*' + + expo-localization@16.1.6: + resolution: {integrity: sha512-v4HwNzs8QvyKHwl40MvETNEKr77v1o9/eVC8WCBY++DIlBAvonHyJe2R9CfqpZbC4Tlpl7XV+07nLXc8O5PQsA==} + peerDependencies: + expo: '*' + react: '*' + + expo-manifests@0.16.6: + resolution: {integrity: sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w==} + peerDependencies: + expo: '*' + + expo-modules-autolinking@2.1.15: + resolution: {integrity: sha512-IUITUERdkgooXjr9bXsX0PmhrZUIGTMyP6NtmQpAxN5+qtf/I7ewbwLx1/rX7tgiAOzaYme+PZOp/o6yqIhFsw==} + hasBin: true + + expo-modules-core@2.5.0: + resolution: {integrity: sha512-aIbQxZE2vdCKsolQUl6Q9Farlf8tjh/ROR4hfN1qT7QBGPl1XrJGnaOKkcgYaGrlzCPg/7IBe0Np67GzKMZKKQ==} + + expo-notifications@0.31.5: + resolution: {integrity: sha512-HsitfTrSESFDWwaX0Y+6GQlWEooQqZKdGbNTwTPHfp5PNCr02tVPwwya9j1tdg3Awj8/vmfXmSxzNhULfmgJhQ==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-router@5.1.11: + resolution: {integrity: sha512-6YQGqQM2rviVSiU6++hrJDPMByHZ7Oiux4XmgoSaGdaHku5QOn9911f2puEUZh2H9ALKBipw5v3ZkrECBd6Zbw==} + peerDependencies: + '@react-navigation/drawer': ^7.3.9 + '@testing-library/jest-native': '*' + expo: '*' + expo-constants: '*' + expo-linking: '*' + react-native-reanimated: '*' + react-native-safe-area-context: '*' + react-native-screens: '*' + react-server-dom-webpack: ~19.0.4 || ~19.1.5 || ~19.2.4 + peerDependenciesMeta: + '@react-navigation/drawer': + optional: true + '@testing-library/jest-native': + optional: true + react-native-reanimated: + optional: true + react-server-dom-webpack: + optional: true + + expo-speech@13.1.7: + resolution: {integrity: sha512-RMMgK6IIPQD9uLhmY2Q9v+2j3wmTGqB/qZ7sdy5//5TLCzFwAq1vlpy5A2psVsctoWVxUO4EmlaNai0ahQmKRg==} + peerDependencies: + expo: '*' + + expo-splash-screen@0.30.10: + resolution: {integrity: sha512-Tt9va/sLENQDQYeOQ6cdLdGvTZ644KR3YG9aRlnpcs2/beYjOX1LHT510EGzVN9ljUTg+1ebEo5GGt2arYtPjw==} + peerDependencies: + expo: '*' + + expo-status-bar@2.2.3: + resolution: {integrity: sha512-+c8R3AESBoduunxTJ8353SqKAKpxL6DvcD8VKBuh81zzJyUUbfB4CVjr1GufSJEKsMzNPXZU+HJwXx7Xh7lx8Q==} + peerDependencies: + react: '*' + react-native: '*' + + expo-updates-interface@1.1.0: + resolution: {integrity: sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w==} + peerDependencies: + expo: '*' + + expo-web-browser@14.2.0: + resolution: {integrity: sha512-6S51d8pVlDRDsgGAp8BPpwnxtyKiMWEFdezNz+5jVIyT+ctReW42uxnjRgtsdn5sXaqzhaX+Tzk/CWaKCyC0hw==} + peerDependencies: + expo: '*' + react-native: '*' + + expo@53.0.27: + resolution: {integrity: sha512-iQwe2uWLb88opUY4vBYEW1d2GUq3lsa43gsMBEdDV+6pw0Oek93l/4nDLe0ODDdrBRjIJm/rdhKqJC/ehHCUqw==} + hasBin: true + peerDependencies: + '@expo/dom-webview': '*' + '@expo/metro-runtime': '*' + react: '*' + react-native: '*' + react-native-webview: '*' + peerDependenciesMeta: + '@expo/dom-webview': + optional: true + '@expo/metro-runtime': + optional: true + react-native-webview: + optional: true + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flow-enums-runtime@0.0.6: + resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} + + fontfaceobserver@2.3.0: + resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + freeport-async@2.0.0: + resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==} + engines: {node: '>=8'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + getenv@2.0.0: + resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==} + engines: {node: '>=6'} + + giget@3.2.0: + resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globby@16.2.0: + resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} + engines: {node: '>=20'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grammex@3.1.12: + resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==} + + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + + groq-sdk@0.7.0: + resolution: {integrity: sha512-OgPqrRtti5MjEVclR8sgBHrhSkTLdFCmi47yrEF29uJZaiCkX3s7bXpnMhq8Lwoe1f4AwgC0qGOeHXpeSgu5lg==} + + gzip-size@7.0.0: + resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-estree@0.29.1: + resolution: {integrity: sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hermes-parser@0.29.1: + resolution: {integrity: sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + hono@4.12.17: + resolution: {integrity: sha512-FbJJNb/XgX7YW0hX/V8w5oYLztKEsRLykCMZWt1WdLtsfjzMvmoqWBA4H4t5norinq8/rh20oiZYr+WSl4UzAQ==} + engines: {node: '>=16.9.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-shutdown@1.2.2: + resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + httpxy@0.5.1: + resolution: {integrity: sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A==} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + i18next@23.16.8: + resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==} + + iceberg-js@0.8.1: + resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} + engines: {node: '>=20.0.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + + imapflow@1.3.3: + resolution: {integrity: sha512-lx7nWcUDfNgITEKYYfunUDqJ3LT6ImuiA1ReqJepVEA2nqBQNUqa3ppF7Yz5CNjuDYG95pmzsCcNqRjMrwh/Vg==} + + import-fresh@2.0.0: + resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} + engines: {node: '>=4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-directory@0.3.1: + resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} + engines: {node: '>=0.10.0'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jimp-compact@0.16.1: + resolution: {integrity: sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsc-safe-url@0.2.4: + resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + + knitwork@1.3.0: + resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} + + lan-network@0.1.7: + resolution: {integrity: sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==} + hasBin: true + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + libbase64@1.3.0: + resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} + + libmime@5.3.8: + resolution: {integrity: sha512-ZrCY+Q66mPvasAfjsQ/IgahzoBvfE1VdtGRpo1hwRB1oK3wJKxhKA3GOcd2a6j7AH5eMFccxK9fBoCpRZTf8ng==} + + libqp@2.1.1: + resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==} + + lighthouse-logger@1.4.2: + resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + + lightningcss-darwin-arm64@1.27.0: + resolution: {integrity: sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.27.0: + resolution: {integrity: sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.27.0: + resolution: {integrity: sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.27.0: + resolution: {integrity: sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.27.0: + resolution: {integrity: sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.27.0: + resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.27.0: + resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.27.0: + resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.27.0: + resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.27.0: + resolution: {integrity: sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.27.0: + resolution: {integrity: sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + listhen@1.10.0: + resolution: {integrity: sha512-kfz4C0OrC6IpaVMtYDJtf6PFjurxe9NBBoDAh/o2p587INryFOO4DQ9OetbCdDrWFt1m1CJKvYrzkGsuPHw8nQ==} + hasBin: true + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lottie-react-native@7.2.2: + resolution: {integrity: sha512-pp3dnFVFZlfZzIL5qKGXju2d6RfnYhPbb8xQL9dYqvPbPy2EbnK2aFlv6jAZLYh0QjUGPEmRAgAAnsOOtT+H9Q==} + peerDependencies: + '@lottiefiles/dotlottie-react': ^0.6.5 + react: '*' + react-native: '>=0.46' + react-native-windows: '>=0.63.x' + peerDependenciesMeta: + '@lottiefiles/dotlottie-react': + optional: true + react-native-windows: + optional: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.3.6: + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + + merge-options@3.0.4: + resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} + engines: {node: '>=10'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + metro-babel-transformer@0.82.5: + resolution: {integrity: sha512-W/scFDnwJXSccJYnOFdGiYr9srhbHPdxX9TvvACOFsIXdLilh3XuxQl/wXW6jEJfgIb0jTvoTlwwrqvuwymr6Q==} + engines: {node: '>=18.18'} + + metro-cache-key@0.82.5: + resolution: {integrity: sha512-qpVmPbDJuRLrT4kcGlUouyqLGssJnbTllVtvIgXfR7ZuzMKf0mGS+8WzcqzNK8+kCyakombQWR0uDd8qhWGJcA==} + engines: {node: '>=18.18'} + + metro-cache@0.82.5: + resolution: {integrity: sha512-AwHV9607xZpedu1NQcjUkua8v7HfOTKfftl6Vc9OGr/jbpiJX6Gpy8E/V9jo/U9UuVYX2PqSUcVNZmu+LTm71Q==} + engines: {node: '>=18.18'} + + metro-config@0.82.5: + resolution: {integrity: sha512-/r83VqE55l0WsBf8IhNmc/3z71y2zIPe5kRSuqA5tY/SL/ULzlHUJEMd1szztd0G45JozLwjvrhAzhDPJ/Qo/g==} + engines: {node: '>=18.18'} + + metro-core@0.82.5: + resolution: {integrity: sha512-OJL18VbSw2RgtBm1f2P3J5kb892LCVJqMvslXxuxjAPex8OH7Eb8RBfgEo7VZSjgb/LOf4jhC4UFk5l5tAOHHA==} + engines: {node: '>=18.18'} + + metro-file-map@0.82.5: + resolution: {integrity: sha512-vpMDxkGIB+MTN8Af5hvSAanc6zXQipsAUO+XUx3PCQieKUfLwdoa8qaZ1WAQYRpaU+CJ8vhBcxtzzo3d9IsCIQ==} + engines: {node: '>=18.18'} + + metro-minify-terser@0.82.5: + resolution: {integrity: sha512-v6Nx7A4We6PqPu/ta1oGTqJ4Usz0P7c+3XNeBxW9kp8zayS3lHUKR0sY0wsCHInxZlNAEICx791x+uXytFUuwg==} + engines: {node: '>=18.18'} + + metro-resolver@0.82.5: + resolution: {integrity: sha512-kFowLnWACt3bEsuVsaRNgwplT8U7kETnaFHaZePlARz4Fg8tZtmRDUmjaD68CGAwc0rwdwNCkWizLYpnyVcs2g==} + engines: {node: '>=18.18'} + + metro-runtime@0.82.5: + resolution: {integrity: sha512-rQZDoCUf7k4Broyw3Ixxlq5ieIPiR1ULONdpcYpbJQ6yQ5GGEyYjtkztGD+OhHlw81LCR2SUAoPvtTus2WDK5g==} + engines: {node: '>=18.18'} + + metro-source-map@0.82.5: + resolution: {integrity: sha512-wH+awTOQJVkbhn2SKyaw+0cd+RVSCZ3sHVgyqJFQXIee/yLs3dZqKjjeKKhhVeudgjXo7aE/vSu/zVfcQEcUfw==} + engines: {node: '>=18.18'} + + metro-symbolicate@0.82.5: + resolution: {integrity: sha512-1u+07gzrvYDJ/oNXuOG1EXSvXZka/0JSW1q2EYBWerVKMOhvv9JzDGyzmuV7hHbF2Hg3T3S2uiM36sLz1qKsiw==} + engines: {node: '>=18.18'} + hasBin: true + + metro-transform-plugins@0.82.5: + resolution: {integrity: sha512-57Bqf3rgq9nPqLrT2d9kf/2WVieTFqsQ6qWHpEng5naIUtc/Iiw9+0bfLLWSAw0GH40iJ4yMjFcFJDtNSYynMA==} + engines: {node: '>=18.18'} + + metro-transform-worker@0.82.5: + resolution: {integrity: sha512-mx0grhAX7xe+XUQH6qoHHlWedI8fhSpDGsfga7CpkO9Lk9W+aPitNtJWNGrW8PfjKEWbT9Uz9O50dkI8bJqigw==} + engines: {node: '>=18.18'} + + metro@0.82.5: + resolution: {integrity: sha512-8oAXxL7do8QckID/WZEKaIFuQJFUTLzfVcC48ghkHhNK2RGuQq8Xvf4AVd+TUA0SZtX0q8TGNXZ/eba1ckeGCg==} + engines: {node: '>=18.18'} + hasBin: true + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@4.1.0: + resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} + engines: {node: '>=16'} + hasBin: true + + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nativewind@4.2.3: + resolution: {integrity: sha512-HglF1v6A8CqBFpXWs0d3yf4qQGurrreLuyE8FTRI/VDH8b0npZa2SDG5tviTkLiBg0s5j09mQALZOjxuocgMLA==} + engines: {node: '>=16'} + peerDependencies: + tailwindcss: '>3.3.0' + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + nested-error-stacks@2.0.1: + resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} + + nitropack@2.13.4: + resolution: {integrity: sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + xml2js: ^0.6.2 + peerDependenciesMeta: + xml2js: + optional: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + nodemailer@8.0.7: + resolution: {integrity: sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==} + engines: {node: '>=6.0.0'} + + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-package-arg@11.0.3: + resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} + engines: {node: ^16.14.0 || >=18.0.0} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nullthrows@1.1.1: + resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + + ob1@0.82.5: + resolution: {integrity: sha512-QyQQ6e66f+Ut/qUVjEce0E/wux5nAGLXYZDn1jr15JWstHsCH3l6VVrg8NKDptW9NEiBXKOJeGF/ydxeSDF3IQ==} + engines: {node: '>=18.18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + ora@3.4.0: + resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} + engines: {node: '>=6'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-png@2.1.0: + resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} + engines: {node: '>=10'} + + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@3.0.2: + resolution: {integrity: sha512-cfDHL6LStTEKlNilboNtobT/kEa30PtAf2Q1OgszfrG/rpVl1xaFWT9ktfkS306GmHgmnad1Sw4wabhlvFtsTw==} + engines: {node: '>=10'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + plist@3.1.1: + resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} + engines: {node: '>=10.4.0'} + + pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + pretty-bytes@7.1.0: + resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} + engines: {node: '>=20'} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + prisma@7.8.0: + resolution: {integrity: sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + hasBin: true + peerDependencies: + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' + peerDependenciesMeta: + better-sqlite3: + optional: true + typescript: + optional: true + + proc-log@4.2.0: + resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise@8.3.0: + resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qrcode-terminal@0.11.0: + resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} + hasBin: true + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-devtools-core@6.1.5: + resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} + + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + peerDependencies: + react: ^19.0.0 + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-freeze@1.0.4: + resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==} + engines: {node: '>=10'} + peerDependencies: + react: '>=17.0.0' + + react-hook-form@7.75.0: + resolution: {integrity: sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-i18next@15.7.4: + resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==} + peerDependencies: + i18next: '>= 23.4.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-is@19.2.5: + resolution: {integrity: sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==} + + react-native-bottom-tabs@1.2.0: + resolution: {integrity: sha512-ScVPko86ts+m6JMNtI24MCSYJCOZc1aZkn9qwS9ly3o0ubajRWDpCzgRJfRFysi08bKrcqAXKVCHZNHvNb2PTA==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-css-interop@0.2.3: + resolution: {integrity: sha512-wc+JI7iUfdFBqnE18HhMTtD0q9vkhuMczToA87UdHGWwMyxdT5sCcNy+i4KInPCE855IY0Ic8kLQqecAIBWz7w==} + engines: {node: '>=18'} + peerDependencies: + react: '>=18' + react-native: '*' + react-native-reanimated: '>=3.6.2' + react-native-safe-area-context: '*' + react-native-svg: '*' + tailwindcss: ~3 + peerDependenciesMeta: + react-native-safe-area-context: + optional: true + react-native-svg: + optional: true + + react-native-edge-to-edge@1.6.0: + resolution: {integrity: sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-gesture-handler@2.24.0: + resolution: {integrity: sha512-ZdWyOd1C8axKJHIfYxjJKCcxjWEpUtUWgTOVY2wynbiveSQDm8X/PDyAKXSer/GOtIpjudUbACOndZXCN3vHsw==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-is-edge-to-edge@1.3.1: + resolution: {integrity: sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-mmkv@3.3.3: + resolution: {integrity: sha512-GMsfOmNzx0p5+CtrCFRVtpOOMYNJXuksBVARSQrCFaZwjUyHJdQzcN900GGaFFNTxw2fs8s5Xje//RDKj9+PZA==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-reanimated@4.0.3: + resolution: {integrity: sha512-apXILxR2gRi3n0Xi0UILr+72vXj1etooOId/4nCgzKfNnvcp+dRzt7UQdFU0/nc+4bPWlSsiIskDxdYXr2KNmw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + react: '*' + react-native: '*' + react-native-worklets: '>=0.4.0' + + react-native-safe-area-context@5.4.0: + resolution: {integrity: sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-screens@4.11.1: + resolution: {integrity: sha512-F0zOzRVa3ptZfLpD0J8ROdo+y1fEPw+VBFq1MTY/iyDu08al7qFUO5hLMd+EYMda5VXGaTFCa8q7bOppUszhJw==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-sse@1.2.1: + resolution: {integrity: sha512-zejanlScF+IB9tYnbdry0MT34qjBXbiV/E72qGz33W/tX1bx8MXsbB4lxiuPETc9v/008vYZ60yjIstW22VlVg==} + + react-native-svg@15.11.2: + resolution: {integrity: sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-url-polyfill@2.0.0: + resolution: {integrity: sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA==} + peerDependencies: + react-native: '*' + + react-native-worklets@0.4.2: + resolution: {integrity: sha512-02IMmU2rOL6vrF7uA6cLAeN4haXOMTBh7opmVYQbjYG8mNAb0cnhmkvkdQupmpFjBpWZRJnBGYJJa471a/9IPg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + react: '*' + react-native: '*' + + react-native@0.79.6: + resolution: {integrity: sha512-kvIWSmf4QPfY41HC25TR285N7Fv0Pyn3DAEK8qRL9dA35usSaxsJkHfw+VqnonqJjXOaoKCEanwudRAJ60TBGA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@types/react': ^19.0.0 + react: ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-promise-suspense@0.3.4: + resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.1: + resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} + hasBin: true + + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requireg@0.2.2: + resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} + engines: {node: '>= 4.0.0'} + + resend@4.8.0: + resolution: {integrity: sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA==} + engines: {node: '>=18'} + + resolve-from@3.0.0: + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-workspace-root@2.0.1: + resolution: {integrity: sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@1.7.1: + resolution: {integrity: sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==} + + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rive-react-native@9.8.3: + resolution: {integrity: sha512-QhLZWNzWUzPiRAPo6wWvtdYxPROAvmUprTsbN7UINjS6LTvgNHsuEYSwRf2hcA+44xVQD2Jv7dSMjqvzc/xqcQ==} + engines: {node: '>=16'} + peerDependencies: + react: '*' + react-native: '*' + + rollup-plugin-visualizer@7.0.1: + resolution: {integrity: sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==} + engines: {node: '>=22'} + hasBin: true + peerDependencies: + rolldown: 1.x || ^1.0.0-beta || ^1.0.0-rc + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup@4.60.3: + resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rtl-detect@1.1.2: + resolution: {integrity: sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + serialize-error@2.1.0: + resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} + engines: {node: '>=0.10.0'} + + serialize-javascript@7.0.5: + resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} + engines: {node: '>=20.0.0'} + + serve-placeholder@2.0.2: + resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sf-symbols-typescript@2.2.0: + resolution: {integrity: sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==} + engines: {node: '>=10'} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-plist@1.3.1: + resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slugify@1.6.9: + resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==} + engines: {node: '>=8.0.0'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + smob@1.6.1: + resolution: {integrity: sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==} + engines: {node: '>=20.0.0'} + + socks@2.8.8: + resolution: {integrity: sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + stacktrace-parser@0.1.11: + resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} + engines: {node: '>=6'} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + stream-buffers@2.2.0: + resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} + engines: {node: '>= 0.10.0'} + + streamx@2.25.0: + resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + stripe@17.7.0: + resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==} + engines: {node: '>=12.*'} + + structured-headers@0.4.1: + resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + + tar@7.5.14: + resolution: {integrity: sha512-/7sHKgQO3JLP9ESlwTYUUftHUadOURUqq23xs1vjcnp8Vss6k0wCfzulyEtk5g91pjvnuriimGlyG7k6msrzRw==} + engines: {node: '>=18'} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + + terminal-link@2.1.1: + resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} + engines: {node: '>=8'} + + terser@5.46.2: + resolution: {integrity: sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + + throat@5.0.0: + resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + + tinyclip@0.1.12: + resolution: {integrity: sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==} + engines: {node: ^16.14.0 || >= 17.3.0} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + + type-fest@5.6.0: + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + engines: {node: '>=20'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + unctx@2.5.0: + resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@6.25.0: + resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} + engines: {node: '>=18.17'} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + + unimport@6.2.0: + resolution: {integrity: sha512-4NcqaphAHQff4eBWQ3pjVOCYNLlmVGGMoLDmboobh8+OQe9yP7UyeoMP043M1bG0YNc3CqtukD2VuINxOqm4rQ==} + engines: {node: '>=18.12.0'} + peerDependencies: + oxc-parser: '*' + peerDependenciesMeta: + oxc-parser: + optional: true + + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + unstorage@1.17.5: + resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + untun@0.1.3: + resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} + hasBin: true + + untyped@2.0.0: + resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==} + hasBin: true + + unwasm@0.5.3: + resolution: {integrity: sha512-keBgTSfp3r6+s9ZcSma+0chwxQdmLbB5+dAD9vjtB21UTMYuKAxHXCU1K2CbCtnP09EaWeRvACnXk0EJtUx+hw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uqr@0.1.3: + resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-latest-callback@0.2.6: + resolution: {integrity: sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==} + peerDependencies: + react: '>=16.8' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@7.0.3: + resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + valibot@1.4.0: + resolution: {integrity: sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vlq@1.0.1: + resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + warn-once@0.1.1: + resolution: {integrity: sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@5.0.0: + resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} + engines: {node: '>=8'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-url-without-unicode@8.0.0-3: + resolution: {integrity: sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==} + engines: {node: '>=10'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wonka@6.3.6: + resolution: {integrity: sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + ws@6.2.3: + resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + xcode@3.0.1: + resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} + engines: {node: '>=10.0.0'} + + xml2js@0.6.0: + resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.1: + resolution: {integrity: sha512-mxW3qiSnl+GRxXsaUMzv2Mbada1Y8CDltET9UxejDQe6DBYlSekghl5U5K0ReAikcHDi0G1vKZEmmo/NWAGKLA==} + + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zustand@5.0.13: + resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@0no-co/graphql.web@1.2.0': {} + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.10.4': + dependencies: + '@babel/highlight': 7.25.9 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/highlight@7.25.9': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-export-default-from@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/template': 7.28.6 + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/preset-react@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@egjs/hammerjs@2.0.17': + dependencies: + '@types/hammerjs': 2.0.46 + + '@electric-sql/pglite-socket@0.1.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': {} + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@expo-google-fonts/nunito@0.2.3': {} + + '@expo/cli@0.24.24': + dependencies: + '@0no-co/graphql.web': 1.2.0 + '@babel/runtime': 7.29.2 + '@expo/code-signing-certificates': 0.0.6 + '@expo/config': 11.0.13 + '@expo/config-plugins': 10.1.2 + '@expo/devcert': 1.2.1 + '@expo/env': 1.0.7 + '@expo/image-utils': 0.7.6 + '@expo/json-file': 9.1.5 + '@expo/metro-config': 0.20.18 + '@expo/osascript': 2.4.3 + '@expo/package-manager': 1.10.5 + '@expo/plist': 0.3.5 + '@expo/prebuild-config': 9.0.12 + '@expo/schema-utils': 0.1.8 + '@expo/spawn-async': 1.7.2 + '@expo/ws-tunnel': 1.0.6 + '@expo/xcpretty': 4.4.3 + '@react-native/dev-middleware': 0.79.6 + '@urql/core': 5.2.0 + '@urql/exchange-retry': 1.3.2(@urql/core@5.2.0) + accepts: 1.3.8 + arg: 5.0.2 + better-opn: 3.0.2 + bplist-creator: 0.1.0 + bplist-parser: 0.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + compression: 1.8.1 + connect: 3.7.0 + debug: 4.4.3 + env-editor: 0.4.2 + freeport-async: 2.0.0 + getenv: 2.0.0 + glob: 10.5.0 + lan-network: 0.1.7 + minimatch: 9.0.9 + node-forge: 1.4.0 + npm-package-arg: 11.0.3 + ora: 3.4.0 + picomatch: 3.0.2 + pretty-bytes: 5.6.0 + pretty-format: 29.7.0 + progress: 2.0.3 + prompts: 2.4.2 + qrcode-terminal: 0.11.0 + require-from-string: 2.0.2 + requireg: 0.2.2 + resolve: 1.22.12 + resolve-from: 5.0.0 + resolve.exports: 2.0.3 + semver: 7.7.4 + send: 0.19.2 + slugify: 1.6.9 + source-map-support: 0.5.21 + stacktrace-parser: 0.1.11 + structured-headers: 0.4.1 + tar: 7.5.14 + terminal-link: 2.1.1 + undici: 6.25.0 + wrap-ansi: 7.0.0 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - graphql + - supports-color + - utf-8-validate + + '@expo/code-signing-certificates@0.0.6': + dependencies: + node-forge: 1.4.0 + + '@expo/config-plugins@10.1.2': + dependencies: + '@expo/config-types': 53.0.5 + '@expo/json-file': 9.1.5 + '@expo/plist': 0.3.5 + '@expo/sdk-runtime-versions': 1.0.0 + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + glob: 10.5.0 + resolve-from: 5.0.0 + semver: 7.7.4 + slash: 3.0.0 + slugify: 1.6.9 + xcode: 3.0.1 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + + '@expo/config-types@53.0.5': {} + + '@expo/config@11.0.13': + dependencies: + '@babel/code-frame': 7.10.4 + '@expo/config-plugins': 10.1.2 + '@expo/config-types': 53.0.5 + '@expo/json-file': 9.1.5 + deepmerge: 4.3.1 + getenv: 2.0.0 + glob: 10.5.0 + require-from-string: 2.0.2 + resolve-from: 5.0.0 + resolve-workspace-root: 2.0.1 + semver: 7.7.4 + slugify: 1.6.9 + sucrase: 3.35.0 + transitivePeerDependencies: + - supports-color + + '@expo/devcert@1.2.1': + dependencies: + '@expo/sudo-prompt': 9.3.2 + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + + '@expo/env@1.0.7': + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + dotenv: 16.4.7 + dotenv-expand: 11.0.7 + getenv: 2.0.0 + transitivePeerDependencies: + - supports-color + + '@expo/fingerprint@0.13.4': + dependencies: + '@expo/spawn-async': 1.7.2 + arg: 5.0.2 + chalk: 4.1.2 + debug: 4.4.3 + find-up: 5.0.0 + getenv: 2.0.0 + glob: 10.5.0 + ignore: 5.3.2 + minimatch: 9.0.9 + p-limit: 3.1.0 + resolve-from: 5.0.0 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + '@expo/image-utils@0.7.6': + dependencies: + '@expo/spawn-async': 1.7.2 + chalk: 4.1.2 + getenv: 2.0.0 + jimp-compact: 0.16.1 + parse-png: 2.1.0 + resolve-from: 5.0.0 + semver: 7.7.4 + temp-dir: 2.0.0 + unique-string: 2.0.0 + + '@expo/json-file@10.0.14': + dependencies: + '@babel/code-frame': 7.29.0 + json5: 2.2.3 + + '@expo/json-file@9.1.5': + dependencies: + '@babel/code-frame': 7.10.4 + json5: 2.2.3 + + '@expo/metro-config@0.20.18': + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@expo/config': 11.0.13 + '@expo/env': 1.0.7 + '@expo/json-file': 9.1.5 + '@expo/spawn-async': 1.7.2 + chalk: 4.1.2 + debug: 4.4.3 + dotenv: 16.4.7 + dotenv-expand: 11.0.7 + getenv: 2.0.0 + glob: 10.5.0 + jsc-safe-url: 0.2.4 + lightningcss: 1.27.0 + minimatch: 9.0.9 + postcss: 8.4.49 + resolve-from: 5.0.0 + transitivePeerDependencies: + - supports-color + + '@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))': + dependencies: + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + '@expo/osascript@2.4.3': + dependencies: + '@expo/spawn-async': 1.7.2 + + '@expo/package-manager@1.10.5': + dependencies: + '@expo/json-file': 10.0.14 + '@expo/spawn-async': 1.7.2 + chalk: 4.1.2 + npm-package-arg: 11.0.3 + ora: 3.4.0 + resolve-workspace-root: 2.0.1 + + '@expo/plist@0.3.5': + dependencies: + '@xmldom/xmldom': 0.8.13 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + '@expo/prebuild-config@9.0.12': + dependencies: + '@expo/config': 11.0.13 + '@expo/config-plugins': 10.1.2 + '@expo/config-types': 53.0.5 + '@expo/image-utils': 0.7.6 + '@expo/json-file': 9.1.5 + '@react-native/normalize-colors': 0.79.6 + debug: 4.4.3 + resolve-from: 5.0.0 + semver: 7.7.4 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + + '@expo/schema-utils@0.1.8': {} + + '@expo/sdk-runtime-versions@1.0.0': {} + + '@expo/server@0.6.3': + dependencies: + abort-controller: 3.0.0 + debug: 4.4.3 + source-map-support: 0.5.21 + undici: 7.25.0 + transitivePeerDependencies: + - supports-color + + '@expo/spawn-async@1.7.2': + dependencies: + cross-spawn: 7.0.6 + + '@expo/sudo-prompt@9.3.2': {} + + '@expo/vector-icons@14.1.0(expo-font@13.0.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)': + dependencies: + expo-font: 13.0.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + '@expo/vector-icons@14.1.0(expo-font@13.3.2(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)': + dependencies: + expo-font: 13.3.2(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + '@expo/ws-tunnel@1.0.6': {} + + '@expo/xcpretty@4.4.3': + dependencies: + '@babel/code-frame': 7.29.0 + chalk: 4.1.2 + js-yaml: 4.1.1 + + '@hono/node-server@1.19.11(hono@4.12.17)': + dependencies: + hono: 4.12.17 + + '@ide/backoff@1.0.0': {} + + '@ioredis/commands@1.5.1': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@isaacs/ttlcache@1.4.1': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jest/create-cache-key-function@29.7.0': + dependencies: + '@jest/types': 29.6.3 + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + jest-mock: 29.7.0 + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.19.17 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.19.17 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kurkle/color@0.3.4': {} + + '@mapbox/node-pre-gyp@2.0.3': + dependencies: + consola: 3.4.2 + detect-libc: 2.1.2 + https-proxy-agent: 7.0.6 + node-fetch: 2.7.0 + nopt: 8.1.0 + semver: 7.7.4 + tar: 7.5.14 + transitivePeerDependencies: + - encoding + - supports-color + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-wasm@2.5.6': + dependencies: + is-glob: 4.0.3 + picomatch: 4.0.4 + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + + '@pinojs/redact@0.4.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.7.0': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@prisma/adapter-pg@7.8.0': + dependencies: + '@prisma/driver-adapter-utils': 7.8.0 + '@types/pg': 8.20.0 + pg: 8.20.0 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + + '@prisma/client-runtime-utils@7.8.0': {} + + '@prisma/client@7.8.0(prisma@7.8.0(@types/react@19.0.14)(magicast@0.5.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@prisma/client-runtime-utils': 7.8.0 + optionalDependencies: + prisma: 7.8.0(@types/react@19.0.14)(magicast@0.5.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@7.8.0(magicast@0.5.2)': + dependencies: + c12: 3.3.4(magicast@0.5.2) + deepmerge-ts: 7.1.5 + effect: 3.20.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@7.2.0': {} + + '@prisma/debug@7.8.0': {} + + '@prisma/dev@0.24.3(typescript@5.9.3)': + dependencies: + '@electric-sql/pglite': 0.4.1 + '@electric-sql/pglite-socket': 0.1.1(@electric-sql/pglite@0.4.1) + '@electric-sql/pglite-tools': 0.3.1(@electric-sql/pglite@0.4.1) + '@hono/node-server': 1.19.11(hono@4.12.17) + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + '@prisma/streams-local': 0.1.2 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.12.17 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.9.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + + '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': {} + + '@prisma/engines@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a + '@prisma/fetch-engine': 7.8.0 + '@prisma/get-platform': 7.8.0 + + '@prisma/fetch-engine@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a + '@prisma/get-platform': 7.8.0 + + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + + '@prisma/get-platform@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + + '@prisma/query-plan-executor@7.2.0': {} + + '@prisma/streams-local@0.1.2': + dependencies: + ajv: 8.20.0 + better-result: 2.9.2 + env-paths: 3.0.0 + proper-lockfile: 4.1.2 + + '@prisma/studio-core@0.27.3(@types/react@19.0.14)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-toggle': 1.1.10(@types/react@19.0.14)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@types/react': 19.0.14 + chart.js: 4.5.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + transitivePeerDependencies: + - '@types/react-dom' + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.0.14)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.14 + + '@radix-ui/react-primitive@2.1.3(@types/react@19.0.14)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.0.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.14 + + '@radix-ui/react-slot@1.2.0(@types/react@19.0.14)(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.14)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.14 + + '@radix-ui/react-slot@1.2.3(@types/react@19.0.14)(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.14)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.14 + + '@radix-ui/react-toggle@1.1.10(@types/react@19.0.14)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react@19.0.14)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.0.14)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.0.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.14)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.0.14)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.14)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.0.14)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.14 + + '@react-email/render@1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.8.3 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-promise-suspense: 0.3.4 + + '@react-native-async-storage/async-storage@2.2.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))': + dependencies: + merge-options: 3.0.4 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + '@react-native-community/slider@5.2.0': {} + + '@react-native/assets-registry@0.79.6': {} + + '@react-native/babel-plugin-codegen@0.79.6(@babel/core@7.29.0)': + dependencies: + '@babel/traverse': 7.29.0 + '@react-native/codegen': 0.79.6(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@react-native/babel-preset@0.79.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@react-native/babel-plugin-codegen': 0.79.6(@babel/core@7.29.0) + babel-plugin-syntax-hermes-parser: 0.25.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) + react-refresh: 0.14.2 + transitivePeerDependencies: + - supports-color + + '@react-native/codegen@0.79.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + glob: 7.2.3 + hermes-parser: 0.25.1 + invariant: 2.2.4 + nullthrows: 1.1.1 + yargs: 17.7.2 + + '@react-native/community-cli-plugin@0.79.6': + dependencies: + '@react-native/dev-middleware': 0.79.6 + chalk: 4.1.2 + debug: 2.6.9 + invariant: 2.2.4 + metro: 0.82.5 + metro-config: 0.82.5 + metro-core: 0.82.5 + semver: 7.7.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/debugger-frontend@0.79.6': {} + + '@react-native/dev-middleware@0.79.6': + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@react-native/debugger-frontend': 0.79.6 + chrome-launcher: 0.15.2 + chromium-edge-launcher: 0.2.0 + connect: 3.7.0 + debug: 2.6.9 + invariant: 2.2.4 + nullthrows: 1.1.1 + open: 7.4.2 + serve-static: 1.16.3 + ws: 6.2.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/gradle-plugin@0.79.6': {} + + '@react-native/js-polyfills@0.79.6': {} + + '@react-native/normalize-colors@0.79.6': {} + + '@react-native/virtualized-lists@0.79.6(@types/react@19.0.14)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.14 + + '@react-navigation/bottom-tabs@7.15.11(@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-screens@4.11.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)': + dependencies: + '@react-navigation/elements': 2.9.15(@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + '@react-navigation/native': 7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + color: 4.2.3 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + react-native-safe-area-context: 5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-screens: 4.11.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + sf-symbols-typescript: 2.2.0 + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + + '@react-navigation/core@7.17.2(react@19.0.0)': + dependencies: + '@react-navigation/routers': 7.5.3 + escape-string-regexp: 4.0.0 + fast-deep-equal: 3.1.3 + nanoid: 3.3.12 + query-string: 7.1.3 + react: 19.0.0 + react-is: 19.2.5 + use-latest-callback: 0.2.6(react@19.0.0) + use-sync-external-store: 1.6.0(react@19.0.0) + + '@react-navigation/elements@2.9.15(@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)': + dependencies: + '@react-navigation/native': 7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + color: 4.2.3 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + react-native-safe-area-context: 5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + use-latest-callback: 0.2.6(react@19.0.0) + use-sync-external-store: 1.6.0(react@19.0.0) + + '@react-navigation/native-stack@7.14.12(@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-screens@4.11.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)': + dependencies: + '@react-navigation/elements': 2.9.15(@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + '@react-navigation/native': 7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + color: 4.2.3 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + react-native-safe-area-context: 5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-screens: 4.11.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + sf-symbols-typescript: 2.2.0 + warn-once: 0.1.1 + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + + '@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)': + dependencies: + '@react-navigation/core': 7.17.2(react@19.0.0) + escape-string-regexp: 4.0.0 + fast-deep-equal: 3.1.3 + nanoid: 3.3.12 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + use-latest-callback: 0.2.6(react@19.0.0) + + '@react-navigation/routers@7.5.3': + dependencies: + nanoid: 3.3.12 + + '@rollup/plugin-alias@6.0.0(rollup@4.60.3)': + optionalDependencies: + rollup: 4.60.3 + + '@rollup/plugin-commonjs@29.0.2(rollup@4.60.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.4) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.3 + + '@rollup/plugin-inject@5.0.5(rollup@4.60.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) + estree-walker: 2.0.2 + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.60.3 + + '@rollup/plugin-json@6.1.0(rollup@4.60.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) + optionalDependencies: + rollup: 4.60.3 + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.60.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.12 + optionalDependencies: + rollup: 4.60.3 + + '@rollup/plugin-replace@6.0.3(rollup@4.60.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.60.3 + + '@rollup/plugin-terser@1.0.0(rollup@4.60.3)': + dependencies: + serialize-javascript: 7.0.5 + smob: 1.6.1 + terser: 5.46.2 + optionalDependencies: + rollup: 4.60.3 + + '@rollup/pluginutils@5.3.0(rollup@4.60.3)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.3 + + '@rollup/rollup-android-arm-eabi@4.60.3': + optional: true + + '@rollup/rollup-android-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-x64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.3': + optional: true + + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + + '@sinclair/typebox@0.27.10': {} + + '@sindresorhus/is@7.2.0': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@speed-highlight/core@1.2.15': {} + + '@standard-schema/spec@1.1.0': {} + + '@supabase/auth-js@2.105.3': + dependencies: + tslib: 2.8.1 + + '@supabase/functions-js@2.105.3': + dependencies: + tslib: 2.8.1 + + '@supabase/phoenix@0.4.1': {} + + '@supabase/postgrest-js@2.105.3': + dependencies: + tslib: 2.8.1 + + '@supabase/realtime-js@2.105.3': + dependencies: + '@supabase/phoenix': 0.4.1 + '@types/ws': 8.18.1 + tslib: 2.8.1 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.105.3': + dependencies: + iceberg-js: 0.8.1 + tslib: 2.8.1 + + '@supabase/supabase-js@2.105.3': + dependencies: + '@supabase/auth-js': 2.105.3 + '@supabase/functions-js': 2.105.3 + '@supabase/postgrest-js': 2.105.3 + '@supabase/realtime-js': 2.105.3 + '@supabase/storage-js': 2.105.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@tanstack/query-core@5.100.9': {} + + '@tanstack/react-query@5.100.9(react@19.0.0)': + dependencies: + '@tanstack/query-core': 5.100.9 + react: 19.0.0 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.19.17 + + '@types/hammerjs@2.0.46': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 22.19.17 + form-data: 4.0.5 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 22.19.17 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + + '@types/react@19.0.14': + dependencies: + csstype: 3.2.3 + + '@types/resolve@1.20.2': {} + + '@types/stack-utils@2.0.3': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.19.17 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@urql/core@5.2.0': + dependencies: + '@0no-co/graphql.web': 1.2.0 + wonka: 6.3.6 + transitivePeerDependencies: + - graphql + + '@urql/exchange-retry@1.3.2(@urql/core@5.2.0)': + dependencies: + '@urql/core': 5.2.0 + wonka: 6.3.6 + + '@vercel/nft@1.5.0(rollup@4.60.3)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.3 + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 13.0.6 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.4 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@xmldom/xmldom@0.8.13': {} + + '@xmldom/xmldom@0.9.10': {} + + '@zone-eu/mailsplit@5.4.9': + dependencies: + libbase64: 1.3.0 + libmime: 5.3.8 + libqp: 2.1.1 + + abbrev@3.0.1: {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + ajv@8.11.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + anser@1.4.10: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.18.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.2.0 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-timsort@1.0.3: {} + + asap@2.0.6: {} + + assert@2.1.0: + dependencies: + call-bind: 1.0.9 + is-nan: 1.3.2 + object-is: 1.1.6 + object.assign: 4.1.7 + util: 0.12.5 + + async-limiter@1.0.1: {} + + async-sema@3.1.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-ssl-profiles@1.1.2: {} + + b4a@1.8.1: {} + + babel-jest@29.7.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.29.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + babel-plugin-react-native-web@0.19.13: {} + + babel-plugin-syntax-hermes-parser@0.25.1: + dependencies: + hermes-parser: 0.25.1 + + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0): + dependencies: + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-expo@13.2.5(@babel/core@7.29.0): + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/preset-react': 7.28.5(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@react-native/babel-preset': 0.79.6(@babel/core@7.29.0) + babel-plugin-react-native-web: 0.19.13 + babel-plugin-syntax-hermes-parser: 0.25.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) + debug: 4.4.3 + react-refresh: 0.14.2 + resolve-from: 5.0.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + babel-preset-jest@29.6.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + + badgin@1.2.3: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + bare-events@2.8.2: {} + + bare-fs@4.7.1: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.13.1(bare-events@2.8.2) + bare-url: 2.4.3 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.9.1: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.9.1 + + bare-stream@2.13.1(bare-events@2.8.2): + dependencies: + streamx: 2.25.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.3: + dependencies: + bare-path: 3.0.0 + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.27: {} + + better-opn@3.0.2: + dependencies: + open: 8.4.2 + + better-result@2.9.2: {} + + big-integer@1.6.52: {} + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + boolbase@1.0.0: {} + + bplist-creator@0.1.0: + dependencies: + stream-buffers: 2.2.0 + + bplist-parser@0.3.1: + dependencies: + big-integer: 1.6.52 + + bplist-parser@0.3.2: + dependencies: + big-integer: 1.6.52 + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.27 + caniuse-lite: 1.0.30001791 + electron-to-chromium: 1.5.351 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-crc32@1.0.0: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + c12@3.3.4(magicast@0.5.2): + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.0.8 + giget: 3.2.0 + jiti: 2.7.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + rc9: 3.0.1 + optionalDependencies: + magicast: 0.5.2 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caller-callsite@2.0.0: + dependencies: + callsites: 2.0.0 + + caller-path@2.0.0: + dependencies: + caller-callsite: 2.0.0 + + callsites@2.0.0: {} + + camelcase-css@2.0.1: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001791: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chownr@3.0.0: {} + + chrome-launcher@0.15.2: + dependencies: + '@types/node': 22.19.17 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + transitivePeerDependencies: + - supports-color + + chromium-edge-launcher@0.2.0: + dependencies: + '@types/node': 22.19.17 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + mkdirp: 1.0.4 + rimraf: 3.0.2 + transitivePeerDependencies: + - supports-color + + ci-info@2.0.0: {} + + ci-info@3.9.0: {} + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.2: {} + + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + + cli-spinners@2.9.2: {} + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + clone@1.0.4: {} + + cluster-key-slot@1.1.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@12.1.0: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + commander@7.2.0: {} + + comment-json@4.6.2: + dependencies: + array-timsort: 1.0.3 + esprima: 4.0.1 + + commondir@1.0.1: {} + + compatx@0.2.0: {} + + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + + cookie-es@1.2.3: {} + + cookie-es@2.0.1: {} + + cookie-es@3.1.1: {} + + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.2 + + core-util-is@1.0.3: {} + + cosmiconfig@5.2.1: + dependencies: + import-fresh: 2.0.0 + is-directory: 0.3.1 + js-yaml: 3.14.2 + parse-json: 4.0.0 + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + + croner@10.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + crypto-random-string@2.0.0: {} + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + css-what@6.2.2: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3): + optionalDependencies: + '@electric-sql/pglite': 0.4.1 + mysql2: 3.15.3 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-uri-component@0.2.2: {} + + deep-extend@0.6.0: {} + + deepmerge-ts@7.1.5: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-lazy-prop@3.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defu@6.1.7: {} + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + destr@2.0.5: {} + + destroy@1.2.0: {} + + detect-libc@1.0.3: {} + + detect-libc@2.1.2: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-prop@10.1.0: + dependencies: + type-fest: 5.6.0 + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.4.7 + + dotenv@16.4.7: {} + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + effect@3.20.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + electron-to-chromium@1.5.351: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + encoding-japanese@2.2.0: {} + + entities@4.5.0: {} + + env-editor@0.4.2: {} + + env-paths@3.0.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser-es@1.0.5: {} + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + + expo-apple-authentication@7.2.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + expo-application@6.1.5(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + + expo-asset@11.1.7(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + '@expo/image-utils': 0.7.6 + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-constants: 17.1.8(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + transitivePeerDependencies: + - supports-color + + expo-av@15.1.7(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + expo-build-properties@0.14.8(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + ajv: 8.20.0 + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + semver: 7.7.4 + + expo-clipboard@55.0.13(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + expo-constants@17.1.8(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)): + dependencies: + '@expo/config': 11.0.13 + '@expo/env': 1.0.7 + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + transitivePeerDependencies: + - supports-color + + expo-dev-client@5.2.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-dev-launcher: 5.1.16(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-dev-menu: 6.1.14(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-dev-menu-interface: 1.10.0(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-manifests: 0.16.6(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-updates-interface: 1.1.0(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + transitivePeerDependencies: + - supports-color + + expo-dev-launcher@5.1.16(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + ajv: 8.11.0 + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-dev-menu: 6.1.14(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-manifests: 0.16.6(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + resolve-from: 5.0.0 + transitivePeerDependencies: + - supports-color + + expo-dev-menu-interface@1.10.0(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + + expo-dev-menu@6.1.14(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-dev-menu-interface: 1.10.0(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + + expo-file-system@18.1.11(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + expo-font@13.0.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + fontfaceobserver: 2.3.0 + react: 19.0.0 + + expo-font@13.3.2(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + fontfaceobserver: 2.3.0 + react: 19.0.0 + + expo-haptics@55.0.14(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + + expo-image-loader@5.1.0(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + + expo-image-picker@16.1.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-image-loader: 5.1.0(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + + expo-json-utils@0.15.0: {} + + expo-keep-awake@14.1.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react: 19.0.0 + + expo-linking@7.1.7(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + expo-constants: 17.1.8(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + invariant: 2.2.4 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + transitivePeerDependencies: + - expo + - supports-color + + expo-localization@16.1.6(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react: 19.0.0 + rtl-detect: 1.1.2 + + expo-manifests@0.16.6(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + '@expo/config': 11.0.13 + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-json-utils: 0.15.0 + transitivePeerDependencies: + - supports-color + + expo-modules-autolinking@2.1.15: + dependencies: + '@expo/spawn-async': 1.7.2 + chalk: 4.1.2 + commander: 7.2.0 + find-up: 5.0.0 + glob: 10.5.0 + require-from-string: 2.0.2 + resolve-from: 5.0.0 + + expo-modules-core@2.5.0: + dependencies: + invariant: 2.2.4 + + expo-notifications@0.31.5(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + '@expo/image-utils': 0.7.6 + '@ide/backoff': 1.0.0 + abort-controller: 3.0.0 + assert: 2.1.0 + badgin: 1.2.3 + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-application: 6.1.5(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)) + expo-constants: 17.1.8(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + transitivePeerDependencies: + - supports-color + + expo-router@5.1.11(15de8a0d2b6e197a248b53123aa45ca3): + dependencies: + '@expo/metro-runtime': 5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + '@expo/schema-utils': 0.1.8 + '@expo/server': 0.6.3 + '@radix-ui/react-slot': 1.2.0(@types/react@19.0.14)(react@19.0.0) + '@react-navigation/bottom-tabs': 7.15.11(@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-screens@4.11.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + '@react-navigation/native': 7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + '@react-navigation/native-stack': 7.14.12(@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-screens@4.11.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + client-only: 0.0.1 + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-constants: 17.1.8(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + expo-linking: 7.1.7(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + invariant: 2.2.4 + react-fast-compare: 3.2.2 + react-native-is-edge-to-edge: 1.3.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-safe-area-context: 5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-screens: 4.11.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + semver: 7.6.3 + server-only: 0.0.1 + shallowequal: 1.1.0 + optionalDependencies: + react-native-reanimated: 4.0.3(@babel/core@7.29.0)(react-native-worklets@0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - react + - react-native + - supports-color + + expo-speech@13.1.7(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + + expo-splash-screen@0.30.10(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + '@expo/prebuild-config': 9.0.12 + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + transitivePeerDependencies: + - supports-color + + expo-status-bar@2.2.3(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + react-native-edge-to-edge: 1.6.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-is-edge-to-edge: 1.3.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + + expo-updates-interface@1.1.0(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + + expo-web-browser@14.2.0(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.29.2 + '@expo/cli': 0.24.24 + '@expo/config': 11.0.13 + '@expo/config-plugins': 10.1.2 + '@expo/fingerprint': 0.13.4 + '@expo/metro-config': 0.20.18 + '@expo/vector-icons': 14.1.0(expo-font@13.3.2(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + babel-preset-expo: 13.2.5(@babel/core@7.29.0) + expo-asset: 11.1.7(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + expo-constants: 17.1.8(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + expo-file-system: 18.1.11(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + expo-font: 13.3.2(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0) + expo-keep-awake: 14.1.4(expo@53.0.27(@babel/core@7.29.0)(@expo/metro-runtime@5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0) + expo-modules-autolinking: 2.1.15 + expo-modules-core: 2.5.0 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + react-native-edge-to-edge: 1.6.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + whatwg-url-without-unicode: 8.0.0-3 + optionalDependencies: + '@expo/metro-runtime': 5.0.5(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)) + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-react-compiler + - bufferutil + - graphql + - supports-color + - utf-8-validate + + exponential-backoff@3.1.3: {} + + exsolve@1.0.8: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-deep-equal@2.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-uri@3.1.2: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@1.1.0: {} + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flow-enums-runtime@0.0.6: {} + + fontfaceobserver@2.3.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data-encoder@1.7.2: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + freeport-async@2.0.0: {} + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-port-please@3.2.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + getenv@2.0.0: {} + + giget@3.2.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globby@16.2.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + is-path-inside: 4.0.0 + slash: 5.1.0 + unicorn-magic: 0.4.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + grammex@3.1.12: {} + + graphmatch@1.1.1: {} + + groq-sdk@0.7.0: + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + gzip-size@7.0.0: + dependencies: + duplexer: 0.1.2 + + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.4 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-estree@0.29.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hermes-parser@0.29.1: + dependencies: + hermes-estree: 0.29.1 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + hono@4.12.17: {} + + hookable@5.5.3: {} + + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-shutdown@1.2.2: {} + + http-status-codes@2.3.0: {} + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + httpxy@0.5.1: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + i18next@23.16.8: + dependencies: + '@babel/runtime': 7.29.2 + + iceberg-js@0.8.1: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + image-size@1.2.1: + dependencies: + queue: 6.0.2 + + imapflow@1.3.3: + dependencies: + '@zone-eu/mailsplit': 5.4.9 + encoding-japanese: 2.2.0 + iconv-lite: 0.7.2 + libbase64: 1.3.0 + libmime: 5.3.8 + libqp: 2.1.1 + nodemailer: 8.0.7 + pino: 10.3.1 + socks: 2.8.8 + + import-fresh@2.0.0: + dependencies: + caller-path: 2.0.0 + resolve-from: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ioredis@5.10.1: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ip-address@10.2.0: {} + + iron-webcrypto@1.2.1: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-directory@0.3.1: {} + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-module@1.0.0: {} + + is-nan@1.3.2: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + is-number@7.0.0: {} + + is-path-inside@4.0.0: {} + + is-plain-obj@2.1.0: {} + + is-property@1.0.2: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.8 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-stream@2.0.1: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.19.17 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + jest-util: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.19.17 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jimp-compact@0.16.1: {} + + jiti@1.21.7: {} + + jiti@2.7.0: {} + + jose@6.2.3: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsc-safe-url@0.2.4: {} + + jsesc@3.1.0: {} + + json-parse-better-errors@1.0.2: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + klona@2.0.6: {} + + knitwork@1.3.0: {} + + lan-network@0.1.7: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + leac@0.6.0: {} + + leven@3.1.0: {} + + libbase64@1.3.0: {} + + libmime@5.3.8: + dependencies: + encoding-japanese: 2.2.0 + iconv-lite: 0.7.2 + libbase64: 1.3.0 + libqp: 2.1.1 + + libqp@2.1.1: {} + + lighthouse-logger@1.4.2: + dependencies: + debug: 2.6.9 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + + lightningcss-darwin-arm64@1.27.0: + optional: true + + lightningcss-darwin-x64@1.27.0: + optional: true + + lightningcss-freebsd-x64@1.27.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.27.0: + optional: true + + lightningcss-linux-arm64-gnu@1.27.0: + optional: true + + lightningcss-linux-arm64-musl@1.27.0: + optional: true + + lightningcss-linux-x64-gnu@1.27.0: + optional: true + + lightningcss-linux-x64-musl@1.27.0: + optional: true + + lightningcss-win32-arm64-msvc@1.27.0: + optional: true + + lightningcss-win32-x64-msvc@1.27.0: + optional: true + + lightningcss@1.27.0: + dependencies: + detect-libc: 1.0.3 + optionalDependencies: + lightningcss-darwin-arm64: 1.27.0 + lightningcss-darwin-x64: 1.27.0 + lightningcss-freebsd-x64: 1.27.0 + lightningcss-linux-arm-gnueabihf: 1.27.0 + lightningcss-linux-arm64-gnu: 1.27.0 + lightningcss-linux-arm64-musl: 1.27.0 + lightningcss-linux-x64-gnu: 1.27.0 + lightningcss-linux-x64-musl: 1.27.0 + lightningcss-win32-arm64-msvc: 1.27.0 + lightningcss-win32-x64-msvc: 1.27.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + listhen@1.10.0: + dependencies: + '@parcel/watcher': 2.5.6 + '@parcel/watcher-wasm': 2.5.6 + citty: 0.2.2 + consola: 3.4.2 + crossws: 0.3.5 + defu: 6.1.7 + get-port-please: 3.2.0 + h3: 1.15.11 + http-shutdown: 1.2.2 + jiti: 2.7.0 + mlly: 1.8.2 + node-forge: 1.4.0 + pathe: 2.0.3 + std-env: 4.1.0 + tinyclip: 0.1.12 + ufo: 1.6.4 + untun: 0.1.3 + uqr: 0.1.3 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.1 + quansync: 0.2.11 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.debounce@4.0.8: {} + + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.throttle@4.1.1: {} + + lodash@4.18.1: {} + + log-symbols@2.2.0: + dependencies: + chalk: 2.4.2 + + long@5.3.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lottie-react-native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + lru-cache@10.4.3: {} + + lru-cache@11.3.6: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru.min@1.1.4: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + marky@1.3.0: {} + + math-intrinsics@1.1.0: {} + + mdn-data@2.0.14: {} + + memoize-one@5.2.1: {} + + merge-options@3.0.4: + dependencies: + is-plain-obj: 2.1.0 + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + metro-babel-transformer@0.82.5: + dependencies: + '@babel/core': 7.29.0 + flow-enums-runtime: 0.0.6 + hermes-parser: 0.29.1 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-cache-key@0.82.5: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-cache@0.82.5: + dependencies: + exponential-backoff: 3.1.3 + flow-enums-runtime: 0.0.6 + https-proxy-agent: 7.0.6 + metro-core: 0.82.5 + transitivePeerDependencies: + - supports-color + + metro-config@0.82.5: + dependencies: + connect: 3.7.0 + cosmiconfig: 5.2.1 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.82.5 + metro-cache: 0.82.5 + metro-core: 0.82.5 + metro-runtime: 0.82.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-core@0.82.5: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.82.5 + + metro-file-map@0.82.5: + dependencies: + debug: 4.4.3 + fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + nullthrows: 1.1.1 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + metro-minify-terser@0.82.5: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.46.2 + + metro-resolver@0.82.5: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-runtime@0.82.5: + dependencies: + '@babel/runtime': 7.29.2 + flow-enums-runtime: 0.0.6 + + metro-source-map@0.82.5: + dependencies: + '@babel/traverse': 7.29.0 + '@babel/traverse--for-generate-function-map': '@babel/traverse@7.29.0' + '@babel/types': 7.29.0 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.82.5 + nullthrows: 1.1.1 + ob1: 0.82.5 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-symbolicate@0.82.5: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.82.5 + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-transform-plugins@0.82.5: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-worker@0.82.5: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + flow-enums-runtime: 0.0.6 + metro: 0.82.5 + metro-babel-transformer: 0.82.5 + metro-cache: 0.82.5 + metro-cache-key: 0.82.5 + metro-minify-terser: 0.82.5 + metro-source-map: 0.82.5 + metro-transform-plugins: 0.82.5 + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro@0.82.5: + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + accepts: 1.3.8 + chalk: 4.1.2 + ci-info: 2.0.0 + connect: 3.7.0 + debug: 4.4.3 + error-stack-parser: 2.1.4 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + hermes-parser: 0.29.1 + image-size: 1.2.1 + invariant: 2.2.4 + jest-worker: 29.7.0 + jsc-safe-url: 0.2.4 + lodash.throttle: 4.1.1 + metro-babel-transformer: 0.82.5 + metro-cache: 0.82.5 + metro-cache-key: 0.82.5 + metro-config: 0.82.5 + metro-core: 0.82.5 + metro-file-map: 0.82.5 + metro-resolver: 0.82.5 + metro-runtime: 0.82.5 + metro-source-map: 0.82.5 + metro-symbolicate: 0.82.5 + metro-transform-plugins: 0.82.5 + metro-transform-worker: 0.82.5 + mime-types: 2.1.35 + nullthrows: 1.1.1 + serialize-error: 2.1.0 + source-map: 0.5.7 + throat: 5.0.0 + ws: 7.5.10 + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + mime@4.1.0: {} + + mimic-fn@1.2.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + mkdirp@1.0.4: {} + + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.0.0: {} + + ms@2.1.3: {} + + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + + nanoid@3.3.12: {} + + nativewind@4.2.3(react-native-reanimated@4.0.3(@babel/core@7.29.0)(react-native-worklets@0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-svg@15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)(tailwindcss@3.4.19): + dependencies: + comment-json: 4.6.2 + debug: 4.4.3 + react-native-css-interop: 0.2.3(react-native-reanimated@4.0.3(@babel/core@7.29.0)(react-native-worklets@0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-svg@15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)(tailwindcss@3.4.19) + tailwindcss: 3.4.19 + transitivePeerDependencies: + - react + - react-native + - react-native-reanimated + - react-native-safe-area-context + - react-native-svg + - supports-color + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + nested-error-stacks@2.0.1: {} + + nitropack@2.13.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@rollup/plugin-alias': 6.0.0(rollup@4.60.3) + '@rollup/plugin-commonjs': 29.0.2(rollup@4.60.3) + '@rollup/plugin-inject': 5.0.5(rollup@4.60.3) + '@rollup/plugin-json': 6.1.0(rollup@4.60.3) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.60.3) + '@rollup/plugin-replace': 6.0.3(rollup@4.60.3) + '@rollup/plugin-terser': 1.0.0(rollup@4.60.3) + '@vercel/nft': 1.5.0(rollup@4.60.3) + archiver: 7.0.1 + c12: 3.3.4(magicast@0.5.2) + chokidar: 5.0.0 + citty: 0.2.2 + compatx: 0.2.0 + confbox: 0.2.4 + consola: 3.4.2 + cookie-es: 2.0.1 + croner: 10.0.1 + crossws: 0.3.5 + db0: 0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3) + defu: 6.1.7 + destr: 2.0.5 + dot-prop: 10.1.0 + esbuild: 0.28.0 + escape-string-regexp: 5.0.0 + etag: 1.8.1 + exsolve: 1.0.8 + globby: 16.2.0 + gzip-size: 7.0.0 + h3: 1.15.11 + hookable: 5.5.3 + httpxy: 0.5.1 + ioredis: 5.10.1 + jiti: 2.7.0 + klona: 2.0.6 + knitwork: 1.3.0 + listhen: 1.10.0 + magic-string: 0.30.21 + magicast: 0.5.2 + mime: 4.1.0 + mlly: 1.8.2 + node-fetch-native: 1.6.7 + node-mock-http: 1.0.4 + ofetch: 1.5.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + pretty-bytes: 7.1.0 + radix3: 1.1.2 + rollup: 4.60.3 + rollup-plugin-visualizer: 7.0.1(rollup@4.60.3) + scule: 1.3.0 + semver: 7.7.4 + serve-placeholder: 2.0.2 + serve-static: 2.2.1 + source-map: 0.7.6 + std-env: 4.1.0 + ufo: 1.6.4 + ultrahtml: 1.6.0 + uncrypto: 0.1.3 + unctx: 2.5.0 + unenv: 2.0.0-rc.24 + unimport: 6.2.0 + unplugin-utils: 0.3.1 + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1) + untyped: 2.0.0 + unwasm: 0.5.3 + youch: 4.1.1 + youch-core: 0.3.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - bare-buffer + - better-sqlite3 + - drizzle-orm + - encoding + - idb-keyval + - mysql2 + - oxc-parser + - react-native-b4a + - rolldown + - sqlite3 + - supports-color + - uploadthing + + node-addon-api@7.1.1: {} + + node-domexception@1.0.0: {} + + node-fetch-native@1.6.7: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-forge@1.4.0: {} + + node-gyp-build@4.8.4: {} + + node-int64@0.4.0: {} + + node-mock-http@1.0.4: {} + + node-releases@2.0.38: {} + + nodemailer@8.0.7: {} + + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + + normalize-path@3.0.0: {} + + npm-package-arg@11.0.3: + dependencies: + hosted-git-info: 7.0.2 + proc-log: 4.2.0 + semver: 7.7.4 + validate-npm-package-name: 5.0.1 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nullthrows@1.1.1: {} + + ob1@0.82.5: + dependencies: + flow-enums-runtime: 0.0.6 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + ohash@2.0.11: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + open@7.4.2: + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openai@4.104.0(ws@8.20.0)(zod@3.25.76): + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + ws: 8.20.0 + zod: 3.25.76 + transitivePeerDependencies: + - encoding + + ora@3.4.0: + dependencies: + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-spinners: 2.9.2 + log-symbols: 2.2.0 + strip-ansi: 5.2.0 + wcwidth: 1.0.1 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.4 + json-parse-better-errors: 1.0.2 + + parse-png@2.1.0: + dependencies: + pngjs: 3.4.0 + + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.3.6 + minipass: 7.1.3 + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + peberminta@0.9.0: {} + + perfect-debounce@2.1.0: {} + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@3.0.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + plist@3.1.1: + dependencies: + '@xmldom/xmldom': 0.9.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pngjs@3.4.0: {} + + possible-typed-array-names@1.1.0: {} + + postcss-import@15.1.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.12 + + postcss-js@4.1.0(postcss@8.5.14): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.14 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.14): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.14 + + postcss-nested@6.2.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.49: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres@3.4.7: {} + + powershell-utils@0.1.0: {} + + prettier@3.8.3: {} + + pretty-bytes@5.6.0: {} + + pretty-bytes@7.1.0: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + prisma@7.8.0(@types/react@19.0.14)(magicast@0.5.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3): + dependencies: + '@prisma/config': 7.8.0(magicast@0.5.2) + '@prisma/dev': 0.24.3(typescript@5.9.3) + '@prisma/engines': 7.8.0 + '@prisma/studio-core': 0.27.3(@types/react@19.0.14)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + mysql2: 3.15.3 + postgres: 3.4.7 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - magicast + - react + - react-dom + + proc-log@4.2.0: {} + + process-nextick-args@2.0.1: {} + + process-warning@5.0.0: {} + + process@0.11.10: {} + + progress@2.0.3: {} + + promise@8.3.0: + dependencies: + asap: 2.0.6 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + qrcode-terminal@0.11.0: {} + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.11: {} + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + queue-microtask@1.2.3: {} + + queue@6.0.2: + dependencies: + inherits: 2.0.4 + + quick-format-unescaped@4.0.4: {} + + radix3@1.1.2: {} + + range-parser@1.2.1: {} + + rc9@3.0.1: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-devtools-core@6.1.5: + dependencies: + shell-quote: 1.8.3 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + react-dom@19.0.0(react@19.0.0): + dependencies: + react: 19.0.0 + scheduler: 0.25.0 + + react-fast-compare@3.2.2: {} + + react-freeze@1.0.4(react@19.0.0): + dependencies: + react: 19.0.0 + + react-hook-form@7.75.0(react@19.0.0): + dependencies: + react: 19.0.0 + + react-i18next@15.7.4(i18next@23.16.8)(react-dom@19.0.0(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)(typescript@5.8.3): + dependencies: + '@babel/runtime': 7.29.2 + html-parse-stringify: 3.0.1 + i18next: 23.16.8 + react: 19.0.0 + optionalDependencies: + react-dom: 19.0.0(react@19.0.0) + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + typescript: 5.8.3 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-is@19.2.5: {} + + react-native-bottom-tabs@1.2.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-freeze: 1.0.4(react@19.0.0) + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + sf-symbols-typescript: 2.2.0 + use-latest-callback: 0.2.6(react@19.0.0) + + react-native-css-interop@0.2.3(react-native-reanimated@4.0.3(@babel/core@7.29.0)(react-native-worklets@0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-svg@15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)(tailwindcss@3.4.19): + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + debug: 4.4.3 + lightningcss: 1.27.0 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + react-native-reanimated: 4.0.3(@babel/core@7.29.0)(react-native-worklets@0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + semver: 7.7.4 + tailwindcss: 3.4.19 + optionalDependencies: + react-native-safe-area-context: 5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-svg: 15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + transitivePeerDependencies: + - supports-color + + react-native-edge-to-edge@1.6.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + react-native-gesture-handler@2.24.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + '@egjs/hammerjs': 2.0.17 + hoist-non-react-statics: 3.3.2 + invariant: 2.2.4 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + react-native-is-edge-to-edge@1.3.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + react-native-mmkv@3.3.3(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + react-native-reanimated@4.0.3(@babel/core@7.29.0)(react-native-worklets@0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/core': 7.29.0 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + react-native-is-edge-to-edge: 1.3.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + react-native-worklets: 0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + semver: 7.7.2 + + react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + react-native-screens@4.11.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-freeze: 1.0.4(react@19.0.0) + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + react-native-is-edge-to-edge: 1.3.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + warn-once: 0.1.1 + + react-native-sse@1.2.1: {} + + react-native-svg@15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + css-select: 5.2.2 + css-tree: 1.1.3 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + warn-once: 0.1.1 + + react-native-url-polyfill@2.0.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0)): + dependencies: + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + whatwg-url-without-unicode: 8.0.0-3 + + react-native-worklets@0.4.2(@babel/core@7.29.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + convert-source-map: 2.0.0 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + transitivePeerDependencies: + - supports-color + + react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native/assets-registry': 0.79.6 + '@react-native/codegen': 0.79.6(@babel/core@7.29.0) + '@react-native/community-cli-plugin': 0.79.6 + '@react-native/gradle-plugin': 0.79.6 + '@react-native/js-polyfills': 0.79.6 + '@react-native/normalize-colors': 0.79.6 + '@react-native/virtualized-lists': 0.79.6(@types/react@19.0.14)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-jest: 29.7.0(@babel/core@7.29.0) + babel-plugin-syntax-hermes-parser: 0.25.1 + base64-js: 1.5.1 + chalk: 4.1.2 + commander: 12.1.0 + event-target-shim: 5.0.1 + flow-enums-runtime: 0.0.6 + glob: 7.2.3 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + memoize-one: 5.2.1 + metro-runtime: 0.82.5 + metro-source-map: 0.82.5 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.0.0 + react-devtools-core: 6.1.5 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.25.0 + semver: 7.7.4 + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + ws: 6.2.3 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 19.0.14 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - bufferutil + - supports-color + - utf-8-validate + + react-promise-suspense@0.3.4: + dependencies: + fast-deep-equal: 2.0.1 + + react-refresh@0.14.2: {} + + react@19.0.0: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + readdirp@5.0.0: {} + + real-require@0.2.0: {} + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.13.11: {} + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.1: + dependencies: + jsesc: 3.1.0 + + remeda@2.33.4: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requireg@0.2.2: + dependencies: + nested-error-stacks: 2.0.1 + rc: 1.2.8 + resolve: 1.7.1 + + resend@4.8.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@react-email/render': 1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + transitivePeerDependencies: + - react + - react-dom + + resolve-from@3.0.0: {} + + resolve-from@5.0.0: {} + + resolve-workspace-root@2.0.1: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@1.7.1: + dependencies: + path-parse: 1.0.7 + + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rive-react-native@9.8.3(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.0.14)(react@19.0.0) + + rollup-plugin-visualizer@7.0.1(rollup@4.60.3): + dependencies: + open: 11.0.0 + picomatch: 4.0.4 + source-map: 0.7.6 + yargs: 18.0.0 + optionalDependencies: + rollup: 4.60.3 + + rollup@4.60.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.3 + '@rollup/rollup-android-arm64': 4.60.3 + '@rollup/rollup-darwin-arm64': 4.60.3 + '@rollup/rollup-darwin-x64': 4.60.3 + '@rollup/rollup-freebsd-arm64': 4.60.3 + '@rollup/rollup-freebsd-x64': 4.60.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 + '@rollup/rollup-linux-arm-musleabihf': 4.60.3 + '@rollup/rollup-linux-arm64-gnu': 4.60.3 + '@rollup/rollup-linux-arm64-musl': 4.60.3 + '@rollup/rollup-linux-loong64-gnu': 4.60.3 + '@rollup/rollup-linux-loong64-musl': 4.60.3 + '@rollup/rollup-linux-ppc64-gnu': 4.60.3 + '@rollup/rollup-linux-ppc64-musl': 4.60.3 + '@rollup/rollup-linux-riscv64-gnu': 4.60.3 + '@rollup/rollup-linux-riscv64-musl': 4.60.3 + '@rollup/rollup-linux-s390x-gnu': 4.60.3 + '@rollup/rollup-linux-x64-gnu': 4.60.3 + '@rollup/rollup-linux-x64-musl': 4.60.3 + '@rollup/rollup-openbsd-x64': 4.60.3 + '@rollup/rollup-openharmony-arm64': 4.60.3 + '@rollup/rollup-win32-arm64-msvc': 4.60.3 + '@rollup/rollup-win32-ia32-msvc': 4.60.3 + '@rollup/rollup-win32-x64-gnu': 4.60.3 + '@rollup/rollup-win32-x64-msvc': 4.60.3 + fsevents: 2.3.3 + + rtl-detect@1.1.2: {} + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sax@1.6.0: {} + + scheduler@0.25.0: {} + + scule@1.3.0: {} + + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + + semver@6.3.1: {} + + semver@7.6.3: {} + + semver@7.7.2: {} + + semver@7.7.4: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + seq-queue@0.0.5: {} + + serialize-error@2.1.0: {} + + serialize-javascript@7.0.5: {} + + serve-placeholder@2.0.2: + dependencies: + defu: 6.1.7 + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + server-only@0.0.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + sf-symbols-typescript@2.2.0: {} + + shallowequal@1.1.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-plist@1.3.1: + dependencies: + bplist-creator: 0.1.0 + bplist-parser: 0.3.1 + plist: 3.1.1 + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + slash@5.1.0: {} + + slugify@1.6.9: {} + + smart-buffer@4.2.0: {} + + smob@1.6.1: {} + + socks@2.8.8: + dependencies: + ip-address: 10.2.0 + smart-buffer: 4.2.0 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + split-on-first@1.1.0: {} + + split2@4.2.0: {} + + sprintf-js@1.0.3: {} + + sqlstring@2.3.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stackframe@1.3.4: {} + + stacktrace-parser@0.1.11: + dependencies: + type-fest: 0.7.1 + + standard-as-callback@2.1.0: {} + + statuses@1.5.0: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + std-env@4.1.0: {} + + stream-buffers@2.2.0: {} + + streamx@2.25.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + strict-uri-encode@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@2.0.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + stripe@17.7.0: + dependencies: + '@types/node': 22.19.17 + qs: 6.15.1 + + structured-headers@0.4.1: {} + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + glob: 10.5.0 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + + supports-color@10.2.2: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@2.3.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tagged-tag@1.0.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.14 + postcss-import: 15.1.0(postcss@8.5.14) + postcss-js: 4.1.0(postcss@8.5.14) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.14) + postcss-nested: 6.2.0(postcss@8.5.14) + postcss-selector-parser: 6.1.2 + resolve: 1.22.12 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.7.1 + fast-fifo: 1.3.2 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar@7.5.14: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + teex@1.0.1: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + temp-dir@2.0.0: {} + + terminal-link@2.1.1: + dependencies: + ansi-escapes: 4.3.2 + supports-hyperlinks: 2.3.0 + + terser@5.46.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 7.2.3 + minimatch: 3.1.5 + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + + throat@5.0.0: {} + + tinyclip@0.1.12: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@0.7.1: {} + + type-fest@5.6.0: + dependencies: + tagged-tag: 1.0.0 + + typescript@5.8.3: {} + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + ultrahtml@1.6.0: {} + + uncrypto@0.1.3: {} + + unctx@2.5.0: + dependencies: + acorn: 8.16.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + unplugin: 2.3.11 + + undici-types@5.26.5: {} + + undici-types@6.21.0: {} + + undici@6.25.0: {} + + undici@7.25.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + unicorn-magic@0.4.0: {} + + unimport@6.2.0: + dependencies: + acorn: 8.16.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.2 + pathe: 2.0.3 + picomatch: 4.0.4 + pkg-types: 2.3.1 + scule: 1.3.0 + strip-literal: 3.1.0 + tinyglobby: 0.2.16 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 + + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + + unpipe@1.0.0: {} + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.4 + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + unstorage@1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1): + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.3.6 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + optionalDependencies: + db0: 0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3) + ioredis: 5.10.1 + + untun@0.1.3: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + pathe: 1.1.2 + + untyped@2.0.0: + dependencies: + citty: 0.1.6 + defu: 6.1.7 + jiti: 2.7.0 + knitwork: 1.3.0 + scule: 1.3.0 + + unwasm@0.5.3: + dependencies: + exsolve: 1.0.8 + knitwork: 1.3.0 + magic-string: 0.30.21 + mlly: 1.8.2 + pathe: 2.0.3 + pkg-types: 2.3.1 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uqr@0.1.3: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-latest-callback@0.2.6(react@19.0.0): + dependencies: + react: 19.0.0 + + use-sync-external-store@1.6.0(react@19.0.0): + dependencies: + react: 19.0.0 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.20 + + utils-merge@1.0.1: {} + + uuid@7.0.3: {} + + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + valibot@1.4.0(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + + validate-npm-package-name@5.0.1: {} + + vary@1.1.2: {} + + vlq@1.0.1: {} + + void-elements@3.1.0: {} + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + warn-once@0.1.1: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + webidl-conversions@5.0.0: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-fetch@3.6.20: {} + + whatwg-url-without-unicode@8.0.0-3: + dependencies: + buffer: 5.7.1 + punycode: 2.3.1 + webidl-conversions: 5.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wonka@6.3.6: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@6.2.3: + dependencies: + async-limiter: 1.0.1 + + ws@7.5.10: {} + + ws@8.20.0: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + xcode@3.0.1: + dependencies: + simple-plist: 1.3.1 + uuid: 7.0.3 + + xml2js@0.6.0: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xmlbuilder@15.1.1: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yargs-parser@21.1.1: {} + + yargs-parser@22.0.0: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yocto-queue@0.1.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.1: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.7.0 + '@speed-highlight/core': 1.2.15 + cookie-es: 3.1.1 + youch-core: 0.3.3 + + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.12 + graphmatch: 1.1.1 + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + + zod@3.25.76: {} + + zustand@5.0.13(@types/react@19.0.14)(react@19.0.0)(use-sync-external-store@1.6.0(react@19.0.0)): + optionalDependencies: + '@types/react': 19.0.14 + react: 19.0.0 + use-sync-external-store: 1.6.0(react@19.0.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..bd86d72 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "backend"
  • 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%0D!0(_6ZD=N5-504R}tP*$cc49oe zf%Y+QV8)Kk(5^N6LKtyeW!UL>fFbFQ20-KezAXWtD{=Nd)BdVQ`nl$erG|L}V$P)x z3Q)mX_o9#TFV*tpom9fAWFQp6mMLbzjNZ{^eub>KF^9rTT^FB9&pv8jeyw(4D^CAo z(rL@zrjg7j*pG5!%5N~|8r%QV?EnO`P`W4(w?PMawg-{jj)+NM8aejrK0aRI-mCM@ z1(Ezv5Os-d+KRFoh$G3Ag)*{;DbHk50Mro>YCMU<}v!cv;e}`4@Z6#4@nRv+nDFolHtWL6}D9c>c6>O@4>` z+w&QL#8uR@^@~zs_v5zJ;8H_kuHc`DLhb|c%rWwXMW1J2T=YQG9aCyhNNB(pvCFWB zW52u0>Io~eij|Nmp}`^R33fpYZXB5)k$pP0y+q}AYDmo3_l_UfvNEoy4!%{CJxv$! zPwJME4YsQ2Nqv+r+sUtK^dsSX{XH*Jgb7gQVTG^}qSMM$p|Bw1;-}Wwc*`!3Wk1em z-ws*&URnC~t{$D6bAwm(E|R`=G_;;}^hM4}NCNie zL#O~DMP?vimm&a}g+plBuoRKdkV+Zf6D8w)c1)KfK6bMk<>!&FATPRm*LZNY= z({rx*>2XU!hjm;C()$efO7DkS*`1kdh`(a;x`BEnqI=V+M6ooPJt5r`NYI>F>-<@} z05s01SlsnYOV#y>cdY66*M~mVvs+ViDZ|N+w*=T6L-kX>eDS( zT^)Iq-k}>tlh&tQcB+(ju;dr8#g|Epm<$YjoateX>Pc8ROyU}E%Z3XI0}}4;IMNG? z(aTi%N~@LSMnkOfr^o|jh!T{GTO844K0nmG`oxmoF{=y%&EXO-!Y)MPj4;SB+}=j5 z0YzA@Ud0J(meAn?m*25H1qSd{6q{>n|5kqSTAe*|LmWj`8Hn6%OCrcdjhiDHE}w^l zC}>pT1v*k0F<-F;1;rT%ZjBV&IggqUGT@?MGURl14=UUIosiTT-bF|hmkr-ZWAKA8 z!-wb|1$9e9npF3qi|)5W*h72qI^XgaqMme+&$i&)p}|qmB15~InN60T&l^E ztrCgpeO~%@w^M&^?2t@!`}WrK@SNZR5yLvj;Y1v!&UoPyd8Z7RnGG1`j(0Vj)j9L27|x) zZb~V1kBCkDV<|JXnle%W6oZB3w~BG0@o?tAh$eCJWR0G!ma&F@2joeVl8ZuRxV3&o zs^3Ry-i~XJie6qLy{Gxo1ZV;>388&H_$1)H&m0NJnQkb{S~R{@(G$5uEB8mmCsSzy z`WV-Tw^#mqhS{9S*lNVO&(G|+!MGt95so0Rn;3&b2+vjQaN!0q4i3)M!U84rVu<;? zh~CRfW&wdUVhz?jMGuj0ON{zh3&wn9be|1(tByWByyN955{j0*FYEp`E1}RsR)~Sh zjm%+2Px=#nd(uuD6ju#UP?XW~nexAavv&a-2#-v=v63e$Ty#*^vComp=+B>L3f~T| zBwHPx&Q7bu9&Inr<|FG{YhzA_mci*?k>i88aS3Jn{SuDG>NU9X)mtL;?&JbXW^^?! z`tb-*R0GNmreS&&A$H|4>$ya#86v^4z8CpI&VNr|2oF42tipX5Oma(a1%b5HgEAll z!o^1+WnADYbb()rc(&6jC!6Xtk{0^!hkq7mtwfJHau4`A6>)`VeSy+~0uoRtp5#Co z{y?Ef6F)3KH&+mlX_y0IlP)k>Buk33`r?V{Ll