diff --git a/.cursor/rules/ios-views.mdc b/.cursor/rules/ios-views.mdc new file mode 100644 index 00000000..fc3a073b --- /dev/null +++ b/.cursor/rules/ios-views.mdc @@ -0,0 +1,169 @@ +--- +description: Regras para criar telas iOS no projeto Plotwist +globs: + - "apps/ios/**/*.swift" +alwaysApply: false +--- + +# Regras para Telas iOS - Plotwist + +## Estrutura de Arquivos + +``` +apps/ios/Plotwist/Plotwist/ +├── App/ # Entry point e RootView +├── Components/ # Componentes reutilizáveis +│ └── PrimaryButton.swift # Botões padrão +├── Views/ # Telas organizadas por feature +│ ├── Auth/ # Login, SignUp, etc +│ ├── Home/ # Dashboard +│ ├── Movies/ # Catálogo de filmes +│ └── ... +├── Services/ # Serviços de API +├── Theme/ # Cores e estilos +├── Localization/ # Multi-idioma +│ ├── Language.swift # Enum de idiomas +│ └── Strings.swift # Todas as strings traduzidas +└── Utils/ # Constantes +``` + +## Regras de Criação de Views + +1. **Mantenha tudo simples** - State local na View, sem ViewModel separado para telas simples +2. **Use @State** para estados locais (loading, error, form fields) +3. **API sempre em `localhost:3333`** - Configurado em `Utils/Constants.swift` +4. **Use as cores do tema** - Definidas em `Theme/Colors.swift` +5. **Use L10n.current para strings** - Todas as strings devem ser traduzidas + +## Localização (Multi-idioma) + +Use `L10n.current` para acessar strings traduzidas: + +```swift +@State private var strings = L10n.current + +// Na view +Text(strings.accessPlotwist) +Text(strings.loginLabel) + +// Reagir a mudanças de idioma +.onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current +} +``` + +**Idiomas suportados:** en-US, pt-BR, es-ES, fr-FR, de-DE, it-IT, ja-JP + +**Adicionar novas strings:** Edite `Localization/Strings.swift` + +## Template de View Simples + +```swift +import SwiftUI + +struct NomeDaTelaView: View { + @State private var isLoading = false + @State private var error: String? + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 16) { + // Conteúdo + } + .padding(.horizontal, 24) + } + } +} + +#Preview { + NomeDaTelaView() +} +``` + +## Cores (use sempre estas) + +- `Color.appBackgroundAdaptive` - Background principal +- `Color.appForegroundAdaptive` - Texto principal +- `Color.appBorderAdaptive` - Bordas (inputs e botões usam transparente + borda) +- `Color.appMutedForegroundAdaptive` - Texto secundário +- `Color.appDestructive` - Erros e ações destrutivas + +## Componentes Comuns + +### Input Field (transparente com borda) + +```swift +TextField("Placeholder", text: $value) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(12) + .background(Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.appBorderAdaptive, lineWidth: 1) + ) +``` + +### Botão Primário (use o componente) + +```swift +// Filled (preto) +PrimaryButton("Access", variant: .filled, isLoading: isLoading) { + // ação +} + +// Outline (transparente com borda) +PrimaryButton("Cancel") { + // ação +} +``` + +### Botão Social (desabilitado) + +```swift +SocialButton("Continue with Google", icon: "globe", isDisabled: true) {} +SocialButton("Continue with Apple", icon: "apple.logo", isDisabled: true) {} +``` + +### Mensagem de Erro + +```swift +if let error { + Text(error) + .font(.caption) + .foregroundColor(.appDestructive) +} +``` + +## Chamadas de API + +Use `AuthService.shared` para autenticação ou crie services específicos: + +```swift +Task { + isLoading = true + defer { isLoading = false } + + do { + // chamada async + } catch { + self.error = error.localizedDescription + } +} +``` + +## Navegação + +- Use `NavigationView` na view raiz +- Use `NavigationLink` para navegação +- `RootView` gerencia auth state via `NotificationCenter` + +## NÃO FAZER + +- ❌ Criar ViewModels separados para telas simples +- ❌ Criar arquivos de documentação (.md) desnecessários +- ❌ Usar cores hardcoded (sempre use Theme/Colors.swift) +- ❌ Criar abstrações desnecessárias +- ❌ Usar URLs diferentes de localhost:3333 diff --git a/.env.example b/.env.example index ece880ac..2e799934 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,5 @@ NEXT_PUBLIC_TMDB_API_KEY= - NEXT_PUBLIC_MEASUREMENT_ID= - NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= STRIPE_SECRET_KEY= diff --git a/IOS_TASKS.md b/IOS_TASKS.md new file mode 100644 index 00000000..8f830817 --- /dev/null +++ b/IOS_TASKS.md @@ -0,0 +1,1197 @@ +# 📱 Plotwist - Tarefas para App iOS Nativo + +Este documento contém o mapeamento completo das funcionalidades do site web e as tarefas necessárias para criar um aplicativo iOS nativo usando Swift e SwiftUI. + +--- + +## 📋 Índice + +1. [Setup Inicial](#1-setup-inicial) +2. [Autenticação](#2-autenticação) +3. [Navegação](#3-navegação) +4. [Home/Dashboard](#4-homedashboard) +5. [Catálogo de Filmes](#5-catálogo-de-filmes) +6. [Catálogo de Séries](#6-catálogo-de-séries) +7. [Detalhes de Mídia](#7-detalhes-de-mídia) +8. [Sistema de Reviews](#8-sistema-de-reviews) +9. [Listas Personalizadas](#9-listas-personalizadas) +10. [Perfil do Usuário](#10-perfil-do-usuário) +11. [Coleção do Usuário](#11-coleção-do-usuário) +12. [Estatísticas](#12-estatísticas) +13. [Sistema Social](#13-sistema-social) +14. [Busca](#14-busca) +15. [Configurações](#15-configurações) +16. [Internacionalização](#16-internacionalização) +17. [Funcionalidades Premium](#17-funcionalidades-premium) +18. [Importação de Dados](#18-importação-de-dados) + +--- + +## 1. Setup Inicial + +### 1.1 Configuração do Projeto + +- [ ] Criar projeto Xcode com SwiftUI +- [ ] Configurar versões mínimas (iOS 16+) +- [ ] Configurar SwiftLint para linting +- [ ] Configurar SwiftFormat para formatação +- [ ] Configurar esquemas de build (Debug, Release) +- [ ] Configurar Code Signing & Capabilities + +### 1.2 Gerenciador de Dependências + +- [ ] Escolher Swift Package Manager (SPM) como principal +- [ ] Configurar estrutura de dependências + +### 1.3 Dependências Principais + +- [ ] **Alamofire** - Requisições HTTP +- [ ] **Kingfisher** - Cache e carregamento de imagens +- [ ] **KeychainAccess** - Armazenamento seguro de tokens +- [ ] **SwiftUIIntrospect** - Acesso a UIKit quando necessário +- [ ] **Lottie** - Animações complexas (opcional) +- [ ] **SwiftUICharts** ou **Charts (Apple)** - Gráficos para estatísticas + +### 1.4 Arquitetura + +- [ ] **Padrão MVVM** (Model-View-ViewModel) +- [ ] **Combine** para gerenciamento de estado reativo +- [ ] **async/await** para operações assíncronas +- [ ] **Protocol-oriented programming** para abstrações + +### 1.5 Configuração de Ambiente + +- [ ] Criar arquivo de configuração `Configuration.swift` +- [ ] Configurar variáveis: `API_BASE_URL`, `TMDB_API_KEY` +- [ ] Criar diferentes configurações para Debug/Release +- [ ] Usar `xcconfig` files para variáveis de ambiente + +### 1.6 Estrutura de Pastas + +``` +Plotwist/ +├── App/ +│ ├── PlotwistApp.swift # Entry point ✅ +│ └── RootView.swift # Root navigation ✅ +│ └── AppDelegate.swift # Lifecycle +├── Core/ +│ ├── Network/ # Networking layer +│ │ ├── APIClient.swift +│ │ ├── APIEndpoint.swift +│ │ └── APIError.swift +│ ├── Storage/ # Persistência +│ │ ├── UserDefaults/ +│ │ ├── Keychain/ +│ │ └── CoreData/ (opcional) +│ └── Extensions/ # Swift extensions +├── Models/ # Modelos de dados (Codable) +├── ViewModels/ # ViewModels (ObservableObject) +│ └── LoginViewModel.swift # ✅ +├── Views/ # SwiftUI Views +│ ├── Auth/ # ✅ +│ │ ├── LoginView.swift # ✅ +│ │ └── SignUpView.swift # ✅ (placeholder) +│ ├── Home/ # ✅ +│ │ └── HomeView.swift # ✅ +│ ├── Movies/ +│ ├── Series/ +│ ├── Profile/ +│ ├── Lists/ +│ └── Components/ # Componentes reutilizáveis +├── Services/ # Serviços de negócio +│ └── AuthService.swift # ✅ +│ ├── MovieService.swift +│ ├── ReviewService.swift +│ └── ... +├── Models/ # ✅ +│ └── User.swift # ✅ +├── Extensions/ # ✅ +│ ├── NotificationName+Extensions.swift # ✅ +│ └── View+Extensions.swift # ✅ +├── Utils/ # Utilitários ✅ +│ ├── Constants.swift # ✅ +│ ├── Localizable.swift # ✅ +│ ├── Formatters.swift +│ └── Validators.swift +├── Resources/ +│ ├── Assets.xcassets # Imagens e cores +│ ├── Localizable/ # i18n strings +│ └── Fonts/ +└── Configuration/ + ├── Debug.xcconfig + └── Release.xcconfig +``` + +--- + +## 2. Autenticação + +### 2.1 Views de Auth + +- [x] **LoginView** ✅ + + - [x] TextField para login (email ou username) + - [x] SecureField para senha com botão de toggle + - [x] Botão de login com loading state + - [ ] NavigationLink para "Esqueci a senha" + - [x] NavigationLink para cadastro + - [x] Validação com Property Wrappers + +- [ ] **SignUpView** (placeholder criado) + + - [ ] TextField para username com validação em tempo real + - [ ] TextField para email com validação + - [ ] SecureField para senha (mínimo 8 caracteres) + - [ ] Força da senha visual + - [ ] Toggle de termos de uso + - [ ] Validação inline + +- [ ] **ForgotPasswordView** + + - [ ] TextField para email + - [ ] Botão de envio com confirmação + - [ ] Feedback de sucesso/erro + +- [ ] **ResetPasswordView** + - [ ] SecureField para nova senha + - [ ] SecureField para confirmação + - [ ] Validação de token via deep link + +### 2.2 ViewModels + +- [x] **LoginViewModel** ✅ + - [x] `@Published var isLoading: Bool` + - [x] `@Published var errorMessage: String?` + - [x] Validação de campos + - [x] Método: `login()` + - [ ] Método: `signUp()` + +### 2.3 Gerenciamento de Sessão + +- [x] **AuthService** (singleton criado) ✅ + - [x] Armazenar JWT no UserDefaults (migrar para Keychain) + - [x] Métodos: `signIn()`, `signOut()`, `getToken()`, `isAuthenticated()` + - [x] Integração com API + - [x] NotificationCenter para mudanças de estado +- [ ] Armazenar JWT no Keychain via KeychainAccess (recomendado) +- [ ] Implementar auto-refresh de token +- [ ] Interceptor para adicionar token automaticamente +- [ ] Proteção de rotas com `@EnvironmentObject` + +### 2.4 Biometria (Opcional) + +- [ ] Face ID / Touch ID para login rápido +- [ ] LocalAuthentication framework +- [ ] Salvar preferência no UserDefaults + +--- + +## 3. Navegação + +### 3.1 Estrutura de Navegação + +- [ ] **TabView Principal** + + - [ ] Home + - [ ] Filmes + - [ ] Séries + - [ ] Busca + - [ ] Perfil + +- [ ] **NavigationStack** (iOS 16+) + - [ ] Stack de Autenticação + - [ ] Stack de Filmes (Lista, Detalhes) + - [ ] Stack de Séries (Lista, Detalhes, Temporadas, Episódios) + - [ ] Stack de Listas + - [ ] Stack de Perfil + +### 3.2 Deep Linking + +- [ ] Configurar URL Schemes no Info.plist +- [ ] Configurar Universal Links (Associated Domains) +- [ ] Implementar `.onOpenURL()` modifier +- [ ] Rotas: + - [ ] `plotwist://movie/:id` + - [ ] `plotwist://series/:id` + - [ ] `plotwist://list/:id` + - [ ] `plotwist://user/:username` + +### 3.3 Coordenação + +- [ ] Criar `Router` ou `Coordinator` para navegação complexa +- [ ] Implementar `NavigationPath` gerenciado + +--- + +## 4. Home/Dashboard + +### 4.1 Componentes da Home + +- [ ] **Header** + + - [ ] Logo (SF Symbol ou custom) + - [ ] Botão de busca (magnifyingglass.circle) + - [ ] AsyncImage para avatar do usuário + +- [ ] **LastUserReviewSection** + + - [ ] Card customizado com última review + - [ ] NavigationLink para o item + - [ ] Skeleton loading + +- [ ] **PopularReviewsSection** + + - [ ] ScrollView horizontal com LazyHStack + - [ ] Picker para filtros (hoje, semana, mês, todos) + - [ ] Pull to refresh + - [ ] Infinite scroll com `.onAppear` no último item + +- [ ] **NetworkActivityFeedSection** + + - [ ] LazyVStack com atividades + - [ ] Tipos de atividade: + - [ ] Status change + - [ ] Nova review + - [ ] Nova lista + - [ ] Follow/Unfollow + - [ ] Episódios assistidos + - [ ] Likes + +- [ ] **SidebarPopularMovies** (iPad) + + - [ ] Grid 3x1 de posters + - [ ] NavigationLink para lista completa + +- [ ] **SidebarPopularSeries** (iPad) + - [ ] Grid 3x1 de posters + - [ ] NavigationLink para lista completa + +### 4.2 ViewModel + +- [ ] **HomeViewModel** + - [ ] Carregar dados em paralelo com `async let` + - [ ] Gerenciar estados de loading/error + - [ ] Pagination para reviews + +--- + +## 5. Catálogo de Filmes + +### 5.1 Views de Listagem + +- [ ] **PopularMoviesView** + + - [ ] LazyVGrid com posters + - [ ] Pull to refresh + - [ ] Infinite scroll + - [ ] Skeleton placeholders + +- [ ] **NowPlayingMoviesView** + + - [ ] Lista de filmes em cartaz + - [ ] Badge "Em Cartaz" + +- [ ] **UpcomingMoviesView** + + - [ ] Lista de lançamentos futuros + - [ ] Data de lançamento em destaque + +- [ ] **TopRatedMoviesView** + + - [ ] Lista ordenada por rating + - [ ] Rating TMDB visível + +- [ ] **DiscoverMoviesView** + - [ ] Filtros avançados via Sheet: + - [ ] MultiSelector de gêneros + - [ ] Slider para ano (Date picker range) + - [ ] Slider para nota mínima + - [ ] Picker de ordenação + - [ ] MultiSelector de provedores de streaming + - [ ] Picker de região + +### 5.2 Componentes de Filme + +- [ ] **MoviePosterCard** + + - [ ] KFImage (Kingfisher) para poster + - [ ] VStack com título, ano, rating + - [ ] Gradient overlay + - [ ] Tap gesture para navegação + +- [ ] **MovieFiltersSheet** + - [ ] Sheet modal com ScrollView + - [ ] GenreChipGrid (FlowLayout) + - [ ] Custom Slider views + - [ ] Date picker para ano + - [ ] Botões "Aplicar" e "Limpar" + +### 5.3 ViewModels + +- [ ] **MoviesListViewModel** + - [ ] `@Published var movies: [Movie]` + - [ ] `@Published var filters: MovieFilters` + - [ ] Métodos de fetch com paginação + +--- + +## 6. Catálogo de Séries + +### 6.1 Views de Listagem + +- [ ] **PopularSeriesView** + + - [ ] LazyVGrid com posters + - [ ] Infinite scroll + +- [ ] **AiringTodaySeriesView** + + - [ ] Séries com episódios hoje + - [ ] Badge "Hoje" + +- [ ] **OnTheAirSeriesView** + + - [ ] Séries em exibição + - [ ] Status de exibição + +- [ ] **TopRatedSeriesView** + + - [ ] Lista ordenada por rating + +- [ ] **DiscoverSeriesView** + - [ ] Mesmos filtros dos filmes + - [ ] Filtro adicional: status (em andamento, finalizada) + +### 6.2 Categorias Especiais + +- [ ] **AnimesView** + - [ ] Filtro pré-aplicado para animação japonesa + - [ ] Estilo visual customizado (opcional) +- [ ] **DoramasView** + - [ ] Filtro pré-aplicado para séries coreanas + +### 6.3 ViewModels + +- [ ] **SeriesListViewModel** + - [ ] Similar ao MoviesListViewModel + - [ ] Filtros específicos de séries + +--- + +## 7. Detalhes de Mídia + +### 7.1 MovieDetailView + +- [ ] **Header com Backdrop** + + - [ ] ZStack com KFImage + - [ ] LinearGradient overlay + - [ ] Botão de voltar customizado + - [ ] Parallax scroll effect (opcional) + +- [ ] **Informações Principais** + + - [ ] HStack com poster + info + - [ ] Títulos (original e traduzido) + - [ ] Year, runtime, genres + - [ ] Sinopse expandível com "Ler mais" + - [ ] Rating TMDB com SF Symbols (star.fill) + +- [ ] **Ações do Usuário** + + - [ ] Menu de Status (Watchlist, Watching, Watched, Dropped) + - [ ] Botão "Adicionar à Lista" + - [ ] Botão "Escrever Review" + - [ ] Animações de feedback + +- [ ] **Informações Adicionais** + + - [ ] Diretor + - [ ] Elenco - ScrollView horizontal + - [ ] Orçamento e Receita formatados + - [ ] Idioma original + - [ ] Países de produção + +- [ ] **TabView para Seções** + + - [ ] Reviews do app + - [ ] Elenco completo (List) + - [ ] Galeria de imagens (LazyVGrid) + - [ ] Vídeos (WebView ou Safari) + - [ ] Filmes relacionados (ScrollView) + - [ ] Onde assistir (provedores com logos) + +- [ ] **Seção de Coleção** + - [ ] Se pertence a coleção, exibir outros filmes + - [ ] ScrollView horizontal + +### 7.2 SeriesDetailView + +- [ ] Todos os itens de MovieDetailView + +- [ ] **Lista de Temporadas** + + - [ ] List ou LazyVStack + - [ ] SeasonCard com número de episódios + - [ ] ProgressView do assistidos + - [ ] NavigationLink para SeasonDetailView + +- [ ] **Progresso Geral** + - [ ] ProgressView customizada + - [ ] Texto "X de Y episódios" + +### 7.3 SeasonDetailView + +- [ ] Header com informações da temporada +- [ ] Lista de episódios (List) +- [ ] EpisodeRow com: + - [ ] Thumbnail do episódio + - [ ] Número e título + - [ ] Duração + - [ ] Checkbox de assistido +- [ ] Botão "Marcar todos como assistidos" +- [ ] Picker de navegação entre temporadas + +### 7.4 EpisodeDetailView + +- [ ] Banner do episódio +- [ ] Informações (número, título, duração) +- [ ] Sinopse +- [ ] Elenco convidado +- [ ] Toggle de marcar como assistido +- [ ] Seção de review (opcional) +- [ ] Botões de navegação (anterior/próximo) + +### 7.5 PersonDetailView (Ator/Diretor) + +- [ ] Header com foto +- [ ] Nome +- [ ] Biografia (Text expandível) +- [ ] Data e local de nascimento +- [ ] Idade calculada +- [ ] Seção de Filmografia: + - [ ] Segmented control (Filmes/Séries) + - [ ] LazyVStack de participações + - [ ] Ordenado por data + +### 7.6 ViewModels + +- [ ] **MovieDetailViewModel** +- [ ] **SeriesDetailViewModel** +- [ ] **SeasonDetailViewModel** +- [ ] **EpisodeDetailViewModel** +- [ ] **PersonDetailViewModel** + +--- + +## 8. Sistema de Reviews + +### 8.1 Componentes de Review + +- [ ] **ReviewRowView** + + - [ ] HStack com AsyncImage do avatar + - [ ] VStack com username (NavigationLink) + - [ ] RatingView (estrelas ou 0-10) + - [ ] Text da review (com spoiler blur) + - [ ] Data formatada (RelativeDateTimeFormatter) + - [ ] Badge "PRO" se aplicável + - [ ] HStack de ações: + - [ ] Botão de like (heart.fill animation) + - [ ] Contador de likes + - [ ] Botão de responder (bubble) + - [ ] Menu de ações (…) + +- [ ] **ReviewFormSheet** + + - [ ] Sheet presentation + - [ ] RatingPicker customizado (Slider ou Stepper) + - [ ] TextEditor para review + - [ ] Toggle "Contém spoilers" + - [ ] Botão "Publicar" com loading + - [ ] Validação de campos + +- [ ] **ReviewRepliesView** + - [ ] List de respostas + - [ ] ReplyRow similar ao ReviewRow + - [ ] TextField para nova resposta + - [ ] Like em respostas + +### 8.2 Listagem de Reviews + +- [ ] ReviewsListView genérico +- [ ] Filtros: + - [ ] Picker de idioma + - [ ] Picker de ordenação (data, likes) +- [ ] Pull to refresh +- [ ] Infinite scroll + +### 8.3 ViewModels + +- [ ] **ReviewsViewModel** +- [ ] **ReviewFormViewModel** + +--- + +## 9. Listas Personalizadas + +### 9.1 Views de Listas + +- [ ] **MyListsView** + + - [ ] LazyVGrid de ListCard + - [ ] Botão + (plus.circle.fill) para criar + - [ ] Pull to refresh + - [ ] Empty state customizado + +- [ ] **DiscoverListsView** + + - [ ] LazyVStack de listas públicas + - [ ] Toggle "Apenas com banner" + - [ ] Infinite scroll + +- [ ] **ListDetailView** + - [ ] Banner header (se existir) + - [ ] Título e descrição + - [ ] Creator com NavigationLink + - [ ] Contador de likes + botão + - [ ] ProgressView (assistidos/total) + - [ ] LazyVGrid de itens + - [ ] Modo edição: + - [ ] Drag & drop para reordenar + - [ ] Botão de remover item + - [ ] Botão + para adicionar item + +### 9.2 Formulário de Lista + +- [ ] **ListFormView** + - [ ] TextField para título + - [ ] TextEditor para descrição + - [ ] Picker de visibilidade (Pública, Rede, Privada) + - [ ] PhotosPicker para banner + - [ ] ImageCropper (opcional, via library) + - [ ] Botões "Cancelar" e "Salvar" + +### 9.3 Adicionar Item à Lista + +- [ ] **AddItemToListView** + - [ ] SearchBar + - [ ] Resultados de busca (filmes/séries) + - [ ] Checkboxes de listas + - [ ] Quick add via context menu na tela de detalhes + +### 9.4 ViewModels + +- [ ] **ListsViewModel** +- [ ] **ListDetailViewModel** +- [ ] **ListFormViewModel** + +--- + +## 10. Perfil do Usuário + +### 10.1 ProfileView + +- [ ] **Header** + + - [ ] Banner (KFImage ou cor sólida) + - [ ] Avatar (Circle overlay) + - [ ] Username + - [ ] Badge PRO (se aplicável) + - [ ] Biografia (Text) + - [ ] Botões: + - [ ] Seguir/Deixar de seguir (outros perfis) + - [ ] Editar (próprio perfil) + +- [ ] **Estatísticas Resumidas** + + - [ ] HStack com VStacks: + - [ ] Filmes assistidos + - [ ] Séries assistidas + - [ ] Seguidores (NavigationLink) + - [ ] Seguindo (NavigationLink) + +- [ ] **Links Sociais** + + - [ ] HStack de ícones clicáveis + - [ ] SF Symbols ou custom icons + - [ ] Abrir com `.openURL()` + +- [ ] **TabView de Conteúdo** + - [ ] Atividades + - [ ] Coleção + - [ ] Listas + - [ ] Reviews + - [ ] Estatísticas + +### 10.2 Edição de Perfil + +- [ ] **EditProfileView** + - [ ] PhotosPicker para avatar + - [ ] ImageCropper circular + - [ ] PhotosPicker para banner + - [ ] TextField para username (validação async) + - [ ] TextEditor para biografia + - [ ] TextFields para links sociais + - [ ] Botão "Salvar" com loading + +### 10.3 ViewModels + +- [ ] **ProfileViewModel** +- [ ] **EditProfileViewModel** + +--- + +## 11. Coleção do Usuário + +### 11.1 CollectionView + +- [ ] **Filtros** + + - [ ] Picker de Status (Todos, Watchlist, Watching, Watched, Dropped) + - [ ] Picker de Tipo (Filmes, Séries, Ambos) + - [ ] Filtro de rating (Slider) + - [ ] Picker de ordenação + - [ ] Toggle "Apenas sem review" + +- [ ] **Grid de Itens** + - [ ] LazyVGrid adaptativo + - [ ] CollectionItemCard: + - [ ] Poster + - [ ] Badge de status + - [ ] Rating (se existir) + - [ ] Context menu: + - [ ] Alterar status + - [ ] Remover + - [ ] Ver detalhes + - [ ] Infinite scroll + +### 11.2 ViewModels + +- [ ] **CollectionViewModel** + +--- + +## 12. Estatísticas + +### 12.1 StatsView + +- [ ] **Total de Horas** + + - [ ] Seção com ícone + - [ ] Cálculo de runtime total + - [ ] Formatação amigável (ex: "120h 30min") + +- [ ] **Contagem de Reviews** + + - [ ] Número total de reviews + +- [ ] **Séries Mais Assistidas** + + - [ ] Chart com BarMark (Apple Charts) + - [ ] Top 5 séries + - [ ] Ordenado por episódios + +- [ ] **Distribuição de Gêneros** + + - [ ] PieChart ou BarChart + - [ ] Cores distintas + - [ ] Legenda + +- [ ] **Atores Mais Vistos** + + - [ ] List ou LazyVStack + - [ ] Foto + nome + contagem + +- [ ] **Países de Produção** + + - [ ] Map (MapKit) com pins (opcional) + - [ ] Ou lista simples com bandeiras (emoji ou SF Symbols) + +- [ ] **Melhores Avaliações** + + - [ ] ScrollView horizontal de itens nota 10 + - [ ] Média geral do usuário + +- [ ] **Status das Mídias** + - [ ] PieChart + - [ ] Porcentagens + +### 12.2 ViewModels + +- [ ] **StatsViewModel** + - [ ] Cálculos complexos + - [ ] Cache de dados pesados + +--- + +## 13. Sistema Social + +### 13.1 Followers/Following + +- [ ] **FollowersListView** + + - [ ] List de UserRowView + - [ ] Botão "Seguir de volta" + - [ ] Pull to refresh + - [ ] Infinite scroll + +- [ ] **FollowingListView** + - [ ] List de UserRowView + - [ ] Botão "Deixar de seguir" + - [ ] Confirmação de unfollow + +### 13.2 Busca de Usuários + +- [ ] **UserSearchView** + - [ ] SearchBar com debounce + - [ ] Resultados em tempo real + - [ ] Indicador se já segue + - [ ] NavigationLink para perfil + +### 13.3 Likes + +- [ ] LikeButton com animação +- [ ] Heart animation (scaleEffect + spring) +- [ ] Haptic feedback +- [ ] Sheet de "Curtido por" (lista de usuários) + +### 13.4 ViewModels + +- [ ] **FollowersViewModel** +- [ ] **FollowingViewModel** +- [ ] **UserSearchViewModel** + +--- + +## 14. Busca + +### 14.1 SearchView + +- [ ] **SearchBar** + + - [ ] TextField com debounce (300ms) + - [ ] Botão de limpar (xmark.circle) + - [ ] SearchSuggestionsView com histórico + +- [ ] **Resultados Multi-tipo** + - [ ] List com Sections: + - [ ] Filmes + - [ ] Séries + - [ ] Pessoas + - [ ] NavigationLink "Ver todos" para cada seção + +### 14.2 Command Search (iOS Spotlight-like) + +- [ ] Implementar via `.searchable()` modifier +- [ ] Sugestões inline +- [ ] Navegação por teclado (iPad + teclado externo) + +### 14.3 ViewModels + +- [ ] **SearchViewModel** + - [ ] Combine para debounce + - [ ] Gerenciar múltiplas queries + +--- + +## 15. Configurações + +### 15.1 SettingsView + +- [ ] **Preferências de Streaming** + + - [ ] NavigationLink para StreamingProvidersView + - [ ] MultiSelector de provedores + - [ ] Picker de região + +- [ ] **Preferências de Exibição** + + - [ ] Picker de tema (Light, Dark, System) + - [ ] Picker de idioma do app + - [ ] Picker de idioma TMDB + +- [ ] **Conta** + - [ ] NavigationLink para ChangePasswordView + - [ ] NavigationLink para NotificationsSettingsView + - [ ] NavigationLink para PrivacySettingsView + - [ ] Botão "Excluir Conta" (destructive) + - [ ] Botão "Logout" + +### 15.2 ViewModels + +- [ ] **SettingsViewModel** + +--- + +## 16. Internacionalização + +### 16.1 Idiomas Suportados + +- [ ] Português (pt-BR) +- [ ] Inglês (en-US) +- [ ] Espanhol (es-ES) +- [ ] Francês (fr-FR) +- [ ] Alemão (de-DE) +- [ ] Italiano (it-IT) +- [ ] Japonês (ja-JP) + +### 16.2 Implementação + +- [ ] Criar `Localizable.strings` para cada idioma +- [ ] Converter JSON dos dicionários web para .strings +- [ ] Usar `NSLocalizedString()` ou String interpolation +- [ ] Criar enum `LocalizedStringKey` helper +- [ ] Persistir preferência no UserDefaults +- [ ] Criar `LanguageManager` para troca em runtime + +### 16.3 Formatação + +- [ ] `NumberFormatter` para moeda +- [ ] `DateFormatter` para datas +- [ ] `RelativeDateTimeFormatter` para datas relativas +- [ ] `MeasurementFormatter` para horas + +--- + +## 17. Funcionalidades Premium (PRO) + +### 17.1 Features PRO + +- [ ] Badge PRO no perfil +- [ ] Importação de dados externos +- [ ] [Outras features a definir] + +### 17.2 Integração com In-App Purchase + +- [ ] **Configurar no App Store Connect** + + - [ ] Criar produtos (assinatura mensal/anual) + - [ ] Configurar preços + +- [ ] **StoreKit 2** + + - [ ] Implementar `StoreKitManager` + - [ ] Exibir produtos disponíveis + - [ ] Processar compras + - [ ] Validar recibos + - [ ] Restaurar compras + +- [ ] **PricingView** + - [ ] Design atraente + - [ ] Comparação de planos + - [ ] Botões de compra + - [ ] Loading states + +### 17.3 ViewModels + +- [ ] **SubscriptionViewModel** + +--- + +## 18. Importação de Dados + +### 18.1 Provedores Suportados + +- [ ] **MyAnimeList** + + - [ ] UIDocumentPickerViewController para XML + - [ ] Parse XML com XMLParser + - [ ] Mapeamento para modelo interno + +- [ ] **Letterboxd** + - [ ] UIDocumentPickerViewController para CSV + - [ ] Parse CSV + - [ ] Mapeamento para modelo interno + +### 18.2 ImportView + +- [ ] Picker de provedor (Segmented Control) +- [ ] Botão "Selecionar Arquivo" +- [ ] ProgressView durante importação +- [ ] ResultsView com sucesso/falha por item +- [ ] List de itens importados/falhados + +### 18.3 ViewModels + +- [ ] **ImportViewModel** + - [ ] Processar arquivo em background + - [ ] Progress tracking + +--- + +## 🎨 Componentes UI Reutilizáveis (SwiftUI) + +### Componentes Base + +- [ ] **CustomButton** (variantes: primary, secondary, outline, destructive) +- [ ] **CustomTextField** +- [ ] **CustomSecureField** +- [ ] **CustomTextEditor** +- [ ] **CustomPicker** +- [ ] **CustomToggle** +- [ ] **CustomSlider** +- [ ] **AvatarView** (AsyncImage circular) +- [ ] **BadgeView** +- [ ] **CardView** (com sombra e corner radius) +- [ ] **SkeletonView** (shimmer effect) +- [ ] **ToastView** (overlay com animação) +- [ ] **LoadingView** (ProgressView customizado) +- [ ] **EmptyStateView** +- [ ] **ErrorView** + +### Componentes de Mídia + +- [ ] **PosterCard** +- [ ] **PosterGrid** (LazyVGrid wrapper) +- [ ] **BannerView** +- [ ] **PersonCard** +- [ ] **RatingView** (estrelas ou 0-10) +- [ ] **StatusBadge** +- [ ] **GenreChip** + +### Componentes de Interação + +- [ ] **LikeButton** (com animação de coração) +- [ ] **FollowButton** +- [ ] **StatusMenu** (Menu com opções) +- [ ] **AddToListButton** +- [ ] **ShareButton** (usar UIActivityViewController) + +### Layouts Customizados + +- [ ] **FlowLayout** (para chips de gêneros) +- [ ] **WaterfallLayout** (para grids irregulares) + +--- + +## 📱 Considerações iOS-Specific + +### UX Nativa + +- [ ] **Gestos Nativos** + + - [ ] Swipe back para navegação + - [ ] Pull to refresh em Lists + - [ ] Context menus (long press) + - [ ] Drag & drop para reordenar + +- [ ] **Haptic Feedback** + + - [ ] `UIImpactFeedbackGenerator` para ações + - [ ] `UINotificationFeedbackGenerator` para sucesso/erro + - [ ] `UISelectionFeedbackGenerator` para seleções + +- [ ] **Launch Screen** + + - [ ] Storyboard ou Asset + - [ ] Logo centralizado + +- [ ] **App Icon** + - [ ] Asset Catalog com todos os tamanhos + - [ ] Design consistente + +### Performance + +- [ ] **Lazy Loading** + + - [ ] LazyVStack/LazyHStack/LazyVGrid + - [ ] `.task()` modifier para carregar dados + +- [ ] **Image Caching** + + - [ ] Kingfisher com configurações otimizadas + - [ ] Downsampling automático + +- [ ] **List Optimization** + + - [ ] Identificadores estáveis (.id()) + - [ ] Evitar renders desnecessários + +- [ ] **Memory Management** + - [ ] Weak references em closures + - [ ] Deallocação adequada + +### Offline + +- [ ] **Cache Strategy** + + - [ ] URLCache configurado + - [ ] Core Data ou Realm para persistência offline + - [ ] Queue de ações offline para sincronizar + +- [ ] **Network Monitoring** + - [ ] NWPathMonitor (Network framework) + - [ ] Indicador de modo offline + - [ ] Retry automático quando conectar + +### Push Notifications (Futuro) + +- [ ] **APNs Setup** + + - [ ] Certificados no Apple Developer + - [ ] Backend: enviar device token + +- [ ] **Notificações** + + - [ ] Novo seguidor + - [ ] Like na review + - [ ] Resposta na review + - [ ] Lançamento de filme/série na watchlist + +- [ ] **Local Notifications** + - [ ] Lembrete de episódio novo + - [ ] Lembrete de filme estreando + +### Widgets (iOS 14+) + +- [ ] **WidgetKit** + - [ ] Widget de estatísticas + - [ ] Widget de próximos lançamentos + - [ ] Widget de últimas reviews + - [ ] Timelines para atualização + +### App Clips (Opcional) + +- [ ] App Clip para visualização rápida de filme/série +- [ ] QR Codes para compartilhamento + +### Siri Shortcuts (Opcional) + +- [ ] Adicionar à watchlist via Siri +- [ ] Marcar como assistido via Siri +- [ ] Buscar filme/série via Siri + +--- + +## 📊 Estimativa de Complexidade + +| Módulo | Complexidade | Prioridade | +| ------------------ | ------------ | ---------- | +| Setup Inicial | Baixa | Alta | +| Autenticação | Média | Alta | +| Navegação | Média | Alta | +| Catálogo de Filmes | Média | Alta | +| Catálogo de Séries | Média | Alta | +| Detalhes de Mídia | Alta | Alta | +| Sistema de Reviews | Alta | Alta | +| Listas | Alta | Média | +| Perfil | Média | Alta | +| Coleção | Média | Média | +| Estatísticas | Alta | Baixa | +| Sistema Social | Média | Média | +| Busca | Baixa | Alta | +| Configurações | Baixa | Baixa | +| i18n | Média | Média | +| Premium/IAP | Alta | Baixa | +| Importação | Alta | Baixa | + +--- + +## 🚀 Sugestão de Sprints + +### Sprint 1 - MVP Base (2-3 semanas) + +- Setup inicial do projeto Xcode +- Arquitetura base (MVVM + Network Layer) +- Autenticação (login/cadastro) +- Navegação básica (TabView + NavigationStack) +- Catálogo de filmes (popular, detalhes básicos) +- Busca simples + +### Sprint 2 - Core Features (2-3 semanas) + +- Catálogo de séries +- Sistema de status (watchlist, watched, etc) +- Perfil básico +- Coleção do usuário +- Deep linking + +### Sprint 3 - Social Features (2 semanas) + +- Sistema de reviews completo +- Likes com animações +- Follow/Unfollow +- Feed de atividades +- Review replies + +### Sprint 4 - Listas e Polish (2 semanas) + +- Listas personalizadas (criar, editar, adicionar itens) +- Detalhes de temporadas/episódios +- Internacionalização +- Performance optimization +- Dark mode polish + +### Sprint 5 - Extras (1-2 semanas) + +- Estatísticas com gráficos +- Configurações avançadas +- In-App Purchases (PRO) +- Importação de dados +- Widgets básicos + +### Sprint 6 - QA & Publicação (1 semana) + +- Testes em dispositivos reais +- Correção de bugs +- App Store assets (screenshots, descrição) +- Submissão para App Review + +--- + +## 📚 Referências + +### Backend + +- **API Backend**: `apps/api/` - Mesma API usada pelo web +- **Schemas Gerados**: `apps/web/src/api/endpoints.schemas.ts` (referência para modelos Codable) + +### Web (Referência UI/UX) + +- **Dicionários i18n**: `apps/web/public/dictionaries/` → converter para .strings +- **Componentes Web**: `apps/web/src/components/` (referência de design) +- **Serviços TMDB**: `apps/web/src/services/tmdb.ts` (referência de lógica) + +### iOS Resources + +- **Human Interface Guidelines**: https://developer.apple.com/design/human-interface-guidelines/ +- **Swift Style Guide**: https://google.github.io/swift/ +- **SwiftUI by Example**: https://www.hackingwithswift.com/quick-start/swiftui + +### Bibliotecas Recomendadas + +- **Alamofire**: https://github.com/Alamofire/Alamofire +- **Kingfisher**: https://github.com/onevcat/Kingfisher +- **KeychainAccess**: https://github.com/kishikawakatsumi/KeychainAccess +- **SwiftLint**: https://github.com/realm/SwiftLint + +--- + +## 🛠 Ferramentas de Desenvolvimento + +### Xcode Tools + +- [ ] Configurar Instruments para profiling +- [ ] Usar Memory Graph Debugger +- [ ] View Hierarchy Debugger para debug de UI + +### Testing + +- [ ] XCTest para testes unitários +- [ ] XCUITest para testes de UI +- [ ] Quick + Nimble (opcional) +- [ ] Code coverage mínima de 70% + +### CI/CD + +- [ ] Xcode Cloud ou Fastlane +- [ ] Automação de builds +- [ ] TestFlight para beta testing + +--- + +_Documento gerado em: Janeiro 2026_ +_Versão do projeto web: 0.1.0_ +_Plataforma: iOS 16.0+_ +_Linguagem: Swift 5.9+_ +_Framework: SwiftUI_ diff --git a/REACT_NATIVE_TASKS.md b/REACT_NATIVE_TASKS.md new file mode 100644 index 00000000..9631352e --- /dev/null +++ b/REACT_NATIVE_TASKS.md @@ -0,0 +1,743 @@ +# 📱 Plotwist - Tarefas para App React Native + +Este documento contém o mapeamento completo das funcionalidades do site web e as tarefas necessárias para criar um aplicativo React Native equivalente. + +--- + +## 📋 Índice + +1. [Setup Inicial](#1-setup-inicial) +2. [Autenticação](#2-autenticação) +3. [Navegação](#3-navegação) +4. [Home/Dashboard](#4-homedashboard) +5. [Catálogo de Filmes](#5-catálogo-de-filmes) +6. [Catálogo de Séries](#6-catálogo-de-séries) +7. [Detalhes de Mídia](#7-detalhes-de-mídia) +8. [Sistema de Reviews](#8-sistema-de-reviews) +9. [Listas Personalizadas](#9-listas-personalizadas) +10. [Perfil do Usuário](#10-perfil-do-usuário) +11. [Coleção do Usuário](#11-coleção-do-usuário) +12. [Estatísticas](#12-estatísticas) +13. [Sistema Social](#13-sistema-social) +14. [Busca](#14-busca) +15. [Configurações](#15-configurações) +16. [Internacionalização](#16-internacionalização) +17. [Funcionalidades Premium](#17-funcionalidades-premium) +18. [Importação de Dados](#18-importação-de-dados) + +--- + +## 1. Setup Inicial + +### 1.1 Configuração do Projeto +- [ ] Criar projeto com Expo ou React Native CLI +- [ ] Configurar TypeScript +- [ ] Configurar ESLint e Prettier (usar biome como no web) +- [ ] Configurar path aliases (@/ para src/) + +### 1.2 Dependências Principais +- [ ] Instalar React Navigation (navegação) +- [ ] Instalar React Query / TanStack Query (gerenciamento de estado servidor) +- [ ] Instalar Axios (requisições HTTP) +- [ ] Instalar React Hook Form + Zod (formulários e validação) +- [ ] Instalar AsyncStorage (persistência local) +- [ ] Instalar react-native-fast-image (imagens otimizadas) +- [ ] Instalar react-native-reanimated (animações) +- [ ] Instalar Nativewind ou Tamagui (estilização) + +### 1.3 Configuração de Ambiente +- [ ] Criar arquivo de configuração de ambiente (.env) +- [ ] Configurar variáveis: `API_URL`, `TMDB_API_KEY` +- [ ] Criar serviço de API base (axios instance) + +### 1.4 Estrutura de Pastas +``` +src/ +├── api/ # Serviços de API (mesmo padrão do web) +├── components/ # Componentes reutilizáveis +├── screens/ # Telas do app +├── navigation/ # Configuração de navegação +├── context/ # Contextos React +├── hooks/ # Hooks customizados +├── types/ # Tipos TypeScript +├── utils/ # Utilitários +└── i18n/ # Internacionalização +``` + +--- + +## 2. Autenticação + +### 2.1 Telas de Auth +- [ ] **Tela de Login** + - [ ] Campo de login (email ou username) + - [ ] Campo de senha com toggle de visibilidade + - [ ] Botão de login + - [ ] Link para "Esqueci a senha" + - [ ] Link para cadastro + - [ ] Validação com Zod + +- [ ] **Tela de Cadastro** + - [ ] Campo de username (validação de disponibilidade) + - [ ] Campo de email (validação de disponibilidade) + - [ ] Campo de senha (mínimo 8 caracteres) + - [ ] Validação em tempo real + - [ ] Termos de uso + +- [ ] **Tela de Esqueci a Senha** + - [ ] Campo de email + - [ ] Envio de email de recuperação + +- [ ] **Tela de Reset de Senha** + - [ ] Campo de nova senha + - [ ] Confirmação de senha + - [ ] Validação de token + +### 2.2 Gerenciamento de Sessão +- [ ] Armazenar token JWT no AsyncStorage/SecureStore +- [ ] Criar contexto de autenticação (SessionContext) +- [ ] Implementar refresh de sessão +- [ ] Implementar logout +- [ ] Proteção de rotas autenticadas + +--- + +## 3. Navegação + +### 3.1 Estrutura de Navegação +- [ ] **Tab Navigator Principal** + - [ ] Home + - [ ] Filmes + - [ ] Séries + - [ ] Busca + - [ ] Perfil + +- [ ] **Stack Navigators** + - [ ] Stack de Autenticação (Login, Cadastro, Reset) + - [ ] Stack de Filmes (Lista, Detalhes) + - [ ] Stack de Séries (Lista, Detalhes, Temporadas, Episódios) + - [ ] Stack de Listas (Minhas Listas, Detalhes da Lista) + - [ ] Stack de Perfil (Perfil, Editar, Configurações) + +### 3.2 Deep Linking +- [ ] Configurar deep links para filmes +- [ ] Configurar deep links para séries +- [ ] Configurar deep links para listas +- [ ] Configurar deep links para perfis + +--- + +## 4. Home/Dashboard + +### 4.1 Componentes da Home +- [ ] **Header** + - [ ] Logo + - [ ] Botão de busca + - [ ] Avatar do usuário (se logado) + +- [ ] **Seção: Última Review do Usuário** + - [ ] Card com a última review feita + - [ ] Link para o item avaliado + +- [ ] **Seção: Reviews Populares** + - [ ] Lista horizontal de reviews em destaque + - [ ] Filtros por período (hoje, semana, mês, todos) + - [ ] Paginação infinita + +- [ ] **Seção: Atividades da Rede** + - [ ] Feed de atividades de usuários seguidos + - [ ] Tipos de atividade: + - [ ] Mudança de status (watched, watching, etc) + - [ ] Nova review criada + - [ ] Nova lista criada + - [ ] Follow/Unfollow + - [ ] Episódios assistidos + - [ ] Likes em reviews/listas + +- [ ] **Sidebar: Filmes Populares** + - [ ] Grid 3x1 de posters + - [ ] Link para ver mais + +- [ ] **Sidebar: Séries Populares** + - [ ] Grid 3x1 de posters + - [ ] Link para ver mais + +--- + +## 5. Catálogo de Filmes + +### 5.1 Telas de Listagem +- [ ] **Filmes Populares** + - [ ] Lista em grid com posters + - [ ] Paginação infinita + - [ ] Pull to refresh + +- [ ] **Em Cartaz (Now Playing)** + - [ ] Lista de filmes em exibição + +- [ ] **Em Breve (Upcoming)** + - [ ] Lista de lançamentos futuros + +- [ ] **Mais Bem Avaliados (Top Rated)** + - [ ] Lista ordenada por rating + +- [ ] **Descobrir Filmes** + - [ ] Filtros avançados: + - [ ] Gêneros (múltipla seleção) + - [ ] Ano de lançamento + - [ ] Nota mínima + - [ ] Ordenação (popularidade, data, nota) + - [ ] Provedores de streaming + - [ ] Região + +### 5.2 Componentes de Filme +- [ ] **PosterCard** + - [ ] Imagem do poster + - [ ] Título + - [ ] Ano + - [ ] Rating + +- [ ] **MovieListFilters** + - [ ] Bottom sheet com filtros + - [ ] Chips de gêneros + - [ ] Slider de rating + - [ ] Date picker para ano + +--- + +## 6. Catálogo de Séries + +### 6.1 Telas de Listagem +- [ ] **Séries Populares** + - [ ] Lista em grid com posters + - [ ] Paginação infinita + +- [ ] **No Ar Hoje (Airing Today)** + - [ ] Séries com episódios hoje + +- [ ] **Em Exibição (On The Air)** + - [ ] Séries em exibição atual + +- [ ] **Mais Bem Avaliadas (Top Rated)** + - [ ] Lista ordenada por rating + +- [ ] **Descobrir Séries** + - [ ] Mesmos filtros dos filmes + - [ ] Filtro adicional: status (em andamento, finalizada) + +### 6.2 Categorias Especiais +- [ ] **Animes** + - [ ] Filtro pré-aplicado para animação japonesa + +- [ ] **Doramas** + - [ ] Filtro pré-aplicado para séries coreanas + +--- + +## 7. Detalhes de Mídia + +### 7.1 Tela de Detalhes de Filme +- [ ] **Banner/Backdrop** + - [ ] Imagem de fundo com gradiente + - [ ] Botão de voltar + +- [ ] **Informações Principais** + - [ ] Poster + - [ ] Título (original e traduzido) + - [ ] Ano de lançamento + - [ ] Duração + - [ ] Gêneros (chips clicáveis) + - [ ] Sinopse (expandível) + - [ ] Rating TMDB + +- [ ] **Ações do Usuário** + - [ ] Botão de Status (Watchlist, Watching, Watched, Dropped) + - [ ] Botão de Adicionar à Lista + - [ ] Botão de Escrever Review + +- [ ] **Informações Adicionais** + - [ ] Diretor + - [ ] Elenco principal (lista horizontal) + - [ ] Orçamento e Receita + - [ ] Idioma original + - [ ] País de produção + +- [ ] **Seções em Abas** + - [ ] Reviews (do app) + - [ ] Elenco e Equipe completos + - [ ] Imagens (posters, backdrops) + - [ ] Vídeos (trailers, teasers) + - [ ] Filmes Relacionados + - [ ] Onde Assistir (streaming) + +- [ ] **Coleção** + - [ ] Se pertence a uma coleção, mostrar outros filmes + +### 7.2 Tela de Detalhes de Série +- [ ] Todos os itens de filme + +- [ ] **Lista de Temporadas** + - [ ] Cards de cada temporada + - [ ] Número de episódios + - [ ] Data de exibição + - [ ] Progresso de assistidos + +- [ ] **Progresso da Série** + - [ ] Barra de progresso geral + - [ ] Episódios assistidos / Total + +### 7.3 Tela de Temporada +- [ ] Informações da temporada +- [ ] Lista de episódios +- [ ] Botão "Marcar todos como assistidos" +- [ ] Navegação entre temporadas + +### 7.4 Tela de Episódio +- [ ] Informações do episódio +- [ ] Sinopse +- [ ] Elenco convidado +- [ ] Botão de marcar como assistido +- [ ] Seção de review do episódio +- [ ] Navegação entre episódios + +### 7.5 Tela de Pessoa (Ator/Diretor) +- [ ] Foto +- [ ] Nome +- [ ] Biografia +- [ ] Data de nascimento +- [ ] Local de nascimento +- [ ] Filmografia (filmes e séries) + +--- + +## 8. Sistema de Reviews + +### 8.1 Componentes de Review +- [ ] **ReviewItem** + - [ ] Avatar do usuário + - [ ] Username (link para perfil) + - [ ] Rating (estrelas) + - [ ] Texto da review + - [ ] Data + - [ ] Badge de PRO + - [ ] Indicador de spoiler + - [ ] Contador de likes + - [ ] Botão de like + - [ ] Botão de responder + - [ ] Menu de ações (editar, excluir) + +- [ ] **ReviewFormDialog** + - [ ] Modal/Bottom sheet + - [ ] Seletor de rating (0-10 ou estrelas) + - [ ] Campo de texto da review + - [ ] Toggle de spoiler + - [ ] Botão de publicar + +- [ ] **ReviewReply** + - [ ] Lista de respostas + - [ ] Formulário de resposta + - [ ] Like em respostas + +### 8.2 Listagem de Reviews +- [ ] Reviews do item (filme/série/episódio) +- [ ] Reviews do usuário (no perfil) +- [ ] Reviews populares (na home) +- [ ] Filtros por idioma +- [ ] Ordenação (data, likes) + +--- + +## 9. Listas Personalizadas + +### 9.1 Telas de Listas +- [ ] **Minhas Listas** + - [ ] Grid de listas do usuário + - [ ] Botão de criar nova lista + - [ ] Pull to refresh + +- [ ] **Descobrir Listas** + - [ ] Listas públicas populares + - [ ] Filtro por listas com banner + +- [ ] **Detalhes da Lista** + - [ ] Banner customizável + - [ ] Título e descrição + - [ ] Criador (link para perfil) + - [ ] Contador de likes + - [ ] Botão de like + - [ ] Progresso (itens assistidos) + - [ ] Grid de itens da lista + - [ ] Reordenação por drag and drop (modo edição) + - [ ] Botão de adicionar item + +### 9.2 Formulário de Lista +- [ ] Campo de título +- [ ] Campo de descrição +- [ ] Seletor de visibilidade (Pública, Rede, Privada) +- [ ] Upload de banner (image picker) + +### 9.3 Adicionar Item à Lista +- [ ] Busca de filme/série +- [ ] Quick add (a partir da tela de detalhes) +- [ ] Sugestões baseadas em outras listas + +--- + +## 10. Perfil do Usuário + +### 10.1 Tela de Perfil +- [ ] **Header do Perfil** + - [ ] Banner customizável + - [ ] Avatar customizável + - [ ] Username + - [ ] Badge PRO (se aplicável) + - [ ] Biografia + - [ ] Botão de seguir/deixar de seguir + - [ ] Botão de editar (próprio perfil) + +- [ ] **Estatísticas Resumidas** + - [ ] Filmes assistidos + - [ ] Séries assistidas + - [ ] Seguidores + - [ ] Seguindo + +- [ ] **Links Sociais** + - [ ] Instagram + - [ ] TikTok + - [ ] YouTube + - [ ] X (Twitter) + +- [ ] **Navegação em Abas** + - [ ] Atividades + - [ ] Coleção + - [ ] Listas + - [ ] Reviews + - [ ] Estatísticas + +### 10.2 Edição de Perfil +- [ ] Upload de avatar (image picker + crop) +- [ ] Upload de banner (image picker + crop) +- [ ] Edição de username +- [ ] Edição de biografia +- [ ] Edição de links sociais + +--- + +## 11. Coleção do Usuário + +### 11.1 Tela de Coleção +- [ ] **Filtros** + - [ ] Status (Todos, Watchlist, Watching, Watched, Dropped) + - [ ] Tipo de mídia (Filmes, Séries, Ambos) + - [ ] Rating dado + - [ ] Ordenação (data adição, data atualização, rating) + - [ ] Apenas sem review + +- [ ] **Lista de Itens** + - [ ] Grid de posters + - [ ] Indicador de status + - [ ] Rating dado (se existir) + - [ ] Paginação infinita + +### 11.2 Gestão de Itens +- [ ] Alterar status rapidamente +- [ ] Remover da coleção +- [ ] Ver detalhes + +--- + +## 12. Estatísticas + +### 12.1 Tela de Estatísticas do Usuário +- [ ] **Total de Horas** + - [ ] Cálculo baseado em runtime dos filmes + - [ ] Cálculo baseado em episódios assistidos + +- [ ] **Contagem de Reviews** + - [ ] Total de reviews feitas + +- [ ] **Séries Mais Assistidas** + - [ ] Top 5 séries por episódios + - [ ] Gráfico de barras + +- [ ] **Gêneros Assistidos** + - [ ] Distribuição por gênero + - [ ] Gráfico de pizza/barras + +- [ ] **Atores Mais Vistos** + - [ ] Top atores nas mídias assistidas + +- [ ] **Países de Produção** + - [ ] Mapa ou lista de países + - [ ] Distribuição por país + +- [ ] **Melhores Avaliações** + - [ ] Itens com nota 10 + - [ ] Média geral do usuário + +- [ ] **Status das Mídias** + - [ ] Distribuição por status + - [ ] Gráfico de pizza + +--- + +## 13. Sistema Social + +### 13.1 Followers/Following +- [ ] **Lista de Seguidores** + - [ ] Avatar + - [ ] Username + - [ ] Botão de seguir de volta + - [ ] Paginação infinita + +- [ ] **Lista de Seguindo** + - [ ] Avatar + - [ ] Username + - [ ] Botão de deixar de seguir + - [ ] Paginação infinita + +### 13.2 Busca de Usuários +- [ ] Busca por username +- [ ] Resultados em tempo real +- [ ] Indicador se já segue + +### 13.3 Likes +- [ ] Like em reviews +- [ ] Like em respostas +- [ ] Like em listas +- [ ] Animação de like +- [ ] Lista de quem curtiu + +--- + +## 14. Busca + +### 14.1 Tela de Busca +- [ ] **Campo de Busca** + - [ ] Debounce de 300ms + - [ ] Limpeza rápida + - [ ] Histórico de buscas recentes + +- [ ] **Resultados Multi-tipo** + - [ ] Seção de Filmes + - [ ] Seção de Séries + - [ ] Seção de Pessoas + - [ ] Ver todos de cada tipo + +### 14.2 Command Search (Quick Search) +- [ ] Atalho para busca rápida (pull down?) +- [ ] Resultados inline +- [ ] Navegação por teclado (se tablet) + +--- + +## 15. Configurações + +### 15.1 Preferências do Usuário +- [ ] **Preferências de Streaming** + - [ ] Seleção de provedores favoritos + - [ ] Região de streaming + +- [ ] **Preferências de Exibição** + - [ ] Tema (claro, escuro, sistema) + - [ ] Idioma do app + - [ ] Idioma preferido para dados do TMDB + +### 15.2 Conta +- [ ] Alterar senha +- [ ] Notificações +- [ ] Privacidade +- [ ] Excluir conta +- [ ] Logout + +--- + +## 16. Internacionalização + +### 16.1 Idiomas Suportados +- [ ] Português (pt-BR) +- [ ] Inglês (en-US) +- [ ] Espanhol (es-ES) +- [ ] Francês (fr-FR) +- [ ] Alemão (de-DE) +- [ ] Italiano (it-IT) +- [ ] Japonês (ja-JP) + +### 16.2 Implementação +- [ ] Instalar i18next + react-i18next +- [ ] Copiar dicionários do web (public/dictionaries/) +- [ ] Criar contexto de idioma +- [ ] Persistir preferência de idioma + +--- + +## 17. Funcionalidades Premium (PRO) + +### 17.1 Features PRO +- [ ] Badge PRO no perfil +- [ ] Importação de dados externos +- [ ] [Outras features PRO a definir] + +### 17.2 Integração com Pagamento +- [ ] Tela de Pricing +- [ ] Integração com Stripe (via WebView ou deeplinking) +- [ ] Ou: usar In-App Purchases nativas + +--- + +## 18. Importação de Dados + +### 18.1 Provedores Suportados +- [ ] **MyAnimeList** + - [ ] Upload de arquivo XML + - [ ] Processamento e mapeamento + +- [ ] **Letterboxd** + - [ ] Upload de arquivo CSV + - [ ] Processamento e mapeamento + +### 18.2 Tela de Importação +- [ ] Seleção de provedor +- [ ] Upload de arquivo (document picker) +- [ ] Progresso de importação +- [ ] Resultados (sucesso/falha por item) + +--- + +## 🎨 Componentes UI Reutilizáveis + +### Componentes Base +- [ ] Button (variantes: default, outline, ghost, destructive) +- [ ] Input +- [ ] Textarea +- [ ] Select / Picker +- [ ] Checkbox +- [ ] Switch +- [ ] Slider +- [ ] Avatar +- [ ] Badge +- [ ] Card +- [ ] Skeleton +- [ ] Toast / Snackbar +- [ ] Dialog / Modal +- [ ] Bottom Sheet +- [ ] Tabs +- [ ] Accordion +- [ ] Separator + +### Componentes de Mídia +- [ ] PosterCard +- [ ] PosterGrid +- [ ] Banner +- [ ] PersonCard +- [ ] RatingStars +- [ ] StatusBadge +- [ ] GenreChip + +### Componentes de Interação +- [ ] LikeButton (com animação) +- [ ] FollowButton +- [ ] StatusDropdown +- [ ] AddToListButton +- [ ] ShareButton + +--- + +## 📱 Considerações Mobile-Specific + +### UX Nativa +- [ ] Gestos de swipe para navegação +- [ ] Pull to refresh em todas as listas +- [ ] Haptic feedback em ações importantes +- [ ] Splash screen +- [ ] App icon + +### Performance +- [ ] Lazy loading de imagens +- [ ] Cache de imagens com react-native-fast-image +- [ ] Virtualização de listas longas (FlashList) +- [ ] Skeleton loading em todas as telas + +### Offline +- [ ] Cache de dados visualizados recentemente +- [ ] Indicador de modo offline +- [ ] Retry automático quando online + +### Push Notifications (Futuro) +- [ ] Novo seguidor +- [ ] Like na review +- [ ] Resposta na review +- [ ] Lançamento de filme/série na watchlist + +--- + +## 📊 Estimativa de Complexidade + +| Módulo | Complexidade | Prioridade | +|--------|--------------|------------| +| Setup Inicial | Baixa | Alta | +| Autenticação | Média | Alta | +| Navegação | Média | Alta | +| Catálogo de Filmes | Média | Alta | +| Catálogo de Séries | Média | Alta | +| Detalhes de Mídia | Alta | Alta | +| Sistema de Reviews | Alta | Alta | +| Listas | Alta | Média | +| Perfil | Média | Alta | +| Coleção | Média | Média | +| Estatísticas | Alta | Baixa | +| Sistema Social | Média | Média | +| Busca | Baixa | Alta | +| Configurações | Baixa | Baixa | +| i18n | Média | Média | +| Premium | Média | Baixa | +| Importação | Alta | Baixa | + +--- + +## 🚀 Sugestão de Sprints + +### Sprint 1 - MVP Base (2-3 semanas) +- Setup inicial +- Autenticação (login/cadastro) +- Navegação básica +- Catálogo de filmes (popular, detalhes) +- Busca simples + +### Sprint 2 - Core Features (2-3 semanas) +- Catálogo de séries +- Sistema de status (watchlist, watched, etc) +- Perfil básico +- Coleção do usuário + +### Sprint 3 - Social Features (2 semanas) +- Sistema de reviews completo +- Likes +- Follow/Unfollow +- Feed de atividades + +### Sprint 4 - Listas e Polish (2 semanas) +- Listas personalizadas +- Detalhes de temporadas/episódios +- Internacionalização +- Performance e polish + +### Sprint 5 - Extras (1-2 semanas) +- Estatísticas +- Configurações avançadas +- Funcionalidades PRO +- Importação de dados + +--- + +## 📚 Referências + +- **API Backend**: `apps/api/` - Mesma API usada pelo web +- **Schemas Gerados**: `apps/web/src/api/endpoints.schemas.ts` +- **Dicionários i18n**: `apps/web/public/dictionaries/` +- **Componentes Web**: `apps/web/src/components/` (referência de UI) +- **Serviços TMDB**: `apps/web/src/services/tmdb.ts` + +--- + +*Documento gerado em: Janeiro 2026* +*Versão do projeto web: 0.1.0* diff --git a/apps/backend/src/db/migrations/20260117160000_create_user_watch_entries_table.sql b/apps/backend/src/db/migrations/20260117160000_create_user_watch_entries_table.sql new file mode 100644 index 00000000..85d57f83 --- /dev/null +++ b/apps/backend/src/db/migrations/20260117160000_create_user_watch_entries_table.sql @@ -0,0 +1,10 @@ +-- Create user_watch_entries table for tracking rewatch history +CREATE TABLE IF NOT EXISTS "user_watch_entries" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "user_item_id" uuid NOT NULL REFERENCES "user_items"("id") ON DELETE CASCADE, + "watched_at" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); + +-- Create index for faster lookups by user_item_id +CREATE INDEX IF NOT EXISTS "user_watch_entries_user_item_idx" ON "user_watch_entries" ("user_item_id"); diff --git a/apps/backend/src/db/migrations/20260117211057_tearful_vertigo.sql b/apps/backend/src/db/migrations/20260117211057_tearful_vertigo.sql new file mode 100644 index 00000000..2a73ac2d --- /dev/null +++ b/apps/backend/src/db/migrations/20260117211057_tearful_vertigo.sql @@ -0,0 +1,9 @@ +CREATE TABLE "user_watch_entries" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_item_id" uuid NOT NULL, + "watched_at" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "user_watch_entries" ADD CONSTRAINT "user_watch_entries_user_item_id_user_items_id_fk" FOREIGN KEY ("user_item_id") REFERENCES "public"."user_items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "user_watch_entries_user_item_idx" ON "user_watch_entries" USING btree ("user_item_id"); \ No newline at end of file diff --git a/apps/backend/src/db/migrations/meta/20260117211057_snapshot.json b/apps/backend/src/db/migrations/meta/20260117211057_snapshot.json new file mode 100644 index 00000000..6123a85a --- /dev/null +++ b/apps/backend/src/db/migrations/meta/20260117211057_snapshot.json @@ -0,0 +1,1644 @@ +{ + "id": "cae7945f-8a8c-44db-a3fe-3309df133a97", + "prevId": "d8206ef4-3adb-4b26-8e63-dfc5c96b5555", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.followers": { + "name": "followers", + "schema": "", + "columns": { + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "followed_id": { + "name": "followed_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "followers_follower_id_users_id_fk": { + "name": "followers_follower_id_users_id_fk", + "tableFrom": "followers", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "followers_followed_id_users_id_fk": { + "name": "followers_followed_id_users_id_fk", + "tableFrom": "followers", + "tableTo": "users", + "columnsFrom": [ + "followed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "followers_followed_id_follower_id_pk": { + "name": "followers_followed_id_follower_id_pk", + "columns": [ + "followed_id", + "follower_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.import_movies": { + "name": "import_movies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "import_id": { + "name": "import_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "item_status": { + "name": "item_status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "import_status": { + "name": "import_status", + "type": "import_item_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "TMDB_ID": { + "name": "TMDB_ID", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "__metadata": { + "name": "__metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "import_movies_import_id_user_imports_id_fk": { + "name": "import_movies_import_id_user_imports_id_fk", + "tableFrom": "import_movies", + "tableTo": "user_imports", + "columnsFrom": [ + "import_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.import_series": { + "name": "import_series", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "import_id": { + "name": "import_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "item_status": { + "name": "item_status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "import_status": { + "name": "import_status", + "type": "import_item_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "TMDB_ID": { + "name": "TMDB_ID", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "watched_episodes": { + "name": "watched_episodes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "series_episodes": { + "name": "series_episodes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "__metadata": { + "name": "__metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "import_series_import_id_user_imports_id_fk": { + "name": "import_series_import_id_user_imports_id_fk", + "tableFrom": "import_series", + "tableTo": "user_imports", + "columnsFrom": [ + "import_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "like_entity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_entity_id": { + "name": "idx_entity_id", + "columns": [ + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.list_items": { + "name": "list_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "list_id": { + "name": "list_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tmdb_id": { + "name": "tmdb_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "media_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "list_items_list_id_lists_id_fk": { + "name": "list_items_list_id_lists_id_fk", + "tableFrom": "list_items", + "tableTo": "lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "list_items_id_list_id_pk": { + "name": "list_items_id_list_id_pk", + "columns": [ + "id", + "list_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lists": { + "name": "lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "list_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lists_user_id_users_id_fk": { + "name": "lists_user_id_users_id_fk", + "tableFrom": "lists", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_tokens": { + "name": "magic_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "used": { + "name": "used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "token_user_id_idx": { + "name": "token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "token_idx": { + "name": "token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "magic_tokens_user_id_users_id_fk": { + "name": "magic_tokens_user_id_users_id_fk", + "tableFrom": "magic_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_replies": { + "name": "review_replies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reply": { + "name": "reply", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "review_replies_user_id_users_id_fk": { + "name": "review_replies_user_id_users_id_fk", + "tableFrom": "review_replies", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_replies_review_id_reviews_id_fk": { + "name": "review_replies_review_id_reviews_id_fk", + "tableFrom": "review_replies", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reviews": { + "name": "reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tmdb_id": { + "name": "tmdb_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "media_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "review": { + "name": "review", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "rating": { + "name": "rating", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "has_spoilers": { + "name": "has_spoilers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "language": { + "name": "language", + "type": "languages", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "season_number": { + "name": "season_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "episode_number": { + "name": "episode_number", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "reviews_user_id_users_id_fk": { + "name": "reviews_user_id_users_id_fk", + "tableFrom": "reviews", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.social_links": { + "name": "social_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "social_platforms", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "social_links_user_id_users_id_fk": { + "name": "social_links_user_id_users_id_fk", + "tableFrom": "social_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_platform_unique": { + "name": "user_platform_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "platform" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "subscription_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "active_subscription_idx": { + "name": "active_subscription_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"subscriptions\".\"status\" = $1", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscriptions_user_id_users_id_fk": { + "name": "subscriptions_user_id_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_activities": { + "name": "user_activities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "activity_type": { + "name": "activity_type", + "type": "activity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "like_entity", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_activity_idx": { + "name": "user_activity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_activities_user_id_users_id_fk": { + "name": "user_activities_user_id_users_id_fk", + "tableFrom": "user_activities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_episodes": { + "name": "user_episodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tmdb_id": { + "name": "tmdb_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "season_number": { + "name": "season_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "episode_number": { + "name": "episode_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "watched_at": { + "name": "watched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "runtime": { + "name": "runtime", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "user_episodes_user_id_users_id_fk": { + "name": "user_episodes_user_id_users_id_fk", + "tableFrom": "user_episodes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_episode_unique": { + "name": "user_episode_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "tmdb_id", + "season_number", + "episode_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_imports": { + "name": "user_imports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "items_count": { + "name": "items_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "import_status": { + "name": "import_status", + "type": "import_status_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "providers_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_imports_user_id_users_id_fk": { + "name": "user_imports_user_id_users_id_fk", + "tableFrom": "user_imports", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_items": { + "name": "user_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tmdb_id": { + "name": "tmdb_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "media_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_items_user_id_users_id_fk": { + "name": "user_items_user_id_users_id_fk", + "tableFrom": "user_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_items_userid_tmdbid_media_type_unique": { + "name": "user_items_userid_tmdbid_media_type_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "tmdb_id", + "media_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_watch_entries": { + "name": "user_watch_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_item_id": { + "name": "user_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watched_at": { + "name": "watched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_watch_entries_user_item_idx": { + "name": "user_watch_entries_user_item_idx", + "columns": [ + { + "expression": "user_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_watch_entries_user_item_id_user_items_id_fk": { + "name": "user_watch_entries_user_item_id_user_items_id_fk", + "tableFrom": "user_watch_entries", + "tableTo": "user_items", + "columnsFrom": [ + "user_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "banner_url": { + "name": "banner_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "is_legacy": { + "name": "is_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "biography": { + "name": "biography", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "username_lower_idx": { + "name": "username_lower_idx", + "columns": [ + { + "expression": "LOWER(\"username\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_lower_idx": { + "name": "email_lower_idx", + "columns": [ + { + "expression": "LOWER(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watch_providers_ids": { + "name": "watch_providers_ids", + "type": "integer[]", + "primaryKey": false, + "notNull": false + }, + "watch_region": { + "name": "watch_region", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_unique": { + "name": "user_preferences_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.activity_type": { + "name": "activity_type", + "schema": "public", + "values": [ + "CREATE_LIST", + "ADD_ITEM", + "DELETE_ITEM", + "LIKE_REVIEW", + "LIKE_REPLY", + "LIKE_LIST", + "CREATE_REVIEW", + "CREATE_REPLY", + "FOLLOW_USER", + "WATCH_EPISODE", + "CHANGE_STATUS", + "CREATE_ACCOUNT" + ] + }, + "public.import_item_status": { + "name": "import_item_status", + "schema": "public", + "values": [ + "COMPLETED", + "FAILED", + "NOT_STARTED" + ] + }, + "public.import_status_enum": { + "name": "import_status_enum", + "schema": "public", + "values": [ + "PARTIAL", + "COMPLETED", + "FAILED", + "NOT_STARTED" + ] + }, + "public.languages": { + "name": "languages", + "schema": "public", + "values": [ + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "de-DE", + "pt-BR", + "ja-JP" + ] + }, + "public.like_entity": { + "name": "like_entity", + "schema": "public", + "values": [ + "REVIEW", + "REPLY", + "LIST" + ] + }, + "public.list_visibility": { + "name": "list_visibility", + "schema": "public", + "values": [ + "PUBLIC", + "NETWORK", + "PRIVATE" + ] + }, + "public.media_type": { + "name": "media_type", + "schema": "public", + "values": [ + "TV_SHOW", + "MOVIE" + ] + }, + "public.providers_enum": { + "name": "providers_enum", + "schema": "public", + "values": [ + "MY_ANIME_LIST", + "LETTERBOXD" + ] + }, + "public.social_platforms": { + "name": "social_platforms", + "schema": "public", + "values": [ + "INSTAGRAM", + "TIKTOK", + "YOUTUBE", + "X" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "WATCHLIST", + "WATCHED", + "WATCHING", + "DROPPED" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "ACTIVE", + "CANCELED", + "EXPIRED", + "PENDING_CANCELLATION" + ] + }, + "public.subscription_type": { + "name": "subscription_type", + "schema": "public", + "values": [ + "MEMBER", + "PRO" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/backend/src/db/migrations/meta/_journal.json b/apps/backend/src/db/migrations/meta/_journal.json index 8692f823..eef9c295 100644 --- a/apps/backend/src/db/migrations/meta/_journal.json +++ b/apps/backend/src/db/migrations/meta/_journal.json @@ -379,6 +379,13 @@ "when": 1743464364062, "tag": "20250331233924_alter_subscription_table", "breakpoints": true + }, + { + "idx": 54, + "version": "7", + "when": 1768684257805, + "tag": "20260117211057_tearful_vertigo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/backend/src/db/repositories/user-item-repository.ts b/apps/backend/src/db/repositories/user-item-repository.ts index 2c6b6524..59f7ba3e 100644 --- a/apps/backend/src/db/repositories/user-item-repository.ts +++ b/apps/backend/src/db/repositories/user-item-repository.ts @@ -181,6 +181,17 @@ export async function selectAllUserItems(userId: string) { .where(eq(schema.userItems.userId, userId)) } +export async function selectUserItemsCount(userId: string) { + const result = await db + .select({ + count: sql`COUNT(*)::int`, + }) + .from(schema.userItems) + .where(eq(schema.userItems.userId, userId)) + + return result[0]?.count ?? 0 +} + function getOrderColumn(orderBy: string) { switch (orderBy) { case 'updatedAt': diff --git a/apps/backend/src/db/repositories/user-watch-entries-repository.ts b/apps/backend/src/db/repositories/user-watch-entries-repository.ts new file mode 100644 index 00000000..741df0a7 --- /dev/null +++ b/apps/backend/src/db/repositories/user-watch-entries-repository.ts @@ -0,0 +1,51 @@ +import { eq } from 'drizzle-orm' +import { db } from '..' +import { userWatchEntries } from '../schema' + +export async function createWatchEntry(data: { + userItemId: string + watchedAt?: Date +}) { + const [entry] = await db + .insert(userWatchEntries) + .values({ + userItemId: data.userItemId, + watchedAt: data.watchedAt ?? new Date(), + }) + .returning() + + return entry +} + +export async function getWatchEntriesByUserItemId(userItemId: string) { + return db + .select() + .from(userWatchEntries) + .where(eq(userWatchEntries.userItemId, userItemId)) + .orderBy(userWatchEntries.watchedAt) +} + +export async function updateWatchEntry(id: string, watchedAt: Date) { + const [entry] = await db + .update(userWatchEntries) + .set({ watchedAt }) + .where(eq(userWatchEntries.id, id)) + .returning() + + return entry +} + +export async function deleteWatchEntry(id: string) { + const [entry] = await db + .delete(userWatchEntries) + .where(eq(userWatchEntries.id, id)) + .returning() + + return entry +} + +export async function deleteWatchEntriesByUserItemId(userItemId: string) { + await db + .delete(userWatchEntries) + .where(eq(userWatchEntries.userItemId, userItemId)) +} diff --git a/apps/backend/src/db/schema/index.ts b/apps/backend/src/db/schema/index.ts index 9f14c918..6f1863d0 100644 --- a/apps/backend/src/db/schema/index.ts +++ b/apps/backend/src/db/schema/index.ts @@ -285,11 +285,34 @@ export const userItems = pgTable( }) ) -export const userItemsRelations = relations(userItems, ({ one }) => ({ +export const userItemsRelations = relations(userItems, ({ one, many }) => ({ user: one(users, { fields: [userItems.userId], references: [users.id], }), + watchEntries: many(userWatchEntries), +})) + +export const userWatchEntries = pgTable( + 'user_watch_entries', + { + id: uuid('id').default(sql`gen_random_uuid()`).primaryKey(), + userItemId: uuid('user_item_id') + .references(() => userItems.id, { onDelete: 'cascade' }) + .notNull(), + watchedAt: timestamp('watched_at').defaultNow().notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + }, + table => ({ + userItemIdx: index('user_watch_entries_user_item_idx').on(table.userItemId), + }) +) + +export const userWatchEntriesRelations = relations(userWatchEntries, ({ one }) => ({ + userItem: one(userItems, { + fields: [userWatchEntries.userItemId], + references: [userItems.id], + }), })) export const magicTokens = pgTable( @@ -570,6 +593,7 @@ export const subscriptionsRelations = relations(subscriptions, ({ one }) => ({ export const schema = { users, userItems, + userWatchEntries, reviews, reviewReplies, lists, diff --git a/apps/backend/src/domain/services/user-items/get-user-items-count.ts b/apps/backend/src/domain/services/user-items/get-user-items-count.ts new file mode 100644 index 00000000..417524c4 --- /dev/null +++ b/apps/backend/src/domain/services/user-items/get-user-items-count.ts @@ -0,0 +1,13 @@ +import { selectUserItemsCount } from '@/db/repositories/user-item-repository' + +type GetUserItemsCountInput = { + userId: string +} + +export async function getUserItemsCountService({ userId }: GetUserItemsCountInput) { + const count = await selectUserItemsCount(userId) + + return { + count, + } +} diff --git a/apps/backend/src/http/controllers/user-items-controller.ts b/apps/backend/src/http/controllers/user-items-controller.ts index 83ec3c61..466a9009 100644 --- a/apps/backend/src/http/controllers/user-items-controller.ts +++ b/apps/backend/src/http/controllers/user-items-controller.ts @@ -1,5 +1,10 @@ import type { FastifyRedis } from '@fastify/redis' import type { FastifyReply, FastifyRequest } from 'fastify' +import { + createWatchEntry, + deleteWatchEntriesByUserItemId, + getWatchEntriesByUserItemId, +} from '@/db/repositories/user-watch-entries-repository' import { DomainError } from '@/domain/errors/domain-error' import { getTMDBDataService } from '@/domain/services/tmdb/get-tmdb-data' import { createUserActivity } from '@/domain/services/user-activities/create-user-activity' @@ -9,12 +14,14 @@ import { deleteUserItemEpisodesService } from '@/domain/services/user-items/dele import { getAllUserItemsService } from '@/domain/services/user-items/get-all-user-items' import { getUserItemService } from '@/domain/services/user-items/get-user-item' import { getUserItemsService } from '@/domain/services/user-items/get-user-items' +import { getUserItemsCountService } from '@/domain/services/user-items/get-user-items-count' import { upsertUserItemService } from '@/domain/services/user-items/upsert-user-item' import { deleteUserItemParamsSchema, getAllUserItemsQuerySchema, getUserItemQuerySchema, getUserItemsBodySchema, + getUserItemsCountQuerySchema, upsertUserItemBodySchema, } from '../schemas/user-items' @@ -46,6 +53,17 @@ export async function upsertUserItemController( return reply.status(result.status).send({ message: result.message }) } + // Create first watch entry if status is WATCHED and no entries exist + if (status === 'WATCHED') { + const existingEntries = await getWatchEntriesByUserItemId(result.userItem.id) + if (existingEntries.length === 0) { + await createWatchEntry({ userItemId: result.userItem.id }) + } + } else { + // If status changed from WATCHED to something else, delete watch entries + await deleteWatchEntriesByUserItemId(result.userItem.id) + } + await createUserActivity({ userId: request.user.id, activityType: 'CHANGE_STATUS', @@ -132,6 +150,20 @@ export async function getUserItemController( userId: request.user.id, }) + // Include watch entries if user item exists + if (result.userItem) { + const watchEntries = await getWatchEntriesByUserItemId(result.userItem.id) + return reply.status(200).send({ + userItem: { + ...result.userItem, + watchEntries: watchEntries.map(entry => ({ + id: entry.id, + watchedAt: entry.watchedAt.toISOString(), + })), + }, + }) + } + return reply.status(200).send(result) } @@ -144,3 +176,13 @@ export async function getAllUserItemsController( return reply.status(200).send(result) } + +export async function getUserItemsCountController( + request: FastifyRequest, + reply: FastifyReply +) { + const { userId } = getUserItemsCountQuerySchema.parse(request.query) + const result = await getUserItemsCountService({ userId }) + + return reply.status(200).send(result) +} diff --git a/apps/backend/src/http/controllers/watch-entries-controller.ts b/apps/backend/src/http/controllers/watch-entries-controller.ts new file mode 100644 index 00000000..1482afc7 --- /dev/null +++ b/apps/backend/src/http/controllers/watch-entries-controller.ts @@ -0,0 +1,90 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' +import { + createWatchEntry, + deleteWatchEntry, + getWatchEntriesByUserItemId, + updateWatchEntry, +} from '@/db/repositories/user-watch-entries-repository' +import { + createWatchEntryBodySchema, + deleteWatchEntryParamsSchema, + getWatchEntriesQuerySchema, + updateWatchEntryBodySchema, + updateWatchEntryParamsSchema, +} from '../schemas/watch-entries' + +export async function createWatchEntryController( + request: FastifyRequest, + reply: FastifyReply +) { + const { userItemId, watchedAt } = createWatchEntryBodySchema.parse( + request.body + ) + + const entry = await createWatchEntry({ + userItemId, + watchedAt: watchedAt ? new Date(watchedAt) : undefined, + }) + + return reply.status(201).send({ + watchEntry: { + ...entry, + watchedAt: entry.watchedAt.toISOString(), + createdAt: entry.createdAt.toISOString(), + }, + }) +} + +export async function getWatchEntriesController( + request: FastifyRequest, + reply: FastifyReply +) { + const { userItemId } = getWatchEntriesQuerySchema.parse(request.query) + + const entries = await getWatchEntriesByUserItemId(userItemId) + + return reply.status(200).send({ + watchEntries: entries.map(entry => ({ + ...entry, + watchedAt: entry.watchedAt.toISOString(), + createdAt: entry.createdAt.toISOString(), + })), + }) +} + +export async function updateWatchEntryController( + request: FastifyRequest, + reply: FastifyReply +) { + const { id } = updateWatchEntryParamsSchema.parse(request.params) + const { watchedAt } = updateWatchEntryBodySchema.parse(request.body) + + const entry = await updateWatchEntry(id, new Date(watchedAt)) + + if (!entry) { + return reply.status(404).send({ message: 'Watch entry not found' }) + } + + return reply.status(200).send({ + watchEntry: { + ...entry, + watchedAt: entry.watchedAt.toISOString(), + createdAt: entry.createdAt.toISOString(), + }, + }) +} + +export async function deleteWatchEntryController( + request: FastifyRequest, + reply: FastifyReply +) { + const { id } = deleteWatchEntryParamsSchema.parse(request.params) + + const entry = await deleteWatchEntry(id) + + if (!entry) { + return reply.status(404).send({ message: 'Watch entry not found' }) + } + + return reply.status(204).send() +} diff --git a/apps/backend/src/http/routes/index.ts b/apps/backend/src/http/routes/index.ts index 7c6be839..a051bbe4 100644 --- a/apps/backend/src/http/routes/index.ts +++ b/apps/backend/src/http/routes/index.ts @@ -25,6 +25,7 @@ import { userItemsRoutes } from './user-items' import { userStatsRoutes } from './user-stats' import { usersRoute } from './users' import { webhookRoutes } from './webhook' +import { watchEntriesRoutes } from './watch-entries' export function routes(app: FastifyInstance) { if (config.app.APP_ENV === 'dev') { @@ -65,6 +66,7 @@ export function routes(app: FastifyInstance) { app.register(importRoutes) app.register(userActivitiesRoutes) app.register(subscriptionsRoutes) + app.register(watchEntriesRoutes) // app.register(userRecommendationsRoutes) return diff --git a/apps/backend/src/http/routes/user-items.ts b/apps/backend/src/http/routes/user-items.ts index 6ab64330..57130cdd 100644 --- a/apps/backend/src/http/routes/user-items.ts +++ b/apps/backend/src/http/routes/user-items.ts @@ -15,10 +15,20 @@ import { getUserItemQuerySchema, getUserItemResponseSchema, getUserItemsBodySchema, + getUserItemsCountQuerySchema, + getUserItemsCountResponseSchema, getUserItemsResponseSchema, upsertUserItemBodySchema, upsertUserItemResponseSchema, } from '../schemas/user-items' +import { + deleteUserItemController, + getAllUserItemsController, + getUserItemController, + getUserItemsController, + getUserItemsCountController, + upsertUserItemController, +} from '../controllers/user-items-controller' const USER_ITEMS_TAGS = ['User items'] @@ -113,4 +123,19 @@ export async function userItemsRoutes(app: FastifyInstance) { handler: getAllUserItemsController, }) ) + + app.after(() => + app.withTypeProvider().route({ + method: 'GET', + url: '/user/items/count', + schema: { + description: 'Get user items count', + tags: USER_ITEMS_TAGS, + querystring: getUserItemsCountQuerySchema, + response: getUserItemsCountResponseSchema, + operationId: 'getUserItemsCount', + }, + handler: getUserItemsCountController, + }) + ) } diff --git a/apps/backend/src/http/routes/watch-entries.ts b/apps/backend/src/http/routes/watch-entries.ts new file mode 100644 index 00000000..2046845f --- /dev/null +++ b/apps/backend/src/http/routes/watch-entries.ts @@ -0,0 +1,87 @@ +import type { FastifyInstance } from 'fastify' +import type { ZodTypeProvider } from 'fastify-type-provider-zod' +import { verifyJwt } from '../middlewares/verify-jwt' +import { + createWatchEntryController, + deleteWatchEntryController, + getWatchEntriesController, + updateWatchEntryController, +} from '../controllers/watch-entries-controller' +import { + createWatchEntryBodySchema, + createWatchEntryResponseSchema, + deleteWatchEntryParamsSchema, + getWatchEntriesQuerySchema, + getWatchEntriesResponseSchema, + updateWatchEntryBodySchema, + updateWatchEntryParamsSchema, + updateWatchEntryResponseSchema, +} from '../schemas/watch-entries' + +const WATCH_ENTRIES_TAGS = ['Watch Entries'] + +export async function watchEntriesRoutes(app: FastifyInstance) { + app.after(() => + app.withTypeProvider().route({ + method: 'POST', + url: '/watch-entry', + onRequest: [verifyJwt], + schema: { + description: 'Create a watch entry', + tags: WATCH_ENTRIES_TAGS, + body: createWatchEntryBodySchema, + response: createWatchEntryResponseSchema, + security: [{ bearerAuth: [] }], + }, + handler: createWatchEntryController, + }) + ) + + app.after(() => + app.withTypeProvider().route({ + method: 'GET', + url: '/watch-entries', + onRequest: [verifyJwt], + schema: { + description: 'Get watch entries for a user item', + tags: WATCH_ENTRIES_TAGS, + querystring: getWatchEntriesQuerySchema, + response: getWatchEntriesResponseSchema, + security: [{ bearerAuth: [] }], + }, + handler: getWatchEntriesController, + }) + ) + + app.after(() => + app.withTypeProvider().route({ + method: 'PUT', + url: '/watch-entry/:id', + onRequest: [verifyJwt], + schema: { + description: 'Update a watch entry', + tags: WATCH_ENTRIES_TAGS, + params: updateWatchEntryParamsSchema, + body: updateWatchEntryBodySchema, + response: updateWatchEntryResponseSchema, + security: [{ bearerAuth: [] }], + }, + handler: updateWatchEntryController, + }) + ) + + app.after(() => + app.withTypeProvider().route({ + method: 'DELETE', + url: '/watch-entry/:id', + onRequest: [verifyJwt], + schema: { + description: 'Delete a watch entry', + tags: WATCH_ENTRIES_TAGS, + params: deleteWatchEntryParamsSchema, + security: [{ bearerAuth: [] }], + }, + handler: deleteWatchEntryController, + }) + ) +} diff --git a/apps/backend/src/http/schemas/user-items.ts b/apps/backend/src/http/schemas/user-items.ts index de4c44a4..59e2e7e6 100644 --- a/apps/backend/src/http/schemas/user-items.ts +++ b/apps/backend/src/http/schemas/user-items.ts @@ -60,7 +60,18 @@ export const getUserItemQuerySchema = createSelectSchema(schema.userItems) export const getUserItemResponseSchema = { 200: z.object({ - userItem: createSelectSchema(schema.userItems).optional(), + userItem: createSelectSchema(schema.userItems) + .extend({ + watchEntries: z + .array( + z.object({ + id: z.string(), + watchedAt: z.string(), + }) + ) + .optional(), + }) + .optional(), }), } @@ -80,3 +91,13 @@ export const getAllUserItemsResponseSchema = { ), }), } + +export const getUserItemsCountQuerySchema = z.object({ + userId: z.string(), +}) + +export const getUserItemsCountResponseSchema = { + 200: z.object({ + count: z.number(), + }), +} diff --git a/apps/backend/src/http/schemas/watch-entries.ts b/apps/backend/src/http/schemas/watch-entries.ts new file mode 100644 index 00000000..bce40a44 --- /dev/null +++ b/apps/backend/src/http/schemas/watch-entries.ts @@ -0,0 +1,57 @@ +import { z } from 'zod' + +export const createWatchEntryBodySchema = z.object({ + userItemId: z.string().uuid(), + watchedAt: z.string().datetime().optional(), +}) + +export const createWatchEntryResponseSchema = { + 201: z.object({ + watchEntry: z.object({ + id: z.string().uuid(), + userItemId: z.string().uuid(), + watchedAt: z.string(), + createdAt: z.string(), + }), + }), +} + +export const getWatchEntriesQuerySchema = z.object({ + userItemId: z.string().uuid(), +}) + +export const getWatchEntriesResponseSchema = { + 200: z.object({ + watchEntries: z.array( + z.object({ + id: z.string().uuid(), + userItemId: z.string().uuid(), + watchedAt: z.string(), + createdAt: z.string(), + }) + ), + }), +} + +export const updateWatchEntryParamsSchema = z.object({ + id: z.string().uuid(), +}) + +export const updateWatchEntryBodySchema = z.object({ + watchedAt: z.string().datetime(), +}) + +export const updateWatchEntryResponseSchema = { + 200: z.object({ + watchEntry: z.object({ + id: z.string().uuid(), + userItemId: z.string().uuid(), + watchedAt: z.string(), + createdAt: z.string(), + }), + }), +} + +export const deleteWatchEntryParamsSchema = z.object({ + id: z.string().uuid(), +}) diff --git a/apps/ios/.gitignore b/apps/ios/.gitignore new file mode 100644 index 00000000..8aa0d953 --- /dev/null +++ b/apps/ios/.gitignore @@ -0,0 +1,93 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## Compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +Packages/ +Package.pins +Package.resolved +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +.swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +*.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +# Configuration files with secrets +*.xcconfig +!Default.xcconfig diff --git a/apps/ios/.swiftlint.yml b/apps/ios/.swiftlint.yml new file mode 100644 index 00000000..beb5f43d --- /dev/null +++ b/apps/ios/.swiftlint.yml @@ -0,0 +1,50 @@ +disabled_rules: + - trailing_whitespace + - line_length + - force_cast + - force_try + +opt_in_rules: + - empty_count + - empty_string + - closure_spacing + - contains_over_first_not_nil + - first_where + - sorted_first_last + - modifier_order + - redundant_type_annotation + +excluded: + - Pods + - .build + - DerivedData + +line_length: + warning: 120 + error: 200 + ignores_comments: true + +file_length: + warning: 500 + error: 1000 + +type_body_length: + warning: 300 + error: 500 + +function_body_length: + warning: 50 + error: 100 + +identifier_name: + min_length: + warning: 2 + max_length: + warning: 40 + error: 50 + excluded: + - id + - URL + - url + +reporter: "xcode" diff --git a/apps/ios/Plotwist/Plotwist.xcodeproj/project.pbxproj b/apps/ios/Plotwist/Plotwist.xcodeproj/project.pbxproj new file mode 100644 index 00000000..4c82ee95 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist.xcodeproj/project.pbxproj @@ -0,0 +1,329 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + BE1232732F129895003F1FBA /* Plotwist.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Plotwist.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + BE1232752F129895003F1FBA /* Plotwist */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Plotwist; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + BE1232702F129895003F1FBA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + BE12326A2F129895003F1FBA = { + isa = PBXGroup; + children = ( + BE1232752F129895003F1FBA /* Plotwist */, + BE1232742F129895003F1FBA /* Products */, + ); + sourceTree = ""; + }; + BE1232742F129895003F1FBA /* Products */ = { + isa = PBXGroup; + children = ( + BE1232732F129895003F1FBA /* Plotwist.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + BE1232722F129895003F1FBA /* Plotwist */ = { + isa = PBXNativeTarget; + buildConfigurationList = BE12327E2F129897003F1FBA /* Build configuration list for PBXNativeTarget "Plotwist" */; + buildPhases = ( + BE12326F2F129895003F1FBA /* Sources */, + BE1232702F129895003F1FBA /* Frameworks */, + BE1232712F129895003F1FBA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + BE1232752F129895003F1FBA /* Plotwist */, + ); + name = Plotwist; + packageProductDependencies = ( + ); + productName = Plotwist; + productReference = BE1232732F129895003F1FBA /* Plotwist.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BE12326B2F129895003F1FBA /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + BE1232722F129895003F1FBA = { + CreatedOnToolsVersion = 16.4; + }; + }; + }; + buildConfigurationList = BE12326E2F129895003F1FBA /* Build configuration list for PBXProject "Plotwist" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = BE12326A2F129895003F1FBA; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = BE1232742F129895003F1FBA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BE1232722F129895003F1FBA /* Plotwist */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + BE1232712F129895003F1FBA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + BE12326F2F129895003F1FBA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + BE12327C2F129897003F1FBA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 54XPVTP5PA; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + BE12327D2F129897003F1FBA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 54XPVTP5PA; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + BE12327F2F129897003F1FBA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_TEAM = 54XPVTP5PA; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = henrique.Plotwist; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BE1232802F129897003F1FBA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_TEAM = 54XPVTP5PA; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = henrique.Plotwist; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BE12326E2F129895003F1FBA /* Build configuration list for PBXProject "Plotwist" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BE12327C2F129897003F1FBA /* Debug */, + BE12327D2F129897003F1FBA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BE12327E2F129897003F1FBA /* Build configuration list for PBXNativeTarget "Plotwist" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BE12327F2F129897003F1FBA /* Debug */, + BE1232802F129897003F1FBA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BE12326B2F129895003F1FBA /* Project object */; +} diff --git a/apps/ios/Plotwist/Plotwist/App/RootView.swift b/apps/ios/Plotwist/Plotwist/App/RootView.swift new file mode 100644 index 00000000..e8223b11 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/App/RootView.swift @@ -0,0 +1,29 @@ +// +// RootView.swift +// Plotwist +// + +import SwiftUI + +struct RootView: View { + @State private var isAuthenticated = AuthService.shared.isAuthenticated + @ObservedObject private var themeManager = ThemeManager.shared + + var body: some View { + Group { + if isAuthenticated { + HomeView() + } else { + LoginView() + } + } + .preferredColorScheme(themeManager.current.colorScheme) + .onReceive(NotificationCenter.default.publisher(for: .authChanged)) { _ in + isAuthenticated = AuthService.shared.isAuthenticated + } + } +} + +#Preview { + RootView() +} diff --git a/apps/ios/Plotwist/Plotwist/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/Plotwist/Plotwist/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Plotwist/Plotwist/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/apps/ios/Plotwist/Plotwist/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 00000000..f877a1c8 Binary files /dev/null and b/apps/ios/Plotwist/Plotwist/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/apps/ios/Plotwist/Plotwist/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/ios/Plotwist/Plotwist/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..87d40152 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "filename" : "AppIcon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "AppIcon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "AppIcon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Plotwist/Plotwist/Assets.xcassets/Contents.json b/apps/ios/Plotwist/Plotwist/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Plotwist/Plotwist/Assets.xcassets/FilmStrip.imageset/Contents.json b/apps/ios/Plotwist/Plotwist/Assets.xcassets/FilmStrip.imageset/Contents.json new file mode 100644 index 00000000..67f6f100 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Assets.xcassets/FilmStrip.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "film-strip.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/apps/ios/Plotwist/Plotwist/Assets.xcassets/FilmStrip.imageset/film-strip.png b/apps/ios/Plotwist/Plotwist/Assets.xcassets/FilmStrip.imageset/film-strip.png new file mode 100644 index 00000000..a3e3a54a Binary files /dev/null and b/apps/ios/Plotwist/Plotwist/Assets.xcassets/FilmStrip.imageset/film-strip.png differ diff --git a/apps/ios/Plotwist/Plotwist/Components/EpisodeRowView.swift b/apps/ios/Plotwist/Plotwist/Components/EpisodeRowView.swift new file mode 100644 index 00000000..4b0765b9 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/EpisodeRowView.swift @@ -0,0 +1,78 @@ +// +// EpisodeRowView.swift +// Plotwist +// + +import SwiftUI + +struct EpisodeRowView: View { + let episode: Episode + let isWatched: Bool + let isLoading: Bool + let onToggleWatched: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + // Top row: Backdrop + Title/Duration + Watch button + HStack(alignment: .center, spacing: 12) { + // Episode backdrop + CachedAsyncImage(url: episode.stillURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 8) + .fill(Color.appBorderAdaptive) + .overlay( + Image(systemName: "play.rectangle") + .font(.title3) + .foregroundColor(.appMutedForegroundAdaptive) + ) + } + .frame(width: 140, height: 80) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .posterBorder(cornerRadius: 8) + + // Title and duration + VStack(alignment: .leading, spacing: 4) { + // Duration + if let runtime = episode.formattedRuntime { + Text(runtime) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + + // Episode title with number prefix + Text("\(episode.episodeNumber). \(episode.name)") + .font(.subheadline.weight(.medium)) + .foregroundColor(.appForegroundAdaptive) + .lineLimit(2) + } + + Spacer() + + // Watch button + Button(action: onToggleWatched) { + if isLoading { + ProgressView() + .scaleEffect(0.8) + } else { + Image(systemName: isWatched ? "checkmark.circle.fill" : "circle") + .font(.system(size: 24)) + .foregroundColor(isWatched ? .appForegroundAdaptive : .appMutedForegroundAdaptive) + } + } + .disabled(isLoading) + .frame(width: 44, height: 44) + } + + // Description below + if let overview = episode.overview, !overview.isEmpty { + Text(overview) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + .lineLimit(3) + } + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Components/MediaDetailViewActions.swift b/apps/ios/Plotwist/Plotwist/Components/MediaDetailViewActions.swift new file mode 100644 index 00000000..73229203 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/MediaDetailViewActions.swift @@ -0,0 +1,188 @@ +// +// MediaDetailViewActions.swift +// Plotwist +// + +import SwiftUI + +struct MediaDetailViewActions: View { + let mediaId: Int + let mediaType: String + let userReview: Review? + let userItem: UserItem? + let isLoadingReview: Bool + let isLoadingStatus: Bool + let onReviewTapped: () -> Void + let onStatusChanged: (UserItem?) -> Void + + @State private var showStatusSheet = false + + var body: some View { + HStack(spacing: 12) { + // Review Button + ReviewButton(hasReview: userReview != nil, isLoading: isLoadingReview, action: onReviewTapped) + + // Status Button + StatusButton( + currentStatus: userItem?.statusEnum, + rewatchCount: userItem?.watchEntries?.count ?? 0, + isLoading: isLoadingStatus, + action: { showStatusSheet = true } + ) + + Spacer() + } + .sheet(isPresented: $showStatusSheet) { + StatusSheet( + mediaId: mediaId, + mediaType: mediaType, + currentStatus: userItem?.statusEnum, + currentItemId: userItem?.id, + watchEntries: userItem?.watchEntries ?? [], + onStatusChanged: { newStatus, _ in + if newStatus != nil { + // Reload user item to get the updated data + Task { + await reloadUserItem() + } + } else { + onStatusChanged(nil) + } + } + ) + } + } + + private func reloadUserItem() async { + do { + let apiMediaType = mediaType == "movie" ? "MOVIE" : "TV_SHOW" + let item = try await UserItemService.shared.getUserItem( + tmdbId: mediaId, + mediaType: apiMediaType + ) + await MainActor.run { + onStatusChanged(item) + } + } catch { + // Ignore errors + } + } +} + +// MARK: - Status Button +struct StatusButton: View { + let currentStatus: UserItemStatus? + let rewatchCount: Int + let isLoading: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 6) { + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(0.7) + .frame(width: 13, height: 13) + } else { + Image(systemName: currentStatus?.icon ?? "pencil") + .font(.system(size: 13)) + .foregroundColor(statusIconColor ?? .appForegroundAdaptive) + } + + Text(currentStatus?.displayName(strings: L10n.current) ?? L10n.current.updateStatus) + .font(.footnote.weight(.medium)) + .foregroundColor(.appForegroundAdaptive) + + // Rewatch count badge + if currentStatus == .watched && rewatchCount > 1 { + Text("\(rewatchCount)x") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background(Color.green) + .clipShape(Capsule()) + } + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background(Color.appInputFilled) + .cornerRadius(10) + .opacity(isLoading ? 0.5 : 1) + } + .disabled(isLoading) + } + + private var statusIconColor: Color? { + guard let status = currentStatus else { return nil } + switch status { + case .watched: return .green + case .watching: return .blue + case .watchlist: return .orange + case .dropped: return .red + } + } +} + +// MARK: - Preview +#Preview { + VStack(spacing: 16) { + MediaDetailViewActions( + mediaId: 550, + mediaType: "movie", + userReview: nil, + userItem: nil, + isLoadingReview: true, + isLoadingStatus: true, + onReviewTapped: {}, + onStatusChanged: { _ in } + ) + .padding(.horizontal, 24) + + MediaDetailViewActions( + mediaId: 550, + mediaType: "movie", + userReview: nil, + userItem: nil, + isLoadingReview: false, + isLoadingStatus: false, + onReviewTapped: {}, + onStatusChanged: { _ in } + ) + .padding(.horizontal, 24) + + MediaDetailViewActions( + mediaId: 550, + mediaType: "movie", + userReview: Review( + id: "1", + userId: "user1", + tmdbId: 550, + mediaType: "MOVIE", + review: "Great movie!", + rating: 4.5, + hasSpoilers: false, + seasonNumber: nil, + episodeNumber: nil, + language: "en-US", + createdAt: "2025-01-10T12:00:00.000Z" + ), + userItem: UserItem( + id: "1", + userId: "user1", + tmdbId: 550, + mediaType: "MOVIE", + status: "WATCHED", + addedAt: "2025-01-10T12:00:00.000Z", + updatedAt: "2025-01-10T12:00:00.000Z", + watchEntries: [WatchEntry(id: "1", watchedAt: "2025-01-10T12:00:00.000Z")] + ), + isLoadingReview: false, + isLoadingStatus: false, + onReviewTapped: {}, + onStatusChanged: { _ in } + ) + .padding(.horizontal, 24) + } +} diff --git a/apps/ios/Plotwist/Plotwist/Components/PreferencesBadge.swift b/apps/ios/Plotwist/Plotwist/Components/PreferencesBadge.swift new file mode 100644 index 00000000..a35fadae --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/PreferencesBadge.swift @@ -0,0 +1,753 @@ +// +// PreferencesBadge.swift +// Plotwist +// + +import SwiftUI + +struct PreferencesBadge: View { + @ObservedObject private var preferencesManager = UserPreferencesManager.shared + @State private var strings = L10n.current + @State private var showPreferences = false + + var body: some View { + if preferencesManager.hasStreamingServices { + Button { + showPreferences = true + } label: { + HStack(spacing: 6) { + Image(systemName: "sparkles") + .font(.caption) + Text(strings.resultsBasedOnPreferences) + .font(.caption) + } + .foregroundColor(.appForegroundAdaptive) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(Color.appInputFilled) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + .sheet(isPresented: $showPreferences) { + PreferencesQuickSheet() + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } + } +} + +// MARK: - Preferences Quick Sheet +struct PreferencesQuickSheet: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @ObservedObject private var preferencesManager = UserPreferencesManager.shared + @State private var strings = L10n.current + @State private var showRegionPicker = false + @State private var showServicesPicker = false + @State private var streamingProviders: [StreamingProvider] = [] + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + private var selectedProviders: [StreamingProvider] { + streamingProviders.filter { preferencesManager.watchProvidersIds.contains($0.providerId) } + } + + var body: some View { + NavigationView { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 36, height: 36) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.preferences) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + Color.clear + .frame(width: 36, height: 36) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Content + ScrollView { + VStack(alignment: .leading, spacing: 0) { + // Region + if let region = preferencesManager.watchRegion { + Button { + showRegionPicker = true + } label: { + PreferencesBadgeRow(label: strings.region) { + PreferencesItemBadge( + text: regionName(for: region), + prefix: flagEmoji(for: region) + ) + } + } + .sheet(isPresented: $showRegionPicker) { + RegionPickerSheet(currentRegion: region) + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 24) + + // Streaming Services + Button { + showServicesPicker = true + } label: { + PreferencesBadgeRow(label: strings.streamingServices) { + if selectedProviders.isEmpty { + Text(strings.notSet) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } else { + PreferencesFlowLayout(spacing: 8) { + ForEach(selectedProviders) { provider in + PreferencesItemBadge( + text: provider.providerName, + logoURL: provider.logoURL + ) + } + } + } + } + } + .sheet(isPresented: $showServicesPicker) { + ServicesPickerSheet( + watchRegion: region, + selectedIds: preferencesManager.watchProvidersIds + ) + } + } else { + Button { + showRegionPicker = true + } label: { + PreferencesBadgeRow(label: strings.region) { + Text(strings.notSet) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + } + .sheet(isPresented: $showRegionPicker) { + RegionPickerSheet(currentRegion: nil) + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 24) + + PreferencesBadgeRow(label: strings.streamingServices) { + Text(strings.selectRegionFirst) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + .opacity(0.5) + } + } + } + + Spacer() + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + } + .task { + await loadProviders() + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + .onReceive(NotificationCenter.default.publisher(for: .profileUpdated)) { _ in + Task { await loadProviders() } + } + } + + private func loadProviders() async { + guard let region = preferencesManager.watchRegion else { return } + do { + streamingProviders = try await TMDBService.shared.getStreamingProviders( + watchRegion: region, + language: Language.current.rawValue + ) + } catch { + print("Error loading providers: \(error)") + } + } + + private func regionName(for code: String) -> String { + let locale = Locale(identifier: Language.current.rawValue) + return locale.localizedString(forRegionCode: code) ?? code + } + + private func flagEmoji(for code: String) -> String { + let base: UInt32 = 127397 + var emoji = "" + for scalar in code.uppercased().unicodeScalars { + if let unicode = UnicodeScalar(base + scalar.value) { + emoji.append(String(unicode)) + } + } + return emoji + } +} + +// MARK: - Preferences Badge Row +struct PreferencesBadgeRow: View { + let label: String + @ViewBuilder let content: Content + + var body: some View { + HStack(alignment: .top, spacing: 16) { + Text(label) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .frame(width: 100, alignment: .topLeading) + .multilineTextAlignment(.leading) + + content + .frame(maxWidth: .infinity, alignment: .leading) + + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.appMutedForegroundAdaptive) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + .contentShape(Rectangle()) + } +} + +// MARK: - Preferences Item Badge +struct PreferencesItemBadge: View { + let text: String + var prefix: String? = nil + var logoURL: URL? = nil + + var body: some View { + HStack(spacing: 6) { + if let prefix { + Text(prefix) + .font(.caption) + } + + if let logoURL { + CachedAsyncImage(url: logoURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + .fill(Color.appInputFilled) + } + .frame(width: 18, height: 18) + .cornerRadius(4) + } + + Text(text) + .font(.caption) + .foregroundColor(.appForegroundAdaptive) + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Color.appInputFilled) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } +} + +// MARK: - Preferences Flow Layout +struct PreferencesFlowLayout: Layout { + var spacing: CGFloat = 8 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let maxWidth = proposal.width ?? .infinity + var height: CGFloat = 0 + var currentRowWidth: CGFloat = 0 + var currentRowHeight: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + + if currentRowWidth + size.width > maxWidth && currentRowWidth > 0 { + height += currentRowHeight + spacing + currentRowWidth = 0 + currentRowHeight = 0 + } + + currentRowWidth += size.width + spacing + currentRowHeight = max(currentRowHeight, size.height) + } + + height += currentRowHeight + return CGSize(width: maxWidth, height: height) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + var x = bounds.minX + var y = bounds.minY + var currentRowHeight: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + + if x + size.width > bounds.maxX && x > bounds.minX { + x = bounds.minX + y += currentRowHeight + spacing + currentRowHeight = 0 + } + + subview.place(at: CGPoint(x: x, y: y), proposal: .unspecified) + x += size.width + spacing + currentRowHeight = max(currentRowHeight, size.height) + } + } +} + +// MARK: - Region Picker Sheet +struct RegionPickerSheet: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + @State private var searchText = "" + @State private var regions: [WatchRegion] = [] + @State private var filteredRegions: [WatchRegion] = [] + @State private var isLoading = true + @State private var isSaving = false + @State private var selectedRegion: String + + init(currentRegion: String?) { + _selectedRegion = State(initialValue: currentRegion ?? "") + } + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + private var hasChanges: Bool { + !selectedRegion.isEmpty + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 36, height: 36) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.region) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + Button { + Task { await saveRegion() } + } label: { + if isSaving { + ProgressView() + .frame(width: 36, height: 36) + } else { + Image(systemName: "checkmark") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(hasChanges ? .appBackgroundAdaptive : .appMutedForegroundAdaptive) + .frame(width: 36, height: 36) + .background(hasChanges ? Color.appForegroundAdaptive : Color.appInputFilled) + .clipShape(Circle()) + } + } + .disabled(!hasChanges || isSaving) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Search Field + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .foregroundColor(.appMutedForegroundAdaptive) + TextField(strings.searchRegion, text: $searchText) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .onChange(of: searchText) { _ in + filterRegions() + } + } + .padding(12) + .background(Color.appInputFilled) + .cornerRadius(12) + .padding(.horizontal, 24) + .padding(.vertical, 16) + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + + // Content + if isLoading { + Spacer() + ProgressView() + Spacer() + } else { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(filteredRegions) { region in + Button { + selectedRegion = region.iso31661 + } label: { + HStack(spacing: 12) { + Text(region.flagEmoji) + .font(.title2) + + Text(region.nativeName) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + if selectedRegion == region.iso31661 { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 22)) + .foregroundColor(.appForegroundAdaptive) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background( + selectedRegion == region.iso31661 + ? Color.appInputFilled : Color.clear + ) + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 60) + } + } + } + } + } + } + .preferredColorScheme(effectiveColorScheme) + .task { + await loadRegions() + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } + + private func loadRegions() async { + isLoading = true + defer { isLoading = false } + + do { + regions = try await TMDBService.shared.getAvailableRegions( + language: Language.current.rawValue + ) + filterRegions() + } catch { + print("Error loading regions: \(error)") + } + } + + private func filterRegions() { + if searchText.isEmpty { + filteredRegions = regions + } else { + filteredRegions = regions.filter { + $0.englishName.localizedCaseInsensitiveContains(searchText) + || $0.nativeName.localizedCaseInsensitiveContains(searchText) + } + } + } + + private func saveRegion() async { + guard hasChanges else { return } + + isSaving = true + defer { isSaving = false } + + do { + try await AuthService.shared.updateUserPreferences(watchRegion: selectedRegion) + NotificationCenter.default.post(name: .profileUpdated, object: nil) + dismiss() + } catch { + print("Error saving region: \(error)") + } + } +} + +// MARK: - Services Picker Sheet +struct ServicesPickerSheet: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + @State private var searchText = "" + @State private var providers: [StreamingProvider] = [] + @State private var filteredProviders: [StreamingProvider] = [] + @State private var isLoading = true + @State private var isSaving = false + @State private var selectedIds: Set + + let watchRegion: String + + init(watchRegion: String, selectedIds: [Int]) { + self.watchRegion = watchRegion + _selectedIds = State(initialValue: Set(selectedIds)) + } + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 36, height: 36) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.streamingServices) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + Button { + Task { await saveServices() } + } label: { + if isSaving { + ProgressView() + .frame(width: 36, height: 36) + } else { + Image(systemName: "checkmark") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.appBackgroundAdaptive) + .frame(width: 36, height: 36) + .background(Color.appForegroundAdaptive) + .clipShape(Circle()) + } + } + .disabled(isSaving) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Search Field + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .foregroundColor(.appMutedForegroundAdaptive) + TextField(strings.searchStreamingServices, text: $searchText) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .onChange(of: searchText) { _ in + filterProviders() + } + } + .padding(12) + .background(Color.appInputFilled) + .cornerRadius(12) + .padding(.horizontal, 24) + .padding(.bottom, 16) + + // Hint message + HStack(spacing: 8) { + Image(systemName: "info.circle") + .font(.caption) + Text(strings.streamingServicesHint) + .font(.caption) + } + .foregroundColor(.appMutedForegroundAdaptive) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + .padding(.bottom, 16) + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + + // Content + if isLoading { + Spacer() + ProgressView() + Spacer() + } else { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(filteredProviders) { provider in + Button { + toggleProvider(provider.providerId) + } label: { + HStack(spacing: 12) { + CachedAsyncImage(url: provider.logoURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + .fill(Color.appInputFilled) + } + .frame(width: 40, height: 40) + .cornerRadius(8) + + Text(provider.providerName) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + if selectedIds.contains(provider.providerId) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 22)) + .foregroundColor(.appForegroundAdaptive) + } else { + Image(systemName: "circle") + .font(.system(size: 22)) + .foregroundColor(.appMutedForegroundAdaptive) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background( + selectedIds.contains(provider.providerId) + ? Color.appInputFilled : Color.clear + ) + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 76) + } + } + } + } + } + } + .preferredColorScheme(effectiveColorScheme) + .task { + await loadProviders() + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } + + private func toggleProvider(_ id: Int) { + if selectedIds.contains(id) { + selectedIds.remove(id) + } else { + selectedIds.insert(id) + } + } + + private func loadProviders() async { + isLoading = true + defer { isLoading = false } + + do { + providers = try await TMDBService.shared.getStreamingProviders( + watchRegion: watchRegion, + language: Language.current.rawValue + ) + filterProviders() + } catch { + print("Error loading providers: \(error)") + } + } + + private func filterProviders() { + if searchText.isEmpty { + filteredProviders = providers + } else { + filteredProviders = providers.filter { + $0.providerName.localizedCaseInsensitiveContains(searchText) + } + } + } + + private func saveServices() async { + isSaving = true + defer { isSaving = false } + + do { + try await AuthService.shared.updateUserPreferences( + watchRegion: watchRegion, + watchProvidersIds: Array(selectedIds) + ) + NotificationCenter.default.post(name: .profileUpdated, object: nil) + dismiss() + } catch { + print("Error saving services: \(error)") + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Components/PrimaryButton.swift b/apps/ios/Plotwist/Plotwist/Components/PrimaryButton.swift new file mode 100644 index 00000000..611f3940 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/PrimaryButton.swift @@ -0,0 +1,143 @@ +// +// PrimaryButton.swift +// Plotwist +// + +import SwiftUI + +enum ButtonVariant { + case filled + case outline +} + +struct PrimaryButton: View { + let title: String + let variant: ButtonVariant + let isLoading: Bool + let isDisabled: Bool + let action: () -> Void + + init( + _ title: String, variant: ButtonVariant = .outline, isLoading: Bool = false, + isDisabled: Bool = false, action: @escaping () -> Void + ) { + self.title = title + self.variant = variant + self.isLoading = isLoading + self.isDisabled = isDisabled + self.action = action + } + + var body: some View { + Button(action: action) { + Group { + if isLoading { + ProgressView().tint(variant == .filled ? .appBackgroundAdaptive : .appForegroundAdaptive) + } else { + Text(title) + .fontWeight(.semibold) + } + } + .frame(maxWidth: .infinity) + .frame(height: 48) + .background(variant == .filled ? Color.appForegroundAdaptive : Color.clear) + .foregroundColor(variant == .filled ? .appBackgroundAdaptive : .appForegroundAdaptive) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(variant == .filled ? Color.clear : Color.appBorderAdaptive, lineWidth: 1) + ) + } + .disabled(isLoading || isDisabled) + .opacity(isDisabled ? 0.5 : 1) + } +} + +struct SocialButton: View { + let icon: String + let title: String + let isDisabled: Bool + let action: () -> Void + + init(_ title: String, icon: String, isDisabled: Bool = false, action: @escaping () -> Void) { + self.title = title + self.icon = icon + self.isDisabled = isDisabled + self.action = action + } + + var body: some View { + Button(action: action) { + HStack(spacing: 8) { + Image(systemName: icon) + Text(title) + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .frame(height: 48) + .background(Color.clear) + .foregroundColor(.appForegroundAdaptive) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.appBorderAdaptive, lineWidth: 1) + ) + } + .disabled(isDisabled) + .opacity(isDisabled ? 0.5 : 1) + } +} + +struct ActionButton: View { + let title: String + let icon: String + let iconColor: Color? + let action: () -> Void + + init( + _ title: String, + icon: String, + iconColor: Color? = nil, + action: @escaping () -> Void + ) { + self.title = title + self.icon = icon + self.iconColor = iconColor + self.action = action + } + + var body: some View { + Button(action: action) { + HStack(spacing: 6) { + Image(systemName: icon) + .font(.system(size: 13)) + .foregroundColor(iconColor ?? .appForegroundAdaptive) + + Text(title) + .font(.footnote.weight(.medium)) + .foregroundColor(.appForegroundAdaptive) + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background(Color.appInputFilled) + .cornerRadius(10) + } + } +} + +#Preview { + VStack(spacing: 16) { + PrimaryButton("Access", variant: .filled) {} + PrimaryButton("Outline") {} + PrimaryButton("Loading", variant: .filled, isLoading: true) {} + PrimaryButton("Disabled", isDisabled: true) {} + SocialButton("Continue with Google", icon: "globe") {} + SocialButton("Continue with Apple", icon: "apple.logo", isDisabled: true) {} + + HStack { + ActionButton("Review", icon: "star") {} + ActionButton("Reviewed", icon: "star.fill", iconColor: .appStarYellow) {} + Spacer() + } + } + .padding() +} diff --git a/apps/ios/Plotwist/Plotwist/Components/ReviewButton.swift b/apps/ios/Plotwist/Plotwist/Components/ReviewButton.swift new file mode 100644 index 00000000..3b8e39f8 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/ReviewButton.swift @@ -0,0 +1,60 @@ +// +// ReviewButton.swift +// Plotwist +// + +import SwiftUI + +struct ReviewButton: View { + let hasReview: Bool + let isLoading: Bool + let action: () -> Void + + init(hasReview: Bool, isLoading: Bool = false, action: @escaping () -> Void) { + self.hasReview = hasReview + self.isLoading = isLoading + self.action = action + } + + var body: some View { + Button(action: action) { + HStack(spacing: 6) { + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(0.7) + .frame(width: 13, height: 13) + } else { + Image(systemName: hasReview ? "star.fill" : "star") + .font(.system(size: 13)) + .foregroundColor(hasReview ? .appStarYellow : .appForegroundAdaptive) + } + + Text(hasReview ? L10n.current.reviewed : L10n.current.review) + .font(.footnote.weight(.medium)) + .foregroundColor(.appForegroundAdaptive) + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background(Color.appInputFilled) + .cornerRadius(10) + .opacity(isLoading ? 0.5 : 1) + } + .disabled(isLoading) + } +} + +#Preview { + VStack(spacing: 16) { + HStack { + ReviewButton(hasReview: false) {} + Spacer() + } + + HStack { + ReviewButton(hasReview: true) {} + Spacer() + } + } + .padding() +} diff --git a/apps/ios/Plotwist/Plotwist/Components/ReviewItemView.swift b/apps/ios/Plotwist/Plotwist/Components/ReviewItemView.swift new file mode 100644 index 00000000..e87cba35 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/ReviewItemView.swift @@ -0,0 +1,172 @@ +// +// ReviewItemView.swift +// Plotwist +// + +import SwiftUI + +struct ReviewItemView: View { + let review: ReviewListItem + + private var usernameInitial: String { + review.user.username.first?.uppercased() ?? "?" + } + + private var timeAgo: String { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .abbreviated + + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + if let date = dateFormatter.date(from: review.createdAt) { + return formatter.localizedString(for: date, relativeTo: Date()) + } + return "" + } + + var body: some View { + HStack(alignment: .top, spacing: 12) { + // Avatar + if let avatarUrl = review.user.avatarUrl, + let url = URL(string: avatarUrl) + { + CachedAsyncImage(url: url) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + avatarFallback + } + .frame(width: 40, height: 40) + .clipShape(Circle()) + } else { + avatarFallback + } + + // Content + VStack(alignment: .leading, spacing: 0) { + // Header: Username + Time (aligned to top) + HStack(alignment: .top) { + Text(review.user.username) + .font(.subheadline.weight(.medium)) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + Text(timeAgo) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + + // Rating stars (below username) + HStack(spacing: 2) { + ForEach(1...5, id: \.self) { index in + Image(systemName: ratingIcon(for: index)) + .font(.system(size: 14)) + .foregroundColor(ratingColor(for: index)) + } + } + .padding(.top, 4) + + // Review content (below stars) + if !review.review.isEmpty { + ZStack(alignment: .topLeading) { + Text(review.review) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + .lineSpacing(4) + .blur(radius: review.hasSpoilers ? 6 : 0) + + if review.hasSpoilers { + Text(L10n.current.containSpoilers) + .font(.caption.weight(.medium)) + .foregroundColor(.appMutedForegroundAdaptive) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.appInputFilled) + .cornerRadius(6) + } + } + .padding(.top, 8) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var avatarFallback: some View { + Circle() + .fill(Color.appForegroundAdaptive) + .frame(width: 40, height: 40) + .overlay( + Text(usernameInitial) + .font(.subheadline.weight(.medium)) + .foregroundColor(.appBackgroundAdaptive) + ) + } + + private func ratingIcon(for index: Int) -> String { + let rating = review.rating + if Double(index) <= rating { + return "star.fill" + } else if Double(index) - 0.5 <= rating { + return "star.leadinghalf.filled" + } else { + return "star" + } + } + + private func ratingColor(for index: Int) -> Color { + let rating = review.rating + if Double(index) <= rating || Double(index) - 0.5 <= rating { + return .appStarYellow + } else { + return .gray.opacity(0.3) + } + } +} + +#Preview { + VStack(spacing: 32) { + ReviewItemView( + review: ReviewListItem( + id: "1", + userId: "user1", + tmdbId: 123, + mediaType: "MOVIE", + review: + "This is an amazing movie! The cinematography was beautiful and the acting was superb.", + rating: 4.5, + hasSpoilers: false, + seasonNumber: nil, + episodeNumber: nil, + language: "en-US", + createdAt: "2025-01-10T12:00:00.000Z", + user: ReviewUser(id: "user1", username: "johndoe", avatarUrl: nil), + likeCount: 5, + replyCount: 2, + userLike: nil + )) + + ReviewItemView( + review: ReviewListItem( + id: "2", + userId: "user2", + tmdbId: 123, + mediaType: "MOVIE", + review: "Contains major plot spoilers about the ending!", + rating: 3.0, + hasSpoilers: true, + seasonNumber: nil, + episodeNumber: nil, + language: "en-US", + createdAt: "2025-01-09T12:00:00.000Z", + user: ReviewUser(id: "user2", username: "janedoe", avatarUrl: nil), + likeCount: 10, + replyCount: 0, + userLike: nil + )) + } + .padding() +} diff --git a/apps/ios/Plotwist/Plotwist/Components/SegmentedTabBar.swift b/apps/ios/Plotwist/Plotwist/Components/SegmentedTabBar.swift new file mode 100644 index 00000000..b88c97ae --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/SegmentedTabBar.swift @@ -0,0 +1,89 @@ +// +// SegmentedTabBar.swift +// Plotwist +// + +import SwiftUI + +// MARK: - Segmented Tab Protocol +protocol SegmentedTab: Hashable, CaseIterable { + var title: String { get } + var isDisabled: Bool { get } +} + +// Default implementation for isDisabled +extension SegmentedTab { + var isDisabled: Bool { false } +} + +// MARK: - Segmented Tab Bar +struct SegmentedTabBar: View where Tab.AllCases: RandomAccessCollection { + @Binding var selectedTab: Tab + var onTabChange: (() -> Void)? + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 4) { + ForEach(Array(Tab.allCases), id: \.self) { tab in + SegmentedTabItem( + title: tab.title, + isSelected: selectedTab == tab, + isDisabled: tab.isDisabled + ) { + if !tab.isDisabled { + selectedTab = tab + onTabChange?() + } + } + } + } + .padding(4) + .background(Color.appInputFilled) + .clipShape(RoundedRectangle(cornerRadius: 14)) + } + .scrollClipDisabled() + } +} + +// MARK: - Segmented Tab Item +struct SegmentedTabItem: View { + let title: String + let isSelected: Bool + let isDisabled: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .font(.subheadline.weight(.medium)) + .foregroundColor(foregroundColor) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background(backgroundColor) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow( + color: isSelected && !isDisabled + ? Color.black.opacity(0.08) + : Color.clear, + radius: 2, + x: 0, + y: 1 + ) + } + .disabled(isDisabled) + } + + private var foregroundColor: Color { + if isDisabled { + return Color.appMutedForegroundAdaptive.opacity(0.5) + } + return isSelected ? .appForegroundAdaptive : .appMutedForegroundAdaptive + } + + private var backgroundColor: Color { + if isSelected && !isDisabled { + return Color.appBackgroundAdaptive + } + return Color.clear + } +} diff --git a/apps/ios/Plotwist/Plotwist/Components/StarRatingView.swift b/apps/ios/Plotwist/Plotwist/Components/StarRatingView.swift new file mode 100644 index 00000000..064f5b9f --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/StarRatingView.swift @@ -0,0 +1,91 @@ +// +// StarRatingView.swift +// Plotwist +// + +import SwiftUI + +struct StarRatingView: View { + @Binding var rating: Double + let maxRating: Int = 5 + let size: CGFloat + let interactive: Bool + + init(rating: Binding, size: CGFloat = 24, interactive: Bool = true) { + self._rating = rating + self.size = size + self.interactive = interactive + } + + var body: some View { + HStack(spacing: 4) { + ForEach(1...maxRating, id: \.self) { index in + GeometryReader { geometry in + ZStack { + Image(systemName: starImage(for: index)) + .font(.system(size: size)) + .foregroundColor(starColor(for: index)) + .frame(width: geometry.size.width, height: geometry.size.height) + } + .contentShape(Rectangle()) + .gesture( + DragGesture(minimumDistance: 0) + .onEnded { value in + if interactive { + handleTap(at: value.location, in: geometry.size, for: index) + } + } + ) + } + .frame(width: size, height: size) + } + } + .frame(height: size) + } + + private func handleTap(at location: CGPoint, in size: CGSize, for index: Int) { + let tapPosition = location.x + + // If tapped on left half, use .5, if right half use 1.0 + if tapPosition < size.width / 2 { + rating = Double(index) - 0.5 + } else { + rating = Double(index) + } + } + + private func starImage(for index: Int) -> String { + let fillLevel = rating - Double(index - 1) + + if fillLevel >= 1.0 { + return "star.fill" + } else if fillLevel >= 0.5 { + return "star.leadinghalf.filled" + } else { + return "star.fill" + } + } + + private func starColor(for index: Int) -> Color { + let fillLevel = rating - Double(index - 1) + + if fillLevel >= 1.0 { + return .appStarYellow + } else if fillLevel >= 0.5 { + return .appStarYellow + } else { + return Color.gray.opacity(0.3) + } + } +} + +// MARK: - Preview +#Preview { + VStack(spacing: 20) { + StarRatingView(rating: .constant(0), size: 32) + StarRatingView(rating: .constant(2.5), size: 32) + StarRatingView(rating: .constant(5), size: 32) + StarRatingView(rating: .constant(3.5), size: 24, interactive: false) + } + .padding() +} diff --git a/apps/ios/Plotwist/Plotwist/Components/StatusSheet.swift b/apps/ios/Plotwist/Plotwist/Components/StatusSheet.swift new file mode 100644 index 00000000..03958908 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/StatusSheet.swift @@ -0,0 +1,451 @@ +// +// StatusSheet.swift +// Plotwist +// + +import SwiftUI + +struct StatusSheet: View { + let mediaId: Int + let mediaType: String + let initialStatus: UserItemStatus? + let initialItemId: String? + let initialWatchEntries: [WatchEntry] + let onStatusChanged: (UserItemStatus?, [WatchEntry]) -> Void + + @Environment(\.dismiss) private var dismiss + @ObservedObject private var themeManager = ThemeManager.shared + @State private var isLoading = false + @State private var isAddingRewatch = false + @State private var selectedStatus: UserItemStatus? + @State private var currentItemId: String? + @State private var watchEntries: [WatchEntry] = [] + @State private var showErrorAlert = false + @State private var errorMessage = "" + + init( + mediaId: Int, + mediaType: String, + currentStatus: UserItemStatus?, + currentItemId: String?, + watchEntries: [WatchEntry] = [], + onStatusChanged: @escaping (UserItemStatus?, [WatchEntry]) -> Void + ) { + self.mediaId = mediaId + self.mediaType = mediaType + self.initialStatus = currentStatus + self.initialItemId = currentItemId + self.initialWatchEntries = watchEntries + self.onStatusChanged = onStatusChanged + _selectedStatus = State(initialValue: currentStatus) + _currentItemId = State(initialValue: currentItemId) + _watchEntries = State(initialValue: watchEntries) + } + + private var sheetHeight: CGFloat { + // Base: drag indicator (33) + title (44) + grid 2x2 (172) + bottom (24) = 273 + let baseHeight: CGFloat = 280 + + if selectedStatus == .watched && !watchEntries.isEmpty { + // Base height + rewatch section + let rewatchHeaderHeight: CGFloat = 50 + let entryHeight: CGFloat = 32 + let entriesHeight = CGFloat(watchEntries.count) * entryHeight + return min(baseHeight + rewatchHeaderHeight + entriesHeight + 24, 580) + } + return baseHeight + } + + var body: some View { + FloatingSheetContainer { + ScrollView { + VStack(spacing: 0) { + // Drag Indicator + RoundedRectangle(cornerRadius: 2.5) + .fill(Color.gray.opacity(0.4)) + .frame(width: 36, height: 5) + .padding(.top, 12) + .padding(.bottom, 16) + + // Title + Text(L10n.current.updateStatus) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 20) + + // Status Options Grid + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { + ForEach(UserItemStatus.allCases, id: \.rawValue) { status in + StatusOptionButton( + status: status, + isSelected: selectedStatus == status, + isLoading: isLoading && selectedStatus == status, + rewatchCount: status == .watched ? watchEntries.count : 0, + action: { + handleStatusChange(status) + } + ) + } + } + .padding(.horizontal, 24) + + // Rewatch Section - Only shows when status is WATCHED + if selectedStatus == .watched && !watchEntries.isEmpty { + VStack(alignment: .leading, spacing: 16) { + // Header + HStack { + Text(L10n.current.watchLog) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + Button(action: addRewatch) { + if isAddingRewatch { + ProgressView() + .tint(.appForegroundAdaptive) + } else { + Text("+ Rewatch") + .font(.caption) + .foregroundColor(.appForegroundAdaptive) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Color.appInputFilled) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .disabled(isAddingRewatch) + } + + // Watch entries timeline + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(watchEntries.enumerated()), id: \.element.id) { index, entry in + WatchEntryRow( + index: index, + entry: entry, + isLast: index == watchEntries.count - 1, + canDelete: watchEntries.count > 1, + onDelete: { + deleteWatchEntry(entry) + } + ) + } + } + } + .padding(.horizontal, 24) + .padding(.top, 24) + } + + Spacer() + .frame(height: 24) + } + } + } + .floatingSheetPresentation(height: sheetHeight) + .preferredColorScheme(themeManager.current.colorScheme) + .alert("Error", isPresented: $showErrorAlert) { + Button("OK", role: .cancel) {} + } message: { + Text(errorMessage) + } + } + + private func handleStatusChange(_ status: UserItemStatus) { + // If tapping the same status, remove it + if status == selectedStatus, let itemId = currentItemId { + isLoading = true + + Task { + do { + try await UserItemService.shared.deleteUserItem(id: itemId, tmdbId: mediaId, mediaType: mediaType) + + // Invalidate collection cache + CollectionCache.shared.invalidateCache() + + await MainActor.run { + isLoading = false + selectedStatus = nil + currentItemId = nil + watchEntries = [] + onStatusChanged(nil, []) + dismiss() + } + } catch { + await MainActor.run { + isLoading = false + errorMessage = error.localizedDescription + showErrorAlert = true + } + } + } + } else { + // Set new status + isLoading = true + selectedStatus = status + + Task { + do { + let apiMediaType = mediaType == "movie" ? "MOVIE" : "TV_SHOW" + let userItem = try await UserItemService.shared.upsertUserItem( + tmdbId: mediaId, + mediaType: apiMediaType, + status: status + ) + + // Invalidate collection cache + CollectionCache.shared.invalidateCache() + + await MainActor.run { + isLoading = false + // Update currentItemId with the newly created/updated item + currentItemId = userItem.id + + // If status is WATCHED, fetch the watch entries and stay on sheet + if status == .watched { + watchEntries = userItem.watchEntries ?? [] + onStatusChanged(status, watchEntries) + // Don't dismiss - let user see/add rewatches + } else { + watchEntries = [] + onStatusChanged(status, []) + dismiss() + } + } + } catch { + await MainActor.run { + isLoading = false + errorMessage = error.localizedDescription + showErrorAlert = true + } + } + } + } + } + + private func addRewatch() { + guard let itemId = currentItemId else { return } + + isAddingRewatch = true + + Task { + do { + let newEntry = try await UserItemService.shared.addWatchEntry(userItemId: itemId) + + await MainActor.run { + isAddingRewatch = false + watchEntries.append(newEntry) + onStatusChanged(selectedStatus, watchEntries) + } + } catch { + await MainActor.run { + isAddingRewatch = false + errorMessage = error.localizedDescription + showErrorAlert = true + } + } + } + } + + private func deleteWatchEntry(_ entry: WatchEntry) { + Task { + do { + try await UserItemService.shared.deleteWatchEntry(id: entry.id) + + await MainActor.run { + watchEntries.removeAll { $0.id == entry.id } + onStatusChanged(selectedStatus, watchEntries) + } + } catch { + await MainActor.run { + errorMessage = error.localizedDescription + showErrorAlert = true + } + } + } + } +} + +// MARK: - Watch Entry Row +struct WatchEntryRow: View { + let index: Int + let entry: WatchEntry + let isLast: Bool + let canDelete: Bool + let onDelete: () -> Void + + private var formattedDate: String { + guard let date = entry.date else { return "" } + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter.string(from: date) + } + + private var ordinalLabel: String { + if index == 0 { + return L10n.current.firstTime + } else { + return L10n.current.nthTime.replacingOccurrences(of: "%@", with: ordinalNumber) + } + } + + private var ordinalNumber: String { + let number = index + 1 + let language = Language.current + + switch language { + case .enUS: + // English ordinals: 1st, 2nd, 3rd, 4th... + switch number { + case 1: return "1st" + case 2: return "2nd" + case 3: return "3rd" + default: return "\(number)th" + } + case .ptBR, .esES, .itIT: + // Portuguese/Spanish/Italian: 1ª, 2ª, 3ª... + return "\(number)ª" + case .frFR: + // French: 1ère, 2ème, 3ème... + return number == 1 ? "1ère" : "\(number)ème" + case .deDE: + // German: 1., 2., 3.... + return "\(number)." + case .jaJP: + // Japanese: 1, 2, 3... + return "\(number)" + } + } + + private var isFirst: Bool { + index == 0 + } + + var body: some View { + HStack(spacing: 12) { + // Timeline indicator + ZStack(alignment: .center) { + VStack(spacing: 0) { + // Top line (for non-first items) - connects from previous dot + Rectangle() + .fill(isFirst ? Color.clear : Color.appMutedForegroundAdaptive.opacity(0.3)) + .frame(width: 1, height: 12) + + // Space for dot + Color.clear + .frame(height: 8) + + // Bottom line (for non-last items) - connects to next dot + Rectangle() + .fill(isLast ? Color.clear : Color.appMutedForegroundAdaptive.opacity(0.3)) + .frame(width: 1, height: 12) + } + + // Dot - centered + Circle() + .fill(Color.appMutedForegroundAdaptive.opacity(0.5)) + .frame(width: 8, height: 8) + } + .frame(width: 8, height: 32) + + // Content + HStack(spacing: 6) { + Text(ordinalLabel) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + + Circle() + .fill(Color.appMutedForegroundAdaptive.opacity(0.5)) + .frame(width: 4, height: 4) + + Text(formattedDate) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + + Spacer() + + if canDelete { + Button(action: onDelete) { + Image(systemName: "xmark") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.appMutedForegroundAdaptive) + } + } + } + } + .frame(height: 32) + } +} + +// MARK: - Status Option Button +struct StatusOptionButton: View { + let status: UserItemStatus + let isSelected: Bool + let isLoading: Bool + let rewatchCount: Int + let action: () -> Void + + private var iconColor: Color { + switch status { + case .watched: return .green + case .watching: return .blue + case .watchlist: return .orange + case .dropped: return .red + } + } + + var body: some View { + Button(action: action) { + VStack(spacing: 10) { + if isLoading { + ProgressView() + .tint(.appMutedForegroundAdaptive) + } else { + ZStack(alignment: .topTrailing) { + Image(systemName: status.icon) + .font(.system(size: 22)) + .foregroundColor(isSelected ? iconColor : .appMutedForegroundAdaptive) + + // Rewatch count badge + if isSelected && rewatchCount > 1 { + Text("\(rewatchCount)x") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .background(iconColor) + .clipShape(Capsule()) + .offset(x: 16, y: -8) + } + } + + Text(status.displayName(strings: L10n.current)) + .font(.subheadline.weight(.medium)) + .foregroundColor(.appMutedForegroundAdaptive) + } + } + .frame(maxWidth: .infinity) + .frame(height: 80) + .background(Color.appInputFilled) + .cornerRadius(12) + } + .disabled(isLoading) + } +} + +// MARK: - Preview +#Preview { + StatusSheet( + mediaId: 550, + mediaType: "movie", + currentStatus: .watched, + currentItemId: "123", + watchEntries: [ + WatchEntry(id: "1", watchedAt: "2025-01-10T12:00:00.000Z"), + WatchEntry(id: "2", watchedAt: "2025-01-15T12:00:00.000Z"), + WatchEntry(id: "3", watchedAt: "2025-01-17T12:00:00.000Z"), + ], + onStatusChanged: { _, _ in } + ) +} diff --git a/apps/ios/Plotwist/Plotwist/Components/UnderlineTabBar.swift b/apps/ios/Plotwist/Plotwist/Components/UnderlineTabBar.swift new file mode 100644 index 00000000..0fd128b8 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Components/UnderlineTabBar.swift @@ -0,0 +1,90 @@ +// +// UnderlineTabBar.swift +// Plotwist +// + +import SwiftUI + +// MARK: - Underline Tab Bar +struct UnderlineTabBar: View where Tab.AllCases: RandomAccessCollection { + @Binding var selectedTab: Tab + var onTabChange: (() -> Void)? + @Namespace private var namespace + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 0) { + ForEach(Array(Tab.allCases.enumerated()), id: \.element) { index, tab in + UnderlineTabItem( + title: tab.title, + isSelected: selectedTab == tab, + isDisabled: tab.isDisabled, + isFirst: index == 0, + namespace: namespace + ) { + if !tab.isDisabled { + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + selectedTab = tab + } + onTabChange?() + } + } + } + } + .padding(.leading, 24) + .padding(.trailing, 24) + } + .scrollClipDisabled() + .background( + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1), + alignment: .bottom + ) + } +} + +// MARK: - Underline Tab Item +struct UnderlineTabItem: View { + let title: String + let isSelected: Bool + let isDisabled: Bool + let isFirst: Bool + let namespace: Namespace.ID + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 0) { + Text(title) + .font(.subheadline.weight(.medium)) + .foregroundColor(foregroundColor) + .padding(.leading, isFirst ? 0 : 16) + .padding(.trailing, 16) + .padding(.vertical, 12) + + // Indicator matching text width, attached to bottom border + // Only rounded at top + UnevenRoundedRectangle( + topLeadingRadius: 4, + bottomLeadingRadius: 0, + bottomTrailingRadius: 0, + topTrailingRadius: 4 + ) + .fill(isSelected ? Color.appForegroundAdaptive : Color.clear) + .frame(height: 4) + .padding(.leading, isFirst ? 0 : 16) + .padding(.trailing, 16) + .matchedGeometryEffect(id: "underline", in: namespace, isSource: isSelected) + } + } + .disabled(isDisabled) + } + + private var foregroundColor: Color { + if isDisabled { + return Color.appMutedForegroundAdaptive.opacity(0.5) + } + return isSelected ? .appForegroundAdaptive : .appMutedForegroundAdaptive + } +} diff --git a/apps/ios/Plotwist/Plotwist/Extensions/View+Sheet.swift b/apps/ios/Plotwist/Plotwist/Extensions/View+Sheet.swift new file mode 100644 index 00000000..57b4154f --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Extensions/View+Sheet.swift @@ -0,0 +1,55 @@ +// +// View+Sheet.swift +// Plotwist +// + +import SwiftUI + +// MARK: - Sheet Style Configuration +enum SheetStyle { + /// Margem horizontal do sheet flutuante + static let horizontalPadding: CGFloat = 16 + /// Raio de arredondamento do sheet + static let cornerRadius: CGFloat = 32 + /// Altura extra para compensar o padding (padding * 2) + static let heightOffset: CGFloat = 32 +} + +// MARK: - Floating Sheet Container +/// Container que aplica o estilo flutuante com margem e arredondamento +struct FloatingSheetContainer: View { + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + VStack { + Spacer() + content + .background(Color.appBackgroundAdaptive) + .clipShape(RoundedRectangle(cornerRadius: SheetStyle.cornerRadius)) + .padding(.horizontal, SheetStyle.horizontalPadding) + } + } +} + +// MARK: - View Extension +extension View { + /// Aplica os modificadores de apresentação para sheet flutuante + func floatingSheetPresentation(height: CGFloat) -> some View { + self + .presentationDetents([.height(height + SheetStyle.heightOffset)]) + .presentationBackground(.clear) + .presentationDragIndicator(.hidden) + } + + /// Aplica os modificadores de apresentação para sheet flutuante com detents customizados + func floatingSheetPresentation(detents: Set) -> some View { + self + .presentationDetents(detents) + .presentationBackground(.clear) + .presentationDragIndicator(.hidden) + } +} diff --git a/apps/ios/Plotwist/Plotwist/Localization/Language.swift b/apps/ios/Plotwist/Plotwist/Localization/Language.swift new file mode 100644 index 00000000..fbee8cad --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Localization/Language.swift @@ -0,0 +1,60 @@ +// +// Language.swift +// Plotwist +// + +import Foundation + +enum Language: String, CaseIterable { + case enUS = "en-US" + case ptBR = "pt-BR" + case esES = "es-ES" + case frFR = "fr-FR" + case deDE = "de-DE" + case itIT = "it-IT" + case jaJP = "ja-JP" + + var displayName: String { + switch self { + case .enUS: return "English" + case .ptBR: return "Português" + case .esES: return "Español" + case .frFR: return "Français" + case .deDE: return "Deutsch" + case .itIT: return "Italiano" + case .jaJP: return "日本語" + } + } + + var flag: String { + switch self { + case .enUS: return "🇺🇸" + case .ptBR: return "🇧🇷" + case .esES: return "🇪🇸" + case .frFR: return "🇫🇷" + case .deDE: return "🇩🇪" + case .itIT: return "🇮🇹" + case .jaJP: return "🇯🇵" + } + } + + static var current: Language { + get { + if let saved = UserDefaults.standard.string(forKey: "language"), + let lang = Language(rawValue: saved) { + return lang + } + + let preferredLanguage = Locale.preferredLanguages.first ?? "en-US" + return Language(rawValue: preferredLanguage) ?? .enUS + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "language") + NotificationCenter.default.post(name: .languageChanged, object: nil) + } + } +} + +extension Notification.Name { + static let languageChanged = Notification.Name("languageChanged") +} diff --git a/apps/ios/Plotwist/Plotwist/Localization/Strings.swift b/apps/ios/Plotwist/Plotwist/Localization/Strings.swift new file mode 100644 index 00000000..04b43b70 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Localization/Strings.swift @@ -0,0 +1,1241 @@ +// +// Strings.swift +// Plotwist +// + +import Foundation + +enum L10n { + static var current: Strings { strings[Language.current] ?? strings[.enUS]! } + + private static let strings: [Language: Strings] = [ + .enUS: Strings( + // Login + loginLabel: "Email or username", + loginPlaceholder: "Email or username", + passwordLabel: "Password", + passwordPlaceholder: "*********", + accessButton: "Access", + doNotHaveAccount: "Don't have an account?", + createNow: "Create now", + loginRequired: "Please enter your email or username.", + passwordRequired: "Please enter your password.", + passwordLength: "Your password must be at least 8 characters long.", + invalidCredentials: "Invalid login credentials.", + continueWithGoogle: "Continue with Google", + continueWithApple: "Continue with Apple", + or: "or", + // Sign Up + startNow: "Start now", + startYourJourney: "Start your journey in just a few steps.", + emailLabel: "Email", + emailPlaceholder: "email@domain.com", + continueButton: "Continue", + alreadyHaveAccount: "Already have an account?", + accessNow: "Access now", + selectUsername: "Select your username", + selectUsernameDescription: + "Choose your username and finish your sign-up to start using the platform.", + usernamePlaceholder: "john-doe", + finishSignUp: "Finish sign-up", + emailRequired: "Please enter your email.", + emailInvalid: "Please enter a valid email.", + usernameRequired: "Please enter a username.", + emailAlreadyTaken: "This email is already in use.", + usernameAlreadyTaken: "This username is already taken.", + signUpSuccess: "Registration successful. Welcome! 🎉", + // Search + searchPlaceholder: "Search movies, series, people...", + movies: "Movies", + tvSeries: "TV Series", + people: "People", + noResults: "No results found.", + cancel: "Cancel", + popularMovies: "Popular Movies", + popularTVSeries: "Popular TV Series", + popularAnimes: "Popular Animes", + popularDoramas: "Popular Doramas", + animes: "Animes", + doramas: "Doramas", + seeAllMovies: "See all movies", + seeAllTVSeries: "See all series", + seeAllAnimes: "See all animes", + seeAllDoramas: "See all doramas", + settings: "Settings", + theme: "Theme", + themeSystem: "System", + themeLight: "Light", + themeDark: "Dark", + language: "Language", + done: "Done", + signOut: "Sign Out", + // Reviews + whatDidYouThink: "What did you think?", + review: "Review", + reviewed: "Reviewed", + shareYourOpinion: "Share your opinion here...", + containSpoilers: "Contains spoilers", + submitReview: "Submit review", + editReview: "Edit review", + reviewRequired: "Please write your review.", + reviewSuccess: "Review submitted successfully!", + reviewUpdateSuccess: "Review updated successfully!", + reviewDeleteSuccess: "Review deleted successfully!", + tabReviews: "Reviews", + reviewSingular: "Review", + tabWhereToWatch: "Where to Watch", + tabCredits: "Credits", + tabRecommendations: "Recommendations", + tabSimilar: "Similar", + tabImages: "Images", + tabVideos: "Videos", + beFirstToReview: "Be the first to leave your opinion", + seeAll: "See all reviews", + showMore: "Show more", + // Movie Categories + nowPlaying: "Now Playing", + popular: "Popular", + topRated: "Top Rated", + upcoming: "Upcoming", + discover: "Discover", + // TV Series Categories + airingToday: "Airing Today", + onTheAir: "On The Air", + // Images + images: "Images", + backdrops: "Backdrops", + posters: "Posters", + noImagesFound: "No images found.", + deleteReview: "Delete Review", + deleteReviewConfirmation: "Are you sure you want to delete this review?", + delete: "Delete", + // Status + updateStatus: "Update Status", + watched: "Watched", + watching: "Watching", + watchlist: "Watchlist", + dropped: "Dropped", + // Rewatch + watchLog: "Watch Log", + addRewatch: "Add Rewatch", + firstTime: "1st time", + nthTime: "%@ time", + // Where to Watch + stream: "Stream", + rent: "Rent", + buy: "Buy", + unavailable: "Unavailable", + // Profile + profile: "Profile", + memberSince: "Member since", + editProfile: "Edit", + accountData: "Account", + preferences: "Preferences", + editPicture: "Edit picture", + username: "Username", + region: "Region", + streamingServices: "Streaming Services", + notSet: "Not set", + searchRegion: "Search region...", + searchStreamingServices: "Search services...", + selectRegionFirst: "Select a region first", + servicesSelected: "%d selected", + streamingServicesHint: "Showing services available in your selected region.", + resultsBasedOnPreferences: "Results based on your preferences", + collection: "Collection", + reviews: "Reviews", + soundtracks: "Soundtracks", + biography: "Biography", + biographyPlaceholder: "Tell us about yourself...", + // Home + goodMorning: "Good morning", + goodAfternoon: "Good afternoon", + goodEvening: "Good evening", + continueWatching: "Continue Watching", + upNext: "Up Next", + // Collection + partOf: "Part of", + seeCollection: "See Collection", + // Seasons + tabSeasons: "Seasons", + episodes: "Episodes", + episodesCount: "%d episodes", + episodesWatchedCount: "%d of %total watched", + grid: "Grid", + overview: "Overview", + // Ratings + ratingAwesome: "Awesome", + ratingGreat: "Great", + ratingGood: "Good", + ratingBad: "Bad", + ratingTerrible: "Terrible" + ), + .ptBR: Strings( + loginLabel: "E-mail ou nome de usuário", + loginPlaceholder: "E-mail ou nome de usuário", + passwordLabel: "Senha", + passwordPlaceholder: "*********", + accessButton: "Acessar", + doNotHaveAccount: "Não tem uma conta?", + createNow: "Crie agora", + loginRequired: "Por favor, insira seu e-mail ou nome de usuário.", + passwordRequired: "Por favor, insira sua senha.", + passwordLength: "Sua senha deve ter pelo menos 8 caracteres.", + invalidCredentials: "Credenciais de login inválidas.", + continueWithGoogle: "Continuar com Google", + continueWithApple: "Continuar com Apple", + or: "ou", + startNow: "Comece agora", + startYourJourney: "Comece sua jornada em poucos passos.", + emailLabel: "E-mail", + emailPlaceholder: "email@dominio.com", + continueButton: "Continuar", + alreadyHaveAccount: "Já tem uma conta?", + accessNow: "Acesse agora", + selectUsername: "Escolha seu nome de usuário", + selectUsernameDescription: + "Escolha seu nome de usuário e finalize seu cadastro para começar a usar a plataforma.", + usernamePlaceholder: "joao-silva", + finishSignUp: "Finalizar cadastro", + emailRequired: "Por favor, insira seu e-mail.", + emailInvalid: "Por favor, insira um e-mail válido.", + usernameRequired: "Por favor, insira um nome de usuário.", + emailAlreadyTaken: "Este e-mail já está em uso.", + usernameAlreadyTaken: "Este nome de usuário já está em uso.", + signUpSuccess: "Cadastro realizado com sucesso. Bem-vindo! 🎉", + searchPlaceholder: "Buscar filmes, séries, pessoas...", + movies: "Filmes", + tvSeries: "Séries de TV", + people: "Pessoas", + noResults: "Nenhum resultado encontrado.", + cancel: "Cancelar", + popularMovies: "Filmes Populares", + popularTVSeries: "Séries Populares", + popularAnimes: "Animes Populares", + popularDoramas: "Doramas Populares", + animes: "Animes", + doramas: "Doramas", + seeAllMovies: "Ver todos os filmes", + seeAllTVSeries: "Ver todas as séries", + seeAllAnimes: "Ver todos os animes", + seeAllDoramas: "Ver todos os doramas", + settings: "Configurações", + theme: "Tema", + themeSystem: "Sistema", + themeLight: "Claro", + themeDark: "Escuro", + language: "Idioma", + done: "Concluído", + signOut: "Sair", + whatDidYouThink: "O que você achou?", + review: "Avaliar", + reviewed: "Avaliado", + shareYourOpinion: "Compartilhe sua opinião aqui...", + containSpoilers: "Contém spoilers", + submitReview: "Enviar avaliação", + editReview: "Editar avaliação", + reviewRequired: "Por favor, escreva sua avaliação.", + reviewSuccess: "Avaliação enviada com sucesso!", + reviewUpdateSuccess: "Avaliação atualizada com sucesso!", + reviewDeleteSuccess: "Avaliação excluída com sucesso!", + tabReviews: "Avaliações", + reviewSingular: "Avaliação", + tabWhereToWatch: "Onde Assistir", + tabCredits: "Elenco", + tabRecommendations: "Recomendações", + tabSimilar: "Similares", + tabImages: "Imagens", + tabVideos: "Vídeos", + beFirstToReview: "Seja o primeiro a deixar sua opinião", + seeAll: "Ver todas as avaliações", + showMore: "Ver mais", + nowPlaying: "Em Cartaz", + popular: "Popular", + topRated: "Mais Bem Avaliados", + upcoming: "Em Breve", + discover: "Descobrir", + airingToday: "No Ar Hoje", + onTheAir: "Em Exibição", + images: "Imagens", + backdrops: "Backdrops", + posters: "Pôsteres", + noImagesFound: "Nenhuma imagem encontrada.", + deleteReview: "Excluir Avaliação", + deleteReviewConfirmation: "Tem certeza que deseja excluir esta avaliação?", + delete: "Excluir", + updateStatus: "Atualizar Status", + watched: "Assistido", + watching: "Assistindo", + watchlist: "Lista", + dropped: "Abandonado", + watchLog: "Registro de visualizações", + addRewatch: "Adicionar rewatch", + firstTime: "1ª vez", + nthTime: "%@ vez", + stream: "Streaming", + rent: "Alugar", + buy: "Comprar", + unavailable: "Indisponível", + profile: "Perfil", + memberSince: "Membro desde", + editProfile: "Editar", + accountData: "Conta", + preferences: "Preferências", + editPicture: "Editar foto", + username: "Nome de usuário", + region: "Região", + streamingServices: "Serviços de Streaming", + notSet: "Não definido", + searchRegion: "Buscar região...", + searchStreamingServices: "Buscar serviços...", + selectRegionFirst: "Selecione uma região primeiro", + servicesSelected: "%d selecionados", + streamingServicesHint: "Exibindo serviços disponíveis na região selecionada.", + resultsBasedOnPreferences: "Resultados baseados nas suas preferências", + collection: "Coleção", + reviews: "Reviews", + soundtracks: "Trilhas", + biography: "Biografia", + biographyPlaceholder: "Conte um pouco sobre você...", + // Home + goodMorning: "Bom dia", + goodAfternoon: "Boa tarde", + goodEvening: "Boa noite", + continueWatching: "Continuar Assistindo", + upNext: "Para Assistir", + // Collection + partOf: "Parte de", + seeCollection: "Ver Coleção", + // Seasons + tabSeasons: "Temporadas", + episodes: "Episódios", + episodesCount: "%d episódios", + episodesWatchedCount: "%d de %total assistidos", + grid: "Grade", + overview: "Visão Geral", + // Ratings + ratingAwesome: "Incrível", + ratingGreat: "Ótimo", + ratingGood: "Bom", + ratingBad: "Ruim", + ratingTerrible: "Péssimo" + ), + .esES: Strings( + loginLabel: "Correo electrónico o nombre de usuario", + loginPlaceholder: "Correo electrónico o nombre de usuario", + passwordLabel: "Contraseña", + passwordPlaceholder: "*********", + accessButton: "Acceder", + doNotHaveAccount: "¿No tienes una cuenta?", + createNow: "Crea una ahora", + loginRequired: "Por favor, introduce tu correo electrónico o nombre de usuario.", + passwordRequired: "Por favor, introduce tu contraseña.", + passwordLength: "Tu contraseña debe tener al menos 8 caracteres.", + invalidCredentials: "Credenciales de inicio de sesión no válidas.", + continueWithGoogle: "Continuar con Google", + continueWithApple: "Continuar con Apple", + or: "o", + startNow: "Empieza ahora", + startYourJourney: "Comienza tu viaje en unos pocos pasos.", + emailLabel: "Correo electrónico", + emailPlaceholder: "email@dominio.com", + continueButton: "Continuar", + alreadyHaveAccount: "¿Ya tienes una cuenta?", + accessNow: "Accede ahora", + selectUsername: "Selecciona tu nombre de usuario", + selectUsernameDescription: + "Elige tu nombre de usuario y finaliza tu registro para comenzar a usar la plataforma.", + usernamePlaceholder: "juan-perez", + finishSignUp: "Finalizar registro", + emailRequired: "Por favor, introduce tu correo electrónico.", + emailInvalid: "Por favor, introduce un correo electrónico válido.", + usernameRequired: "Por favor, introduce un nombre de usuario.", + emailAlreadyTaken: "Este correo electrónico ya está en uso.", + usernameAlreadyTaken: "Este nombre de usuario ya está en uso.", + signUpSuccess: "Registro exitoso. ¡Bienvenido! 🎉", + searchPlaceholder: "Buscar películas, series, personas...", + movies: "Películas", + tvSeries: "Series de TV", + people: "Personas", + noResults: "No se encontraron resultados.", + cancel: "Cancelar", + popularMovies: "Películas Populares", + popularTVSeries: "Series Populares", + popularAnimes: "Animes Populares", + popularDoramas: "Doramas Populares", + animes: "Animes", + doramas: "Doramas", + seeAllMovies: "Ver todas las películas", + seeAllTVSeries: "Ver todas las series", + seeAllAnimes: "Ver todos los animes", + seeAllDoramas: "Ver todos los doramas", + settings: "Configuración", + theme: "Tema", + themeSystem: "Sistema", + themeLight: "Claro", + themeDark: "Oscuro", + language: "Idioma", + done: "Listo", + signOut: "Cerrar sesión", + whatDidYouThink: "¿Qué te pareció?", + review: "Reseñar", + reviewed: "Reseñado", + shareYourOpinion: "Comparte tu opinión aquí...", + containSpoilers: "Contiene spoilers", + submitReview: "Enviar reseña", + editReview: "Editar reseña", + reviewRequired: "Por favor, escribe tu reseña.", + reviewSuccess: "¡Reseña enviada con éxito!", + reviewUpdateSuccess: "¡Reseña actualizada con éxito!", + reviewDeleteSuccess: "¡Reseña eliminada con éxito!", + tabReviews: "Reseñas", + reviewSingular: "Reseña", + tabWhereToWatch: "Dónde Ver", + tabCredits: "Créditos", + tabRecommendations: "Recomendaciones", + tabSimilar: "Similares", + tabImages: "Imágenes", + tabVideos: "Videos", + beFirstToReview: "Sé el primero en dejar tu opinión", + seeAll: "Ver todas las reseñas", + showMore: "Ver más", + nowPlaying: "En Cartelera", + popular: "Popular", + topRated: "Mejor Valoradas", + upcoming: "Próximamente", + discover: "Descubrir", + airingToday: "En Emisión Hoy", + onTheAir: "En Emisión", + images: "Imágenes", + backdrops: "Fondos", + posters: "Pósters", + noImagesFound: "No se encontraron imágenes.", + deleteReview: "Eliminar Reseña", + deleteReviewConfirmation: "¿Estás seguro de que deseas eliminar esta reseña?", + delete: "Eliminar", + updateStatus: "Actualizar Estado", + watched: "Visto", + watching: "Viendo", + watchlist: "Lista", + dropped: "Abandonado", + watchLog: "Registro de visualizaciones", + addRewatch: "Agregar rewatch", + firstTime: "1ª vez", + nthTime: "%@ vez", + stream: "Streaming", + rent: "Alquilar", + buy: "Comprar", + unavailable: "No disponible", + profile: "Perfil", + memberSince: "Miembro desde", + editProfile: "Editar", + accountData: "Cuenta", + preferences: "Preferencias", + editPicture: "Editar foto", + username: "Nombre de usuario", + region: "Región", + streamingServices: "Servicios de Streaming", + notSet: "No definido", + searchRegion: "Buscar región...", + searchStreamingServices: "Buscar servicios...", + selectRegionFirst: "Seleccione una región primero", + servicesSelected: "%d seleccionados", + streamingServicesHint: "Mostrando servicios disponibles en la región seleccionada.", + resultsBasedOnPreferences: "Resultados según tus preferencias", + collection: "Colección", + reviews: "Reseñas", + soundtracks: "Bandas Sonoras", + biography: "Biografía", + biographyPlaceholder: "Cuéntanos sobre ti...", + // Home + goodMorning: "Buenos días", + goodAfternoon: "Buenas tardes", + goodEvening: "Buenas noches", + continueWatching: "Seguir Viendo", + upNext: "Próximos", + // Collection + partOf: "Parte de", + seeCollection: "Ver Colección", + // Seasons + tabSeasons: "Temporadas", + episodes: "Episodios", + episodesCount: "%d episodios", + episodesWatchedCount: "%d de %total vistos", + grid: "Cuadrícula", + overview: "Resumen", + // Ratings + ratingAwesome: "Increíble", + ratingGreat: "Genial", + ratingGood: "Bueno", + ratingBad: "Malo", + ratingTerrible: "Terrible" + ), + .frFR: Strings( + loginLabel: "E-mail ou nom d'utilisateur", + loginPlaceholder: "E-mail ou nom d'utilisateur", + passwordLabel: "Mot de passe", + passwordPlaceholder: "*********", + accessButton: "Accéder", + doNotHaveAccount: "Vous n'avez pas de compte?", + createNow: "Créez-en un maintenant", + loginRequired: "Veuillez entrer votre e-mail ou nom d'utilisateur.", + passwordRequired: "Veuillez entrer votre mot de passe.", + passwordLength: "Votre mot de passe doit contenir au moins 8 caractères.", + invalidCredentials: "Identifiants de connexion invalides.", + continueWithGoogle: "Continuer avec Google", + continueWithApple: "Continuer avec Apple", + or: "ou", + startNow: "Commencez maintenant", + startYourJourney: "Commencez votre voyage en quelques étapes.", + emailLabel: "E-mail", + emailPlaceholder: "email@domaine.com", + continueButton: "Continuer", + alreadyHaveAccount: "Vous avez déjà un compte?", + accessNow: "Connectez-vous", + selectUsername: "Choisissez votre nom d'utilisateur", + selectUsernameDescription: + "Choisissez votre nom d'utilisateur et terminez votre inscription pour commencer à utiliser la plateforme.", + usernamePlaceholder: "jean-dupont", + finishSignUp: "Terminer l'inscription", + emailRequired: "Veuillez entrer votre e-mail.", + emailInvalid: "Veuillez entrer un e-mail valide.", + usernameRequired: "Veuillez entrer un nom d'utilisateur.", + emailAlreadyTaken: "Cet e-mail est déjà utilisé.", + usernameAlreadyTaken: "Ce nom d'utilisateur est déjà utilisé.", + signUpSuccess: "Inscription réussie. Bienvenue! 🎉", + searchPlaceholder: "Rechercher films, séries, personnes...", + movies: "Films", + tvSeries: "Séries TV", + people: "Personnes", + noResults: "Aucun résultat trouvé.", + cancel: "Annuler", + popularMovies: "Films Populaires", + popularTVSeries: "Séries Populaires", + popularAnimes: "Animes Populaires", + popularDoramas: "Doramas Populaires", + animes: "Animes", + doramas: "Doramas", + seeAllMovies: "Voir tous les films", + seeAllTVSeries: "Voir toutes les séries", + seeAllAnimes: "Voir tous les animes", + seeAllDoramas: "Voir tous les doramas", + settings: "Paramètres", + theme: "Thème", + themeSystem: "Système", + themeLight: "Clair", + themeDark: "Sombre", + language: "Langue", + done: "Terminé", + signOut: "Déconnexion", + whatDidYouThink: "Qu'en avez-vous pensé?", + review: "Évaluer", + reviewed: "Évalué", + shareYourOpinion: "Partagez votre opinion ici...", + containSpoilers: "Contient des spoilers", + submitReview: "Soumettre l'avis", + editReview: "Modifier l'avis", + reviewRequired: "Veuillez écrire votre avis.", + reviewSuccess: "Avis soumis avec succès!", + reviewUpdateSuccess: "Avis mis à jour avec succès!", + reviewDeleteSuccess: "Avis supprimé avec succès!", + tabReviews: "Avis", + reviewSingular: "Avis", + tabWhereToWatch: "Où Regarder", + tabCredits: "Crédits", + tabRecommendations: "Recommandations", + tabSimilar: "Similaires", + tabImages: "Images", + tabVideos: "Vidéos", + beFirstToReview: "Soyez le premier à donner votre avis", + seeAll: "Voir tous les avis", + showMore: "Voir plus", + nowPlaying: "À l'Affiche", + popular: "Populaire", + topRated: "Mieux Notés", + upcoming: "Prochainement", + discover: "Découvrir", + airingToday: "Diffusé Aujourd'hui", + onTheAir: "En Cours", + images: "Images", + backdrops: "Fonds d'écran", + posters: "Affiches", + noImagesFound: "Aucune image trouvée.", + deleteReview: "Supprimer l'avis", + deleteReviewConfirmation: "Êtes-vous sûr de vouloir supprimer cet avis?", + delete: "Supprimer", + updateStatus: "Mettre à jour le statut", + watched: "Vu", + watching: "En cours", + watchlist: "À voir", + dropped: "Abandonné", + watchLog: "Historique des visionnages", + addRewatch: "Ajouter un revisionnage", + firstTime: "1ère fois", + nthTime: "%@ fois", + stream: "Streaming", + rent: "Louer", + buy: "Acheter", + unavailable: "Indisponible", + profile: "Profil", + memberSince: "Membre depuis", + editProfile: "Modifier", + accountData: "Compte", + preferences: "Préférences", + editPicture: "Modifier la photo", + username: "Nom d'utilisateur", + region: "Région", + streamingServices: "Services de Streaming", + notSet: "Non défini", + searchRegion: "Rechercher une région...", + searchStreamingServices: "Rechercher des services...", + selectRegionFirst: "Sélectionnez d'abord une région", + servicesSelected: "%d sélectionnés", + streamingServicesHint: "Affichage des services disponibles dans la région sélectionnée.", + resultsBasedOnPreferences: "Résultats basés sur vos préférences", + collection: "Collection", + reviews: "Critiques", + soundtracks: "Bandes Sonores", + biography: "Biographie", + biographyPlaceholder: "Parlez-nous de vous...", + // Home + goodMorning: "Bonjour", + goodAfternoon: "Bon après-midi", + goodEvening: "Bonsoir", + continueWatching: "Continuer à Regarder", + upNext: "À Suivre", + // Collection + partOf: "Fait partie de", + seeCollection: "Voir la Collection", + // Seasons + tabSeasons: "Saisons", + episodes: "Épisodes", + episodesCount: "%d épisodes", + episodesWatchedCount: "%d sur %total vus", + grid: "Grille", + overview: "Aperçu", + // Ratings + ratingAwesome: "Incroyable", + ratingGreat: "Génial", + ratingGood: "Bon", + ratingBad: "Mauvais", + ratingTerrible: "Terrible" + ), + .deDE: Strings( + loginLabel: "E-Mail oder Benutzername", + loginPlaceholder: "E-Mail oder Benutzername", + passwordLabel: "Passwort", + passwordPlaceholder: "*********", + accessButton: "Zugreifen", + doNotHaveAccount: "Haben Sie kein Konto?", + createNow: "Jetzt erstellen", + loginRequired: "Bitte geben Sie Ihre E-Mail-Adresse oder Ihren Benutzernamen ein.", + passwordRequired: "Bitte geben Sie Ihr Passwort ein.", + passwordLength: "Ihr Passwort muss mindestens 8 Zeichen lang sein.", + invalidCredentials: "Ungültige Anmeldeinformationen.", + continueWithGoogle: "Weiter mit Google", + continueWithApple: "Weiter mit Apple", + or: "oder", + startNow: "Jetzt starten", + startYourJourney: "Beginnen Sie Ihre Reise in wenigen Schritten.", + emailLabel: "E-Mail", + emailPlaceholder: "email@domain.de", + continueButton: "Weiter", + alreadyHaveAccount: "Haben Sie bereits ein Konto?", + accessNow: "Jetzt anmelden", + selectUsername: "Wählen Sie Ihren Benutzernamen", + selectUsernameDescription: + "Wählen Sie Ihren Benutzernamen und schließen Sie Ihre Registrierung ab, um die Plattform zu nutzen.", + usernamePlaceholder: "max-mustermann", + finishSignUp: "Registrierung abschließen", + emailRequired: "Bitte geben Sie Ihre E-Mail-Adresse ein.", + emailInvalid: "Bitte geben Sie eine gültige E-Mail-Adresse ein.", + usernameRequired: "Bitte geben Sie einen Benutzernamen ein.", + emailAlreadyTaken: "Diese E-Mail-Adresse wird bereits verwendet.", + usernameAlreadyTaken: "Dieser Benutzername ist bereits vergeben.", + signUpSuccess: "Registrierung erfolgreich. Willkommen! 🎉", + searchPlaceholder: "Filme, Serien, Personen suchen...", + movies: "Filme", + tvSeries: "TV-Serien", + people: "Personen", + noResults: "Keine Ergebnisse gefunden.", + cancel: "Abbrechen", + popularMovies: "Beliebte Filme", + popularTVSeries: "Beliebte Serien", + popularAnimes: "Beliebte Animes", + popularDoramas: "Beliebte Doramas", + animes: "Animes", + doramas: "Doramas", + seeAllMovies: "Alle Filme anzeigen", + seeAllTVSeries: "Alle Serien anzeigen", + seeAllAnimes: "Alle Animes anzeigen", + seeAllDoramas: "Alle Doramas anzeigen", + settings: "Einstellungen", + theme: "Design", + themeSystem: "System", + themeLight: "Hell", + themeDark: "Dunkel", + language: "Sprache", + done: "Fertig", + signOut: "Abmelden", + whatDidYouThink: "Was haben Sie gedacht?", + review: "Bewerten", + reviewed: "Bewertet", + shareYourOpinion: "Teilen Sie Ihre Meinung hier...", + containSpoilers: "Enthält Spoiler", + submitReview: "Bewertung abschicken", + editReview: "Bewertung bearbeiten", + reviewRequired: "Bitte schreiben Sie Ihre Bewertung.", + reviewSuccess: "Bewertung erfolgreich eingereicht!", + reviewUpdateSuccess: "Bewertung erfolgreich aktualisiert!", + reviewDeleteSuccess: "Bewertung erfolgreich gelöscht!", + tabReviews: "Bewertungen", + reviewSingular: "Bewertung", + tabWhereToWatch: "Wo Ansehen", + tabCredits: "Besetzung", + tabRecommendations: "Empfehlungen", + tabSimilar: "Ähnliche", + tabImages: "Bilder", + tabVideos: "Videos", + beFirstToReview: "Sei der Erste, der seine Meinung teilt", + seeAll: "Alle Bewertungen anzeigen", + showMore: "Mehr anzeigen", + nowPlaying: "Jetzt im Kino", + popular: "Beliebt", + topRated: "Bestbewertet", + upcoming: "Demnächst", + discover: "Entdecken", + airingToday: "Heute im TV", + onTheAir: "Auf Sendung", + images: "Bilder", + backdrops: "Hintergründe", + posters: "Poster", + noImagesFound: "Keine Bilder gefunden.", + deleteReview: "Bewertung löschen", + deleteReviewConfirmation: "Möchten Sie diese Bewertung wirklich löschen?", + delete: "Löschen", + updateStatus: "Status aktualisieren", + watched: "Gesehen", + watching: "Schaue ich", + watchlist: "Watchlist", + dropped: "Abgebrochen", + watchLog: "Wiedergabeverlauf", + addRewatch: "Rewatch hinzufügen", + firstTime: "1. Mal", + nthTime: "%@ Mal", + stream: "Streaming", + rent: "Leihen", + buy: "Kaufen", + unavailable: "Nicht verfügbar", + profile: "Profil", + memberSince: "Mitglied seit", + editProfile: "Bearbeiten", + accountData: "Konto", + preferences: "Einstellungen", + editPicture: "Bild bearbeiten", + username: "Benutzername", + region: "Region", + streamingServices: "Streaming-Dienste", + notSet: "Nicht festgelegt", + searchRegion: "Region suchen...", + searchStreamingServices: "Dienste suchen...", + selectRegionFirst: "Wählen Sie zuerst eine Region", + servicesSelected: "%d ausgewählt", + streamingServicesHint: "Zeigt Dienste an, die in der ausgewählten Region verfügbar sind.", + resultsBasedOnPreferences: "Ergebnisse basierend auf Ihren Präferenzen", + collection: "Sammlung", + reviews: "Bewertungen", + soundtracks: "Soundtracks", + biography: "Biografie", + biographyPlaceholder: "Erzählen Sie uns von sich...", + // Home + goodMorning: "Guten Morgen", + goodAfternoon: "Guten Tag", + goodEvening: "Guten Abend", + continueWatching: "Weiterschauen", + upNext: "Als Nächstes", + // Collection + partOf: "Teil von", + seeCollection: "Sammlung ansehen", + // Seasons + tabSeasons: "Staffeln", + episodes: "Episoden", + episodesCount: "%d Episoden", + episodesWatchedCount: "%d von %total gesehen", + grid: "Raster", + overview: "Übersicht", + // Ratings + ratingAwesome: "Fantastisch", + ratingGreat: "Toll", + ratingGood: "Gut", + ratingBad: "Schlecht", + ratingTerrible: "Schrecklich" + ), + .itIT: Strings( + loginLabel: "E-mail o nome utente", + loginPlaceholder: "E-mail o nome utente", + passwordLabel: "Password", + passwordPlaceholder: "*********", + accessButton: "Accedi", + doNotHaveAccount: "Non hai un account?", + createNow: "Crea ora", + loginRequired: "Inserisci il tuo indirizzo e-mail o nome utente.", + passwordRequired: "Inserisci la tua password.", + passwordLength: "La tua password deve contenere almeno 8 caratteri.", + invalidCredentials: "Credenziali di accesso non valide.", + continueWithGoogle: "Continua con Google", + continueWithApple: "Continua con Apple", + or: "o", + startNow: "Inizia ora", + startYourJourney: "Inizia il tuo viaggio in pochi passi.", + emailLabel: "E-mail", + emailPlaceholder: "email@dominio.com", + continueButton: "Continua", + alreadyHaveAccount: "Hai già un account?", + accessNow: "Accedi ora", + selectUsername: "Scegli il tuo nome utente", + selectUsernameDescription: + "Scegli il tuo nome utente e completa la registrazione per iniziare a usare la piattaforma.", + usernamePlaceholder: "mario-rossi", + finishSignUp: "Completa registrazione", + emailRequired: "Inserisci la tua e-mail.", + emailInvalid: "Inserisci un'e-mail valida.", + usernameRequired: "Inserisci un nome utente.", + emailAlreadyTaken: "Questa email è già in uso.", + usernameAlreadyTaken: "Questo nome utente è già in uso.", + signUpSuccess: "Registrazione completata. Benvenuto! 🎉", + searchPlaceholder: "Cerca film, serie, persone...", + movies: "Film", + tvSeries: "Serie TV", + people: "Persone", + noResults: "Nessun risultato trovato.", + cancel: "Annulla", + popularMovies: "Film Popolari", + popularTVSeries: "Serie Popolari", + popularAnimes: "Anime Popolari", + popularDoramas: "Dorama Popolari", + animes: "Anime", + doramas: "Dorama", + seeAllMovies: "Vedi tutti i film", + seeAllTVSeries: "Vedi tutte le serie", + seeAllAnimes: "Vedi tutti gli anime", + seeAllDoramas: "Vedi tutti i dorama", + settings: "Impostazioni", + theme: "Tema", + themeSystem: "Sistema", + themeLight: "Chiaro", + themeDark: "Scuro", + language: "Lingua", + done: "Fatto", + signOut: "Esci", + whatDidYouThink: "Cosa ne pensi?", + review: "Recensire", + reviewed: "Recensito", + shareYourOpinion: "Condividi la tua opinione qui...", + containSpoilers: "Contiene spoiler", + submitReview: "Invia recensione", + editReview: "Modifica recensione", + reviewRequired: "Scrivi la tua recensione.", + reviewSuccess: "Recensione inviata con successo!", + reviewUpdateSuccess: "Recensione aggiornata con successo!", + reviewDeleteSuccess: "Recensione eliminata con successo!", + tabReviews: "Recensioni", + reviewSingular: "Recensione", + tabWhereToWatch: "Dove Guardare", + tabCredits: "Cast", + tabRecommendations: "Raccomandazioni", + tabSimilar: "Simili", + tabImages: "Immagini", + tabVideos: "Video", + beFirstToReview: "Sii il primo a lasciare la tua opinione", + seeAll: "Vedi tutte le recensioni", + showMore: "Mostra di più", + nowPlaying: "In Sala", + popular: "Popolari", + topRated: "Più Votati", + upcoming: "Prossimamente", + discover: "Scopri", + airingToday: "In Onda Oggi", + onTheAir: "In Onda", + images: "Immagini", + backdrops: "Sfondi", + posters: "Locandine", + noImagesFound: "Nessuna immagine trovata.", + deleteReview: "Elimina recensione", + deleteReviewConfirmation: "Sei sicuro di voler eliminare questa recensione?", + delete: "Elimina", + updateStatus: "Aggiorna stato", + watched: "Visto", + watching: "In visione", + watchlist: "Da vedere", + dropped: "Abbandonato", + watchLog: "Cronologia visualizzazioni", + addRewatch: "Aggiungi rewatch", + firstTime: "1ª volta", + nthTime: "%@ volta", + stream: "Streaming", + rent: "Noleggia", + buy: "Acquista", + unavailable: "Non disponibile", + profile: "Profilo", + memberSince: "Membro dal", + editProfile: "Modifica", + accountData: "Account", + preferences: "Preferenze", + editPicture: "Modifica foto", + username: "Nome utente", + region: "Regione", + streamingServices: "Servizi di Streaming", + notSet: "Non impostato", + searchRegion: "Cerca regione...", + searchStreamingServices: "Cerca servizi...", + selectRegionFirst: "Seleziona prima una regione", + servicesSelected: "%d selezionati", + streamingServicesHint: "Mostra i servizi disponibili nella regione selezionata.", + resultsBasedOnPreferences: "Risultati basati sulle tue preferenze", + collection: "Collezione", + reviews: "Recensioni", + soundtracks: "Colonne Sonore", + biography: "Biografia", + biographyPlaceholder: "Raccontaci di te...", + // Home + goodMorning: "Buongiorno", + goodAfternoon: "Buon pomeriggio", + goodEvening: "Buonasera", + continueWatching: "Continua a Guardare", + upNext: "Prossimi", + // Collection + partOf: "Parte di", + seeCollection: "Vedi Collezione", + // Seasons + tabSeasons: "Stagioni", + episodes: "Episodi", + episodesCount: "%d episodi", + episodesWatchedCount: "%d di %total visti", + grid: "Griglia", + overview: "Panoramica", + // Ratings + ratingAwesome: "Fantastico", + ratingGreat: "Ottimo", + ratingGood: "Buono", + ratingBad: "Cattivo", + ratingTerrible: "Terribile" + ), + .jaJP: Strings( + loginLabel: "メールアドレスまたはユーザー名", + loginPlaceholder: "メールアドレスまたはユーザー名", + passwordLabel: "パスワード", + passwordPlaceholder: "*********", + accessButton: "アクセス", + doNotHaveAccount: "アカウントをお持ちではありませんか?", + createNow: "今すぐ作成", + loginRequired: "メールアドレスまたはユーザー名を入力してください。", + passwordRequired: "パスワードを入力してください。", + passwordLength: "パスワードは8文字以上でなければなりません。", + invalidCredentials: "ログイン認証情報が無効です。", + continueWithGoogle: "Googleで続ける", + continueWithApple: "Appleで続ける", + or: "または", + startNow: "今すぐ始める", + startYourJourney: "数ステップで旅を始めましょう。", + emailLabel: "メールアドレス", + emailPlaceholder: "email@domain.com", + continueButton: "続ける", + alreadyHaveAccount: "すでにアカウントをお持ちですか?", + accessNow: "ログイン", + selectUsername: "ユーザー名を選択", + selectUsernameDescription: "ユーザー名を選択し、プラットフォームの利用を開始するためにサインアップを完了してください。", + usernamePlaceholder: "taro-yamada", + finishSignUp: "登録を完了", + emailRequired: "メールアドレスを入力してください。", + emailInvalid: "有効なメールアドレスを入力してください。", + usernameRequired: "ユーザー名を入力してください。", + emailAlreadyTaken: "このメールアドレスは既に使用されています。", + usernameAlreadyTaken: "このユーザー名は既に使用されています。", + signUpSuccess: "登録が完了しました。ようこそ!🎉", + searchPlaceholder: "映画、シリーズ、人物を検索...", + movies: "映画", + tvSeries: "テレビシリーズ", + people: "人物", + noResults: "結果が見つかりません。", + cancel: "キャンセル", + popularMovies: "人気の映画", + popularTVSeries: "人気のテレビシリーズ", + popularAnimes: "人気のアニメ", + popularDoramas: "人気のドラマ", + animes: "アニメ", + doramas: "ドラマ", + seeAllMovies: "すべての映画を見る", + seeAllTVSeries: "すべてのシリーズを見る", + seeAllAnimes: "すべてのアニメを見る", + seeAllDoramas: "すべてのドラマを見る", + settings: "設定", + theme: "テーマ", + themeSystem: "システム", + themeLight: "ライト", + themeDark: "ダーク", + language: "言語", + done: "完了", + signOut: "サインアウト", + whatDidYouThink: "どう思いましたか?", + review: "レビュー", + reviewed: "レビュー済み", + shareYourOpinion: "ここにあなたの意見を共有してください...", + containSpoilers: "ネタバレを含む", + submitReview: "レビューを送信", + editReview: "レビューを編集", + reviewRequired: "レビューを書いてください。", + reviewSuccess: "レビューが正常に送信されました!", + reviewUpdateSuccess: "レビューが正常に更新されました!", + reviewDeleteSuccess: "レビューが正常に削除されました!", + tabReviews: "レビュー", + reviewSingular: "レビュー", + tabWhereToWatch: "視聴方法", + tabCredits: "キャスト", + tabRecommendations: "おすすめ", + tabSimilar: "類似作品", + tabImages: "画像", + tabVideos: "動画", + beFirstToReview: "最初にレビューを書いてください", + seeAll: "すべてのレビューを見る", + showMore: "もっと見る", + nowPlaying: "上映中", + popular: "人気", + topRated: "高評価", + upcoming: "近日公開", + discover: "発見", + airingToday: "本日放送", + onTheAir: "放送中", + images: "画像", + backdrops: "背景", + posters: "ポスター", + noImagesFound: "画像が見つかりません。", + deleteReview: "レビューを削除", + deleteReviewConfirmation: "このレビューを削除してもよろしいですか?", + delete: "削除", + updateStatus: "ステータスを更新", + watched: "視聴済み", + watching: "視聴中", + watchlist: "ウォッチリスト", + dropped: "中断", + watchLog: "視聴履歴", + addRewatch: "再視聴を追加", + firstTime: "1回目", + nthTime: "%@回目", + stream: "配信", + rent: "レンタル", + buy: "購入", + unavailable: "利用不可", + profile: "プロフィール", + memberSince: "メンバー登録日", + editProfile: "編集", + accountData: "アカウント", + preferences: "設定", + editPicture: "写真を編集", + username: "ユーザー名", + region: "地域", + streamingServices: "ストリーミング", + notSet: "未設定", + searchRegion: "地域を検索...", + searchStreamingServices: "サービスを検索...", + selectRegionFirst: "最初に地域を選択してください", + servicesSelected: "%d 件選択", + streamingServicesHint: "選択した地域で利用可能なサービスを表示しています。", + resultsBasedOnPreferences: "設定に基づいた結果", + collection: "コレクション", + reviews: "レビュー", + soundtracks: "サウンドトラック", + biography: "自己紹介", + biographyPlaceholder: "自己紹介を書いてください...", + // Home + goodMorning: "おはようございます", + goodAfternoon: "こんにちは", + goodEvening: "こんばんは", + continueWatching: "視聴を続ける", + upNext: "次に見る", + // Collection + partOf: "の一部", + seeCollection: "コレクションを見る", + // Seasons + tabSeasons: "シーズン", + episodes: "エピソード", + episodesCount: "%d 話", + episodesWatchedCount: "%total話中%d話視聴済み", + grid: "グリッド", + overview: "概要", + // Ratings + ratingAwesome: "最高", + ratingGreat: "素晴らしい", + ratingGood: "良い", + ratingBad: "悪い", + ratingTerrible: "最悪" + ), + ] +} + +struct Strings { + // Login + let loginLabel: String + let loginPlaceholder: String + let passwordLabel: String + let passwordPlaceholder: String + let accessButton: String + let doNotHaveAccount: String + let createNow: String + let loginRequired: String + let passwordRequired: String + let passwordLength: String + let invalidCredentials: String + let continueWithGoogle: String + let continueWithApple: String + let or: String + // Sign Up + let startNow: String + let startYourJourney: String + let emailLabel: String + let emailPlaceholder: String + let continueButton: String + let alreadyHaveAccount: String + let accessNow: String + let selectUsername: String + let selectUsernameDescription: String + let usernamePlaceholder: String + let finishSignUp: String + let emailRequired: String + let emailInvalid: String + let usernameRequired: String + let emailAlreadyTaken: String + let usernameAlreadyTaken: String + let signUpSuccess: String + // Search + let searchPlaceholder: String + let movies: String + let tvSeries: String + let people: String + let noResults: String + let cancel: String + let popularMovies: String + let popularTVSeries: String + let popularAnimes: String + let popularDoramas: String + let animes: String + let doramas: String + let seeAllMovies: String + let seeAllTVSeries: String + let seeAllAnimes: String + let seeAllDoramas: String + // Settings + let settings: String + let theme: String + let themeSystem: String + let themeLight: String + let themeDark: String + let language: String + let done: String + let signOut: String + // Reviews + let whatDidYouThink: String + let review: String + let reviewed: String + let shareYourOpinion: String + let containSpoilers: String + let submitReview: String + let editReview: String + let reviewRequired: String + let reviewSuccess: String + let reviewUpdateSuccess: String + let reviewDeleteSuccess: String + // Tabs + let tabReviews: String + let reviewSingular: String + let tabWhereToWatch: String + let tabCredits: String + let tabRecommendations: String + let tabSimilar: String + let tabImages: String + let tabVideos: String + // Review List + let beFirstToReview: String + let seeAll: String + let showMore: String + // Movie Categories + let nowPlaying: String + let popular: String + let topRated: String + let upcoming: String + let discover: String + // TV Series Categories + let airingToday: String + let onTheAir: String + // Images + let images: String + let backdrops: String + let posters: String + let noImagesFound: String + // Delete Review + let deleteReview: String + let deleteReviewConfirmation: String + let delete: String + // Status + let updateStatus: String + let watched: String + let watching: String + let watchlist: String + let dropped: String + // Rewatch + let watchLog: String + let addRewatch: String + let firstTime: String + let nthTime: String + // Where to Watch + let stream: String + let rent: String + let buy: String + let unavailable: String + // Profile + let profile: String + let memberSince: String + let editProfile: String + let accountData: String + let preferences: String + let editPicture: String + let username: String + let region: String + let streamingServices: String + let notSet: String + let searchRegion: String + let searchStreamingServices: String + let selectRegionFirst: String + let servicesSelected: String + let streamingServicesHint: String + let resultsBasedOnPreferences: String + // Profile Tabs + let collection: String + let reviews: String + // Tab Bar + let soundtracks: String + // Biography + let biography: String + let biographyPlaceholder: String + // Home + let goodMorning: String + let goodAfternoon: String + let goodEvening: String + let continueWatching: String + let upNext: String + // Collection + let partOf: String + let seeCollection: String + // Seasons + let tabSeasons: String + let episodes: String + let episodesCount: String + let episodesWatchedCount: String + let grid: String + let overview: String + // Ratings + let ratingAwesome: String + let ratingGreat: String + let ratingGood: String + let ratingBad: String + let ratingTerrible: String +} diff --git a/apps/ios/Plotwist/Plotwist/PlotwistApp.swift b/apps/ios/Plotwist/Plotwist/PlotwistApp.swift new file mode 100644 index 00000000..2855ad0b --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/PlotwistApp.swift @@ -0,0 +1,17 @@ +// +// PlotwistApp.swift +// Plotwist +// +// Created by Luiz Henrique Delfino on 10/01/26. +// + +import SwiftUI + +@main +struct PlotwistApp: App { + var body: some Scene { + WindowGroup { + RootView() + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Services/AuthService.swift b/apps/ios/Plotwist/Plotwist/Services/AuthService.swift new file mode 100644 index 00000000..d9eb9595 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/AuthService.swift @@ -0,0 +1,381 @@ +// +// AuthService.swift +// Plotwist +// + +import Foundation + +class AuthService { + static let shared = AuthService() + private init() {} + + // MARK: - Cache + private var preferencesCache: CachedPreferences? + private let cacheDuration: TimeInterval = 300 // 5 minutes + + private struct CachedPreferences { + let preferences: UserPreferences? + let timestamp: Date + } + + func invalidatePreferencesCache() { + preferencesCache = nil + } + + // MARK: - Sign In + func signIn(login: String, password: String) async throws -> String { + guard let url = URL(string: "\(API.baseURL)/login") else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(["login": login, "password": password]) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw AuthError.invalidResponse + } + + guard http.statusCode == 200 else { + throw AuthError.invalidCredentials + } + + let result = try JSONDecoder().decode(LoginResponse.self, from: data) + UserDefaults.standard.set(result.token, forKey: "token") + NotificationCenter.default.post(name: .authChanged, object: nil) + return result.token + } + + // MARK: - Sign Up + func signUp(email: String, password: String, username: String) async throws { + guard let url = URL(string: "\(API.baseURL)/users/create") else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode([ + "email": email, + "password": password, + "username": username, + ]) + + let (_, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw AuthError.invalidResponse + } + + switch http.statusCode { + case 200, 201: + // Auto sign-in after sign-up + _ = try await signIn(login: email, password: password) + case 409: + throw AuthError.alreadyExists + default: + throw AuthError.invalidCredentials + } + } + + // MARK: - Check Email Availability + func checkEmailAvailable(email: String) async throws -> Bool { + guard let encoded = email.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: "\(API.baseURL)/users/available-email?email=\(encoded)") + else { + throw AuthError.invalidURL + } + + let (_, response) = try await URLSession.shared.data(from: url) + + guard let http = response as? HTTPURLResponse else { + throw AuthError.invalidResponse + } + + return http.statusCode == 200 + } + + // MARK: - Check Username Availability + func checkUsernameAvailable(username: String) async throws -> Bool { + guard let encoded = username.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: "\(API.baseURL)/users/available-username?username=\(encoded)") + else { + throw AuthError.invalidURL + } + + let (_, response) = try await URLSession.shared.data(from: url) + + guard let http = response as? HTTPURLResponse else { + throw AuthError.invalidResponse + } + + return http.statusCode == 200 + } + + // MARK: - Get Current User + func getCurrentUser() async throws -> User { + guard let token = UserDefaults.standard.string(forKey: "token"), + let url = URL(string: "\(API.baseURL)/me") + else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw AuthError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let wrapper = try decoder.decode(MeResponse.self, from: data) + return wrapper.user + } + + // MARK: - Update User + func updateUser( + username: String? = nil, avatarUrl: String? = nil, bannerUrl: String? = nil, + biography: String? = nil + ) async throws -> User { + guard let token = UserDefaults.standard.string(forKey: "token"), + let url = URL(string: "\(API.baseURL)/user") + else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "PATCH" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + var body: [String: Any] = [:] + if let username { body["username"] = username } + if let avatarUrl { body["avatarUrl"] = avatarUrl } + if let bannerUrl { body["bannerUrl"] = bannerUrl } + if let biography { body["biography"] = biography } + + request.httpBody = try JSONSerialization.data(withJSONObject: body) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw AuthError.invalidResponse + } + + if http.statusCode == 409 { + throw AuthError.alreadyExists + } + + guard http.statusCode == 200 else { + throw AuthError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let wrapper = try decoder.decode(MeResponse.self, from: data) + return wrapper.user + } + + // MARK: - Check Username Availability + func isUsernameAvailable(_ username: String) async throws -> Bool { + guard + let url = URL( + string: + "\(API.baseURL)/users/available-username?username=\(username.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? username)" + ) + else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + + let (_, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw AuthError.invalidResponse + } + + // 200 = available, 409 = already taken + return http.statusCode == 200 + } + + // MARK: - Get User Preferences + func getUserPreferences() async throws -> UserPreferences? { + // Check cache + if let cached = preferencesCache, + Date().timeIntervalSince(cached.timestamp) < cacheDuration { + return cached.preferences + } + + guard let token = UserDefaults.standard.string(forKey: "token"), + let url = URL(string: "\(API.baseURL)/user/preferences") + else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw AuthError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let wrapper = try decoder.decode(UserPreferencesResponse.self, from: data) + + // Cache result + preferencesCache = CachedPreferences(preferences: wrapper.userPreferences, timestamp: Date()) + + return wrapper.userPreferences + } + + // MARK: - Update User Preferences + func updateUserPreferences(watchRegion: String, watchProvidersIds: [Int] = []) async throws { + guard let token = UserDefaults.standard.string(forKey: "token"), + let url = URL(string: "\(API.baseURL)/user/preferences") + else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "PATCH" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let body: [String: Any] = [ + "watchRegion": watchRegion, + "watchProvidersIds": watchProvidersIds, + ] + + request.httpBody = try JSONSerialization.data(withJSONObject: body) + + let (_, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw AuthError.invalidResponse + } + + // Invalidate cache after update + invalidatePreferencesCache() + } + + // MARK: - Get User Stats + func getUserStats(userId: String) async throws -> UserStats { + guard let url = URL(string: "\(API.baseURL)/user/\(userId)/stats") else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw AuthError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(UserStats.self, from: data) + } + + // MARK: - Sign Out + func signOut() { + UserDefaults.standard.removeObject(forKey: "token") + invalidatePreferencesCache() + NotificationCenter.default.post(name: .authChanged, object: nil) + } + + var isAuthenticated: Bool { + UserDefaults.standard.string(forKey: "token") != nil + } +} + +// MARK: - Models +struct User: Codable { + let id: String + let username: String + let email: String + let avatarUrl: String? + let bannerUrl: String? + let biography: String? + let createdAt: String + let subscriptionType: String? + + var avatarImageURL: URL? { + guard let avatarUrl else { return nil } + return URL(string: avatarUrl) + } + + var bannerImageURL: URL? { + guard let bannerUrl else { return nil } + return URL(string: bannerUrl) + } + + var memberSinceDate: Date? { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.date(from: createdAt) + } + + var isPro: Bool { + subscriptionType == "PRO" + } +} + +struct LoginResponse: Codable { + let token: String +} + +struct MeResponse: Codable { + let user: User +} + +struct UserPreferencesResponse: Codable { + let userPreferences: UserPreferences? +} + +struct UserPreferences: Codable { + let id: String + let userId: String + let watchProvidersIds: [Int]? + let watchRegion: String? +} + +struct UserStats: Codable { + let followersCount: Int + let followingCount: Int + let watchedMoviesCount: Int + let watchedSeriesCount: Int +} + +enum AuthError: LocalizedError { + case invalidURL, invalidResponse, invalidCredentials, alreadyExists + + var errorDescription: String? { + switch self { + case .invalidURL: return "Invalid URL" + case .invalidResponse: return "Invalid response" + case .invalidCredentials: return "Invalid credentials" + case .alreadyExists: return "Already exists" + } + } +} + +extension Notification.Name { + static let authChanged = Notification.Name("authChanged") + static let profileUpdated = Notification.Name("profileUpdated") + static let navigateToSearch = Notification.Name("navigateToSearch") + static let navigateToProfile = Notification.Name("navigateToProfile") +} diff --git a/apps/ios/Plotwist/Plotwist/Services/CollectionCache.swift b/apps/ios/Plotwist/Plotwist/Services/CollectionCache.swift new file mode 100644 index 00000000..cde3a986 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/CollectionCache.swift @@ -0,0 +1,128 @@ +// +// CollectionCache.swift +// Plotwist +// + +import Foundation + +// MARK: - Collection Cache +final class CollectionCache { + static let shared = CollectionCache() + private init() {} + + // Cache for user items by status + private var itemsCache: [String: [UserItemSummary]] = [:] + // Cache for total count + private var totalCountCache: Int? + // Cache for reviews count + private var reviewsCountCache: Int? + // Cache for user profile + private var userCache: User? + // Cache timestamp + private var lastUpdated: Date? + // Cache duration (5 minutes) + private let cacheDuration: TimeInterval = 300 + // Flag to track if initial load was done + private var hasLoadedOnce = false + + private func cacheKey(userId: String, status: String) -> String { + return "\(userId)_\(status)" + } + + // MARK: - User Cache + + var user: User? { + guard !isCacheExpired else { return nil } + return userCache + } + + func setUser(_ user: User?) { + userCache = user + lastUpdated = Date() + hasLoadedOnce = true + } + + // MARK: - Items Cache + + func getItems(userId: String, status: String) -> [UserItemSummary]? { + guard !isCacheExpired else { + return nil + } + return itemsCache[cacheKey(userId: userId, status: status)] + } + + func setItems(_ items: [UserItemSummary], userId: String, status: String) { + itemsCache[cacheKey(userId: userId, status: status)] = items + lastUpdated = Date() + hasLoadedOnce = true + } + + // MARK: - Total Count Cache + + func getTotalCount() -> Int? { + guard !isCacheExpired else { + return nil + } + return totalCountCache + } + + func setTotalCount(_ count: Int) { + totalCountCache = count + lastUpdated = Date() + } + + // MARK: - Reviews Count Cache + + func getReviewsCount() -> Int? { + guard !isCacheExpired else { + return nil + } + return reviewsCountCache + } + + func setReviewsCount(_ count: Int) { + reviewsCountCache = count + lastUpdated = Date() + } + + // MARK: - Cache State + + var shouldShowSkeleton: Bool { + !hasLoadedOnce + } + + var isDataAvailable: Bool { + userCache != nil || !itemsCache.isEmpty + } + + // MARK: - Cache Management + + private var isCacheExpired: Bool { + guard let lastUpdated else { return true } + return Date().timeIntervalSince(lastUpdated) > cacheDuration + } + + func clearCache() { + itemsCache.removeAll() + totalCountCache = nil + reviewsCountCache = nil + userCache = nil + lastUpdated = nil + // Don't reset hasLoadedOnce to avoid showing skeleton again + } + + func invalidateCache() { + clearCache() + NotificationCenter.default.post(name: .collectionCacheInvalidated, object: nil) + } + + func fullReset() { + clearCache() + hasLoadedOnce = false + } +} + +// MARK: - Notification Names +extension Notification.Name { + static let collectionCacheInvalidated = Notification.Name("collectionCacheInvalidated") +} diff --git a/apps/ios/Plotwist/Plotwist/Services/HomeDataCache.swift b/apps/ios/Plotwist/Plotwist/Services/HomeDataCache.swift new file mode 100644 index 00000000..4e66c13f --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/HomeDataCache.swift @@ -0,0 +1,109 @@ +// +// HomeDataCache.swift +// Plotwist +// + +import Foundation + +// MARK: - Home Data Cache +final class HomeDataCache { + static let shared = HomeDataCache() + private init() {} + + // Cache for watching items + private var watchingItemsCache: [SearchResult]? + // Cache for watchlist items + private var watchlistItemsCache: [SearchResult]? + // Cache for user data + private var userCache: User? + // Cache timestamp + private var lastUpdated: Date? + // Cache duration (5 minutes) + private let cacheDuration: TimeInterval = 300 + // Flag to track if initial load was done + private var hasLoadedOnce = false + + // MARK: - Watching Items + + var watchingItems: [SearchResult]? { + guard !isCacheExpired else { + return nil + } + return watchingItemsCache + } + + func setWatchingItems(_ items: [SearchResult]) { + watchingItemsCache = items + lastUpdated = Date() + hasLoadedOnce = true + } + + // MARK: - Watchlist Items + + var watchlistItems: [SearchResult]? { + guard !isCacheExpired else { + return nil + } + return watchlistItemsCache + } + + func setWatchlistItems(_ items: [SearchResult]) { + watchlistItemsCache = items + lastUpdated = Date() + hasLoadedOnce = true + } + + // MARK: - User + + var user: User? { + guard !isCacheExpired else { + return nil + } + return userCache + } + + func setUser(_ user: User?) { + userCache = user + lastUpdated = Date() + } + + // MARK: - Cache State + + var shouldShowSkeleton: Bool { + !hasLoadedOnce + } + + var isDataAvailable: Bool { + watchingItemsCache != nil || watchlistItemsCache != nil + } + + // MARK: - Cache Management + + private var isCacheExpired: Bool { + guard let lastUpdated else { return true } + return Date().timeIntervalSince(lastUpdated) > cacheDuration + } + + func clearCache() { + watchingItemsCache = nil + watchlistItemsCache = nil + userCache = nil + lastUpdated = nil + // Don't reset hasLoadedOnce to avoid showing skeleton again + } + + func invalidateCache() { + clearCache() + NotificationCenter.default.post(name: .homeDataCacheInvalidated, object: nil) + } + + func fullReset() { + clearCache() + hasLoadedOnce = false + } +} + +// MARK: - Notification Names +extension Notification.Name { + static let homeDataCacheInvalidated = Notification.Name("homeDataCacheInvalidated") +} diff --git a/apps/ios/Plotwist/Plotwist/Services/ImageCache.swift b/apps/ios/Plotwist/Plotwist/Services/ImageCache.swift new file mode 100644 index 00000000..90a5ad33 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/ImageCache.swift @@ -0,0 +1,325 @@ +// +// ImageCache.swift +// Plotwist +// + +import SwiftUI +import UIKit + +// MARK: - Image Cache Manager +final class ImageCache: @unchecked Sendable { + static let shared = ImageCache() + + private let memoryCache = NSCache() + private let fileManager = FileManager.default + private let diskCacheURL: URL + private let ioQueue = DispatchQueue(label: "com.plotwist.imagecache.io", qos: .userInitiated) + + // Actor for managing ongoing tasks safely + private let taskManager = TaskManager() + + private init() { + // Setup disk cache directory + let cacheDir = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] + diskCacheURL = cacheDir.appendingPathComponent("ImageCache", isDirectory: true) + + // Create directory if needed + try? fileManager.createDirectory(at: diskCacheURL, withIntermediateDirectories: true) + + // Configure memory cache + memoryCache.countLimit = 100 + memoryCache.totalCostLimit = 100 * 1024 * 1024 // 100MB + } + + // MARK: - Public API + + func image(for url: URL) -> UIImage? { + // Check memory cache first (fast) + if let cached = memoryCache.object(forKey: url as NSURL) { + return cached + } + + // Check disk cache (slower, but synchronous for simplicity) + if let diskImage = loadFromDisk(url: url) { + memoryCache.setObject(diskImage, forKey: url as NSURL) + return diskImage + } + + return nil + } + + func setImage(_ image: UIImage, for url: URL) { + // Save to memory + memoryCache.setObject(image, forKey: url as NSURL) + + // Save to disk asynchronously + ioQueue.async { [weak self] in + self?.saveToDisk(image: image, url: url) + } + } + + /// Load image with deduplication of concurrent requests + func loadImage(from url: URL, priority: TaskPriority = .medium) async -> UIImage? { + // Check caches first + if let cached = image(for: url) { + return cached + } + + // Use actor for safe concurrent access + return await taskManager.loadImage(url: url, priority: priority) { [weak self] in + do { + let (data, _) = try await URLSession.shared.data(from: url) + guard let image = UIImage(data: data) else { return nil } + + // Use downsampled version for large images + let optimizedImage = self?.downsampleIfNeeded(image, maxDimension: 1920) ?? image + + self?.setImage(optimizedImage, for: url) + return optimizedImage + } catch { + return nil + } + } + } + + /// Prefetch multiple images (for carousel) + func prefetch(urls: [URL], priority: TaskPriority = .low) { + Task(priority: priority) { + await withTaskGroup(of: Void.self) { group in + for url in urls { + group.addTask { [weak self] in + _ = await self?.loadImage(from: url, priority: .low) + } + } + } + } + } + + /// Clear all caches + func clearCache() { + memoryCache.removeAllObjects() + ioQueue.async { [weak self] in + guard let self else { return } + try? self.fileManager.removeItem(at: self.diskCacheURL) + try? self.fileManager.createDirectory(at: self.diskCacheURL, withIntermediateDirectories: true) + } + } + + // MARK: - Private Helpers + + private func cacheKey(for url: URL) -> String { + return url.absoluteString.data(using: .utf8)?.base64EncodedString() ?? url.lastPathComponent + } + + private func diskPath(for url: URL) -> URL { + return diskCacheURL.appendingPathComponent(cacheKey(for: url)) + } + + private func loadFromDisk(url: URL) -> UIImage? { + let path = diskPath(for: url) + guard let data = try? Data(contentsOf: path) else { return nil } + return UIImage(data: data) + } + + private func saveToDisk(image: UIImage, url: URL) { + let path = diskPath(for: url) + guard let data = image.jpegData(compressionQuality: 0.85) else { return } + try? data.write(to: path) + } + + private func downsampleIfNeeded(_ image: UIImage, maxDimension: CGFloat) -> UIImage { + let size = image.size + guard size.width > maxDimension || size.height > maxDimension else { return image } + + let scale = maxDimension / max(size.width, size.height) + let newSize = CGSize(width: size.width * scale, height: size.height * scale) + + let renderer = UIGraphicsImageRenderer(size: newSize) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: newSize)) + } + } +} + +// MARK: - Task Manager Actor (for safe concurrent task management) +private actor TaskManager { + private var ongoingTasks: [URL: Task] = [:] + + func loadImage( + url: URL, + priority: TaskPriority, + loader: @escaping @Sendable () async -> UIImage? + ) async -> UIImage? { + // Check if there's already an ongoing task for this URL + if let existingTask = ongoingTasks[url] { + return await existingTask.value + } + + // Create new task + let task = Task(priority: priority) { + await loader() + } + + ongoingTasks[url] = task + + let result = await task.value + ongoingTasks.removeValue(forKey: url) + + return result + } +} + +// MARK: - Cached Async Image (Enhanced) +struct CachedAsyncImage: View { + let url: URL? + let content: (Image) -> Content + let placeholder: () -> Placeholder + let priority: TaskPriority + + @State private var loadedImage: UIImage? + @State private var isLoading = false + + init( + url: URL?, + priority: TaskPriority = .medium, + @ViewBuilder content: @escaping (Image) -> Content, + @ViewBuilder placeholder: @escaping () -> Placeholder + ) { + self.url = url + self.priority = priority + self.content = content + self.placeholder = placeholder + } + + var body: some View { + Group { + if let loadedImage { + content(Image(uiImage: loadedImage)) + .transition(.opacity.animation(.easeIn(duration: 0.2))) + } else { + placeholder() + .task(id: url, priority: priority) { + await loadImage() + } + } + } + } + + @MainActor + private func loadImage() async { + guard let url, !isLoading else { return } + isLoading = true + + // Check cache synchronously first for instant display + if let cached = ImageCache.shared.image(for: url) { + loadedImage = cached + isLoading = false + return + } + + // Load from network + if let image = await ImageCache.shared.loadImage(from: url, priority: priority) { + withAnimation(.easeIn(duration: 0.2)) { + loadedImage = image + } + } + isLoading = false + } +} + +// MARK: - Backdrop Image View (Optimized for MediaDetailView) +struct BackdropImage: View { + let url: URL? + let height: CGFloat + + @State private var loadedImage: UIImage? + @State private var showImage = false + + var body: some View { + ZStack { + // Solid color placeholder + Rectangle() + .fill(Color.appBorderAdaptive) + .opacity(showImage ? 0 : 1) + + // Actual image with fade-in + if let loadedImage { + Image(uiImage: loadedImage) + .resizable() + .aspectRatio(contentMode: .fill) + .opacity(showImage ? 1 : 0) + } + } + .frame(height: height) + .frame(maxWidth: .infinity) + .clipped() + .task(id: url) { + await loadImage() + } + } + + @MainActor + private func loadImage() async { + guard let url else { return } + + // Check cache for instant display + if let cached = ImageCache.shared.image(for: url) { + loadedImage = cached + withAnimation(.easeIn(duration: 0.15)) { + showImage = true + } + return + } + + // Load with high priority for visible content + if let image = await ImageCache.shared.loadImage(from: url, priority: .high) { + loadedImage = image + withAnimation(.easeIn(duration: 0.25)) { + showImage = true + } + } + } +} + +// MARK: - Carousel Backdrop View (with prefetching) +struct CarouselBackdropView: View { + let images: [TMDBImage] + let height: CGFloat + @Binding var currentIndex: Int + + var body: some View { + TabView(selection: $currentIndex) { + ForEach(Array(images.prefix(10).enumerated()), id: \.offset) { index, backdrop in + BackdropImage(url: backdrop.backdropURL, height: height) + .tag(index) + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .frame(height: height) + .frame(maxWidth: .infinity) + .clipped() + .onAppear { + prefetchImages() + } + .onChange(of: currentIndex) { _, newIndex in + prefetchAdjacentImages(around: newIndex) + } + } + + private func prefetchImages() { + // Prefetch first 3 images immediately + let initialUrls = images.prefix(3).compactMap { $0.backdropURL } + ImageCache.shared.prefetch(urls: initialUrls, priority: .high) + + // Prefetch rest with lower priority + let remainingUrls = images.dropFirst(3).prefix(7).compactMap { $0.backdropURL } + ImageCache.shared.prefetch(urls: remainingUrls, priority: .low) + } + + private func prefetchAdjacentImages(around index: Int) { + // Prefetch next 2 images when user swipes + let nextIndices = [index + 1, index + 2].filter { $0 < images.count && $0 < 10 } + let urls = nextIndices.compactMap { images[$0].backdropURL } + ImageCache.shared.prefetch(urls: urls, priority: .medium) + } +} diff --git a/apps/ios/Plotwist/Plotwist/Services/ReviewService.swift b/apps/ios/Plotwist/Plotwist/Services/ReviewService.swift new file mode 100644 index 00000000..38093327 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/ReviewService.swift @@ -0,0 +1,401 @@ +// +// ReviewService.swift +// Plotwist +// + +import Foundation + +class ReviewService { + static let shared = ReviewService() + private init() {} + + // MARK: - Cache + private var reviewCache: [String: CachedReview] = [:] + private let cacheDuration: TimeInterval = 300 // 5 minutes + + private struct CachedReview { + let review: Review? + let timestamp: Date + } + + private func cacheKey(tmdbId: Int, mediaType: String, seasonNumber: Int?, episodeNumber: Int?) + -> String + { + var key = "\(tmdbId)-\(mediaType)" + if let season = seasonNumber { key += "-s\(season)" } + if let episode = episodeNumber { key += "-e\(episode)" } + return key + } + + func invalidateCache(tmdbId: Int, mediaType: String, seasonNumber: Int? = nil, episodeNumber: Int? = nil) { + let key = cacheKey(tmdbId: tmdbId, mediaType: mediaType, seasonNumber: seasonNumber, episodeNumber: episodeNumber) + reviewCache.removeValue(forKey: key) + } + + func invalidateAllCache() { + reviewCache.removeAll() + } + + // MARK: - Get User Review + func getUserReview( + tmdbId: Int, + mediaType: String, + seasonNumber: Int? = nil, + episodeNumber: Int? = nil + ) async throws -> Review? { + let key = cacheKey(tmdbId: tmdbId, mediaType: mediaType, seasonNumber: seasonNumber, episodeNumber: episodeNumber) + + // Check cache + if let cached = reviewCache[key], + Date().timeIntervalSince(cached.timestamp) < cacheDuration { + return cached.review + } + + var urlString = "\(API.baseURL)/review?tmdbId=\(tmdbId)&mediaType=\(mediaType)" + + if let seasonNumber = seasonNumber { + urlString += "&seasonNumber=\(seasonNumber)" + } + + if let episodeNumber = episodeNumber { + urlString += "&episodeNumber=\(episodeNumber)" + } + + guard let url = URL(string: urlString), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw ReviewError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw ReviewError.invalidResponse + } + + if http.statusCode == 404 { + // Cache nil result + reviewCache[key] = CachedReview(review: nil, timestamp: Date()) + return nil + } + + guard http.statusCode == 200 else { + throw ReviewError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(ReviewResponse.self, from: data) + + // Cache result + reviewCache[key] = CachedReview(review: result.review, timestamp: Date()) + + return result.review + } + + // MARK: - Create Review + func createReview(_ reviewData: ReviewData) async throws { + guard let url = URL(string: "\(API.baseURL)/review"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw ReviewError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // Create JSON dictionary with camelCase keys (as expected by Drizzle schema) + var jsonDict: [String: Any] = [ + "tmdbId": reviewData.tmdbId, + "mediaType": reviewData.mediaType, + "review": reviewData.review, + "rating": reviewData.rating, + "hasSpoilers": reviewData.hasSpoilers, + "language": reviewData.language, + ] + + // Only add optional values if they exist + if let seasonNumber = reviewData.seasonNumber { + jsonDict["seasonNumber"] = seasonNumber + } + if let episodeNumber = reviewData.episodeNumber { + jsonDict["episodeNumber"] = episodeNumber + } + + let jsonData = try JSONSerialization.data(withJSONObject: jsonDict, options: []) + request.httpBody = jsonData + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw ReviewError.invalidResponse + } + + guard http.statusCode == 200 || http.statusCode == 201 else { + if let errorString = String(data: data, encoding: .utf8) { + throw ReviewError.serverError(errorString) + } + throw ReviewError.invalidResponse + } + + // Invalidate cache + invalidateCache( + tmdbId: reviewData.tmdbId, + mediaType: reviewData.mediaType, + seasonNumber: reviewData.seasonNumber, + episodeNumber: reviewData.episodeNumber + ) + } + + // MARK: - Update Review + func updateReview(id: String, _ reviewData: ReviewData) async throws { + guard let url = URL(string: "\(API.baseURL)/review/by/\(id)"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw ReviewError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // For update, we only send the fields that can be updated (camelCase) + let jsonDict: [String: Any] = [ + "rating": reviewData.rating, + "review": reviewData.review, + "hasSpoilers": reviewData.hasSpoilers, + ] + + let jsonData = try JSONSerialization.data(withJSONObject: jsonDict, options: []) + request.httpBody = jsonData + + let (_, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw ReviewError.invalidResponse + } + + guard http.statusCode == 200 else { + throw ReviewError.invalidResponse + } + + // Invalidate cache + invalidateCache( + tmdbId: reviewData.tmdbId, + mediaType: reviewData.mediaType, + seasonNumber: reviewData.seasonNumber, + episodeNumber: reviewData.episodeNumber + ) + } + + // MARK: - Delete Review + func deleteReview(id: String, tmdbId: Int, mediaType: String, seasonNumber: Int? = nil, episodeNumber: Int? = nil) async throws { + guard let url = URL(string: "\(API.baseURL)/review/by/\(id)"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw ReviewError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let (_, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw ReviewError.invalidResponse + } + + guard http.statusCode == 200 || http.statusCode == 204 else { + throw ReviewError.invalidResponse + } + + // Invalidate cache + invalidateCache( + tmdbId: tmdbId, + mediaType: mediaType, + seasonNumber: seasonNumber, + episodeNumber: episodeNumber + ) + } + + // MARK: - Get User Reviews Count + func getUserReviewsCount(userId: String) async throws -> Int { + guard let url = URL(string: "\(API.baseURL)/user/\(userId)/reviews-count") else { + throw ReviewError.invalidURL + } + + let (data, response) = try await URLSession.shared.data(from: url) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw ReviewError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(ReviewsCountResponse.self, from: data) + return result.reviewsCount + } + + // MARK: - Get Reviews List + func getReviews( + tmdbId: Int, + mediaType: String, + orderBy: String = "createdAt", + limit: Int = 50, + seasonNumber: Int? = nil, + episodeNumber: Int? = nil + ) async throws -> [ReviewListItem] { + var urlString = + "\(API.baseURL)/reviews?tmdbId=\(tmdbId)&mediaType=\(mediaType)&orderBy=\(orderBy)&limit=\(limit)" + + if let seasonNumber = seasonNumber { + urlString += "&seasonNumber=\(seasonNumber)" + } + + if let episodeNumber = episodeNumber { + urlString += "&episodeNumber=\(episodeNumber)" + } + + guard let url = URL(string: urlString) else { + throw ReviewError.invalidURL + } + + var request = URLRequest(url: url) + + // Add token if available (optional auth) + if let token = UserDefaults.standard.string(forKey: "token") { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw ReviewError.invalidResponse + } + + guard http.statusCode == 200 else { + throw ReviewError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode([ReviewListItem].self, from: data) + } +} + +// MARK: - Models +struct Review: Codable, Identifiable { + let id: String + let userId: String + let tmdbId: Int + let mediaType: String + let review: String + let rating: Double + let hasSpoilers: Bool + let seasonNumber: Int? + let episodeNumber: Int? + let language: String? + let createdAt: String +} + +struct ReviewResponse: Codable { + let review: Review? +} + +struct ReviewsCountResponse: Codable { + let reviewsCount: Int +} + +struct ReviewUser: Codable { + let id: String + let username: String + let avatarUrl: String? +} + +struct UserLike: Codable { + let id: String + let entityId: String + let userId: String + let createdAt: String +} + +struct ReviewListItem: Codable, Identifiable { + let id: String + let userId: String + let tmdbId: Int + let mediaType: String + let review: String + let rating: Double + let hasSpoilers: Bool + let seasonNumber: Int? + let episodeNumber: Int? + let language: String? + let createdAt: String + let user: ReviewUser + let likeCount: Int + let replyCount: Int + let userLike: UserLike? +} + +struct ReviewData: Codable { + let tmdbId: Int + let mediaType: String + let review: String + let rating: Double + let hasSpoilers: Bool + let seasonNumber: Int? + let episodeNumber: Int? + let language: String + + enum CodingKeys: String, CodingKey { + case tmdbId = "tmdb_id" + case mediaType = "media_type" + case review + case rating + case hasSpoilers = "has_spoilers" + case seasonNumber = "season_number" + case episodeNumber = "episode_number" + case language + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(tmdbId, forKey: .tmdbId) + try container.encode(mediaType, forKey: .mediaType) + try container.encode(review, forKey: .review) + try container.encode(rating, forKey: .rating) + try container.encode(hasSpoilers, forKey: .hasSpoilers) + try container.encode(language, forKey: .language) + + // Only encode optional values if they're not nil + if let seasonNumber = seasonNumber { + try container.encode(seasonNumber, forKey: .seasonNumber) + } + if let episodeNumber = episodeNumber { + try container.encode(episodeNumber, forKey: .episodeNumber) + } + } +} + +enum ReviewError: LocalizedError { + case invalidURL + case invalidResponse + case notFound + case serverError(String) + + var errorDescription: String? { + switch self { + case .invalidURL: return "Invalid URL" + case .invalidResponse: return "Invalid response from server" + case .notFound: return "Review not found" + case .serverError(let message): return "Server error: \(message)" + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Services/SearchDataCache.swift b/apps/ios/Plotwist/Plotwist/Services/SearchDataCache.swift new file mode 100644 index 00000000..beb72a31 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/SearchDataCache.swift @@ -0,0 +1,134 @@ +// +// SearchDataCache.swift +// Plotwist +// + +import Foundation + +// MARK: - Search Data Cache +final class SearchDataCache { + static let shared = SearchDataCache() + private init() {} + + // Cache for popular movies + private var popularMoviesCache: [SearchResult]? + // Cache for popular TV series + private var popularTVSeriesCache: [SearchResult]? + // Cache for popular animes + private var popularAnimesCache: [SearchResult]? + // Cache for popular doramas + private var popularDoramasCache: [SearchResult]? + // Cache timestamp + private var lastUpdated: Date? + // Cache duration (5 minutes) + private let cacheDuration: TimeInterval = 300 + // Flag to track if initial load was done + private var hasLoadedOnce = false + // Store the preferences hash to invalidate when preferences change + private var lastPreferencesHash: String? + + // MARK: - Popular Movies + + var popularMovies: [SearchResult]? { + guard !isCacheExpired else { return nil } + return popularMoviesCache + } + + func setPopularMovies(_ items: [SearchResult]) { + popularMoviesCache = items + lastUpdated = Date() + hasLoadedOnce = true + } + + // MARK: - Popular TV Series + + var popularTVSeries: [SearchResult]? { + guard !isCacheExpired else { return nil } + return popularTVSeriesCache + } + + func setPopularTVSeries(_ items: [SearchResult]) { + popularTVSeriesCache = items + lastUpdated = Date() + hasLoadedOnce = true + } + + // MARK: - Popular Animes + + var popularAnimes: [SearchResult]? { + guard !isCacheExpired else { return nil } + return popularAnimesCache + } + + func setPopularAnimes(_ items: [SearchResult]) { + popularAnimesCache = items + lastUpdated = Date() + hasLoadedOnce = true + } + + // MARK: - Popular Doramas + + var popularDoramas: [SearchResult]? { + guard !isCacheExpired else { return nil } + return popularDoramasCache + } + + func setPopularDoramas(_ items: [SearchResult]) { + popularDoramasCache = items + lastUpdated = Date() + hasLoadedOnce = true + } + + // MARK: - Preferences Hash + + func setPreferencesHash(_ hash: String) { + if lastPreferencesHash != hash { + // Preferences changed, invalidate cache + clearCache() + lastPreferencesHash = hash + } + } + + // MARK: - Cache State + + var shouldShowSkeleton: Bool { + !hasLoadedOnce + } + + var isDataAvailable: Bool { + popularMoviesCache != nil || popularTVSeriesCache != nil || + popularAnimesCache != nil || popularDoramasCache != nil + } + + // MARK: - Cache Management + + private var isCacheExpired: Bool { + guard let lastUpdated else { return true } + return Date().timeIntervalSince(lastUpdated) > cacheDuration + } + + func clearCache() { + popularMoviesCache = nil + popularTVSeriesCache = nil + popularAnimesCache = nil + popularDoramasCache = nil + lastUpdated = nil + // Don't reset hasLoadedOnce to avoid showing skeleton again + } + + func invalidateCache() { + clearCache() + NotificationCenter.default.post(name: .searchDataCacheInvalidated, object: nil) + } + + func fullReset() { + clearCache() + hasLoadedOnce = false + lastPreferencesHash = nil + } +} + +// MARK: - Notification Names +extension Notification.Name { + static let searchDataCacheInvalidated = Notification.Name("searchDataCacheInvalidated") +} diff --git a/apps/ios/Plotwist/Plotwist/Services/TMDBService.swift b/apps/ios/Plotwist/Plotwist/Services/TMDBService.swift new file mode 100644 index 00000000..b6de58fc --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/TMDBService.swift @@ -0,0 +1,1181 @@ +// +// TMDBService.swift +// Plotwist +// + +import Foundation + +class TMDBService { + static let shared = TMDBService() + private init() {} + + private let baseURL = "https://api.themoviedb.org/3" + private let apiKey = + "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI5MGYyYjQyNWU1ZmYxYjgwMWVkOWRjY2Y0YmFmYWRkZSIsIm5iZiI6MTYyNjQ3OTE5Ny41MjYsInN1YiI6IjYwZjIxYTVkN2Q1ZGI1MDAyZmM5MTNiMyIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.HblE_tHKIktjGrwEONxxZFgPGwxNkwKSZEwC24WIrzM" // TODO: Replace with actual API key + + // MARK: - Search Multi + func searchMulti(query: String, language: String = "en-US") async throws -> SearchMultiResponse { + guard !query.isEmpty else { + return SearchMultiResponse(results: []) + } + + guard let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: "\(baseURL)/search/multi?query=\(encodedQuery)&language=\(language)") + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(SearchMultiResponse.self, from: data) + } + + // MARK: - Popular Movies + func getPopularMovies(language: String = "en-US", page: Int = 1) async throws -> PaginatedResult { + guard let url = URL(string: "\(baseURL)/movie/popular?language=\(language)&page=\(page)") else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "movie") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Now Playing Movies + func getNowPlayingMovies(language: String = "en-US", page: Int = 1) async throws + -> PaginatedResult + { + guard let url = URL(string: "\(baseURL)/movie/now_playing?language=\(language)&page=\(page)") + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "movie") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Top Rated Movies + func getTopRatedMovies(language: String = "en-US", page: Int = 1) async throws -> PaginatedResult + { + guard let url = URL(string: "\(baseURL)/movie/top_rated?language=\(language)&page=\(page)") + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "movie") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Upcoming Movies + func getUpcomingMovies(language: String = "en-US", page: Int = 1) async throws -> PaginatedResult + { + guard let url = URL(string: "\(baseURL)/movie/upcoming?language=\(language)&page=\(page)") + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "movie") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Popular TV Series + func getPopularTVSeries(language: String = "en-US", page: Int = 1) async throws -> PaginatedResult + { + guard let url = URL(string: "\(baseURL)/tv/popular?language=\(language)&page=\(page)") else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "tv") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Airing Today TV Series + func getAiringTodayTVSeries(language: String = "en-US", page: Int = 1) async throws + -> PaginatedResult + { + guard let url = URL(string: "\(baseURL)/tv/airing_today?language=\(language)&page=\(page)") + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "tv") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - On The Air TV Series + func getOnTheAirTVSeries(language: String = "en-US", page: Int = 1) async throws + -> PaginatedResult + { + guard let url = URL(string: "\(baseURL)/tv/on_the_air?language=\(language)&page=\(page)") + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "tv") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Top Rated TV Series + func getTopRatedTVSeries(language: String = "en-US", page: Int = 1) async throws + -> PaginatedResult + { + guard let url = URL(string: "\(baseURL)/tv/top_rated?language=\(language)&page=\(page)") + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "tv") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Popular Animes TV (Animation genre from Japan) + func getPopularAnimes(language: String = "en-US", page: Int = 1) async throws -> PaginatedResult { + // Genre 16 = Animation, origin_country = JP + guard + let url = URL( + string: + "\(baseURL)/discover/tv?language=\(language)&sort_by=popularity.desc&with_genres=16&with_origin_country=JP&page=\(page)" + ) + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "tv") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Popular Anime Movies (Animation genre from Japan) + func getPopularAnimeMovies(language: String = "en-US", page: Int = 1) async throws + -> PaginatedResult + { + // Genre 16 = Animation, origin_country = JP + guard + let url = URL( + string: + "\(baseURL)/discover/movie?language=\(language)&sort_by=popularity.desc&with_genres=16&with_origin_country=JP&page=\(page)" + ) + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "movie") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Discover Movies with Watch Providers + func discoverMovies( + language: String = "en-US", + page: Int = 1, + watchRegion: String? = nil, + withWatchProviders: String? = nil + ) async throws -> PaginatedResult { + var urlString = + "\(baseURL)/discover/movie?language=\(language)&sort_by=popularity.desc&page=\(page)" + + if let region = watchRegion, let providers = withWatchProviders, !providers.isEmpty { + urlString += "&watch_region=\(region)&with_watch_providers=\(providers)" + } + + guard let url = URL(string: urlString) else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "movie") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Discover TV with Watch Providers + func discoverTV( + language: String = "en-US", + page: Int = 1, + watchRegion: String? = nil, + withWatchProviders: String? = nil + ) async throws -> PaginatedResult { + var urlString = + "\(baseURL)/discover/tv?language=\(language)&sort_by=popularity.desc&page=\(page)" + + if let region = watchRegion, let providers = withWatchProviders, !providers.isEmpty { + urlString += "&watch_region=\(region)&with_watch_providers=\(providers)" + } + + guard let url = URL(string: urlString) else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "tv") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Discover Animes with Watch Providers + func discoverAnimes( + language: String = "en-US", + page: Int = 1, + watchRegion: String? = nil, + withWatchProviders: String? = nil + ) async throws -> PaginatedResult { + var urlString = + "\(baseURL)/discover/tv?language=\(language)&sort_by=popularity.desc&with_genres=16&with_origin_country=JP&page=\(page)" + + if let region = watchRegion, let providers = withWatchProviders, !providers.isEmpty { + urlString += "&watch_region=\(region)&with_watch_providers=\(providers)" + } + + guard let url = URL(string: urlString) else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "tv") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Discover Anime Movies with Watch Providers + func discoverAnimeMovies( + language: String = "en-US", + page: Int = 1, + watchRegion: String? = nil, + withWatchProviders: String? = nil + ) async throws -> PaginatedResult { + var urlString = + "\(baseURL)/discover/movie?language=\(language)&sort_by=popularity.desc&with_genres=16&with_origin_country=JP&page=\(page)" + + if let region = watchRegion, let providers = withWatchProviders, !providers.isEmpty { + urlString += "&watch_region=\(region)&with_watch_providers=\(providers)" + } + + guard let url = URL(string: urlString) else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "movie") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Discover Doramas with Watch Providers + func discoverDoramas( + language: String = "en-US", + page: Int = 1, + watchRegion: String? = nil, + withWatchProviders: String? = nil + ) async throws -> PaginatedResult { + var urlString = + "\(baseURL)/discover/tv?language=\(language)&sort_by=popularity.desc&with_origin_country=KR&page=\(page)" + + if let region = watchRegion, let providers = withWatchProviders, !providers.isEmpty { + urlString += "&watch_region=\(region)&with_watch_providers=\(providers)" + } + + guard let url = URL(string: urlString) else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "tv") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Popular Doramas (Korean dramas) + func getPopularDoramas(language: String = "en-US", page: Int = 1) async throws -> PaginatedResult + { + // origin_country = KR (Korean dramas) + guard + let url = URL( + string: + "\(baseURL)/discover/tv?language=\(language)&sort_by=popularity.desc&with_origin_country=KR&page=\(page)" + ) + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return PaginatedResult( + results: result.results.map { $0.toSearchResult(mediaType: "tv") }, + page: result.page, + totalPages: result.totalPages + ) + } + + // MARK: - Movie Details + func getMovieDetails(id: Int, language: String = "en-US") async throws -> MovieDetails { + guard let url = URL(string: "\(baseURL)/movie/\(id)?language=\(language)") else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(MovieDetails.self, from: data) + } + + // MARK: - TV Series Details + func getTVSeriesDetails(id: Int, language: String = "en-US") async throws -> MovieDetails { + guard let url = URL(string: "\(baseURL)/tv/\(id)?language=\(language)") else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(MovieDetails.self, from: data) + } + + // MARK: - Season Details + func getSeasonDetails(seriesId: Int, seasonNumber: Int, language: String = "en-US") async throws + -> SeasonDetails + { + guard + let url = URL( + string: "\(baseURL)/tv/\(seriesId)/season/\(seasonNumber)?language=\(language)") + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(SeasonDetails.self, from: data) + } + + // MARK: - Get Images + func getImages(id: Int, mediaType: String) async throws -> MediaImages { + let type = mediaType == "movie" ? "movie" : "tv" + guard let url = URL(string: "\(baseURL)/\(type)/\(id)/images") else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(MediaImages.self, from: data) + } + + // MARK: - Get Watch Providers + func getWatchProviders(id: Int, mediaType: String) async throws -> WatchProvidersResponse { + let type = mediaType == "movie" ? "movie" : "tv" + guard let url = URL(string: "\(baseURL)/\(type)/\(id)/watch/providers") else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(WatchProvidersResponse.self, from: data) + } + + // MARK: - Get Collection Details + func getCollectionDetails(id: Int, language: String = "en-US") async throws -> MovieCollection { + guard let url = URL(string: "\(baseURL)/collection/\(id)?language=\(language)") else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(MovieCollection.self, from: data) + } + + // MARK: - Get Related Content (Recommendations) + func getRelatedContent( + id: Int, mediaType: String, variant: String = "recommendations", language: String = "en-US" + ) async throws -> [SearchResult] { + let type = mediaType == "movie" ? "movie" : "tv" + guard let url = URL(string: "\(baseURL)/\(type)/\(id)/\(variant)?language=\(language)") else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(PopularResponse.self, from: data) + return result.results.map { $0.toSearchResult(mediaType: mediaType) } + } + + // MARK: - Get Available Regions + func getAvailableRegions(language: String = "en-US") async throws -> [WatchRegion] { + guard let url = URL(string: "\(baseURL)/watch/providers/regions?language=\(language)") else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(WatchRegionsResponse.self, from: data) + return result.results.sorted { $0.englishName < $1.englishName } + } + + // MARK: - Get Streaming Providers by Region + func getStreamingProviders(watchRegion: String, language: String = "en-US") async throws + -> [StreamingProvider] + { + // Fetch both movie and tv providers and merge them + async let movieProviders = fetchProviders( + type: "movie", watchRegion: watchRegion, language: language) + async let tvProviders = fetchProviders(type: "tv", watchRegion: watchRegion, language: language) + + let (movies, tv) = try await (movieProviders, tvProviders) + + // Merge and deduplicate + var uniqueProviders: [Int: StreamingProvider] = [:] + for provider in movies { + uniqueProviders[provider.providerId] = provider + } + for provider in tv { + if uniqueProviders[provider.providerId] == nil { + uniqueProviders[provider.providerId] = provider + } + } + + return Array(uniqueProviders.values).sorted { $0.providerName < $1.providerName } + } + + private func fetchProviders(type: String, watchRegion: String, language: String) async throws + -> [StreamingProvider] + { + guard + let url = URL( + string: + "\(baseURL)/watch/providers/\(type)?language=\(language)&watch_region=\(watchRegion)") + else { + throw TMDBError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw TMDBError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(StreamingProvidersResponse.self, from: data) + return result.results + } +} + +// MARK: - Movie Details Model +struct MovieDetails: Codable, Identifiable { + let id: Int + let title: String? + let name: String? + let overview: String? + let posterPath: String? + let backdropPath: String? + let releaseDate: String? + let firstAirDate: String? + let voteAverage: Double? + let runtime: Int? + let genres: [Genre]? + let belongsToCollection: BelongsToCollection? + let seasons: [Season]? // TV Series seasons + + var displayTitle: String { + title ?? name ?? "Unknown" + } + + var year: String? { + let date = releaseDate ?? firstAirDate + guard let date, date.count >= 4 else { return nil } + return String(date.prefix(4)) + } + + func formattedReleaseDate(locale: String) -> String? { + let dateString = releaseDate ?? firstAirDate + guard let dateString else { return nil } + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + guard let date = formatter.date(from: dateString) else { return nil } + + let outputFormatter = DateFormatter() + outputFormatter.dateStyle = .long + outputFormatter.locale = Locale(identifier: locale.replacingOccurrences(of: "-", with: "_")) + return outputFormatter.string(from: date) + } + + var posterURL: URL? { + guard let posterPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/w500\(posterPath)") + } + + var backdropURL: URL? { + guard let backdropPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/original\(backdropPath)") + } + + /// Filtered seasons for display (excludes specials with season 0 and empty seasons) + var displaySeasons: [Season] { + guard let seasons else { return [] } + return seasons.filter { $0.seasonNumber != 0 && $0.episodeCount > 0 } + } +} + +// MARK: - Season Model (TV Series) +struct Season: Codable, Identifiable { + let id: Int + let name: String + let seasonNumber: Int + let episodeCount: Int + let overview: String? + let posterPath: String? + let airDate: String? + + var posterURL: URL? { + guard let posterPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/w500\(posterPath)") + } + + var year: String? { + guard let airDate, airDate.count >= 4 else { return nil } + return String(airDate.prefix(4)) + } +} + +// MARK: - Season Details (with episodes) +struct SeasonDetails: Codable, Identifiable { + let id: Int + let name: String + let seasonNumber: Int + let episodes: [Episode] + let overview: String? + let posterPath: String? + let airDate: String? + + var posterURL: URL? { + guard let posterPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/w500\(posterPath)") + } +} + +// MARK: - Episode Model +struct Episode: Codable, Identifiable { + let id: Int + let name: String + let episodeNumber: Int + let seasonNumber: Int + let overview: String? + let stillPath: String? + let airDate: String? + let voteAverage: Double + let voteCount: Int + let runtime: Int? + + var stillURL: URL? { + guard let stillPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/w500\(stillPath)") + } + + var formattedRuntime: String? { + guard let runtime = runtime, runtime > 0 else { return nil } + if runtime >= 60 { + let hours = runtime / 60 + let minutes = runtime % 60 + return minutes > 0 ? "\(hours)h \(minutes)min" : "\(hours)h" + } + return "\(runtime)min" + } +} + +// MARK: - Belongs To Collection +struct BelongsToCollection: Codable, Identifiable { + let id: Int + let name: String + let posterPath: String? + let backdropPath: String? + + var backdropURL: URL? { + guard let backdropPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/original\(backdropPath)") + } +} + +// MARK: - Movie Collection (Detailed) +struct MovieCollection: Codable, Identifiable { + let id: Int + let name: String + let overview: String? + let posterPath: String? + let backdropPath: String? + let parts: [CollectionPart] + + var backdropURL: URL? { + guard let backdropPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/original\(backdropPath)") + } +} + +struct CollectionPart: Codable, Identifiable { + let id: Int + let title: String + let posterPath: String? + let releaseDate: String? + + var posterURL: URL? { + guard let posterPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/w500\(posterPath)") + } + + var year: String? { + guard let releaseDate, releaseDate.count >= 4 else { return nil } + return String(releaseDate.prefix(4)) + } +} + +struct Genre: Codable, Identifiable { + let id: Int + let name: String +} + +// MARK: - Paginated Result +struct PaginatedResult { + let results: [SearchResult] + let page: Int + let totalPages: Int + + var hasMorePages: Bool { + page < totalPages + } +} + +// MARK: - Popular Response +struct PopularResponse: Codable { + let results: [PopularItem] + let page: Int + let totalPages: Int +} + +struct PopularItem: Codable { + let id: Int + let title: String? + let name: String? + let posterPath: String? + let releaseDate: String? + let firstAirDate: String? + let overview: String? + let voteAverage: Double? + + func toSearchResult(mediaType: String) -> SearchResult { + SearchResult( + id: id, + mediaType: mediaType, + title: title, + name: name, + posterPath: posterPath, + profilePath: nil, + releaseDate: releaseDate, + firstAirDate: firstAirDate, + overview: overview, + voteAverage: voteAverage, + knownForDepartment: nil + ) + } +} + +// MARK: - Response Models +struct SearchMultiResponse: Codable { + let results: [SearchResult] +} + +struct SearchResult: Codable, Identifiable { + let id: Int + let mediaType: String? + let title: String? + let name: String? + let posterPath: String? + let profilePath: String? + let releaseDate: String? + let firstAirDate: String? + let overview: String? + let voteAverage: Double? + let knownForDepartment: String? + + var displayTitle: String { + title ?? name ?? "Unknown" + } + + var displayDate: String? { + releaseDate ?? firstAirDate + } + + var year: String? { + guard let date = displayDate, date.count >= 4 else { return nil } + return String(date.prefix(4)) + } + + var imageURL: URL? { + let path = posterPath ?? profilePath + guard let path else { return nil } + return URL(string: "https://image.tmdb.org/t/p/w200\(path)") + } +} + +enum TMDBError: LocalizedError { + case invalidURL, invalidResponse + + var errorDescription: String? { + switch self { + case .invalidURL: return "Invalid URL" + case .invalidResponse: return "Invalid response" + } + } +} + +// MARK: - Media Images +struct MediaImages: Codable { + let backdrops: [TMDBImage] + let posters: [TMDBImage] + + var sortedBackdrops: [TMDBImage] { + backdrops.sorted { $0.voteCount > $1.voteCount } + } + + var sortedPosters: [TMDBImage] { + posters.sorted { $0.voteCount > $1.voteCount } + } +} + +struct TMDBImage: Codable, Identifiable { + let aspectRatio: Double + let filePath: String + let height: Int + let width: Int + let voteAverage: Double + let voteCount: Int + + var id: String { filePath } + + var thumbnailURL: URL? { + URL(string: "https://image.tmdb.org/t/p/w500\(filePath)") + } + + var fullURL: URL? { + URL(string: "https://image.tmdb.org/t/p/original\(filePath)") + } + + var backdropURL: URL? { + URL(string: "https://image.tmdb.org/t/p/original\(filePath)") + } +} + +// MARK: - Watch Providers +struct WatchProvidersResponse: Codable { + let results: WatchProvidersResults +} + +struct WatchProvidersResults: Codable { + let BR: WatchProviderCountry? + let US: WatchProviderCountry? + let DE: WatchProviderCountry? + let ES: WatchProviderCountry? + let FR: WatchProviderCountry? + let IT: WatchProviderCountry? + let JP: WatchProviderCountry? + + func forLanguage(_ language: Language) -> WatchProviderCountry? { + switch language { + case .ptBR: return BR + case .enUS: return US + case .deDE: return DE + case .esES: return ES + case .frFR: return FR + case .itIT: return IT + case .jaJP: return JP + } + } +} + +struct WatchProviderCountry: Codable { + let flatrate: [WatchProvider]? + let rent: [WatchProvider]? + let buy: [WatchProvider]? +} + +struct WatchProvider: Codable, Identifiable { + let providerId: Int + let providerName: String + let logoPath: String? + + var id: Int { providerId } + + var logoURL: URL? { + guard let logoPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/w92\(logoPath)") + } +} + +// MARK: - Streaming Providers (for preferences) +struct StreamingProvidersResponse: Codable { + let results: [StreamingProvider] +} + +struct StreamingProvider: Codable, Identifiable { + let providerId: Int + let providerName: String + let logoPath: String? + + var id: Int { providerId } + + var logoURL: URL? { + guard let logoPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/w92\(logoPath)") + } +} + +// MARK: - Watch Regions +struct WatchRegionsResponse: Codable { + let results: [WatchRegion] +} + +struct WatchRegion: Codable, Identifiable { + let iso31661: String + let englishName: String + let nativeName: String + + var id: String { iso31661 } + + // Returns flag emoji for the country code + var flagEmoji: String { + let base: UInt32 = 127397 + var emoji = "" + for scalar in iso31661.uppercased().unicodeScalars { + if let unicode = UnicodeScalar(base + scalar.value) { + emoji.append(String(unicode)) + } + } + return emoji + } +} diff --git a/apps/ios/Plotwist/Plotwist/Services/UserEpisodeService.swift b/apps/ios/Plotwist/Plotwist/Services/UserEpisodeService.swift new file mode 100644 index 00000000..d8750490 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/UserEpisodeService.swift @@ -0,0 +1,186 @@ +// +// UserEpisodeService.swift +// Plotwist +// + +import Foundation + +// MARK: - User Episode Models +struct UserEpisode: Codable, Identifiable { + let id: String + let userId: String + let tmdbId: Int + let seasonNumber: Int + let episodeNumber: Int + let watchedAt: String +} + +struct CreateUserEpisodeData: Codable { + let tmdbId: Int + let seasonNumber: Int + let episodeNumber: Int + let runtime: Int? +} + +struct DeleteUserEpisodesData: Codable { + let ids: [String] +} + +// MARK: - User Episode Service +class UserEpisodeService { + static let shared = UserEpisodeService() + private init() {} + + // MARK: - Cache + private var episodeCache: [String: [UserEpisode]] = [:] + private let cacheDuration: TimeInterval = 300 // 5 minutes + private var cacheTimestamps: [String: Date] = [:] + + private func cacheKey(tmdbId: Int) -> String { + return "\(tmdbId)" + } + + func invalidateCache(tmdbId: Int) { + let key = cacheKey(tmdbId: tmdbId) + episodeCache.removeValue(forKey: key) + cacheTimestamps.removeValue(forKey: key) + } + + // MARK: - Get Watched Episodes + func getWatchedEpisodes(tmdbId: Int) async throws -> [UserEpisode] { + let key = cacheKey(tmdbId: tmdbId) + + // Check cache + if let cached = episodeCache[key], + let timestamp = cacheTimestamps[key], + Date().timeIntervalSince(timestamp) < cacheDuration { + return cached + } + + guard let url = URL(string: "\(API.baseURL)/user/episodes?tmdbId=\(tmdbId)"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw UserEpisodeError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw UserEpisodeError.invalidResponse + } + + // Empty array is valid when no episodes are watched + if http.statusCode == 200 { + let decoder = JSONDecoder() + let episodes = try decoder.decode([UserEpisode].self, from: data) + + // Cache result + episodeCache[key] = episodes + cacheTimestamps[key] = Date() + + return episodes + } + + throw UserEpisodeError.invalidResponse + } + + // MARK: - Mark Episode as Watched + func markAsWatched(tmdbId: Int, seasonNumber: Int, episodeNumber: Int, runtime: Int? = nil) async throws -> UserEpisode { + guard let url = URL(string: "\(API.baseURL)/user/episodes"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw UserEpisodeError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let episodeData = CreateUserEpisodeData( + tmdbId: tmdbId, + seasonNumber: seasonNumber, + episodeNumber: episodeNumber, + runtime: runtime + ) + + let encoder = JSONEncoder() + request.httpBody = try encoder.encode([episodeData]) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw UserEpisodeError.invalidResponse + } + + if http.statusCode == 409 { + throw UserEpisodeError.alreadyExists + } + + guard http.statusCode == 201 else { + print("Mark as watched failed with status: \(http.statusCode)") + if let responseString = String(data: data, encoding: .utf8) { + print("Response: \(responseString)") + } + throw UserEpisodeError.invalidResponse + } + + let decoder = JSONDecoder() + let episodes = try decoder.decode([UserEpisode].self, from: data) + + guard let episode = episodes.first else { + throw UserEpisodeError.invalidResponse + } + + // Invalidate cache + invalidateCache(tmdbId: tmdbId) + + return episode + } + + // MARK: - Unmark Episode as Watched + func unmarkAsWatched(id: String, tmdbId: Int) async throws { + guard let url = URL(string: "\(API.baseURL)/user/episodes"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw UserEpisodeError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let deleteData = DeleteUserEpisodesData(ids: [id]) + + let encoder = JSONEncoder() + request.httpBody = try encoder.encode(deleteData) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw UserEpisodeError.invalidResponse + } + + guard http.statusCode == 204 else { + print("Unmark as watched failed with status: \(http.statusCode)") + if let responseString = String(data: data, encoding: .utf8) { + print("Response: \(responseString)") + } + throw UserEpisodeError.invalidResponse + } + + // Invalidate cache + invalidateCache(tmdbId: tmdbId) + } +} + +// MARK: - Error Types +enum UserEpisodeError: Error { + case invalidURL + case invalidResponse + case alreadyExists +} diff --git a/apps/ios/Plotwist/Plotwist/Services/UserItemService.swift b/apps/ios/Plotwist/Plotwist/Services/UserItemService.swift new file mode 100644 index 00000000..65747e5d --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/UserItemService.swift @@ -0,0 +1,388 @@ +// +// UserItemService.swift +// Plotwist +// + +import Foundation + +class UserItemService { + static let shared = UserItemService() + private init() {} + + // MARK: - Cache + private var userItemCache: [String: CachedUserItem] = [:] + private let cacheDuration: TimeInterval = 300 // 5 minutes + + private struct CachedUserItem { + let userItem: UserItem? + let timestamp: Date + } + + private func cacheKey(tmdbId: Int, mediaType: String) -> String { + return "\(tmdbId)-\(mediaType)" + } + + func invalidateCache(tmdbId: Int, mediaType: String) { + let key = cacheKey(tmdbId: tmdbId, mediaType: mediaType) + userItemCache.removeValue(forKey: key) + } + + func invalidateAllCache() { + userItemCache.removeAll() + } + + // MARK: - Get All User Items by Status + func getAllUserItems(userId: String, status: String) async throws -> [UserItemSummary] { + guard let url = URL(string: "\(API.baseURL)/user/items/all?userId=\(userId)&status=\(status)") + else { + throw UserItemError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw UserItemError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(AllUserItemsResponse.self, from: data) + return result.userItems + } + + // MARK: - Get User Items Count + func getUserItemsCount(userId: String) async throws -> Int { + guard let url = URL(string: "\(API.baseURL)/user/items/count?userId=\(userId)") + else { + throw UserItemError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw UserItemError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(UserItemsCountResponse.self, from: data) + return result.count + } + + // MARK: - Get User Item + func getUserItem(tmdbId: Int, mediaType: String) async throws -> UserItem? { + let key = cacheKey(tmdbId: tmdbId, mediaType: mediaType) + + // Check cache + if let cached = userItemCache[key], + Date().timeIntervalSince(cached.timestamp) < cacheDuration { + return cached.userItem + } + + guard let token = UserDefaults.standard.string(forKey: "token"), + let url = URL(string: "\(API.baseURL)/user/item?tmdbId=\(tmdbId)&mediaType=\(mediaType)") + else { + throw UserItemError.invalidURL + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw UserItemError.invalidResponse + } + + if http.statusCode == 404 { + // Cache nil result + userItemCache[key] = CachedUserItem(userItem: nil, timestamp: Date()) + return nil + } + + guard http.statusCode == 200 else { + throw UserItemError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(UserItemResponse.self, from: data) + + // Cache result + userItemCache[key] = CachedUserItem(userItem: result.userItem, timestamp: Date()) + + return result.userItem + } + + // MARK: - Upsert User Item (Create or Update) + func upsertUserItem(tmdbId: Int, mediaType: String, status: UserItemStatus) async throws + -> UserItem + { + guard let url = URL(string: "\(API.baseURL)/user/item"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw UserItemError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let body: [String: Any] = [ + "tmdbId": tmdbId, + "mediaType": mediaType, + "status": status.rawValue, + ] + + request.httpBody = try JSONSerialization.data(withJSONObject: body) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw UserItemError.invalidResponse + } + + guard http.statusCode == 200 || http.statusCode == 201 else { + throw UserItemError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(UpsertUserItemResponse.self, from: data) + + // Update cache with new value + let key = cacheKey(tmdbId: tmdbId, mediaType: mediaType) + userItemCache[key] = CachedUserItem(userItem: result.userItem, timestamp: Date()) + + return result.userItem + } + + // MARK: - Delete User Item + func deleteUserItem(id: String, tmdbId: Int, mediaType: String) async throws { + guard let url = URL(string: "\(API.baseURL)/user/item/\(id)"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw UserItemError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let (_, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw UserItemError.invalidResponse + } + + guard http.statusCode == 200 || http.statusCode == 204 else { + throw UserItemError.invalidResponse + } + + // Invalidate cache + invalidateCache(tmdbId: tmdbId, mediaType: mediaType) + } + + // MARK: - Add Watch Entry (Rewatch) + func addWatchEntry(userItemId: String, watchedAt: Date = Date()) async throws -> WatchEntry { + guard let url = URL(string: "\(API.baseURL)/watch-entry"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw UserItemError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + let body: [String: Any] = [ + "userItemId": userItemId, + "watchedAt": formatter.string(from: watchedAt), + ] + + request.httpBody = try JSONSerialization.data(withJSONObject: body) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw UserItemError.invalidResponse + } + + guard http.statusCode == 200 || http.statusCode == 201 else { + throw UserItemError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(WatchEntryResponse.self, from: data) + return result.watchEntry + } + + // MARK: - Delete Watch Entry + func deleteWatchEntry(id: String) async throws { + guard let url = URL(string: "\(API.baseURL)/watch-entry/\(id)"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw UserItemError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let (_, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw UserItemError.invalidResponse + } + + guard http.statusCode == 200 || http.statusCode == 204 else { + throw UserItemError.invalidResponse + } + } + + // MARK: - Update Watch Entry Date + func updateWatchEntry(id: String, watchedAt: Date) async throws -> WatchEntry { + guard let url = URL(string: "\(API.baseURL)/watch-entry/\(id)"), + let token = UserDefaults.standard.string(forKey: "token") + else { + throw UserItemError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + let body: [String: Any] = [ + "watchedAt": formatter.string(from: watchedAt) + ] + + request.httpBody = try JSONSerialization.data(withJSONObject: body) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let http = response as? HTTPURLResponse else { + throw UserItemError.invalidResponse + } + + guard http.statusCode == 200 else { + throw UserItemError.invalidResponse + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(WatchEntryResponse.self, from: data) + return result.watchEntry + } +} + +// MARK: - Models +enum UserItemStatus: String, CaseIterable { + case watched = "WATCHED" + case watching = "WATCHING" + case watchlist = "WATCHLIST" + case dropped = "DROPPED" + + var icon: String { + switch self { + case .watched: return "eye.fill" + case .watching: return "play.circle.fill" + case .watchlist: return "clock.fill" + case .dropped: return "xmark.circle.fill" + } + } + + func displayName(strings: Strings) -> String { + switch self { + case .watched: return strings.watched + case .watching: return strings.watching + case .watchlist: return strings.watchlist + case .dropped: return strings.dropped + } + } +} + +struct WatchEntry: Codable, Identifiable { + let id: String + let watchedAt: String + + var date: Date? { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.date(from: watchedAt) + } +} + +struct UserItem: Codable, Identifiable { + let id: String + let userId: String + let tmdbId: Int + let mediaType: String + let status: String + let addedAt: String + let updatedAt: String + let watchEntries: [WatchEntry]? + + var statusEnum: UserItemStatus? { + UserItemStatus(rawValue: status) + } +} + +struct UserItemResponse: Codable { + let userItem: UserItem? +} + +struct UpsertUserItemResponse: Codable { + let userItem: UserItem +} + +struct WatchEntryResponse: Codable { + let watchEntry: WatchEntry +} + +struct WatchEntriesResponse: Codable { + let watchEntries: [WatchEntry] +} + +struct UserItemSummary: Codable, Identifiable { + let id: String + let mediaType: String + let tmdbId: Int +} + +struct AllUserItemsResponse: Codable { + let userItems: [UserItemSummary] +} + +struct UserItemsCountResponse: Codable { + let count: Int +} + +enum UserItemError: LocalizedError { + case invalidURL + case invalidResponse + case serverError(String) + + var errorDescription: String? { + switch self { + case .invalidURL: return "Invalid URL" + case .invalidResponse: return "Invalid response from server" + case .serverError(let message): return "Server error: \(message)" + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Services/UserPreferencesManager.swift b/apps/ios/Plotwist/Plotwist/Services/UserPreferencesManager.swift new file mode 100644 index 00000000..fee02336 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Services/UserPreferencesManager.swift @@ -0,0 +1,84 @@ +// +// UserPreferencesManager.swift +// Plotwist +// + +import Foundation +import Combine + +class UserPreferencesManager: ObservableObject { + static let shared = UserPreferencesManager() + + @Published var preferences: UserPreferences? + @Published var isLoading = false + + private init() { + // Listen for profile updates + NotificationCenter.default.addObserver( + self, + selector: #selector(handleProfileUpdated), + name: .profileUpdated, + object: nil + ) + + // Listen for auth changes + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAuthChanged), + name: .authChanged, + object: nil + ) + + // Load preferences if authenticated + if AuthService.shared.isAuthenticated { + Task { await loadPreferences() } + } + } + + @objc private func handleProfileUpdated() { + Task { await loadPreferences() } + } + + @objc private func handleAuthChanged() { + if AuthService.shared.isAuthenticated { + Task { await loadPreferences() } + } else { + preferences = nil + } + } + + func loadPreferences() async { + guard AuthService.shared.isAuthenticated else { + await MainActor.run { preferences = nil } + return + } + + await MainActor.run { isLoading = true } + defer { Task { @MainActor in isLoading = false } } + + do { + let prefs = try await AuthService.shared.getUserPreferences() + await MainActor.run { preferences = prefs } + } catch { + print("Error loading preferences: \(error)") + } + } + + // MARK: - Computed Properties + var hasStreamingServices: Bool { + guard let ids = preferences?.watchProvidersIds else { return false } + return !ids.isEmpty + } + + var watchRegion: String? { + preferences?.watchRegion + } + + var watchProvidersIds: [Int] { + preferences?.watchProvidersIds ?? [] + } + + var watchProvidersString: String { + watchProvidersIds.map { String($0) }.joined(separator: "|") + } +} diff --git a/apps/ios/Plotwist/Plotwist/Theme/Colors.swift b/apps/ios/Plotwist/Plotwist/Theme/Colors.swift new file mode 100644 index 00000000..1299efb3 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Theme/Colors.swift @@ -0,0 +1,188 @@ +// +// Colors.swift +// Plotwist +// +// Dark theme matched to web globals.css + +import SwiftUI +import UIKit + +extension Color { + // MARK: - Hex Initializer + init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + self.init( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) + } + + // MARK: - Adaptive Colors (Light/Dark mode) + + // Dark: --background: 240 10% 3.9% (web) + static var appBackgroundAdaptive: Color { + Color( + UIColor { + $0.userInterfaceStyle == .dark + ? UIColor(hue: 240 / 360, saturation: 0.10, brightness: 0.039, alpha: 1) + : UIColor(hue: 0, saturation: 0, brightness: 1, alpha: 1) + }) + } + + static var appForegroundAdaptive: Color { + Color( + UIColor { + $0.userInterfaceStyle == .dark + ? UIColor(hue: 0, saturation: 0, brightness: 0.98, alpha: 1) + : UIColor(hue: 240 / 360, saturation: 0.10, brightness: 0.039, alpha: 1) + }) + } + + // Dark: --border: 240 3.7% 15.9% (web) + static var appBorderAdaptive: Color { + Color( + UIColor { + $0.userInterfaceStyle == .dark + ? UIColor(hue: 240 / 360, saturation: 0.037, brightness: 0.159, alpha: 1) + : UIColor(hue: 240 / 360, saturation: 0.059, brightness: 0.90, alpha: 1) + }) + } + + // Dark: --muted-foreground: 240 5% 64.9% (web) + static var appMutedForegroundAdaptive: Color { + Color( + UIColor { + $0.userInterfaceStyle == .dark + ? UIColor(hue: 240 / 360, saturation: 0.05, brightness: 0.649, alpha: 1) + : UIColor(hue: 240 / 360, saturation: 0.038, brightness: 0.461, alpha: 1) + }) + } + + // Dark: --secondary: 240 3.7% 15.9% (web) + static var appInputFilled: Color { + Color( + UIColor { + $0.userInterfaceStyle == .dark + ? UIColor(hue: 240 / 360, saturation: 0.037, brightness: 0.159, alpha: 1) + : UIColor(red: 243 / 255, green: 244 / 255, blue: 246 / 255, alpha: 1) + }) + } + + static let appDestructive = Color(hue: 0, saturation: 0.842, brightness: 0.602) + + // Star rating yellow - bright gold that works in both light and dark modes + static let appStarYellow = Color(hex: "FBBF24") +} + +// MARK: - Layered Shadow Modifier +extension View { + /// Applies a smooth layered shadow effect similar to iOS app icons + /// Based on the "Derek Briggs" shadow style with multiple stacked layers + func posterShadow() -> some View { + self + // Base border shadow (spread: 1px simulated with small radius) + .shadow(color: Color.black.opacity(0.05), radius: 0.5, x: 0, y: 0) + // Layer 1: Y: 1px, Blur: 1px + .shadow(color: Color.black.opacity(0.05), radius: 0.5, x: 0, y: 1) + // Layer 2: Y: 2px, Blur: 2px + .shadow(color: Color.black.opacity(0.05), radius: 1, x: 0, y: 2) + // Layer 3: Y: 4px, Blur: 4px + .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 4) + // Layer 4: Y: 8px, Blur: 8px + .shadow(color: Color.black.opacity(0.05), radius: 4, x: 0, y: 8) + // Layer 5: Y: 16px, Blur: 16px + .shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 16) + } + + /// Applies a subtle border to poster cards (dark mode only) + func posterBorder(cornerRadius: CGFloat = 16) -> some View { + self.modifier(PosterBorderModifier(cornerRadius: cornerRadius)) + } + + /// Applies a border with custom top corners only (dark mode only) + func topRoundedBorder(cornerRadius: CGFloat) -> some View { + self.modifier(TopRoundedBorderModifier(cornerRadius: cornerRadius)) + } +} + +// MARK: - Border Modifiers (Dark Mode Only) +struct PosterBorderModifier: ViewModifier { + let cornerRadius: CGFloat + @Environment(\.colorScheme) var colorScheme + + func body(content: Content) -> some View { + content.overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder( + colorScheme == .dark ? Color.appBorderAdaptive : Color.clear, + lineWidth: 1 + ) + ) + } +} + +struct TopRoundedBorderModifier: ViewModifier { + let cornerRadius: CGFloat + @Environment(\.colorScheme) var colorScheme + + func body(content: Content) -> some View { + content.overlay( + TopEdgeShape(cornerRadius: cornerRadius) + .stroke( + colorScheme == .dark ? Color.appBorderAdaptive : Color.clear, + lineWidth: 1 + ) + ) + } +} + +// MARK: - Custom Shape for Top Edge Only (with rounded corners) +struct TopEdgeShape: Shape { + var cornerRadius: CGFloat + + func path(in rect: CGRect) -> Path { + var path = Path() + + // Start from bottom-left, go up to the curve start + path.move(to: CGPoint(x: 0, y: cornerRadius)) + + // Top-left corner curve + path.addArc( + center: CGPoint(x: cornerRadius, y: cornerRadius), + radius: cornerRadius, + startAngle: .degrees(180), + endAngle: .degrees(270), + clockwise: false + ) + + // Top edge + path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: 0)) + + // Top-right corner curve + path.addArc( + center: CGPoint(x: rect.width - cornerRadius, y: cornerRadius), + radius: cornerRadius, + startAngle: .degrees(270), + endAngle: .degrees(0), + clockwise: false + ) + + return path + } +} diff --git a/apps/ios/Plotwist/Plotwist/Theme/ThemeManager.swift b/apps/ios/Plotwist/Plotwist/Theme/ThemeManager.swift new file mode 100644 index 00000000..c90be8f2 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Theme/ThemeManager.swift @@ -0,0 +1,55 @@ +// +// ThemeManager.swift +// Plotwist +// + +import SwiftUI + +enum AppTheme: String, CaseIterable { + case system + case light + case dark + + var colorScheme: ColorScheme? { + switch self { + case .system: return nil + case .light: return .light + case .dark: return .dark + } + } + + var icon: String { + switch self { + case .system: return "circle.lefthalf.filled" + case .light: return "sun.max.fill" + case .dark: return "moon.fill" + } + } +} + +class ThemeManager: ObservableObject { + static let shared = ThemeManager() + + @Published var current: AppTheme { + didSet { + UserDefaults.standard.set(current.rawValue, forKey: "appTheme") + } + } + + private init() { + if let saved = UserDefaults.standard.string(forKey: "appTheme"), + let theme = AppTheme(rawValue: saved) { + self.current = theme + } else { + self.current = .system + } + } + + func toggle() { + switch current { + case .system: current = .light + case .light: current = .dark + case .dark: current = .system + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Utils/Constants.swift b/apps/ios/Plotwist/Plotwist/Utils/Constants.swift new file mode 100644 index 00000000..78331f89 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Utils/Constants.swift @@ -0,0 +1,10 @@ +// +// Constants.swift +// Plotwist +// + +import Foundation + +enum API { + static let baseURL = "https://backend.plotwist.app" +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Auth/LoginView.swift b/apps/ios/Plotwist/Plotwist/Views/Auth/LoginView.swift new file mode 100644 index 00000000..a15f3c18 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Auth/LoginView.swift @@ -0,0 +1,226 @@ +// +// LoginView.swift +// Plotwist +// + +import SwiftUI + +struct LoginView: View { + @State private var login = "" + @State private var password = "" + @State private var showPassword = false + @State private var isLoading = false + @State private var error: String? + @State private var strings = L10n.current + @State private var isKeyboardVisible = false + + var body: some View { + NavigationView { + ZStack { + // Gradient Background + LinearGradient( + colors: [ + Color.appBackgroundAdaptive, + Color.appBackgroundAdaptive.opacity(0.95), + Color.appInputFilled.opacity(0.3), + ], + startPoint: .top, + endPoint: .bottom + ) + .ignoresSafeArea() + + // Noise Overlay + NoiseOverlay() + + VStack(spacing: 0) { + // Language Switcher at top (centered) + Menu { + ForEach(Language.allCases, id: \.self) { lang in + Button { + Language.current = lang + } label: { + HStack { + Text(lang.displayName) + if Language.current == lang { + Image(systemName: "checkmark") + } + } + } + } + } label: { + HStack(spacing: 6) { + Image(systemName: "globe") + Text(Language.current.displayName) + .font(.subheadline) + } + .foregroundColor(.appMutedForegroundAdaptive) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.appBorderAdaptive, lineWidth: 1) + ) + } + .padding(.top, 16) + .opacity(isKeyboardVisible ? 0 : 1) + .scaleEffect(isKeyboardVisible ? 0.8 : 1) + .animation(.spring(response: 0.15, dampingFraction: 0.8), value: isKeyboardVisible) + + Spacer() + + // Centered form content + VStack(spacing: 16) { + // Login Field + VStack(alignment: .leading, spacing: 6) { + Text(strings.loginLabel) + .font(.subheadline.weight(.medium)) + TextField(strings.loginPlaceholder, text: $login) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(12) + .background(Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.appBorderAdaptive, lineWidth: 1) + ) + } + + // Password Field + VStack(alignment: .leading, spacing: 6) { + Text(strings.passwordLabel) + .font(.subheadline.weight(.medium)) + HStack(spacing: 8) { + ZStack { + TextField(strings.passwordPlaceholder, text: $password) + .opacity(showPassword ? 1 : 0) + SecureField(strings.passwordPlaceholder, text: $password) + .opacity(showPassword ? 0 : 1) + } + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(12) + .background(Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.appBorderAdaptive, lineWidth: 1) + ) + + Button { + showPassword.toggle() + } label: { + Image(systemName: showPassword ? "eye" : "eye.slash") + .foregroundColor(.appMutedForegroundAdaptive) + .frame(width: 48, height: 48) + .background(Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.appBorderAdaptive, lineWidth: 1) + ) + } + .buttonStyle(.plain) + } + } + + if let error { + Text(error) + .font(.caption) + .foregroundColor(.appDestructive) + } + + PrimaryButton(strings.accessButton, variant: .filled, isLoading: isLoading) { + Task { await performLogin() } + } + + // Divider + HStack { + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1) + Text(strings.or) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1) + } + + // Social Login Buttons (disabled) + SocialButton(strings.continueWithGoogle, icon: "globe", isDisabled: true) {} + SocialButton(strings.continueWithApple, icon: "apple.logo", isDisabled: true) {} + } + .padding(.horizontal, 24) + .frame(maxWidth: 400) + + Spacer() + + // Bottom link + NavigationLink(destination: SignUpView()) { + Text("\(strings.doNotHaveAccount) \(strings.createNow)") + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + .padding(.bottom, 32) + } + } + .navigationBarHidden(true) + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) + { _ in + isKeyboardVisible = true + } + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) + { _ in + isKeyboardVisible = false + } + } + + private func performLogin() async { + error = nil + + guard !login.isEmpty else { + error = strings.loginRequired + return + } + guard password.count >= 8 else { + error = strings.passwordLength + return + } + + isLoading = true + defer { isLoading = false } + + do { + _ = try await AuthService.shared.signIn(login: login, password: password) + } catch { + self.error = strings.invalidCredentials + } + } +} + +// MARK: - Noise Overlay +struct NoiseOverlay: View { + var body: some View { + Canvas { context, size in + for _ in 0..= 8 else { + error = strings.passwordLength + return + } + + isLoading = true + defer { isLoading = false } + + do { + let available = try await AuthService.shared.checkEmailAvailable(email: email) + if available { + showUsernameSheet = true + } else { + error = strings.emailAlreadyTaken + } + } catch { + self.error = strings.emailAlreadyTaken + } + } +} + +// MARK: - Username Sheet +struct UsernameSheetView: View { + @Environment(\.dismiss) private var dismiss + @Binding var username: String + let email: String + let password: String + let onError: (String) -> Void + + @State private var isLoading = false + @State private var error: String? + @State private var strings = L10n.current + + var body: some View { + FloatingSheetContainer { + VStack(spacing: 0) { + // Drag Indicator + RoundedRectangle(cornerRadius: 2.5) + .fill(Color.gray.opacity(0.4)) + .frame(width: 36, height: 5) + .padding(.top, 12) + .padding(.bottom, 16) + + VStack(spacing: 24) { + VStack(spacing: 8) { + Text(strings.selectUsername) + .font(.system(size: 20, weight: .bold)) + .foregroundColor(.appForegroundAdaptive) + Text(strings.selectUsernameDescription) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .multilineTextAlignment(.center) + } + + VStack(spacing: 16) { + TextField(strings.usernamePlaceholder, text: $username) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(12) + .background(Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.appBorderAdaptive, lineWidth: 1) + ) + + if let error { + Text(error) + .font(.caption) + .foregroundColor(.appDestructive) + } + + PrimaryButton(strings.finishSignUp, variant: .filled, isLoading: isLoading) { + Task { await checkUsernameAndFinish() } + } + } + } + .padding(.horizontal, 24) + .padding(.bottom, 24) + } + } + .floatingSheetPresentation(height: 320) + } + + private func checkUsernameAndFinish() async { + error = nil + + guard !username.isEmpty else { + error = strings.usernameRequired + return + } + + isLoading = true + defer { isLoading = false } + + do { + let available = try await AuthService.shared.checkUsernameAvailable(username: username) + if available { + try await AuthService.shared.signUp(email: email, password: password, username: username) + dismiss() + } else { + error = strings.usernameAlreadyTaken + } + } catch AuthError.alreadyExists { + error = strings.usernameAlreadyTaken + } catch { + onError(strings.invalidCredentials) + dismiss() + } + } +} + +#Preview { + SignUpView() +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/MediaDetailView.swift b/apps/ios/Plotwist/Plotwist/Views/Details/MediaDetailView.swift new file mode 100644 index 00000000..c5c54311 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/MediaDetailView.swift @@ -0,0 +1,593 @@ +// +// MediaDetailView.swift +// Plotwist +// + +import SwiftUI + +struct MediaDetailView: View { + let mediaId: Int + let mediaType: String + + @Environment(\.dismiss) private var dismiss + @State private var details: MovieDetails? + @State private var isLoading = true + @State private var userReview: Review? + @State private var userItem: UserItem? + @State private var isLoadingUserReview = false + @State private var isLoadingUserItem = false + @State private var showReviewSheet = false + @State private var reviewsRefreshId = UUID() + @State private var backdropImages: [TMDBImage] = [] + @State private var currentBackdropIndex = 0 + @ObservedObject private var themeManager = ThemeManager.shared + + // Section visibility state + @State private var hasReviews = false + @State private var hasWhereToWatch = false + @State private var hasSeasons = false + @State private var hasRecommendations = false + + // Collection state + @State private var collection: MovieCollection? + @State private var showCollectionSheet = false + @State private var selectedCollectionMovieId: Int? + + // Layout constants + private let cornerRadius: CGFloat = 24 + private let posterOverlapOffset: CGFloat = -70 + + var body: some View { + ZStack { + // Background color + Color.appBackgroundAdaptive.ignoresSafeArea() + + if isLoading { + MediaDetailSkeletonView(cornerRadius: cornerRadius) + } else if let details { + GeometryReader { geometry in + let backdropHeight = geometry.size.height * 0.45 + + ZStack(alignment: .topLeading) { + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + // Backdrop Section (stays behind content) + ZStack(alignment: .bottomTrailing) { + // Backdrop Image/Carousel (using optimized cached loading) + if backdropImages.isEmpty { + BackdropImage( + url: details.backdropURL, + height: backdropHeight + cornerRadius + ) + } else { + NavigationLink( + destination: MediaImagesView(mediaId: mediaId, mediaType: mediaType) + ) { + CarouselBackdropView( + images: backdropImages, + height: backdropHeight + cornerRadius, + currentIndex: $currentBackdropIndex + ) + } + .buttonStyle(.plain) + } + + // Image counter (only show when we have multiple images) + if !backdropImages.isEmpty { + Text("\(currentBackdropIndex + 1)/\(min(backdropImages.count, 10))") + .font(.caption.weight(.semibold)) + .foregroundColor(.white) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Color.black.opacity(0.6)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .padding(.trailing, 16) + .padding(.bottom, cornerRadius + 12) + } + } + + // Content Card (rounded, overlaps backdrop) + ZStack(alignment: .topLeading) { + // Background card with rounded corners + VStack(alignment: .leading, spacing: 0) { + // Spacer for poster overlap area + Spacer() + .frame(height: 110) + + // Content Section + VStack(alignment: .leading, spacing: 20) { + // Action Buttons (Review + Status) + if AuthService.shared.isAuthenticated { + MediaDetailViewActions( + mediaId: mediaId, + mediaType: mediaType, + userReview: userReview, + userItem: userItem, + isLoadingReview: isLoadingUserReview, + isLoadingStatus: isLoadingUserItem, + onReviewTapped: { + showReviewSheet = true + }, + onStatusChanged: { newItem in + userItem = newItem + } + ) + } + + // Overview + if let overview = details.overview, !overview.isEmpty { + Text(overview) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .lineSpacing(6) + } + } + .padding(.horizontal, 24) + .padding(.top, 16) + + // Genres Badges (outside padding for full-width scroll) + if let genres = details.genres, !genres.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(genres) { genre in + BadgeView(text: genre.name) + } + } + .padding(.horizontal, 24) + } + .scrollClipDisabled() + .padding(.top, 16) + } + + // Collection Section (only for movies that belong to a collection) + if let collection = collection { + MovieCollectionSection( + collection: collection, + onSeeCollectionTapped: { + showCollectionSheet = true + } + ) + .padding(.top, 24) + } + + // Divider before first content section + if hasReviews || hasWhereToWatch || hasSeasons || hasRecommendations { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + .padding(.horizontal, 24) + .padding(.vertical, 24) + } + + // Review Section + ReviewSectionView( + mediaId: mediaId, + mediaType: mediaType, + refreshId: reviewsRefreshId, + onEmptyStateTapped: { + if AuthService.shared.isAuthenticated { + showReviewSheet = true + } + }, + onContentLoaded: { hasContent in + hasReviews = hasContent + } + ) + + // Divider after reviews + if hasReviews && (hasWhereToWatch || hasSeasons || hasRecommendations) { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + .padding(.horizontal, 24) + .padding(.vertical, 24) + } + + // Where to Watch Section + WhereToWatchSection( + mediaId: mediaId, + mediaType: mediaType, + onContentLoaded: { hasContent in + hasWhereToWatch = hasContent + } + ) + + // Divider after where to watch + if hasWhereToWatch && (hasSeasons || hasRecommendations) { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + .padding(.horizontal, 24) + .padding(.vertical, 24) + } + + // Seasons Section (only for TV series) + if mediaType != "movie" { + SeasonsSection( + seasons: details.displaySeasons, + seriesId: mediaId, + seriesName: details.displayTitle, + onContentLoaded: { hasContent in + hasSeasons = hasContent + } + ) + } + + // Divider after seasons + if hasSeasons && hasRecommendations { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + .padding(.horizontal, 24) + .padding(.vertical, 24) + } + + // Recommendations Section + RelatedSection( + mediaId: mediaId, + mediaType: mediaType, + onContentLoaded: { hasContent in + hasRecommendations = hasContent + } + ) + + Spacer() + .frame(height: 80) + } + .background(Color.appBackgroundAdaptive) + .clipShape( + RoundedCorner(radius: cornerRadius, corners: [.topLeft, .topRight]) + ) + + // Poster and Info (overlaid on top, outside clipShape) + HStack(alignment: .bottom, spacing: 16) { + // Poster (using cached image loading) + CachedAsyncImage( + url: details.posterURL, + priority: .high + ) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + } + .frame(width: 120, height: 180) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .posterBorder(cornerRadius: 12) + .posterShadow() + + // Info + VStack(alignment: .leading, spacing: 4) { + if let releaseDate = details.formattedReleaseDate( + locale: Language.current.rawValue) + { + Text(releaseDate) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + + Text(details.displayTitle) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + } + .padding(.bottom, 8) + + Spacer() + } + .padding(.horizontal, 24) + .offset(y: -70) + } + .offset(y: -cornerRadius) + } + } + .ignoresSafeArea(edges: .top) + + // Sticky Back Button + VStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + .frame(width: 40, height: 40) + .background(.ultraThinMaterial) + .clipShape(Circle()) + } + .padding(.leading, 24) + Spacer() + } + .padding(.top, 8) + .safeAreaPadding(.top) + } + } + } + } + .navigationBarHidden(true) + .preferredColorScheme(themeManager.current.colorScheme) + .sheet(isPresented: $showReviewSheet) { + ReviewSheet(mediaId: mediaId, mediaType: mediaType, existingReview: userReview) + } + .sheet(isPresented: $showCollectionSheet) { + if let collection = collection { + MovieCollectionSheet(collection: collection) { movieId in + selectedCollectionMovieId = movieId + } + } + } + .background( + NavigationLink( + destination: Group { + if let movieId = selectedCollectionMovieId { + MediaDetailView(mediaId: movieId, mediaType: "movie") + } + }, + isActive: Binding( + get: { selectedCollectionMovieId != nil }, + set: { if !$0 { selectedCollectionMovieId = nil } } + ) + ) { + EmptyView() + } + .hidden() + ) + .task { + // Start loading user data states immediately if authenticated + if AuthService.shared.isAuthenticated { + isLoadingUserReview = true + isLoadingUserItem = true + } + + await loadDetails() + await loadImages() + await loadCollection() + if AuthService.shared.isAuthenticated { + await loadUserReview() + await loadUserItem() + } + } + .onChange(of: showReviewSheet) { _, isPresented in + if !isPresented && AuthService.shared.isAuthenticated { + Task { + await loadUserReview() + } + // Refresh the reviews list + reviewsRefreshId = UUID() + } + } + } + + private func loadDetails() async { + // Skip if already loaded + guard details == nil else { + isLoading = false + return + } + + isLoading = true + defer { isLoading = false } + + do { + if mediaType == "movie" { + details = try await TMDBService.shared.getMovieDetails( + id: mediaId, + language: Language.current.rawValue + ) + } else { + details = try await TMDBService.shared.getTVSeriesDetails( + id: mediaId, + language: Language.current.rawValue + ) + } + } catch { + details = nil + } + } + + private func loadUserReview() async { + isLoadingUserReview = true + defer { isLoadingUserReview = false } + + do { + userReview = try await ReviewService.shared.getUserReview( + tmdbId: mediaId, + mediaType: mediaType == "movie" ? "MOVIE" : "TV_SHOW" + ) + } catch { + userReview = nil + } + } + + private func loadUserItem() async { + isLoadingUserItem = true + defer { isLoadingUserItem = false } + + do { + userItem = try await UserItemService.shared.getUserItem( + tmdbId: mediaId, + mediaType: mediaType == "movie" ? "MOVIE" : "TV_SHOW" + ) + } catch { + userItem = nil + } + } + + private func loadImages() async { + // Skip if already loaded + guard backdropImages.isEmpty else { return } + + do { + let images = try await TMDBService.shared.getImages(id: mediaId, mediaType: mediaType) + backdropImages = images.sortedBackdrops + // Note: Prefetching is now handled automatically by CarouselBackdropView + } catch { + backdropImages = [] + } + } + + private func loadCollection() async { + // Only load collection for movies + guard mediaType == "movie" else { return } + guard let details = details, let belongsTo = details.belongsToCollection else { return } + + do { + collection = try await TMDBService.shared.getCollectionDetails( + id: belongsTo.id, + language: Language.current.rawValue + ) + } catch { + collection = nil + } + } +} + +// MARK: - Badge View +struct BadgeView: View { + let text: String + + var body: some View { + Text(text) + .font(.caption) + .foregroundColor(.appForegroundAdaptive) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Color.appInputFilled) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } +} + +// MARK: - Rounded Corner Shape +struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} + +// MARK: - Media Detail Skeleton View +struct MediaDetailSkeletonView: View { + let cornerRadius: CGFloat + + var body: some View { + GeometryReader { geometry in + let backdropHeight = geometry.size.height * 0.45 + + ZStack(alignment: .topLeading) { + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + // Backdrop Skeleton + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: backdropHeight + cornerRadius) + + // Content Card Skeleton + ZStack(alignment: .topLeading) { + VStack(alignment: .leading, spacing: 0) { + // Spacer for poster overlap area + Spacer() + .frame(height: 110) + + // Content Section Skeleton + VStack(alignment: .leading, spacing: 20) { + // Action Buttons Skeleton + HStack(spacing: 12) { + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 48) + + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 48) + } + + // Overview Skeleton + VStack(alignment: .leading, spacing: 8) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 14) + + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 14) + + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(width: 200, height: 14) + } + } + .padding(.horizontal, 24) + .padding(.top, 16) + + // Genres Skeleton + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(0..<4, id: \.self) { _ in + RoundedRectangle(cornerRadius: 8) + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(width: 70, height: 28) + } + } + .padding(.horizontal, 24) + } + .padding(.top, 16) + + Spacer() + .frame(height: 80) + } + .background(Color.appBackgroundAdaptive) + .clipShape( + RoundedCorner(radius: cornerRadius, corners: [.topLeft, .topRight]) + ) + + // Poster and Info Skeleton + HStack(alignment: .bottom, spacing: 16) { + // Poster Skeleton + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(width: 120, height: 180) + + // Info Skeleton + VStack(alignment: .leading, spacing: 8) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(width: 80, height: 12) + + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(width: 150, height: 18) + } + .padding(.bottom, 8) + + Spacer() + } + .padding(.horizontal, 24) + .offset(y: -70) + } + .offset(y: -cornerRadius) + } + } + .ignoresSafeArea(edges: .top) + + // Back Button Skeleton + VStack { + Circle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(width: 40, height: 40) + .padding(.leading, 24) + Spacer() + } + .padding(.top, 8) + .safeAreaPadding(.top) + } + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/MediaImagesView.swift b/apps/ios/Plotwist/Plotwist/Views/Details/MediaImagesView.swift new file mode 100644 index 00000000..b3b5b9e9 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/MediaImagesView.swift @@ -0,0 +1,296 @@ +// +// MediaImagesView.swift +// Plotwist +// + +import SwiftUI + +struct MediaImagesView: View { + let mediaId: Int + let mediaType: String + + @Environment(\.dismiss) private var dismiss + @State private var images: MediaImages? + @State private var isLoading = true + @State private var selectedImage: TMDBImage? + @ObservedObject private var themeManager = ThemeManager.shared + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header (same as CategoryListView) + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(L10n.current.images) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + Color.clear + .frame(width: 40, height: 40) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1) + } + + // Content + if isLoading { + Spacer() + ProgressView() + Spacer() + } else if let images, !images.backdrops.isEmpty || !images.posters.isEmpty { + ScrollView(showsIndicators: false) { + // Masonry layout with all images + ImageMasonryView( + images: allImages, + onImageTap: { image in + selectedImage = image + } + ) + .padding(.horizontal, 24) + .padding(.vertical, 24) + } + } else { + Spacer() + VStack(spacing: 12) { + Image(systemName: "photo.on.rectangle.angled") + .font(.system(size: 48)) + .foregroundColor(.appMutedForegroundAdaptive) + Text(L10n.current.noImagesFound) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + } + Spacer() + } + } + } + .navigationBarHidden(true) + .preferredColorScheme(themeManager.current.colorScheme) + .fullScreenCover(item: $selectedImage) { image in + ImageFullScreenView(image: image) + } + .task { + await loadImages() + } + } + + private var allImages: [TMDBImage] { + guard let images else { return [] } + // Combine all images and sort by vote count for better distribution + return (images.backdrops + images.posters).sorted { $0.voteCount > $1.voteCount } + } + + private func loadImages() async { + isLoading = true + defer { isLoading = false } + + do { + images = try await TMDBService.shared.getImages(id: mediaId, mediaType: mediaType) + } catch { + images = nil + } + } +} + +// MARK: - Image Masonry View +struct ImageMasonryView: View { + let images: [TMDBImage] + let onImageTap: (TMDBImage) -> Void + + private let spacing: CGFloat = 8 + + var body: some View { + GeometryReader { geometry in + let columnWidth = (geometry.size.width - spacing) / 2 + + HStack(alignment: .top, spacing: spacing) { + // Left column + LazyVStack(spacing: spacing) { + ForEach(leftColumnImages) { image in + MasonryImageCell( + image: image, + width: columnWidth, + onTap: { onImageTap(image) } + ) + } + } + + // Right column + LazyVStack(spacing: spacing) { + ForEach(rightColumnImages) { image in + MasonryImageCell( + image: image, + width: columnWidth, + onTap: { onImageTap(image) } + ) + } + } + } + } + .frame(height: calculateTotalHeight()) + } + + private var leftColumnImages: [TMDBImage] { + images.enumerated().filter { $0.offset % 2 == 0 }.map { $0.element } + } + + private var rightColumnImages: [TMDBImage] { + images.enumerated().filter { $0.offset % 2 == 1 }.map { $0.element } + } + + private func calculateTotalHeight() -> CGFloat { + let screenWidth = UIScreen.main.bounds.width - 48 // 24 padding on each side + let columnWidth = (screenWidth - spacing) / 2 + + var leftHeight: CGFloat = 0 + var rightHeight: CGFloat = 0 + + for (index, image) in images.enumerated() { + let imageHeight = columnWidth / image.aspectRatio + if index % 2 == 0 { + leftHeight += imageHeight + spacing + } else { + rightHeight += imageHeight + spacing + } + } + + return max(leftHeight, rightHeight) + } +} + +// MARK: - Masonry Image Cell +struct MasonryImageCell: View { + let image: TMDBImage + let width: CGFloat + let onTap: () -> Void + + private var height: CGFloat { + width / image.aspectRatio + } + + var body: some View { + Button(action: onTap) { + CachedAsyncImage(url: image.thumbnailURL) { loadedImage in + loadedImage + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + .fill(Color.appBorderAdaptive) + } + .frame(width: width, height: height) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .posterBorder(cornerRadius: 12) + } + .buttonStyle(.plain) + } +} + +// MARK: - Full Screen Image View +struct ImageFullScreenView: View { + let image: TMDBImage + + @Environment(\.dismiss) private var dismiss + @State private var scale: CGFloat = 1.0 + @State private var lastScale: CGFloat = 1.0 + @State private var offset: CGSize = .zero + @State private var lastOffset: CGSize = .zero + + var body: some View { + ZStack { + Color.black.ignoresSafeArea() + + CachedAsyncImage(url: image.fullURL) { loadedImage in + loadedImage + .resizable() + .aspectRatio(contentMode: .fit) + .scaleEffect(scale) + .offset(offset) + .gesture( + MagnificationGesture() + .onChanged { value in + let delta = value / lastScale + lastScale = value + scale = min(max(scale * delta, 1), 4) + } + .onEnded { _ in + lastScale = 1.0 + if scale < 1 { + withAnimation { + scale = 1 + } + } + } + ) + .gesture( + DragGesture() + .onChanged { value in + if scale > 1 { + offset = CGSize( + width: lastOffset.width + value.translation.width, + height: lastOffset.height + value.translation.height + ) + } + } + .onEnded { _ in + lastOffset = offset + } + ) + .onTapGesture(count: 2) { + withAnimation { + if scale > 1 { + scale = 1 + offset = .zero + lastOffset = .zero + } else { + scale = 2 + } + } + } + } placeholder: { + ProgressView() + .tint(.white) + } + + // Close button + VStack { + HStack { + Spacer() + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + .frame(width: 36, height: 36) + .background(.ultraThinMaterial) + .clipShape(Circle()) + } + .padding(.trailing, 20) + .padding(.top, 60) + } + Spacer() + } + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/MovieCollectionViews.swift b/apps/ios/Plotwist/Plotwist/Views/Details/MovieCollectionViews.swift new file mode 100644 index 00000000..de3cea66 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/MovieCollectionViews.swift @@ -0,0 +1,181 @@ +// +// MovieCollectionViews.swift +// Plotwist +// + +import SwiftUI + +// MARK: - Movie Collection Section +struct MovieCollectionSection: View { + let collection: MovieCollection + let onSeeCollectionTapped: () -> Void + + private var strings: Strings { L10n.current } + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .bottomLeading) { + // Backdrop with darkened overlay + CachedAsyncImage(url: collection.backdropURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.height) + } placeholder: { + Rectangle() + .fill(Color.appBorderAdaptive) + } + .overlay( + LinearGradient( + colors: [ + Color.black.opacity(0.8), + Color.black.opacity(0.4), + Color.black.opacity(0.2), + ], + startPoint: .bottom, + endPoint: .top + ) + ) + + // Content + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text(strings.partOf) + .font(.caption) + .foregroundColor(.white.opacity(0.8)) + + Text(collection.name) + .font(.title3.bold()) + .foregroundColor(.white) + } + + Button { + onSeeCollectionTapped() + } label: { + Text(strings.seeCollection) + .font(.footnote.weight(.medium)) + .foregroundColor(.appForegroundAdaptive) + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background(Color.appBackgroundAdaptive) + .cornerRadius(10) + } + .buttonStyle(.plain) + } + .padding(20) + } + .frame(width: geometry.size.width, height: geometry.size.height) + .clipped() + } + .frame(height: 240) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .posterBorder(cornerRadius: 16) + .padding(.horizontal, 24) + } +} + +// MARK: - Movie Collection Sheet +struct MovieCollectionSheet: View { + let collection: MovieCollection + let onMovieSelected: (Int) -> Void + @Environment(\.dismiss) private var dismiss + @ObservedObject private var themeManager = ThemeManager.shared + + private let columns = [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + ] + + private var sortedParts: [CollectionPart] { + collection.parts.sorted(by: { ($0.releaseDate ?? "") < ($1.releaseDate ?? "") }) + } + + var body: some View { + FloatingSheetContainer { + VStack(spacing: 0) { + // Drag Indicator + RoundedRectangle(cornerRadius: 2.5) + .fill(Color.gray.opacity(0.4)) + .frame(width: 36, height: 5) + .padding(.top, 12) + .padding(.bottom, 8) + + ScrollView { + VStack(alignment: .leading, spacing: 8) { + // Title + Text(collection.name) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, 4) + + // Collection overview if available + if let overview = collection.overview, !overview.isEmpty { + Text(overview) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .padding(.horizontal, 24) + .padding(.top, 8) + } + + // Movies grid - using regular VStack + HStack for eager loading + VStack(spacing: 12) { + ForEach(0..<(sortedParts.count + 2) / 3, id: \.self) { rowIndex in + HStack(spacing: 12) { + ForEach(0..<3) { colIndex in + let index = rowIndex * 3 + colIndex + if index < sortedParts.count { + let movie = sortedParts[index] + CollectionPosterCard(movie: movie) + .contentShape(Rectangle()) + .onTapGesture { + dismiss() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + onMovieSelected(movie.id) + } + } + } else { + // Empty space for incomplete rows + Color.clear + .aspectRatio(2 / 3, contentMode: .fill) + } + } + } + } + } + .padding(.horizontal, 24) + .padding(.top, 16) + } + .padding(.bottom, 24) + } + } + } + .floatingSheetPresentation(detents: [.medium]) + .preferredColorScheme(themeManager.current.colorScheme) + } +} + +// MARK: - Collection Poster Card +struct CollectionPosterCard: View { + let movie: CollectionPart + + var body: some View { + CachedAsyncImage(url: movie.posterURL) { image in + image + .resizable() + .aspectRatio(2 / 3, contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 8) + .fill(Color.appBorderAdaptive) + .aspectRatio(2 / 3, contentMode: .fill) + .overlay( + ProgressView() + .scaleEffect(0.7) + ) + } + .clipShape(RoundedRectangle(cornerRadius: 8)) + .posterBorder(cornerRadius: 8) + .posterShadow() + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/RelatedSection.swift b/apps/ios/Plotwist/Plotwist/Views/Details/RelatedSection.swift new file mode 100644 index 00000000..4256b0ac --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/RelatedSection.swift @@ -0,0 +1,132 @@ +// +// RelatedSection.swift +// Plotwist +// + +import SwiftUI + +struct RelatedSection: View { + let mediaId: Int + let mediaType: String + var onContentLoaded: ((Bool) -> Void)? + + @State private var recommendations: [SearchResult] = [] + @State private var isLoading = true + @State private var hasLoaded = false + + private let strings = L10n.current + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + if isLoading { + RelatedSectionSkeleton() + } else if !recommendations.isEmpty { + // Title + HStack(spacing: 6) { + Text(strings.tabRecommendations) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + } + .padding(.horizontal, 24) + + // Horizontal scroll of recommendations + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(recommendations.prefix(20)) { item in + NavigationLink { + MediaDetailView( + mediaId: item.id, + mediaType: mediaType + ) + } label: { + RelatedPosterCard(item: item) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 4) + } + .scrollClipDisabled() + } + } + .task { + await loadRecommendations() + } + } + + private func loadRecommendations() async { + // Skip if already loaded + guard !hasLoaded else { + isLoading = false + onContentLoaded?(!recommendations.isEmpty) + return + } + + isLoading = true + + do { + recommendations = try await TMDBService.shared.getRelatedContent( + id: mediaId, + mediaType: mediaType, + variant: "recommendations", + language: Language.current.rawValue + ) + isLoading = false + hasLoaded = true + onContentLoaded?(!recommendations.isEmpty) + } catch { + recommendations = [] + isLoading = false + hasLoaded = true + onContentLoaded?(false) + } + } +} + +// MARK: - Related Poster Card +struct RelatedPosterCard: View { + let item: SearchResult + + var body: some View { + CachedAsyncImage(url: item.imageURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 16) + .fill(Color.appBorderAdaptive) + } + .frame(width: 120, height: 180) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .posterBorder(cornerRadius: 16) + .posterShadow() + } +} + +// MARK: - Related Section Skeleton +struct RelatedSectionSkeleton: View { + var body: some View { + VStack(alignment: .leading, spacing: 12) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 140, height: 20) + .padding(.horizontal, 24) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(0..<5, id: \.self) { _ in + RoundedRectangle(cornerRadius: 16) + .fill(Color.appBorderAdaptive) + .frame(width: 120, height: 180) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 4) + } + .scrollClipDisabled() + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/ReviewListView.swift b/apps/ios/Plotwist/Plotwist/Views/Details/ReviewListView.swift new file mode 100644 index 00000000..16f1023e --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/ReviewListView.swift @@ -0,0 +1,214 @@ +// +// ReviewListView.swift +// Plotwist +// + +import SwiftUI + +struct ReviewListView: View { + let mediaId: Int + let mediaType: String + + @State private var reviews: [ReviewListItem] = [] + @State private var isLoading = true + @State private var error: String? + @State private var currentUserId: String? + @State private var selectedReview: ReviewListItem? + @State private var showEditSheet = false + + var body: some View { + VStack(spacing: 0) { + if isLoading { + // Loading state + VStack(spacing: 16) { + ForEach(0..<3, id: \.self) { _ in + ReviewItemSkeleton() + } + } + .padding(.horizontal, 24) + .padding(.top, 16) + } else if let error = error { + // Error state + VStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle") + .font(.title) + .foregroundColor(.appMutedForegroundAdaptive) + Text(error) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + } + .padding(.top, 32) + } else if reviews.isEmpty { + // Empty state + VStack(spacing: 8) { + Text(L10n.current.beFirstToReview) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + Text(L10n.current.shareYourOpinion) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 32) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(style: StrokeStyle(lineWidth: 1, dash: [5])) + .foregroundColor(.appBorderAdaptive) + ) + .padding(.horizontal, 24) + .padding(.top, 16) + } else { + // Reviews list + LazyVStack(spacing: 0) { + ForEach(Array(reviews.filter { !$0.review.isEmpty }.enumerated()), id: \.element.id) { index, review in + VStack(spacing: 0) { + // Make tappable only if it's the current user's review + if review.userId == currentUserId { + ReviewItemView(review: review) + .padding(.vertical, 16) + .contentShape(Rectangle()) + .onTapGesture { + selectedReview = review + showEditSheet = true + } + } else { + ReviewItemView(review: review) + .padding(.vertical, 16) + } + + // Divider (except for last item) + if index < reviews.filter({ !$0.review.isEmpty }).count - 1 { + Divider() + .background(Color.appBorderAdaptive.opacity(0.5)) + } + } + } + } + .padding(.horizontal, 24) + } + } + .task { + await loadCurrentUser() + await loadReviews() + } + .sheet(isPresented: $showEditSheet) { + if let review = selectedReview { + ReviewSheet( + mediaId: mediaId, + mediaType: mediaType, + existingReview: review.toReview(), + onDeleted: { + Task { + await loadReviews() + } + } + ) + } + } + .onChange(of: showEditSheet) { _, isShowing in + if !isShowing { + // Reload reviews when sheet is dismissed (in case of edit) + Task { + await loadReviews() + } + } + } + } + + private func loadCurrentUser() async { + do { + let user = try await AuthService.shared.getCurrentUser() + currentUserId = user.id + } catch { + currentUserId = nil + } + } + + private func loadReviews() async { + isLoading = true + error = nil + + do { + let apiMediaType = mediaType == "movie" ? "MOVIE" : "TV_SHOW" + reviews = try await ReviewService.shared.getReviews( + tmdbId: mediaId, + mediaType: apiMediaType + ) + isLoading = false + } catch { + self.error = error.localizedDescription + isLoading = false + } + } +} + +// MARK: - ReviewListItem Extension +extension ReviewListItem { + func toReview() -> Review { + Review( + id: id, + userId: userId, + tmdbId: tmdbId, + mediaType: mediaType, + review: review, + rating: rating, + hasSpoilers: hasSpoilers, + seasonNumber: seasonNumber, + episodeNumber: episodeNumber, + language: language, + createdAt: createdAt + ) + } +} + +// MARK: - Skeleton +struct ReviewItemSkeleton: View { + var body: some View { + HStack(alignment: .top, spacing: 12) { + // Avatar skeleton + Circle() + .fill(Color.appBorderAdaptive) + .frame(width: 40, height: 40) + + VStack(alignment: .leading, spacing: 0) { + // Header: username + time + HStack { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 100, height: 14) + + Spacer() + + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 40, height: 12) + } + + // Stars skeleton + HStack(spacing: 2) { + ForEach(0..<5, id: \.self) { _ in + RoundedRectangle(cornerRadius: 2) + .fill(Color.appBorderAdaptive) + .frame(width: 14, height: 14) + } + } + .padding(.top, 4) + + // Review text skeleton + VStack(alignment: .leading, spacing: 4) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(height: 14) + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 200, height: 14) + } + .padding(.top, 8) + } + } + } +} + +#Preview { + ReviewListView(mediaId: 123, mediaType: "movie") +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/ReviewSheet.swift b/apps/ios/Plotwist/Plotwist/Views/Details/ReviewSheet.swift new file mode 100644 index 00000000..b0a61e1e --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/ReviewSheet.swift @@ -0,0 +1,240 @@ +// +// ReviewSheet.swift +// Plotwist +// + +import SwiftUI + +struct ReviewSheet: View { + let mediaId: Int + let mediaType: String + let seasonNumber: Int? + let existingReview: Review? + let onDeleted: (() -> Void)? + @Environment(\.dismiss) private var dismiss + @ObservedObject private var themeManager = ThemeManager.shared + + @State private var rating: Double = 0 + @State private var reviewText: String = "" + @State private var hasSpoilers: Bool = false + @State private var isLoading: Bool = false + @State private var isDeleting: Bool = false + @State private var showErrorAlert: Bool = false + @State private var showDeleteConfirmation: Bool = false + @State private var errorMessage: String = "" + + init(mediaId: Int, mediaType: String, seasonNumber: Int? = nil, existingReview: Review? = nil, onDeleted: (() -> Void)? = nil) { + self.mediaId = mediaId + self.mediaType = mediaType + self.seasonNumber = seasonNumber + self.existingReview = existingReview + self.onDeleted = onDeleted + + if let existingReview = existingReview { + _rating = State(initialValue: existingReview.rating) + _reviewText = State(initialValue: existingReview.review) + _hasSpoilers = State(initialValue: existingReview.hasSpoilers) + } + } + + var body: some View { + FloatingSheetContainer { + VStack(spacing: 16) { + // Drag Indicator + RoundedRectangle(cornerRadius: 2.5) + .fill(Color.gray.opacity(0.4)) + .frame(width: 36, height: 5) + .padding(.top, 12) + + // Title + Text(L10n.current.whatDidYouThink) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + .frame(maxWidth: .infinity, alignment: .center) + + // Rating + StarRatingView(rating: $rating, size: 36) + .frame(maxWidth: .infinity) + + // Review Text with Spoilers Toggle + ZStack(alignment: .bottomTrailing) { + ZStack(alignment: .topLeading) { + TextEditor(text: $reviewText) + .frame(height: 120) + .padding(.horizontal, 12) + .padding(.top, 8) + .padding(.bottom, 36) + .background(Color.appInputFilled) + .cornerRadius(12) + .foregroundColor(.appForegroundAdaptive) + .scrollContentBackground(.hidden) + + if reviewText.isEmpty { + Text(L10n.current.shareYourOpinion) + .foregroundColor(.appMutedForegroundAdaptive) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .allowsHitTesting(false) + } + } + + // Spoilers Toggle inside the text field + Button(action: { hasSpoilers.toggle() }) { + HStack(spacing: 6) { + Image(systemName: hasSpoilers ? "checkmark.square.fill" : "square") + .font(.system(size: 16)) + .foregroundColor(hasSpoilers ? .appForegroundAdaptive : .gray) + + Text(L10n.current.containSpoilers) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + } + } + + // Submit Button + Button(action: submitReview) { + Group { + if isLoading { + ProgressView() + .tint(.appBackgroundAdaptive) + } else { + Text(existingReview != nil ? L10n.current.editReview : L10n.current.submitReview) + .fontWeight(.semibold) + } + } + .frame(maxWidth: .infinity) + .frame(height: 48) + .background(Color.appForegroundAdaptive) + .foregroundColor(.appBackgroundAdaptive) + .cornerRadius(12) + } + .disabled(!isFormValid || isLoading || isDeleting) + .opacity(!isFormValid || isLoading || isDeleting ? 0.5 : 1) + + // Delete Button (only when editing) + if existingReview != nil { + Button(action: { showDeleteConfirmation = true }) { + Group { + if isDeleting { + ProgressView() + .tint(.red) + } else { + HStack(spacing: 8) { + Image(systemName: "trash") + Text(L10n.current.deleteReview) + .fontWeight(.semibold) + } + } + } + .frame(maxWidth: .infinity) + .frame(height: 48) + .foregroundColor(.red) + } + .disabled(isLoading || isDeleting) + .opacity(isLoading || isDeleting ? 0.5 : 1) + } + } + .padding(.horizontal, 24) + .padding(.bottom, 24) + } + .floatingSheetPresentation(height: existingReview != nil ? 480 : 420) + .preferredColorScheme(themeManager.current.colorScheme) + .alert("Error", isPresented: $showErrorAlert) { + Button("OK", role: .cancel) {} + } message: { + Text(errorMessage) + } + .alert(L10n.current.deleteReview, isPresented: $showDeleteConfirmation) { + Button(L10n.current.cancel, role: .cancel) {} + Button(L10n.current.delete, role: .destructive) { + deleteReview() + } + } message: { + Text(L10n.current.deleteReviewConfirmation) + } + } + + private var isFormValid: Bool { + rating > 0 && !reviewText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + + private func submitReview() { + guard isFormValid else { + errorMessage = L10n.current.reviewRequired + showErrorAlert = true + return + } + + isLoading = true + + Task { + do { + let reviewData = ReviewData( + tmdbId: mediaId, + mediaType: mediaType == "movie" ? "MOVIE" : "TV_SHOW", + review: reviewText, + rating: rating, + hasSpoilers: hasSpoilers, + seasonNumber: seasonNumber, + episodeNumber: nil, + language: Language.current.rawValue + ) + + if let existingReview = existingReview { + try await ReviewService.shared.updateReview(id: existingReview.id, reviewData) + } else { + try await ReviewService.shared.createReview(reviewData) + } + + await MainActor.run { + isLoading = false + dismiss() + } + } catch { + await MainActor.run { + isLoading = false + errorMessage = error.localizedDescription + showErrorAlert = true + } + } + } + } + + private func deleteReview() { + guard let existingReview = existingReview else { return } + + isDeleting = true + + Task { + do { + try await ReviewService.shared.deleteReview( + id: existingReview.id, + tmdbId: mediaId, + mediaType: mediaType == "movie" ? "MOVIE" : "TV_SHOW", + seasonNumber: existingReview.seasonNumber, + episodeNumber: existingReview.episodeNumber + ) + + await MainActor.run { + isDeleting = false + onDeleted?() + dismiss() + } + } catch { + await MainActor.run { + isDeleting = false + errorMessage = error.localizedDescription + showErrorAlert = true + } + } + } + } +} + +// MARK: - Preview +#Preview { + ReviewSheet(mediaId: 550, mediaType: "movie") +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/SeasonDetailView.swift b/apps/ios/Plotwist/Plotwist/Views/Details/SeasonDetailView.swift new file mode 100644 index 00000000..779c7b42 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/SeasonDetailView.swift @@ -0,0 +1,502 @@ +// +// SeasonDetailView.swift +// Plotwist +// + +import SwiftUI + +struct SeasonDetailView: View { + let seriesId: Int + let seriesName: String + let season: Season + + @Environment(\.dismiss) private var dismiss + @ObservedObject private var themeManager = ThemeManager.shared + @State private var seasonDetails: SeasonDetails? + @State private var isLoading = true + @State private var userReview: Review? + @State private var isLoadingUserReview = false + @State private var showReviewSheet = false + @State private var reviewsRefreshId = UUID() + @State private var hasReviews = false + @State private var scrollOffset: CGFloat = 0 + @State private var initialScrollOffset: CGFloat? = nil + + // Episodes watched state (shared with header) + @State private var watchedEpisodes: [UserEpisode] = [] + @State private var loadingEpisodeIds: Set = [] + + + private let scrollThreshold: CGFloat = 20 + private let navigationHeaderHeight: CGFloat = 64 + + private var isScrolled: Bool { + guard let initial = initialScrollOffset else { return false } + return scrollOffset < initial - scrollThreshold + } + + private var episodes: [Episode] { + seasonDetails?.episodes ?? [] + } + + private var watchedCount: Int { + episodes.filter { episode in + watchedEpisodes.contains { $0.episodeNumber == episode.episodeNumber && $0.seasonNumber == season.seasonNumber } + }.count + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + ScrollView(showsIndicators: false) { + LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) { + // Main content section (not pinned) + Section { + // Header with poster and info (adjusted for safeAreaInset) + SeasonHeaderView( + season: season, + scrollOffset: $scrollOffset, + initialScrollOffset: $initialScrollOffset, + topPadding: 24 // Reduced since safeAreaInset handles nav header + ) + + // Review Button + if AuthService.shared.isAuthenticated { + ReviewButton( + hasReview: userReview != nil, + isLoading: isLoadingUserReview, + action: { showReviewSheet = true } + ) + .padding(.horizontal, 24) + .padding(.top, 24) + } + + // Overview + if let overview = seasonDetails?.overview ?? season.overview, !overview.isEmpty { + Text(overview) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .lineSpacing(6) + .padding(.horizontal, 24) + .padding(.top, 20) + } + + // Divider before reviews + if hasReviews { + sectionDivider + } + + // Reviews Section + SeasonReviewSectionView( + seriesId: seriesId, + seasonNumber: season.seasonNumber, + refreshId: reviewsRefreshId, + onEmptyStateTapped: { + if AuthService.shared.isAuthenticated { + showReviewSheet = true + } + }, + onContentLoaded: { hasContent in + hasReviews = hasContent + } + ) + + // Divider before episodes (reduced bottom spacing) + if !episodes.isEmpty { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + .padding(.horizontal, 24) + .padding(.top, 24) + .padding(.bottom, 8) + } + } + + // Episodes Section with pinned header + if !episodes.isEmpty { + Section { + EpisodesListView( + seriesId: seriesId, + seasonNumber: season.seasonNumber, + episodes: episodes, + watchedEpisodes: $watchedEpisodes, + loadingEpisodeIds: $loadingEpisodeIds + ) + + Spacer() + .frame(height: 80) + } header: { + EpisodesHeaderView( + episodesCount: episodes.count, + watchedCount: watchedCount + ) + } + .onChange(of: watchedCount) { oldValue, newValue in + // Haptic feedback when all episodes are watched + if newValue == episodes.count && oldValue < episodes.count && episodes.count > 0 { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + } + } + } else { + Spacer() + .frame(height: 80) + } + } + } + .coordinateSpace(name: "episodesScroll") + .safeAreaInset(edge: .top, spacing: 0) { + // Reserve space for navigation header so pinned headers appear below it + Color.clear + .frame(height: navigationHeaderHeight) + } + + // Navigation Header + navigationHeader + } + .navigationBarHidden(true) + .preferredColorScheme(themeManager.current.colorScheme) + .sheet(isPresented: $showReviewSheet) { + ReviewSheet( + mediaId: seriesId, + mediaType: "tv", + seasonNumber: season.seasonNumber, + existingReview: userReview + ) + } + .task { + if AuthService.shared.isAuthenticated { + isLoadingUserReview = true + } + await loadSeasonDetails() + if AuthService.shared.isAuthenticated { + await loadUserReview() + await loadWatchedEpisodes() + } + } + .onChange(of: showReviewSheet) { _, isPresented in + if !isPresented && AuthService.shared.isAuthenticated { + Task { + await loadUserReview() + } + reviewsRefreshId = UUID() + } + } + } + + // MARK: - Section Divider + private var sectionDivider: some View { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + .padding(.horizontal, 24) + .padding(.vertical, 24) + } + + // MARK: - Navigation Header + private var navigationHeader: some View { + VStack { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + } + + Spacer() + + Text(seriesName) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + .lineLimit(1) + + Spacer() + + Color.clear + .frame(width: 40, height: 40) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color.appBackgroundAdaptive) + .overlay( + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1) + .opacity(isScrolled ? 1 : 0), + alignment: .bottom + ) + .animation(.easeInOut(duration: 0.2), value: isScrolled) + + Spacer() + } + } + + // MARK: - Load Season Details + private func loadSeasonDetails() async { + isLoading = true + defer { isLoading = false } + + do { + seasonDetails = try await TMDBService.shared.getSeasonDetails( + seriesId: seriesId, + seasonNumber: season.seasonNumber, + language: Language.current.rawValue + ) + } catch { + seasonDetails = nil + } + } + + // MARK: - Load User Review + private func loadUserReview() async { + isLoadingUserReview = true + defer { isLoadingUserReview = false } + + do { + userReview = try await ReviewService.shared.getUserReview( + tmdbId: seriesId, + mediaType: "TV_SHOW", + seasonNumber: season.seasonNumber + ) + } catch { + userReview = nil + } + } + + // MARK: - Load Watched Episodes + private func loadWatchedEpisodes() async { + do { + watchedEpisodes = try await UserEpisodeService.shared.getWatchedEpisodes(tmdbId: seriesId) + } catch { + watchedEpisodes = [] + } + } +} + +// MARK: - Episodes Header View (Sticky) +struct EpisodesHeaderView: View { + let episodesCount: Int + let watchedCount: Int + + @State private var isPinned = false + + // Approximate pinned position: safe area (~50) + nav header (64) + small buffer + private let pinnedPositionThreshold: CGFloat = 130 + + var body: some View { + VStack(spacing: 0) { + VStack(spacing: 12) { + HStack { + Text(L10n.current.episodes) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + if AuthService.shared.isAuthenticated { + Text(L10n.current.episodesWatchedCount + .replacingOccurrences(of: "%d", with: "\(watchedCount)") + .replacingOccurrences(of: "%total", with: "\(episodesCount)") + ) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + } + + if AuthService.shared.isAuthenticated && episodesCount > 0 { + SegmentedProgressBar( + totalSegments: episodesCount, + filledSegments: watchedCount + ) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + + // Bottom border - only when pinned + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1) + .opacity(isPinned ? 1 : 0) + } + .background(Color.appBackgroundAdaptive) + .background( + GeometryReader { geo -> Color in + DispatchQueue.main.async { + let minY = geo.frame(in: .global).minY + // Header is pinned when it's at the expected pinned position (top of scroll area) + let newPinned = minY <= pinnedPositionThreshold + if newPinned != isPinned { + isPinned = newPinned + } + } + return Color.clear + } + ) + .animation(.easeInOut(duration: 0.15), value: isPinned) + } +} + +// MARK: - Segmented Progress Bar +struct SegmentedProgressBar: View { + let totalSegments: Int + let filledSegments: Int + + private let segmentHeight: CGFloat = 6 + private let segmentSpacing: CGFloat = 2 + + var body: some View { + GeometryReader { geometry in + let availableWidth = geometry.size.width - (CGFloat(totalSegments - 1) * segmentSpacing) + let segmentWidth = availableWidth / CGFloat(totalSegments) + + HStack(spacing: segmentSpacing) { + ForEach(0.. + + private func isEpisodeWatched(_ episode: Episode) -> Bool { + watchedEpisodes.contains { $0.episodeNumber == episode.episodeNumber && $0.seasonNumber == seasonNumber } + } + + private func watchedEpisodeId(for episode: Episode) -> String? { + watchedEpisodes.first { $0.episodeNumber == episode.episodeNumber && $0.seasonNumber == seasonNumber }?.id + } + + var body: some View { + VStack(spacing: 16) { + ForEach(episodes) { episode in + EpisodeRowView( + episode: episode, + isWatched: isEpisodeWatched(episode), + isLoading: loadingEpisodeIds.contains(episode.episodeNumber), + onToggleWatched: { + Task { + await toggleWatched(episode) + } + } + ) + } + } + .padding(.horizontal, 24) + .padding(.top, 16) + } + + private func toggleWatched(_ episode: Episode) async { + loadingEpisodeIds.insert(episode.episodeNumber) + defer { loadingEpisodeIds.remove(episode.episodeNumber) } + + if let watchedId = watchedEpisodeId(for: episode) { + do { + try await UserEpisodeService.shared.unmarkAsWatched(id: watchedId, tmdbId: seriesId) + watchedEpisodes.removeAll { $0.id == watchedId } + } catch { + print("Error unmarking episode: \(error)") + } + } else { + do { + let userEpisode = try await UserEpisodeService.shared.markAsWatched( + tmdbId: seriesId, + seasonNumber: seasonNumber, + episodeNumber: episode.episodeNumber, + runtime: episode.runtime + ) + watchedEpisodes.append(userEpisode) + } catch { + print("Error marking episode: \(error)") + } + } + } +} + +// MARK: - Season Header View +struct SeasonHeaderView: View { + let season: Season + @Binding var scrollOffset: CGFloat + @Binding var initialScrollOffset: CGFloat? + var topPadding: CGFloat = 80 + + private var formattedAirDate: String? { + guard let airDate = season.airDate else { return nil } + let inputFormatter = DateFormatter() + inputFormatter.dateFormat = "yyyy-MM-dd" + guard let date = inputFormatter.date(from: airDate) else { return nil } + + let outputFormatter = DateFormatter() + outputFormatter.dateFormat = "d 'de' MMMM 'de' yyyy" + outputFormatter.locale = Locale(identifier: Language.current.rawValue) + return outputFormatter.string(from: date) + } + + var body: some View { + HStack(alignment: .bottom, spacing: 16) { + // Poster + CachedAsyncImage(url: season.posterURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + .overlay( + ProgressView() + .scaleEffect(0.8) + ) + } + .frame(width: 120, height: 180) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .posterBorder(cornerRadius: 12) + .posterShadow() + + // Info + VStack(alignment: .leading, spacing: 4) { + if let airDate = formattedAirDate { + Text(airDate) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + + Text(season.name) + .font(.title2.bold()) + .foregroundColor(.appForegroundAdaptive) + } + .padding(.bottom, 8) + + Spacer() + } + .padding(.horizontal, 24) + .padding(.top, topPadding) + .background( + GeometryReader { geo -> Color in + DispatchQueue.main.async { + let offset = geo.frame(in: .global).minY + if initialScrollOffset == nil { + initialScrollOffset = offset + } + scrollOffset = offset + } + return Color.clear + } + ) + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/SeasonReviewSectionView.swift b/apps/ios/Plotwist/Plotwist/Views/Details/SeasonReviewSectionView.swift new file mode 100644 index 00000000..8abb5f4e --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/SeasonReviewSectionView.swift @@ -0,0 +1,148 @@ +// +// SeasonReviewSectionView.swift +// Plotwist +// + +import SwiftUI + +struct SeasonReviewSectionView: View { + let seriesId: Int + let seasonNumber: Int + let refreshId: UUID + var onEmptyStateTapped: (() -> Void)? + var onContentLoaded: ((Bool) -> Void)? + + @State private var reviews: [ReviewListItem] = [] + @State private var isLoading = true + @State private var hasLoaded = false + + private var averageRating: Double { + guard !reviews.isEmpty else { return 0 } + let total = reviews.reduce(0) { $0 + $1.rating } + return total / Double(reviews.count) + } + + private var reviewsWithText: [ReviewListItem] { + reviews.filter { !$0.review.isEmpty } + } + + var body: some View { + Group { + if isLoading { + reviewsSkeleton + } else if reviews.isEmpty { + EmptyView() + } else { + reviewsContent + } + } + .task { + await loadReviews() + } + .onChange(of: refreshId) { _, _ in + Task { + await loadReviews(forceReload: true) + } + } + } + + // MARK: - Reviews Skeleton + private var reviewsSkeleton: some View { + VStack(spacing: 16) { + HStack(spacing: 6) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 16, height: 16) + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 30, height: 18) + Circle() + .fill(Color.appBorderAdaptive) + .frame(width: 4, height: 4) + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 80, height: 14) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + } + } + + // MARK: - Reviews Content + private var reviewsContent: some View { + VStack(spacing: 16) { + // Rating Header + HStack(spacing: 6) { + Image(systemName: "star.fill") + .font(.system(size: 16)) + .foregroundColor(.appStarYellow) + + Text(String(format: "%.1f", averageRating)) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + + Circle() + .fill(Color.appMutedForegroundAdaptive.opacity(0.5)) + .frame(width: 4, height: 4) + + Text( + "\(reviews.count) \(reviews.count == 1 ? L10n.current.reviewSingular.lowercased() : L10n.current.tabReviews.lowercased())" + ) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + + // Horizontal scrolling reviews + if !reviewsWithText.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: 0) { + ForEach(Array(reviewsWithText.enumerated()), id: \.element.id) { index, review in + HStack(alignment: .top, spacing: 0) { + ReviewCardView(review: review) + .frame(width: min(UIScreen.main.bounds.width * 0.75, 300)) + .padding(.leading, index == 0 ? 24 : 0) + .padding(.trailing, 24) + + if index < reviewsWithText.count - 1 { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(width: 1) + .frame(height: 140) + .padding(.trailing, 24) + } + } + } + } + } + } + } + } + + // MARK: - Load Reviews + private func loadReviews(forceReload: Bool = false) async { + guard !hasLoaded || forceReload else { + isLoading = false + onContentLoaded?(!reviews.isEmpty) + return + } + + isLoading = true + + do { + reviews = try await ReviewService.shared.getReviews( + tmdbId: seriesId, + mediaType: "TV_SHOW", + seasonNumber: seasonNumber + ) + isLoading = false + hasLoaded = true + onContentLoaded?(!reviews.isEmpty) + } catch { + isLoading = false + hasLoaded = true + onContentLoaded?(false) + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/SeasonsListView.swift b/apps/ios/Plotwist/Plotwist/Views/Details/SeasonsListView.swift new file mode 100644 index 00000000..f07dc397 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/SeasonsListView.swift @@ -0,0 +1,445 @@ +// +// SeasonsListView.swift +// Plotwist +// + +import SwiftUI + +// MARK: - Seasons Tab +enum SeasonsTab: CaseIterable { + case grid + case overview + + func displayName(strings: Strings) -> String { + switch self { + case .grid: return strings.grid + case .overview: return strings.overview + } + } +} + +// MARK: - Seasons List View +struct SeasonsListView: View { + let seasons: [Season] + let seriesId: Int + let seriesName: String + + @Environment(\.dismiss) private var dismiss + @State private var selectedTab: SeasonsTab = .grid + @State private var seasonsDetails: [SeasonDetails] = [] + @State private var isLoadingDetails = false + @ObservedObject private var themeManager = ThemeManager.shared + + private let strings = L10n.current + + private let columns = [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + ] + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + } + + Spacer() + + Text(strings.tabSeasons) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + // Placeholder for symmetry + Color.clear + .frame(width: 40, height: 40) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + + // Tabs + SeasonsTabs(selectedTab: $selectedTab, strings: strings) + .padding(.top, 8) + + // Content + ScrollView { + switch selectedTab { + case .grid: + gridContent + case .overview: + overviewContent + } + } + } + } + .navigationBarHidden(true) + .preferredColorScheme(themeManager.current.colorScheme) + .task { + await loadSeasonsDetails() + } + } + + // MARK: - Grid Content + private var gridContent: some View { + LazyVGrid(columns: columns, spacing: 16) { + ForEach(seasons) { season in + NavigationLink { + SeasonDetailView(seriesId: seriesId, seriesName: seriesName, season: season) + } label: { + VStack(alignment: .leading, spacing: 8) { + CachedAsyncImage(url: season.posterURL) { image in + image + .resizable() + .aspectRatio(2 / 3, contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + .aspectRatio(2 / 3, contentMode: .fill) + .overlay( + VStack(spacing: 4) { + Image(systemName: "photo") + .font(.title3) + .foregroundColor(.appMutedForegroundAdaptive) + Text(season.name) + .font(.caption2) + .foregroundColor(.appMutedForegroundAdaptive) + .multilineTextAlignment(.center) + .lineLimit(2) + .padding(.horizontal, 4) + } + ) + } + .clipShape(RoundedRectangle(cornerRadius: 12)) + .posterBorder(cornerRadius: 12) + .posterShadow() + } + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 24) + .padding(.top, 16) + .padding(.bottom, 24) + } + + // MARK: - Overview Content + private var overviewContent: some View { + VStack(spacing: 0) { + if isLoadingDetails { + // Loading skeleton + VStack(spacing: 0) { + ForEach(0..<10, id: \.self) { _ in + HStack(spacing: 0) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 30, height: 20) + .padding(.horizontal, 12) + .padding(.vertical, 10) + + ForEach(0.. 0 { + RatingBadge(rating: episode.voteAverage) + .frame(maxWidth: .infinity) + .padding(.vertical, 6) + } else { + Text("–") + .font(.caption) + .foregroundColor(.appBorderAdaptive) + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + } + } + } + .background(Color.appBackgroundAdaptive) + + // Divider (except for last row) + if episodeNumber < maxEpisodes { + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1) + } + } + } + } +} + +// MARK: - Rating Badge +struct RatingBadge: View { + let rating: Double + + private var ratingColor: Color { + switch rating { + case 8.0...: return RatingColor.awesome + case 6.0..<8.0: return RatingColor.great + case 4.0..<6.0: return RatingColor.good + case 2.0..<4.0: return RatingColor.bad + default: return RatingColor.terrible + } + } + + var body: some View { + Text(String(format: "%.1f", rating)) + .font(.caption.weight(.semibold)) + .foregroundColor(ratingColor) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(ratingColor.opacity(0.15)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/SeasonsSection.swift b/apps/ios/Plotwist/Plotwist/Views/Details/SeasonsSection.swift new file mode 100644 index 00000000..24064dcc --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/SeasonsSection.swift @@ -0,0 +1,118 @@ +// +// SeasonsSection.swift +// Plotwist +// + +import SwiftUI + +struct SeasonsSection: View { + let seasons: [Season] + let seriesId: Int + let seriesName: String + var onContentLoaded: ((Bool) -> Void)? + + private let strings = L10n.current + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + if !seasons.isEmpty { + // Title with navigation + NavigationLink { + SeasonsListView(seasons: seasons, seriesId: seriesId, seriesName: seriesName) + } label: { + HStack(spacing: 6) { + Text(strings.tabSeasons) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + + Image(systemName: "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundColor(.appMutedForegroundAdaptive) + + Spacer() + } + .padding(.horizontal, 24) + } + .buttonStyle(.plain) + + // Horizontal scroll of seasons (posters only) + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(seasons) { season in + NavigationLink { + SeasonDetailView(seriesId: seriesId, seriesName: seriesName, season: season) + } label: { + SeasonPosterCard(season: season) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 4) + } + .scrollClipDisabled() + } + } + .onAppear { + onContentLoaded?(!seasons.isEmpty) + } + } +} + +// MARK: - Season Poster Card +struct SeasonPosterCard: View { + let season: Season + + var body: some View { + CachedAsyncImage(url: season.posterURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + .overlay( + VStack(spacing: 4) { + Image(systemName: "photo") + .font(.title2) + .foregroundColor(.appMutedForegroundAdaptive) + Text(season.name) + .font(.caption2) + .foregroundColor(.appMutedForegroundAdaptive) + .multilineTextAlignment(.center) + .lineLimit(2) + .padding(.horizontal, 4) + } + ) + } + .frame(width: 120, height: 180) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .posterBorder(cornerRadius: 12) + .posterShadow() + } +} + +// MARK: - Seasons Section Skeleton +struct SeasonsSectionSkeleton: View { + var body: some View { + VStack(alignment: .leading, spacing: 12) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 100, height: 20) + .padding(.horizontal, 24) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(0..<4, id: \.self) { _ in + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + .frame(width: 120, height: 180) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 4) + } + .scrollClipDisabled() + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Details/WhereToWatchSection.swift b/apps/ios/Plotwist/Plotwist/Views/Details/WhereToWatchSection.swift new file mode 100644 index 00000000..ff56aecd --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Details/WhereToWatchSection.swift @@ -0,0 +1,246 @@ +// +// WhereToWatchSection.swift +// Plotwist +// + +import SwiftUI + +struct WhereToWatchSection: View { + let mediaId: Int + let mediaType: String + var onContentLoaded: ((Bool) -> Void)? + + @State private var providers: WatchProviderCountry? + @State private var isLoading = true + @State private var hasLoaded = false + @State private var isExpanded = false + + + private var hasAnyProvider: Bool { + let flatrate = providers?.flatrate ?? [] + let rent = providers?.rent ?? [] + let buy = providers?.buy ?? [] + return !flatrate.isEmpty || !rent.isEmpty || !buy.isEmpty + } + + var body: some View { + Group { + if isLoading { + VStack(alignment: .leading, spacing: 16) { + // Title + Text(L10n.current.tabWhereToWatch) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + .padding(.horizontal, 24) + + HStack { + Spacer() + ProgressView() + Spacer() + } + .padding(.vertical, 20) + } + } else if hasAnyProvider { + VStack(alignment: .leading, spacing: 16) { + // Header with title and expand arrow + Button(action: { + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + isExpanded.toggle() + } + }) { + HStack(spacing: 8) { + Text(L10n.current.tabWhereToWatch) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + + Image(systemName: "chevron.down") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.appMutedForegroundAdaptive) + .rotationEffect(.degrees(isExpanded ? 180 : 0)) + + Spacer() + } + .padding(.horizontal, 24) + } + .buttonStyle(.plain) + + // Categories + VStack(alignment: .leading, spacing: 20) { + // Stream + if let flatrate = providers?.flatrate, !flatrate.isEmpty { + ProviderCategoryAnimated( + title: L10n.current.stream, + providers: flatrate, + isExpanded: isExpanded + ) + } + + // Rent + if let rent = providers?.rent, !rent.isEmpty { + ProviderCategoryAnimated( + title: L10n.current.rent, + providers: rent, + isExpanded: isExpanded + ) + } + + // Buy + if let buy = providers?.buy, !buy.isEmpty { + ProviderCategoryAnimated( + title: L10n.current.buy, + providers: buy, + isExpanded: isExpanded + ) + } + } + .padding(.horizontal, 24) + } + } else { + // No providers - show nothing + EmptyView() + } + } + .task { + await loadProviders() + } + } + + private func loadProviders() async { + // Skip if already loaded + guard !hasLoaded else { + isLoading = false + onContentLoaded?(hasAnyProvider) + return + } + + isLoading = true + + do { + let response = try await TMDBService.shared.getWatchProviders( + id: mediaId, + mediaType: mediaType + ) + providers = response.results.forLanguage(Language.current) ?? response.results.US + isLoading = false + hasLoaded = true + onContentLoaded?(hasAnyProvider) + } catch { + providers = nil + isLoading = false + hasLoaded = true + onContentLoaded?(false) + } + } +} + +// MARK: - Provider Category with Layout Animation +struct ProviderCategoryAnimated: View { + let title: String + let providers: [WatchProvider] + let isExpanded: Bool + + private let iconSize: CGFloat = 36 + private let collapsedSpacing: CGFloat = -16 + private let expandedSpacing: CGFloat = 6 + + private var displayProviders: [WatchProvider] { + isExpanded ? providers : Array(providers.prefix(5)) + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + // Title - stays fixed + Text(title.uppercased()) + .font(.caption.weight(.semibold)) + .foregroundColor(.appMutedForegroundAdaptive) + + // Single layout that morphs between states + ProviderIconsLayout( + providers: displayProviders, + allProviders: providers, + isExpanded: isExpanded, + iconSize: iconSize, + collapsedSpacing: collapsedSpacing, + expandedSpacing: expandedSpacing + ) + } + } +} + +// MARK: - Custom Layout for Provider Icons +struct ProviderIconsLayout: View { + let providers: [WatchProvider] + let allProviders: [WatchProvider] + let isExpanded: Bool + let iconSize: CGFloat + let collapsedSpacing: CGFloat + let expandedSpacing: CGFloat + + private let cornerRadius: CGFloat = 9 + + var body: some View { + let layout = isExpanded + ? AnyLayout(VStackLayout(alignment: .leading, spacing: expandedSpacing)) + : AnyLayout(HStackLayout(spacing: collapsedSpacing)) + + layout { + ForEach(Array(providers.enumerated()), id: \.element.id) { index, provider in + HStack(spacing: 10) { + ProviderIconView(provider: provider, size: iconSize, cornerRadius: cornerRadius) + .zIndex(Double(providers.count - index)) + + if isExpanded { + Text(provider.providerName) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + } + } + } + + // Show +N badge when collapsed and more than 5 providers + if !isExpanded && allProviders.count > 5 { + Text("+\(allProviders.count - 5)") + .font(.caption.weight(.semibold)) + .foregroundColor(.appMutedForegroundAdaptive) + .frame(width: iconSize, height: iconSize) + .background(Color.appInputFilled) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color.appBorderAdaptive, lineWidth: 1.5) + ) + } + } + } +} + +// MARK: - Provider Icon View +struct ProviderIconView: View { + let provider: WatchProvider + let size: CGFloat + let cornerRadius: CGFloat + + var body: some View { + CachedAsyncImage(url: provider.logoURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: cornerRadius) + .fill(Color.appBorderAdaptive) + } + .frame(width: size, height: size) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color.appBorderAdaptive, lineWidth: 1.5) + ) + } +} + +// MARK: - Preview +#Preview { + WhereToWatchSection(mediaId: 550, mediaType: "movie") +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/CategoryListView.swift b/apps/ios/Plotwist/Plotwist/Views/Home/CategoryListView.swift new file mode 100644 index 00000000..e7f41185 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Home/CategoryListView.swift @@ -0,0 +1,564 @@ +// +// CategoryListView.swift +// Plotwist +// + +import SwiftUI + +// MARK: - Category Tab Protocol +protocol CategoryTab: Hashable, CaseIterable { + var title: String { get } + var isDisabled: Bool { get } +} + +// MARK: - Movie Subcategory +enum MovieSubcategory: CaseIterable, CategoryTab { + case popular + case nowPlaying + case topRated + case upcoming + case discover + + var title: String { + let strings = L10n.current + switch self { + case .popular: return strings.popular + case .nowPlaying: return strings.nowPlaying + case .topRated: return strings.topRated + case .upcoming: return strings.upcoming + case .discover: return strings.discover + } + } + + var isDisabled: Bool { + self == .discover + } +} + +// MARK: - TV Series Subcategory +enum TVSeriesSubcategory: CaseIterable, CategoryTab { + case popular + case airingToday + case onTheAir + case topRated + case discover + + var title: String { + let strings = L10n.current + switch self { + case .popular: return strings.popular + case .airingToday: return strings.airingToday + case .onTheAir: return strings.onTheAir + case .topRated: return strings.topRated + case .discover: return strings.discover + } + } + + var isDisabled: Bool { + self == .discover + } +} + +// MARK: - Anime Type +enum AnimeType: CaseIterable, CategoryTab { + case tvSeries + case movies + + var title: String { + let strings = L10n.current + switch self { + case .tvSeries: return strings.tvSeries + case .movies: return strings.movies + } + } + + var isDisabled: Bool { + false + } +} + +// MARK: - Category Tabs View (same style as Profile tabs) +struct CategoryTabsView: View where Tab.AllCases: RandomAccessCollection { + @Binding var selectedTab: Tab + var onTabChange: (() -> Void)? + @Namespace private var tabNamespace + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 0) { + ForEach(Array(Tab.allCases), id: \.self) { tab in + Button { + if !tab.isDisabled { + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + selectedTab = tab + } + onTabChange?() + } + } label: { + VStack(spacing: 8) { + Text(tab.title) + .font(.subheadline.weight(.medium)) + .foregroundColor( + tab.isDisabled + ? .appMutedForegroundAdaptive.opacity(0.5) + : (selectedTab == tab + ? .appForegroundAdaptive + : .appMutedForegroundAdaptive) + ) + .padding(.horizontal, 16) + + // Sliding indicator + ZStack { + Rectangle() + .fill(Color.clear) + .frame(height: 3) + + if selectedTab == tab { + Rectangle() + .fill(Color.appForegroundAdaptive) + .frame(height: 3) + .matchedGeometryEffect(id: "categoryTabIndicator", in: tabNamespace) + } + } + } + } + .buttonStyle(.plain) + .disabled(tab.isDisabled) + } + } + .padding(.horizontal, 8) + } + .overlay( + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1), + alignment: .bottom + ) + } +} + +struct CategoryListView: View { + let categoryType: HomeCategoryType + var initialMovieSubcategory: MovieSubcategory? + var initialTVSeriesSubcategory: TVSeriesSubcategory? + + @Environment(\.dismiss) private var dismiss + @State private var items: [SearchResult] = [] + @State private var isLoading = true + @State private var isLoadingMore = false + @State private var currentPage = 1 + @State private var totalPages = 1 + @State private var strings = L10n.current + @State private var selectedMovieSubcategory: MovieSubcategory = .popular + @State private var selectedTVSeriesSubcategory: TVSeriesSubcategory = .popular + @State private var selectedAnimeType: AnimeType = .tvSeries + @ObservedObject private var themeManager = ThemeManager.shared + @ObservedObject private var preferencesManager = UserPreferencesManager.shared + @State private var hasAppliedInitialSubcategory = false + + private var title: String { + switch categoryType { + case .movies: return strings.movies + case .tvSeries: return strings.tvSeries + case .animes: return strings.animes + case .doramas: return strings.doramas + } + } + + private var mediaType: String { + switch categoryType { + case .movies: return "movie" + case .tvSeries, .doramas: return "tv" + case .animes: return selectedAnimeType == .movies ? "movie" : "tv" + } + } + + private var hasMorePages: Bool { + currentPage < totalPages + } + + private let columns = [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + ] + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(title) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + Color.clear + .frame(width: 40, height: 40) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + // Tabs in header + if categoryType == .movies { + CategoryTabsView( + selectedTab: $selectedMovieSubcategory, + onTabChange: { + Task { + await loadItems() + } + } + ) + } else if categoryType == .tvSeries { + CategoryTabsView( + selectedTab: $selectedTVSeriesSubcategory, + onTabChange: { + Task { + await loadItems() + } + } + ) + } else if categoryType == .animes { + CategoryTabsView( + selectedTab: $selectedAnimeType, + onTabChange: { + Task { + await loadItems() + } + } + ) + } else { + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1) + } + } + + // Content + if isLoading && items.isEmpty { + ScrollView { + LazyVGrid(columns: columns, spacing: 16) { + ForEach(0..<12, id: \.self) { _ in + RoundedRectangle(cornerRadius: 16) + .fill(Color.appBorderAdaptive) + .aspectRatio(2 / 3, contentMode: .fit) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 24) + } + } else { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + // Preferences Badge + HStack { + PreferencesBadge() + Spacer() + } + .padding(.horizontal, 24) + .padding(.top, 16) + .padding(.bottom, 16) + + LazyVGrid(columns: columns, spacing: 16) { + ForEach(items) { item in + NavigationLink { + MediaDetailView(mediaId: item.id, mediaType: mediaType) + } label: { + CategoryPosterCard(item: item) + } + .buttonStyle(.plain) + .onAppear { + if item.id == items.suffix(6).first?.id && hasMorePages && !isLoadingMore { + Task { + await loadMoreItems() + } + } + } + } + + if isLoadingMore { + ForEach(0..<3, id: \.self) { _ in + RoundedRectangle(cornerRadius: 16) + .fill(Color.appBorderAdaptive) + .aspectRatio(2 / 3, contentMode: .fit) + } + } + } + .padding(.horizontal, 24) + .padding(.bottom, 24) + } + } + } + } + } + .navigationBarHidden(true) + .preferredColorScheme(themeManager.current.colorScheme) + .task { + if !hasAppliedInitialSubcategory { + if let initialMovie = initialMovieSubcategory { + selectedMovieSubcategory = initialMovie + } + if let initialTVSeries = initialTVSeriesSubcategory { + selectedTVSeriesSubcategory = initialTVSeries + } + hasAppliedInitialSubcategory = true + } + await loadItems() + } + } + + private func loadItems() async { + isLoading = true + currentPage = 1 + + let language = Language.current.rawValue + let watchRegion = preferencesManager.watchRegion + let watchProviders = + preferencesManager.hasStreamingServices ? preferencesManager.watchProvidersString : nil + + do { + let result: PaginatedResult + switch categoryType { + case .movies: + result = try await loadMoviesForSubcategory( + language: language, + page: 1, + watchRegion: watchRegion, + watchProviders: watchProviders + ) + case .tvSeries: + result = try await loadTVSeriesForSubcategory( + language: language, + page: 1, + watchRegion: watchRegion, + watchProviders: watchProviders + ) + case .animes: + result = try await loadAnimesForType( + language: language, + page: 1, + watchRegion: watchRegion, + watchProviders: watchProviders + ) + case .doramas: + if let region = watchRegion, let providers = watchProviders { + result = try await TMDBService.shared.discoverDoramas( + language: language, + page: 1, + watchRegion: region, + withWatchProviders: providers + ) + } else { + result = try await TMDBService.shared.getPopularDoramas(language: language, page: 1) + } + } + items = result.results + currentPage = result.page + totalPages = result.totalPages + } catch { + items = [] + } + + isLoading = false + } + + private func loadMoviesForSubcategory( + language: String, + page: Int, + watchRegion: String? = nil, + watchProviders: String? = nil + ) async throws -> PaginatedResult { + // When streaming services are selected, use discover for popular + if let region = watchRegion, let providers = watchProviders, + selectedMovieSubcategory == .popular + { + return try await TMDBService.shared.discoverMovies( + language: language, + page: page, + watchRegion: region, + withWatchProviders: providers + ) + } + + switch selectedMovieSubcategory { + case .nowPlaying: + return try await TMDBService.shared.getNowPlayingMovies(language: language, page: page) + case .popular: + return try await TMDBService.shared.getPopularMovies(language: language, page: page) + case .topRated: + return try await TMDBService.shared.getTopRatedMovies(language: language, page: page) + case .upcoming: + return try await TMDBService.shared.getUpcomingMovies(language: language, page: page) + case .discover: + // Discover is disabled, fallback to popular + return try await TMDBService.shared.getPopularMovies(language: language, page: page) + } + } + + private func loadTVSeriesForSubcategory( + language: String, + page: Int, + watchRegion: String? = nil, + watchProviders: String? = nil + ) async throws -> PaginatedResult { + // When streaming services are selected, use discover for popular + if let region = watchRegion, let providers = watchProviders, + selectedTVSeriesSubcategory == .popular + { + return try await TMDBService.shared.discoverTV( + language: language, + page: page, + watchRegion: region, + withWatchProviders: providers + ) + } + + switch selectedTVSeriesSubcategory { + case .airingToday: + return try await TMDBService.shared.getAiringTodayTVSeries(language: language, page: page) + case .onTheAir: + return try await TMDBService.shared.getOnTheAirTVSeries(language: language, page: page) + case .popular: + return try await TMDBService.shared.getPopularTVSeries(language: language, page: page) + case .topRated: + return try await TMDBService.shared.getTopRatedTVSeries(language: language, page: page) + case .discover: + // Discover is disabled, fallback to popular + return try await TMDBService.shared.getPopularTVSeries(language: language, page: page) + } + } + + private func loadAnimesForType( + language: String, + page: Int, + watchRegion: String? = nil, + watchProviders: String? = nil + ) async throws -> PaginatedResult { + if let region = watchRegion, let providers = watchProviders { + switch selectedAnimeType { + case .tvSeries: + return try await TMDBService.shared.discoverAnimes( + language: language, + page: page, + watchRegion: region, + withWatchProviders: providers + ) + case .movies: + return try await TMDBService.shared.discoverAnimeMovies( + language: language, + page: page, + watchRegion: region, + withWatchProviders: providers + ) + } + } + + switch selectedAnimeType { + case .tvSeries: + return try await TMDBService.shared.getPopularAnimes(language: language, page: page) + case .movies: + return try await TMDBService.shared.getPopularAnimeMovies(language: language, page: page) + } + } + + private func loadMoreItems() async { + guard hasMorePages && !isLoadingMore else { return } + + isLoadingMore = true + let nextPage = currentPage + 1 + let language = Language.current.rawValue + let watchRegion = preferencesManager.watchRegion + let watchProviders = + preferencesManager.hasStreamingServices ? preferencesManager.watchProvidersString : nil + + do { + let result: PaginatedResult + switch categoryType { + case .movies: + result = try await loadMoviesForSubcategory( + language: language, + page: nextPage, + watchRegion: watchRegion, + watchProviders: watchProviders + ) + case .tvSeries: + result = try await loadTVSeriesForSubcategory( + language: language, + page: nextPage, + watchRegion: watchRegion, + watchProviders: watchProviders + ) + case .animes: + result = try await loadAnimesForType( + language: language, + page: nextPage, + watchRegion: watchRegion, + watchProviders: watchProviders + ) + case .doramas: + if let region = watchRegion, let providers = watchProviders { + result = try await TMDBService.shared.discoverDoramas( + language: language, + page: nextPage, + watchRegion: region, + withWatchProviders: providers + ) + } else { + result = try await TMDBService.shared.getPopularDoramas( + language: language, + page: nextPage + ) + } + } + + let newItems = result.results.filter { newItem in + !items.contains { $0.id == newItem.id } + } + + items.append(contentsOf: newItems) + currentPage = result.page + totalPages = result.totalPages + } catch { + // Silently fail on pagination errors + } + + isLoadingMore = false + } +} + +// MARK: - Category Poster Card +struct CategoryPosterCard: View { + let item: SearchResult + + var body: some View { + CachedAsyncImage(url: item.imageURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 16) + .fill(Color.appBorderAdaptive) + } + .aspectRatio(2 / 3, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .posterBorder(cornerRadius: 16) + .shadow(color: Color.black.opacity(0.15), radius: 4, x: 0, y: 2) + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/HomeTabView.swift b/apps/ios/Plotwist/Plotwist/Views/Home/HomeTabView.swift new file mode 100644 index 00000000..2fbbd628 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Home/HomeTabView.swift @@ -0,0 +1,563 @@ +// +// HomeTabView.swift +// Plotwist +// + +import SwiftUI + +struct HomeTabView: View { + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + @State private var user: User? + @State private var watchingItems: [SearchResult] = [] + @State private var watchlistItems: [SearchResult] = [] + @State private var isInitialLoad = true + @State private var needsRefresh = false + @State private var hasAppeared = false + + private let cache = HomeDataCache.shared + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + private var greeting: String { + let hour = Calendar.current.component(.hour, from: Date()) + if hour >= 5 && hour < 12 { + return strings.goodMorning + } else if hour >= 12 && hour < 18 { + return strings.goodAfternoon + } else { + return strings.goodEvening + } + } + + // Only show skeleton on first load when no cached data + private var showWatchingSkeleton: Bool { + isInitialLoad && cache.shouldShowSkeleton && watchingItems.isEmpty + } + + private var showWatchlistSkeleton: Bool { + isInitialLoad && cache.shouldShowSkeleton && watchlistItems.isEmpty + } + + private var showUserSkeleton: Bool { + isInitialLoad && user == nil && cache.user == nil + } + + var body: some View { + NavigationView { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 24) { + // Header with greeting + HomeHeaderView( + greeting: greeting, + username: user?.username, + avatarURL: user?.avatarImageURL, + isLoading: showUserSkeleton, + onAvatarTapped: { + NotificationCenter.default.post(name: .navigateToProfile, object: nil) + } + ) + .padding(.horizontal, 24) + .padding(.top, 16) + + // Continue Watching Section + if showWatchingSkeleton { + HomeSectionSkeleton() + } else if !watchingItems.isEmpty { + ContinueWatchingSection( + items: watchingItems, + title: strings.continueWatching + ) + } + + // Watchlist Section + if showWatchlistSkeleton { + HomeSectionSkeleton() + } else if !watchlistItems.isEmpty { + WatchlistSection( + items: watchlistItems, + title: strings.upNext + ) + } + + Spacer(minLength: 100) + } + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + .onAppear { + // Restore from cache immediately on appear + if !hasAppeared { + hasAppeared = true + restoreFromCache() + } + + // Refresh when returning to view if needed + if needsRefresh { + needsRefresh = false + Task { + await loadWatchingItems(forceRefresh: true) + await loadWatchlistItems(forceRefresh: true) + } + } + } + } + .task { + await loadData() + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + .onReceive(NotificationCenter.default.publisher(for: .profileUpdated)) { _ in + Task { await loadUser(forceRefresh: true) } + } + .onReceive(NotificationCenter.default.publisher(for: .collectionCacheInvalidated)) { _ in + needsRefresh = true + } + .onReceive(NotificationCenter.default.publisher(for: .homeDataCacheInvalidated)) { _ in + needsRefresh = true + } + } + + private func restoreFromCache() { + if let cachedUser = cache.user { + user = cachedUser + } + if let cachedWatching = cache.watchingItems { + watchingItems = cachedWatching + } + if let cachedWatchlist = cache.watchlistItems { + watchlistItems = cachedWatchlist + } + } + + private func loadData() async { + // If we have cached data, don't show loading state + if cache.isDataAvailable { + isInitialLoad = false + } + + await withTaskGroup(of: Void.self) { group in + group.addTask { await loadUser() } + group.addTask { await loadWatchingItems() } + group.addTask { await loadWatchlistItems() } + } + + isInitialLoad = false + } + + @MainActor + private func loadUser(forceRefresh: Bool = false) async { + // Use cache if available and not forcing refresh + if !forceRefresh, let cachedUser = cache.user { + user = cachedUser + return + } + + guard AuthService.shared.isAuthenticated else { return } + + do { + let fetchedUser = try await AuthService.shared.getCurrentUser() + user = fetchedUser + cache.setUser(fetchedUser) + } catch { + print("Error loading user: \(error)") + } + } + + @MainActor + private func loadWatchingItems(forceRefresh: Bool = false) async { + // Use cache if available and not forcing refresh + if !forceRefresh, let cachedItems = cache.watchingItems { + watchingItems = cachedItems + return + } + + guard AuthService.shared.isAuthenticated else { return } + + let fetchedUser = try? await AuthService.shared.getCurrentUser() + guard let currentUser = user ?? fetchedUser else { return } + + do { + let items = try await UserItemService.shared.getAllUserItems( + userId: currentUser.id, + status: UserItemStatus.watching.rawValue + ) + + // Fetch details for each item from TMDB + var results: [SearchResult] = [] + for item in items.prefix(10) { + do { + if item.mediaType == "MOVIE" { + let details = try await TMDBService.shared.getMovieDetails( + id: item.tmdbId, + language: Language.current.rawValue + ) + results.append( + SearchResult( + id: details.id, + mediaType: "movie", + title: details.title, + name: details.name, + posterPath: details.posterPath, + profilePath: nil, + releaseDate: details.releaseDate, + firstAirDate: details.firstAirDate, + overview: details.overview, + voteAverage: details.voteAverage, + knownForDepartment: nil + )) + } else { + let details = try await TMDBService.shared.getTVSeriesDetails( + id: item.tmdbId, + language: Language.current.rawValue + ) + results.append( + SearchResult( + id: details.id, + mediaType: "tv", + title: details.title, + name: details.name, + posterPath: details.posterPath, + profilePath: nil, + releaseDate: details.releaseDate, + firstAirDate: details.firstAirDate, + overview: details.overview, + voteAverage: details.voteAverage, + knownForDepartment: nil + )) + } + } catch { + print("Error fetching details for \(item.tmdbId): \(error)") + } + } + + watchingItems = results + cache.setWatchingItems(results) + } catch { + print("Error loading watching items: \(error)") + } + } + + @MainActor + private func loadWatchlistItems(forceRefresh: Bool = false) async { + // Use cache if available and not forcing refresh + if !forceRefresh, let cachedItems = cache.watchlistItems { + watchlistItems = cachedItems + return + } + + guard AuthService.shared.isAuthenticated else { return } + + let fetchedUser = try? await AuthService.shared.getCurrentUser() + guard let currentUser = user ?? fetchedUser else { return } + + do { + let items = try await UserItemService.shared.getAllUserItems( + userId: currentUser.id, + status: UserItemStatus.watchlist.rawValue + ) + + // Fetch details for each item from TMDB + var results: [SearchResult] = [] + for item in items.prefix(10) { + do { + if item.mediaType == "MOVIE" { + let details = try await TMDBService.shared.getMovieDetails( + id: item.tmdbId, + language: Language.current.rawValue + ) + results.append( + SearchResult( + id: details.id, + mediaType: "movie", + title: details.title, + name: details.name, + posterPath: details.posterPath, + profilePath: nil, + releaseDate: details.releaseDate, + firstAirDate: details.firstAirDate, + overview: details.overview, + voteAverage: details.voteAverage, + knownForDepartment: nil + )) + } else { + let details = try await TMDBService.shared.getTVSeriesDetails( + id: item.tmdbId, + language: Language.current.rawValue + ) + results.append( + SearchResult( + id: details.id, + mediaType: "tv", + title: details.title, + name: details.name, + posterPath: details.posterPath, + profilePath: nil, + releaseDate: details.releaseDate, + firstAirDate: details.firstAirDate, + overview: details.overview, + voteAverage: details.voteAverage, + knownForDepartment: nil + )) + } + } catch { + print("Error fetching details for \(item.tmdbId): \(error)") + } + } + + watchlistItems = results + cache.setWatchlistItems(results) + } catch { + print("Error loading watchlist items: \(error)") + } + } +} + +// MARK: - Home Header View +struct HomeHeaderView: View { + let greeting: String + let username: String? + let avatarURL: URL? + let isLoading: Bool + var onAvatarTapped: (() -> Void)? + + var body: some View { + HStack(spacing: 16) { + if isLoading { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 180, height: 24) + } else if let username { + (Text("\(greeting), ") + .font(.title2.bold()) + .foregroundColor(.appForegroundAdaptive) + + Text("@\(username)") + .font(.title2.bold()) + .foregroundColor(.appMutedForegroundAdaptive)) + } else { + Text(greeting) + .font(.title2.bold()) + .foregroundColor(.appForegroundAdaptive) + } + + Spacer() + + if isLoading { + Circle() + .fill(Color.appBorderAdaptive) + .frame(width: 44, height: 44) + } else { + Button { + onAvatarTapped?() + } label: { + ProfileAvatar(avatarURL: avatarURL, username: username ?? "", size: 44) + } + .buttonStyle(.plain) + } + } + } +} + +// MARK: - Home Section Card +struct HomeSectionCard: View { + let item: SearchResult + + var body: some View { + CachedAsyncImage(url: item.imageURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + } + .frame(width: 120, height: 180) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .posterBorder(cornerRadius: 12) + .posterShadow() + } +} + +// MARK: - Continue Watching Section +struct ContinueWatchingSection: View { + let items: [SearchResult] + let title: String + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + .padding(.horizontal, 24) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(items) { item in + NavigationLink { + MediaDetailView( + mediaId: item.id, + mediaType: item.mediaType ?? "movie" + ) + } label: { + HomeSectionCard(item: item) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 4) + } + .scrollClipDisabled() + } + } +} + +// MARK: - Watchlist Section +struct WatchlistSection: View { + let items: [SearchResult] + let title: String + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + .padding(.horizontal, 24) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(items) { item in + NavigationLink { + MediaDetailView( + mediaId: item.id, + mediaType: item.mediaType ?? "movie" + ) + } label: { + HomeSectionCard(item: item) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 4) + } + .scrollClipDisabled() + } + } +} + +// MARK: - Home Category Type +enum HomeCategoryType { + case movies + case tvSeries + case animes + case doramas +} + +// MARK: - Home Section View +struct HomeSectionView: View { + let title: String + let items: [SearchResult] + let mediaType: String + let categoryType: HomeCategoryType + var initialMovieSubcategory: MovieSubcategory? + var initialTVSeriesSubcategory: TVSeriesSubcategory? + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + NavigationLink { + CategoryListView( + categoryType: categoryType, + initialMovieSubcategory: initialMovieSubcategory, + initialTVSeriesSubcategory: initialTVSeriesSubcategory + ) + } label: { + HStack(spacing: 6) { + Text(title) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + } + .padding(.horizontal, 24) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(items.prefix(10)) { item in + NavigationLink { + MediaDetailView( + mediaId: item.id, + mediaType: mediaType + ) + } label: { + HomePosterCard(item: item) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 4) + } + .scrollClipDisabled() + } + } +} + +// MARK: - Home Poster Card +struct HomePosterCard: View { + let item: SearchResult + + var body: some View { + CachedAsyncImage(url: item.imageURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 16) + .fill(Color.appBorderAdaptive) + } + .frame(width: 120, height: 180) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .posterBorder(cornerRadius: 16) + .posterShadow() + } +} + +// MARK: - Home Section Skeleton +struct HomeSectionSkeleton: View { + var body: some View { + VStack(alignment: .leading, spacing: 12) { + // Title skeleton - matches .font(.headline) height + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 140, height: 17) + .padding(.horizontal, 24) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(0..<5, id: \.self) { _ in + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + .frame(width: 120, height: 180) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 4) + } + .scrollClipDisabled() + } + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/HomeView.swift b/apps/ios/Plotwist/Plotwist/Views/Home/HomeView.swift new file mode 100644 index 00000000..299a16ca --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Home/HomeView.swift @@ -0,0 +1,69 @@ +// +// HomeView.swift +// Plotwist +// + +import SwiftUI + +struct HomeView: View { + @State private var selectedTab = 0 + + var body: some View { + TabView(selection: $selectedTab) { + HomeTabView() + .tabItem { + Image(systemName: "house.fill") + } + .tag(0) + + SearchTabView() + .tabItem { + Image(systemName: "magnifyingglass") + } + .tag(1) + + // TODO: Re-enable when Soundtracks feature is ready + // SoundtracksTabView() + // .tabItem { + // Image(systemName: "flame.fill") + // } + // .tag(2) + + ProfileTabView() + .tabItem { + Image(systemName: "person.fill") + } + .tag(2) + } + .tint(.appForegroundAdaptive) + .onReceive(NotificationCenter.default.publisher(for: .navigateToSearch)) { _ in + selectedTab = 1 + } + .onReceive(NotificationCenter.default.publisher(for: .navigateToProfile)) { _ in + selectedTab = 2 + } + .onAppear { + let appearance = UITabBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.shadowColor = UIColor(Color.appBorderAdaptive) + appearance.stackedLayoutAppearance.normal.iconColor = UIColor( + Color.appMutedForegroundAdaptive) + appearance.stackedLayoutAppearance.selected.iconColor = UIColor(Color.appForegroundAdaptive) + + // Add vertical padding to icons - move them down from top border + let iconOffset = UIOffset(horizontal: 0, vertical: 4) + appearance.stackedLayoutAppearance.normal.titlePositionAdjustment = iconOffset + appearance.stackedLayoutAppearance.selected.titlePositionAdjustment = iconOffset + + UITabBar.appearance().standardAppearance = appearance + UITabBar.appearance().scrollEdgeAppearance = appearance + } + .safeAreaInset(edge: .bottom) { + Color.clear.frame(height: 24) + } + } +} + +#Preview { + HomeView() +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift new file mode 100644 index 00000000..8d8fbfa6 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift @@ -0,0 +1,2263 @@ +// +// ProfileTabView.swift +// Plotwist +// + +import SwiftUI + +// MARK: - Scroll Offset Preference Key +struct ScrollOffsetPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +// MARK: - Profile Status Tab +enum ProfileStatusTab: String, CaseIterable { + case watched = "WATCHED" + case watching = "WATCHING" + case watchlist = "WATCHLIST" + case dropped = "DROPPED" + + func displayName(strings: Strings) -> String { + switch self { + case .watched: return strings.watched + case .watching: return strings.watching + case .watchlist: return strings.watchlist + case .dropped: return strings.dropped + } + } +} + +// MARK: - Profile Main Tab +enum ProfileMainTab: CaseIterable { + case collection + case reviews + + func displayName(strings: Strings) -> String { + switch self { + case .collection: return strings.collection + case .reviews: return strings.reviews + } + } +} + +struct ProfileTabView: View { + @State private var user: User? + @State private var isInitialLoad = true + @State private var strings = L10n.current + @State private var selectedMainTab: ProfileMainTab = .collection + @State private var selectedStatusTab: ProfileStatusTab = .watched + @State private var userItems: [UserItemSummary] = [] + @State private var isLoadingItems = false + @State private var totalCollectionCount: Int = 0 + @State private var totalReviewsCount: Int = 0 + @State private var scrollOffset: CGFloat = 0 + @State private var initialScrollOffset: CGFloat? = nil + @State private var hasAppeared = false + @ObservedObject private var themeManager = ThemeManager.shared + + private let cache = CollectionCache.shared + + // Avatar size + private let avatarSize: CGFloat = 56 + // Scroll threshold to show header content (height of profile info section) + private let scrollThreshold: CGFloat = 80 + + private var isScrolled: Bool { + guard let initial = initialScrollOffset else { return false } + return scrollOffset < initial - scrollThreshold + } + + // Only show loading on first load when no cached data + private var showLoading: Bool { + isInitialLoad && cache.shouldShowSkeleton && user == nil + } + + var body: some View { + NavigationView { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + if showLoading { + VStack { + Spacer() + ProgressView() + Spacer() + } + } else if let user { + VStack(spacing: 0) { + // Header with action buttons + HStack(spacing: 12) { + // Avatar + Username (appears when scrolled) + if isScrolled { + HStack(spacing: 10) { + ProfileAvatar( + avatarURL: user.avatarImageURL, + username: user.username, + size: 32 + ) + .transition(.opacity.combined(with: .move(edge: .bottom))) + + Text(user.username) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + .transition(.opacity.combined(with: .move(edge: .bottom))) + } + } + + Spacer() + + // Action Buttons + NavigationLink(destination: EditProfileView(user: user)) { + Image(systemName: "ellipsis") + .font(.system(size: 14)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 36, height: 36) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(Color.appBackgroundAdaptive) + .overlay( + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1) + .opacity(isScrolled ? 1 : 0), + alignment: .bottom + ) + .animation(.easeInOut(duration: 0.2), value: isScrolled) + + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + // Profile Info - Avatar left, info right + HStack(alignment: .center, spacing: 12) { + // Avatar + ProfileAvatar( + avatarURL: user.avatarImageURL, + username: user.username, + size: avatarSize + ) + + // User Info + VStack(alignment: .leading, spacing: 2) { + // Username + Pro Badge + HStack(spacing: 8) { + Text(user.username) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + if user.isPro { + ProBadge() + } + } + + // Member Since + if let memberDate = user.memberSinceDate { + Text("\(strings.memberSince) \(formattedMemberDate(memberDate))") + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + } + + Spacer() + } + .padding(.horizontal, 24) + .padding(.bottom, 12) + + // Biography + if let biography = user.biography, !biography.isEmpty { + Text(biography) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .lineSpacing(4) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + } + + // Main Tabs (Collection / Reviews) + ProfileMainTabs( + selectedTab: $selectedMainTab, + strings: strings, + collectionCount: totalCollectionCount, + reviewsCount: totalReviewsCount + ) + .padding(.top, 20) + .padding(.bottom, 8) + + // Tab Content + switch selectedMainTab { + case .collection: + // Status Tabs inside Collection + ProfileStatusTabs(selectedTab: $selectedStatusTab, strings: strings) + .padding(.top, 8) + .onChange(of: selectedStatusTab) { _ in + Task { await loadUserItems() } + } + + // User Items Grid + if isLoadingItems { + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + ], + spacing: 16 + ) { + ForEach(0..<6, id: \.self) { _ in + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + .aspectRatio(2 / 3, contentMode: .fit) + } + } + .padding(.horizontal, 24) + .padding(.top, 16) + } else if userItems.isEmpty { + // Empty state - Add first item + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + ], + spacing: 16 + ) { + Button { + NotificationCenter.default.post(name: .navigateToSearch, object: nil) + } label: { + RoundedRectangle(cornerRadius: 12) + .strokeBorder( + style: StrokeStyle(lineWidth: 2, dash: [8, 4]) + ) + .foregroundColor(.appBorderAdaptive) + .aspectRatio(2 / 3, contentMode: .fit) + .overlay( + Image(systemName: "plus") + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.appMutedForegroundAdaptive) + ) + } + .buttonStyle(.plain) + } + .padding(.horizontal, 24) + .padding(.top, 16) + } else { + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + ], + spacing: 16 + ) { + ForEach(userItems) { item in + NavigationLink { + MediaDetailView( + mediaId: item.tmdbId, + mediaType: item.mediaType == "MOVIE" ? "movie" : "tv" + ) + } label: { + ProfileItemCard(tmdbId: item.tmdbId, mediaType: item.mediaType) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 24) + .padding(.top, 16) + } + + case .reviews: + // Reviews tab content - placeholder for now + VStack(spacing: 16) { + Image(systemName: "text.bubble") + .font(.system(size: 48)) + .foregroundColor(.appMutedForegroundAdaptive) + Text(strings.beFirstToReview) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + } + .frame(maxWidth: .infinity) + .padding(.top, 60) + } + + Spacer() + .frame(height: 100) + } + .background( + GeometryReader { geo -> Color in + DispatchQueue.main.async { + let offset = geo.frame(in: .global).minY + if initialScrollOffset == nil { + initialScrollOffset = offset + } + scrollOffset = offset + } + return Color.clear + } + ) + } + } + } else { + // Error state - no user + VStack(spacing: 16) { + Spacer() + Image(systemName: "person.crop.circle.badge.exclamationmark") + .font(.system(size: 48)) + .foregroundColor(.appMutedForegroundAdaptive) + Text("Could not load profile") + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + Button("Try again") { + Task { + await loadUser() + } + } + .foregroundColor(.appForegroundAdaptive) + Spacer() + } + } + } + .onAppear { + // Restore from cache immediately on appear + if !hasAppeared { + hasAppeared = true + restoreFromCache() + } + } + .task { + await loadData() + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + .onReceive(NotificationCenter.default.publisher(for: .profileUpdated)) { _ in + Task { await loadUser(forceRefresh: true) } + } + .onReceive(NotificationCenter.default.publisher(for: .collectionCacheInvalidated)) { _ in + Task { + await loadUserItems(forceRefresh: true) + await loadTotalCollectionCount(forceRefresh: true) + } + } + .navigationBarHidden(true) + } + } + + private func restoreFromCache() { + if let cachedUser = cache.user { + user = cachedUser + } + if let cachedCount = cache.getTotalCount() { + totalCollectionCount = cachedCount + } + if let cachedReviewsCount = cache.getReviewsCount() { + totalReviewsCount = cachedReviewsCount + } + if let userId = user?.id ?? cache.user?.id, + let cachedItems = cache.getItems(userId: userId, status: selectedStatusTab.rawValue) { + userItems = cachedItems + } + } + + private func loadData() async { + // If we have cached data, don't show loading state + if cache.isDataAvailable { + isInitialLoad = false + } + + await loadUser() + await loadUserItems() + await loadTotalCollectionCount() + await loadTotalReviewsCount() + + isInitialLoad = false + } + + private func loadUser(forceRefresh: Bool = false) async { + // Use cache if available and not forcing refresh + if !forceRefresh, let cachedUser = cache.user { + user = cachedUser + return + } + + do { + let fetchedUser = try await AuthService.shared.getCurrentUser() + user = fetchedUser + cache.setUser(fetchedUser) + } catch { + print("Error loading user: \(error)") + user = nil + } + } + + private func formattedMemberDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "MMM/yyyy" + formatter.locale = Locale(identifier: Language.current.rawValue) + return formatter.string(from: date) + } + + private func loadUserItems(forceRefresh: Bool = false) async { + guard let userId = user?.id else { return } + + // Check cache first + if !forceRefresh, + let cachedItems = cache.getItems(userId: userId, status: selectedStatusTab.rawValue) + { + userItems = cachedItems + return + } + + isLoadingItems = true + defer { isLoadingItems = false } + + do { + let items = try await UserItemService.shared.getAllUserItems( + userId: userId, + status: selectedStatusTab.rawValue + ) + userItems = items + // Save to cache + cache.setItems(items, userId: userId, status: selectedStatusTab.rawValue) + } catch { + print("Error loading user items: \(error)") + userItems = [] + } + } + + private func loadTotalCollectionCount(forceRefresh: Bool = false) async { + guard let userId = user?.id else { return } + + // Check cache first + if !forceRefresh, let cachedCount = cache.getTotalCount() { + totalCollectionCount = cachedCount + return + } + + do { + let count = try await UserItemService.shared.getUserItemsCount(userId: userId) + totalCollectionCount = count + // Save to cache + cache.setTotalCount(count) + } catch { + print("Error loading collection count: \(error)") + totalCollectionCount = 0 + } + } + + private func loadTotalReviewsCount(forceRefresh: Bool = false) async { + guard let userId = user?.id else { return } + + // Check cache first + if !forceRefresh, let cachedCount = cache.getReviewsCount() { + totalReviewsCount = cachedCount + return + } + + do { + let count = try await ReviewService.shared.getUserReviewsCount(userId: userId) + totalReviewsCount = count + // Save to cache + cache.setReviewsCount(count) + } catch { + print("Error loading reviews count: \(error)") + totalReviewsCount = 0 + } + } +} + +// MARK: - Profile Main Tabs (Collection / Reviews) +struct ProfileMainTabs: View { + @Binding var selectedTab: ProfileMainTab + let strings: Strings + var collectionCount: Int = 0 + var reviewsCount: Int = 0 + @Namespace private var tabNamespace + + private func badgeCount(for tab: ProfileMainTab) -> Int { + switch tab { + case .collection: return collectionCount + case .reviews: return reviewsCount + } + } + + var body: some View { + HStack(spacing: 0) { + ForEach(ProfileMainTab.allCases, id: \.self) { tab in + Button { + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + selectedTab = tab + } + } label: { + VStack(spacing: 8) { + HStack(spacing: 6) { + Text(tab.displayName(strings: strings)) + .font(.subheadline.weight(.medium)) + .foregroundColor( + selectedTab == tab + ? .appForegroundAdaptive + : .appMutedForegroundAdaptive + ) + + // Animated badge + if badgeCount(for: tab) > 0 { + if selectedTab == tab { + CollectionCountBadge(count: badgeCount(for: tab)) + .transition( + .asymmetric( + insertion: .move(edge: .leading).combined(with: .opacity), + removal: .scale(scale: 0.8).combined(with: .opacity) + ) + ) + .animation(.easeOut(duration: 0.15), value: selectedTab) + } + } + } + + // Sliding indicator + ZStack { + Rectangle() + .fill(Color.clear) + .frame(height: 3) + + if selectedTab == tab { + Rectangle() + .fill(Color.appForegroundAdaptive) + .frame(height: 3) + .matchedGeometryEffect(id: "tabIndicator", in: tabNamespace) + } + } + } + } + .buttonStyle(.plain) + .frame(maxWidth: .infinity) + } + } + .padding(.horizontal, 24) + .overlay( + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1), + alignment: .bottom + ) + } +} + +// MARK: - Collection Count Badge +struct CollectionCountBadge: View { + let count: Int + @Environment(\.colorScheme) var colorScheme + + var body: some View { + Text("\(count)") + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(colorScheme == .dark ? .white : .appForegroundAdaptive) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background( + colorScheme == .dark + ? Color(hex: "0a0a0f") + : Color(hex: "f5f5f5") + ) + .clipShape(Capsule()) + .overlay( + Capsule() + .stroke(Color.appBorderAdaptive, lineWidth: 1) + ) + } +} + +// MARK: - Profile Status Tabs +struct ProfileStatusTabs: View { + @Binding var selectedTab: ProfileStatusTab + let strings: Strings + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 2) { + ForEach(ProfileStatusTab.allCases, id: \.self) { tab in + Button { + withAnimation(.easeInOut(duration: 0.2)) { + selectedTab = tab + } + } label: { + Text(tab.displayName(strings: strings)) + .font(.footnote.weight(.medium)) + .foregroundColor( + selectedTab == tab + ? .appForegroundAdaptive + : .appMutedForegroundAdaptive + ) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + selectedTab == tab + ? Color.appBackgroundAdaptive + : Color.clear + ) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .shadow( + color: selectedTab == tab ? Color.black.opacity(0.08) : Color.clear, + radius: 2, + x: 0, + y: 1 + ) + } + .buttonStyle(.plain) + } + } + .padding(3) + .background(Color.appInputFilled) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding(.horizontal, 24) + } + .scrollClipDisabled() + } +} + +// MARK: - Profile Item Card +struct ProfileItemCard: View { + let tmdbId: Int + let mediaType: String + @State private var posterURL: URL? + @State private var isLoading = true + + var body: some View { + CachedAsyncImage(url: posterURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + } + .aspectRatio(2 / 3, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .posterBorder(cornerRadius: 12) + .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1) + .task { + await loadPoster() + } + } + + private func loadPoster() async { + do { + let type = mediaType == "MOVIE" ? "movie" : "tv" + if type == "movie" { + let details = try await TMDBService.shared.getMovieDetails( + id: tmdbId, + language: Language.current.rawValue + ) + posterURL = details.posterURL + } else { + let details = try await TMDBService.shared.getTVSeriesDetails( + id: tmdbId, + language: Language.current.rawValue + ) + posterURL = details.posterURL + } + } catch { + print("Error loading poster: \(error)") + } + isLoading = false + } +} + +// MARK: - Profile Avatar +struct ProfileAvatar: View { + let avatarURL: URL? + let username: String + let size: CGFloat + + @Environment(\.colorScheme) var colorScheme + + var body: some View { + ZStack { + if let avatarURL { + CachedAsyncImage(url: avatarURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + avatarPlaceholder + } + } else { + avatarPlaceholder + } + } + .frame(width: size, height: size) + .clipShape(Circle()) + .overlay( + Circle() + .stroke( + colorScheme == .dark ? Color.appBorderAdaptive : Color.clear, + lineWidth: 1 + ) + ) + } + + private var avatarPlaceholder: some View { + Circle() + .fill(Color.appInputFilled) + .overlay( + Text(String(username.prefix(1)).uppercased()) + .font(.system(size: size * 0.4, weight: .bold)) + .foregroundColor(.appForegroundAdaptive) + ) + } +} + +// MARK: - Pro Badge +struct ProBadge: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Text("PRO") + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(colorScheme == .dark ? .white : .appForegroundAdaptive) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background( + colorScheme == .dark + ? Color(hex: "0a0a0f") + : Color(hex: "f5f5f5") + ) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(Color.appBorderAdaptive, lineWidth: 1) + ) + } +} + +// MARK: - Edit Profile View +struct EditProfileView: View { + let user: User + + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + @State private var userPreferences: UserPreferences? + @State private var isLoadingPreferences = true + @State private var streamingProviders: [StreamingProvider] = [] + + // Fixed label width for alignment + private let labelWidth: CGFloat = 100 + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + private var currentRegionName: String { + guard let region = userPreferences?.watchRegion else { + return "-" + } + return regionName(for: region) + } + + private var currentRegionFlag: String? { + guard let region = userPreferences?.watchRegion else { + return nil + } + return flagEmoji(for: region) + } + + private func regionName(for code: String) -> String { + let locale = Locale(identifier: Language.current.rawValue) + return locale.localizedString(forRegionCode: code) ?? code + } + + private func flagEmoji(for code: String) -> String { + let base: UInt32 = 127397 + var emoji = "" + for scalar in code.uppercased().unicodeScalars { + if let unicode = UnicodeScalar(base + scalar.value) { + emoji.append(String(unicode)) + } + } + return emoji + } + + private var selectedProviders: [StreamingProvider] { + guard let ids = userPreferences?.watchProvidersIds else { return [] } + return streamingProviders.filter { ids.contains($0.providerId) } + } + + private var canEditStreamingServices: Bool { + userPreferences?.watchRegion != nil + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header with back button and title + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.profile) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + // Invisible placeholder for balance + Color.clear + .frame(width: 40, height: 40) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + // Border bottom + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + // Profile Picture Section + VStack(spacing: 16) { + // Avatar + ProfileAvatar( + avatarURL: user.avatarImageURL, + username: user.username, + size: 100 + ) + + // Edit Picture Button (disabled) + Button { + // Disabled for now + } label: { + Text(strings.editPicture) + .font(.subheadline.weight(.medium)) + .foregroundColor(.appMutedForegroundAdaptive) + } + .disabled(true) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 24) + + // Border bottom + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + + // Fields Section + VStack(spacing: 0) { + // Username Field + NavigationLink(destination: EditUsernameView(currentUsername: user.username)) { + EditProfileRow( + label: strings.username, + value: user.username, + labelWidth: labelWidth + ) + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 24) + + // Biography Field + NavigationLink(destination: EditBiographyView(currentBiography: user.biography)) { + EditProfileRow( + label: strings.biography, + value: user.biography?.isEmpty == false ? user.biography! : "-", + labelWidth: labelWidth + ) + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 24) + + // Region Field + NavigationLink( + destination: EditRegionView(currentRegion: userPreferences?.watchRegion) + ) { + EditProfileBadgeRow(label: strings.region) { + if let region = userPreferences?.watchRegion { + ProfileBadge( + text: currentRegionName, + prefix: currentRegionFlag + ) + } else { + Text("-") + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + } + } + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 24) + + // Streaming Services Field + if canEditStreamingServices { + NavigationLink( + destination: EditStreamingServicesView( + watchRegion: userPreferences?.watchRegion ?? "", + selectedIds: userPreferences?.watchProvidersIds ?? [] + ) + ) { + EditProfileBadgeRow(label: strings.streamingServices) { + if selectedProviders.isEmpty { + Text("-") + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + } else { + FlowLayout(spacing: 6) { + ForEach(selectedProviders) { provider in + ProfileBadge( + text: provider.providerName, + logoURL: provider.logoURL + ) + } + } + } + } + } + } else { + EditProfileBadgeRow(label: strings.streamingServices) { + Text(strings.selectRegionFirst) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + } + .opacity(0.5) + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 24) + + // Theme Field + NavigationLink(destination: EditThemeView()) { + EditProfileBadgeRow(label: strings.theme) { + ProfileBadge( + text: themeDisplayName(themeManager.current), icon: themeManager.current.icon) + } + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 24) + + // Language Field + NavigationLink(destination: EditLanguageView()) { + EditProfileBadgeRow(label: strings.language) { + ProfileBadge(text: Language.current.displayName, prefix: Language.current.flag) + } + } + } + + // Sign Out Section + VStack(spacing: 0) { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + .padding(.top, 24) + + Button { + AuthService.shared.signOut() + } label: { + HStack(spacing: 8) { + Image(systemName: "rectangle.portrait.and.arrow.right") + Text(strings.signOut) + } + .font(.subheadline.weight(.medium)) + .foregroundColor(.appDestructive) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + } + } + .padding(.horizontal, 24) + .padding(.bottom, 40) + } + } + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + .task { + await loadPreferences() + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + .onReceive(NotificationCenter.default.publisher(for: .profileUpdated)) { _ in + Task { await loadPreferences() } + } + } + + private func loadPreferences() async { + isLoadingPreferences = true + defer { isLoadingPreferences = false } + + do { + userPreferences = try await AuthService.shared.getUserPreferences() + + // Load providers if region is set + if let region = userPreferences?.watchRegion { + streamingProviders = try await TMDBService.shared.getStreamingProviders( + watchRegion: region, + language: Language.current.rawValue + ) + } + } catch { + print("Error loading preferences: \(error)") + } + } + + private func themeDisplayName(_ theme: AppTheme) -> String { + switch theme { + case .system: return strings.themeSystem + case .light: return strings.themeLight + case .dark: return strings.themeDark + } + } +} + +// MARK: - Edit Theme View +struct EditThemeView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.theme) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + Color.clear + .frame(width: 40, height: 40) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Theme Options + VStack(spacing: 0) { + ForEach(AppTheme.allCases, id: \.self) { theme in + Button { + themeManager.current = theme + } label: { + HStack { + Image(systemName: theme.icon) + .font(.system(size: 18)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 32) + + Text(themeDisplayName(theme)) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + if themeManager.current == theme { + Image(systemName: "checkmark") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + .contentShape(Rectangle()) + } + + if theme != AppTheme.allCases.last { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 24) + } + } + } + .padding(.top, 8) + + Spacer() + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } + + private func themeDisplayName(_ theme: AppTheme) -> String { + switch theme { + case .system: return strings.themeSystem + case .light: return strings.themeLight + case .dark: return strings.themeDark + } + } +} + +// MARK: - Edit Language View +struct EditLanguageView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.language) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + Color.clear + .frame(width: 40, height: 40) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Language Options + VStack(spacing: 0) { + ForEach(Language.allCases, id: \.self) { lang in + Button { + Language.current = lang + } label: { + HStack(spacing: 12) { + Text(lang.flag) + .font(.title2) + + Text(lang.displayName) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + if Language.current == lang { + Image(systemName: "checkmark") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + .contentShape(Rectangle()) + } + + if lang != Language.allCases.last { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 24) + } + } + } + .padding(.top, 8) + + Spacer() + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } +} + +// MARK: - Edit Profile Row +struct EditProfileRow: View { + let label: String + let value: String + let labelWidth: CGFloat + var prefix: String? = nil + + var body: some View { + HStack(alignment: .top, spacing: 16) { + Text(label) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .frame(width: labelWidth, alignment: .topLeading) + .multilineTextAlignment(.leading) + + HStack(spacing: 8) { + if let prefix { + Text(prefix) + .font(.title3) + } + Text(value) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + .multilineTextAlignment(.leading) + } + .frame(maxWidth: .infinity, alignment: .leading) + + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.appMutedForegroundAdaptive) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + .contentShape(Rectangle()) + } +} + +// MARK: - Edit Profile Badge Row +struct EditProfileBadgeRow: View { + let label: String + @ViewBuilder let content: Content + + var body: some View { + HStack(alignment: .top, spacing: 16) { + Text(label) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .frame(width: 100, alignment: .topLeading) + .multilineTextAlignment(.leading) + + content + .frame(maxWidth: .infinity, alignment: .leading) + + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.appMutedForegroundAdaptive) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + .contentShape(Rectangle()) + } +} + +// MARK: - Profile Badge +struct ProfileBadge: View { + let text: String + var prefix: String? = nil + var icon: String? = nil + var logoURL: URL? = nil + + var body: some View { + HStack(spacing: 6) { + if let prefix { + Text(prefix) + .font(.caption) + } + + if let icon { + Image(systemName: icon) + .font(.system(size: 12)) + .foregroundColor(.appForegroundAdaptive) + } + + if let logoURL { + CachedAsyncImage(url: logoURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + .fill(Color.appInputFilled) + } + .frame(width: 18, height: 18) + .cornerRadius(4) + } + + Text(text) + .font(.caption) + .foregroundColor(.appForegroundAdaptive) + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Color.appInputFilled) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } +} + +// MARK: - Flow Layout +struct FlowLayout: Layout { + var spacing: CGFloat = 8 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let result = FlowResult(in: proposal.width ?? 0, subviews: subviews, spacing: spacing) + return result.size + } + + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () + ) { + let result = FlowResult(in: bounds.width, subviews: subviews, spacing: spacing) + for (index, subview) in subviews.enumerated() { + subview.place( + at: CGPoint( + x: bounds.minX + result.positions[index].x, + y: bounds.minY + result.positions[index].y), + proposal: .unspecified) + } + } + + struct FlowResult { + var size: CGSize = .zero + var positions: [CGPoint] = [] + + init(in maxWidth: CGFloat, subviews: Subviews, spacing: CGFloat) { + var x: CGFloat = 0 + var y: CGFloat = 0 + var rowHeight: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + + if x + size.width > maxWidth && x > 0 { + x = 0 + y += rowHeight + spacing + rowHeight = 0 + } + + positions.append(CGPoint(x: x, y: y)) + rowHeight = max(rowHeight, size.height) + x += size.width + spacing + self.size.width = max(self.size.width, x - spacing) + } + + self.size.height = y + rowHeight + } + } +} + +// MARK: - Edit Username View +struct EditUsernameView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + @State private var username: String + @State private var isLoading = false + @State private var isCheckingAvailability = false + @State private var isAvailable: Bool? + @State private var error: String? + @State private var checkTask: Task? + + let currentUsername: String + + init(currentUsername: String) { + self.currentUsername = currentUsername + _username = State(initialValue: currentUsername) + } + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + private var hasChanges: Bool { + username != currentUsername && !username.isEmpty + } + + private var canSave: Bool { + hasChanges && isAvailable == true && !isCheckingAvailability + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header with back button, title, and Done button + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.username) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + // Check Button (Primary) + Button { + Task { await saveUsername() } + } label: { + if isLoading { + ProgressView() + .tint(.appBackgroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appForegroundAdaptive) + .clipShape(Circle()) + } else { + Image(systemName: "checkmark") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor( + canSave ? .appBackgroundAdaptive : .appMutedForegroundAdaptive + ) + .frame(width: 40, height: 40) + .background(canSave ? Color.appForegroundAdaptive : Color.clear) + .clipShape(Circle()) + } + } + .disabled(!canSave || isLoading) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + // Border bottom + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Content + VStack(alignment: .leading, spacing: 8) { + Text(strings.username) + .font(.subheadline.weight(.medium)) + .foregroundColor(.appMutedForegroundAdaptive) + + HStack(spacing: 0) { + TextField(strings.usernamePlaceholder, text: $username) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .onChange(of: username) { newValue in + checkUsernameAvailability(newValue) + } + + // Availability indicator inside field (only show loading or error) + if hasChanges { + if isCheckingAvailability { + ProgressView() + .frame(width: 20, height: 20) + } else if isAvailable == false { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 20)) + .foregroundColor(.appDestructive) + } + } + } + .padding(12) + .background(Color.appInputFilled) + .cornerRadius(12) + + if let error { + Text(error) + .font(.caption) + .foregroundColor(.appDestructive) + } else if hasChanges && isAvailable == false && !isCheckingAvailability { + Text(strings.usernameAlreadyTaken) + .font(.caption) + .foregroundColor(.appDestructive) + } + + Spacer() + } + .padding(.horizontal, 24) + .padding(.top, 24) + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } + + private func checkUsernameAvailability(_ newUsername: String) { + // Cancel previous check + checkTask?.cancel() + error = nil + isAvailable = nil + + // Don't check if same as current or empty + guard newUsername != currentUsername, !newUsername.isEmpty else { + isCheckingAvailability = false + return + } + + isCheckingAvailability = true + + // Debounce: wait 500ms before checking + checkTask = Task { + try? await Task.sleep(nanoseconds: 500_000_000) + + guard !Task.isCancelled else { return } + + do { + let available = try await AuthService.shared.isUsernameAvailable(newUsername) + await MainActor.run { + guard !Task.isCancelled else { return } + isAvailable = available + isCheckingAvailability = false + } + } catch { + await MainActor.run { + guard !Task.isCancelled else { return } + isCheckingAvailability = false + } + } + } + } + + private func saveUsername() async { + error = nil + isLoading = true + defer { isLoading = false } + + do { + _ = try await AuthService.shared.updateUser(username: username) + NotificationCenter.default.post(name: .profileUpdated, object: nil) + dismiss() + } catch AuthError.alreadyExists { + error = strings.usernameAlreadyTaken + isAvailable = false + } catch { + self.error = error.localizedDescription + } + } +} + +// MARK: - Edit Biography View +struct EditBiographyView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + @State private var biography: String + @State private var isLoading = false + @State private var error: String? + + let currentBiography: String? + + init(currentBiography: String?) { + self.currentBiography = currentBiography + _biography = State(initialValue: currentBiography ?? "") + } + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + private var hasChanges: Bool { + biography != (currentBiography ?? "") + } + + private var canSave: Bool { + hasChanges + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header with back button, title, and Done button + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.biography) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + // Save Button (Primary) + Button { + Task { await saveBiography() } + } label: { + if isLoading { + ProgressView() + .tint(.appBackgroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appForegroundAdaptive) + .clipShape(Circle()) + } else { + Image(systemName: "checkmark") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor( + canSave ? .appBackgroundAdaptive : .appMutedForegroundAdaptive + ) + .frame(width: 40, height: 40) + .background(canSave ? Color.appForegroundAdaptive : Color.clear) + .clipShape(Circle()) + } + } + .disabled(!canSave || isLoading) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + // Border bottom + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Content + VStack(alignment: .leading, spacing: 8) { + Text(strings.biography) + .font(.subheadline.weight(.medium)) + .foregroundColor(.appMutedForegroundAdaptive) + + TextEditor(text: $biography) + .frame(minHeight: 120, maxHeight: 200) + .padding(12) + .multilineTextAlignment(.leading) + .scrollContentBackground(.hidden) + .background(Color.appInputFilled) + .cornerRadius(12) + .overlay( + Group { + if biography.isEmpty { + Text(strings.biographyPlaceholder) + .font(.body) + .foregroundColor(.appMutedForegroundAdaptive) + .padding(.horizontal, 16) + .padding(.vertical, 20) + .allowsHitTesting(false) + } + }, + alignment: .topLeading + ) + + if let error { + Text(error) + .font(.caption) + .foregroundColor(.appDestructive) + } + + Spacer() + } + .padding(.horizontal, 24) + .padding(.top, 24) + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } + + private func saveBiography() async { + error = nil + isLoading = true + defer { isLoading = false } + + do { + _ = try await AuthService.shared.updateUser(biography: biography) + NotificationCenter.default.post(name: .profileUpdated, object: nil) + dismiss() + } catch { + self.error = error.localizedDescription + } + } +} + +// MARK: - Edit Region View +struct EditRegionView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + @State private var regions: [WatchRegion] = [] + @State private var filteredRegions: [WatchRegion] = [] + @State private var selectedRegion: String? + @State private var searchText = "" + @State private var isLoading = true + @State private var isSaving = false + + let currentRegion: String? + + init(currentRegion: String?) { + self.currentRegion = currentRegion + _selectedRegion = State(initialValue: currentRegion) + } + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + private var hasChanges: Bool { + selectedRegion != currentRegion && selectedRegion != nil + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.region) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + // Save Button (Primary) + Button { + Task { await saveRegion() } + } label: { + if isSaving { + ProgressView() + .tint(.appBackgroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appForegroundAdaptive) + .clipShape(Circle()) + } else { + Image(systemName: "checkmark") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor( + hasChanges ? .appBackgroundAdaptive : .appMutedForegroundAdaptive + ) + .frame(width: 40, height: 40) + .background(hasChanges ? Color.appForegroundAdaptive : Color.clear) + .clipShape(Circle()) + } + } + .disabled(!hasChanges || isSaving) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + // Search Field + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .foregroundColor(.appMutedForegroundAdaptive) + TextField(strings.searchRegion, text: $searchText) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .onChange(of: searchText) { _ in + filterRegions() + } + } + .padding(12) + .background(Color.appInputFilled) + .cornerRadius(12) + .padding(.horizontal, 24) + .padding(.bottom, 16) + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Content + if isLoading { + VStack { + Spacer() + ProgressView() + Spacer() + } + } else { + ScrollView(showsIndicators: false) { + LazyVStack(spacing: 0) { + ForEach(filteredRegions) { region in + Button { + selectedRegion = region.iso31661 + } label: { + HStack(spacing: 12) { + Text(region.flagEmoji) + .font(.title2) + + Text(region.englishName) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + if selectedRegion == region.iso31661 { + Image(systemName: "checkmark") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 14) + .background( + selectedRegion == region.iso31661 + ? Color.appInputFilled : Color.clear + ) + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 60) + } + } + } + } + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + .task { + await loadRegions() + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } + + private func loadRegions() async { + isLoading = true + defer { isLoading = false } + + do { + regions = try await TMDBService.shared.getAvailableRegions( + language: Language.current.rawValue) + filterRegions() + } catch { + print("Error loading regions: \(error)") + } + } + + private func filterRegions() { + if searchText.isEmpty { + filteredRegions = regions + } else { + filteredRegions = regions.filter { + $0.englishName.localizedCaseInsensitiveContains(searchText) + || $0.nativeName.localizedCaseInsensitiveContains(searchText) + || $0.iso31661.localizedCaseInsensitiveContains(searchText) + } + } + } + + private func saveRegion() async { + guard let selectedRegion else { return } + + isSaving = true + defer { isSaving = false } + + do { + try await AuthService.shared.updateUserPreferences(watchRegion: selectedRegion) + NotificationCenter.default.post(name: .profileUpdated, object: nil) + dismiss() + } catch { + print("Error saving region: \(error)") + } + } +} + +// MARK: - Edit Streaming Services View +struct EditStreamingServicesView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + @State private var strings = L10n.current + @State private var providers: [StreamingProvider] = [] + @State private var filteredProviders: [StreamingProvider] = [] + @State private var selectedIds: Set + @State private var searchText = "" + @State private var isLoading = true + @State private var isSaving = false + + let watchRegion: String + let initialSelectedIds: [Int] + + init(watchRegion: String, selectedIds: [Int]) { + self.watchRegion = watchRegion + self.initialSelectedIds = selectedIds + _selectedIds = State(initialValue: Set(selectedIds)) + } + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + private var hasChanges: Bool { + Set(initialSelectedIds) != selectedIds + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(strings.streamingServices) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + // Save Button (Primary) + Button { + Task { await saveServices() } + } label: { + if isSaving { + ProgressView() + .tint(.appBackgroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appForegroundAdaptive) + .clipShape(Circle()) + } else { + Image(systemName: "checkmark") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor( + hasChanges ? .appBackgroundAdaptive : .appMutedForegroundAdaptive + ) + .frame(width: 40, height: 40) + .background(hasChanges ? Color.appForegroundAdaptive : Color.clear) + .clipShape(Circle()) + } + } + .disabled(!hasChanges || isSaving) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + // Search Field + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .foregroundColor(.appMutedForegroundAdaptive) + TextField(strings.searchStreamingServices, text: $searchText) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .onChange(of: searchText) { _ in + filterProviders() + } + } + .padding(12) + .background(Color.appInputFilled) + .cornerRadius(12) + .padding(.horizontal, 24) + .padding(.bottom, 16) + + // Hint message + HStack(spacing: 8) { + Image(systemName: "info.circle") + .font(.caption) + Text(strings.streamingServicesHint) + .font(.caption) + } + .foregroundColor(.appMutedForegroundAdaptive) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + .padding(.bottom, 16) + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Content + if isLoading { + VStack { + Spacer() + ProgressView() + Spacer() + } + } else { + ScrollView(showsIndicators: false) { + LazyVStack(spacing: 0) { + ForEach(filteredProviders) { provider in + Button { + toggleProvider(provider.providerId) + } label: { + HStack(spacing: 12) { + // Provider Logo + CachedAsyncImage(url: provider.logoURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + .fill(Color.appInputFilled) + } + .frame(width: 40, height: 40) + .cornerRadius(8) + + Text(provider.providerName) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + if selectedIds.contains(provider.providerId) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 22)) + .foregroundColor(.appForegroundAdaptive) + } else { + Image(systemName: "circle") + .font(.system(size: 22)) + .foregroundColor(.appMutedForegroundAdaptive) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background( + selectedIds.contains(provider.providerId) + ? Color.appInputFilled : Color.clear + ) + } + + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.3)) + .frame(height: 1) + .padding(.leading, 76) + } + } + } + } + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + .task { + await loadProviders() + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } + + private func toggleProvider(_ id: Int) { + if selectedIds.contains(id) { + selectedIds.remove(id) + } else { + selectedIds.insert(id) + } + } + + private func loadProviders() async { + isLoading = true + defer { isLoading = false } + + do { + providers = try await TMDBService.shared.getStreamingProviders( + watchRegion: watchRegion, + language: Language.current.rawValue + ) + filterProviders() + } catch { + print("Error loading providers: \(error)") + } + } + + private func filterProviders() { + if searchText.isEmpty { + filteredProviders = providers + } else { + filteredProviders = providers.filter { + $0.providerName.localizedCaseInsensitiveContains(searchText) + } + } + } + + private func saveServices() async { + isSaving = true + defer { isSaving = false } + + do { + try await AuthService.shared.updateUserPreferences( + watchRegion: watchRegion, + watchProvidersIds: Array(selectedIds) + ) + NotificationCenter.default.post(name: .profileUpdated, object: nil) + dismiss() + } catch { + print("Error saving services: \(error)") + } + } +} + +// MARK: - Edit Field View +struct EditFieldView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var systemColorScheme + @ObservedObject private var themeManager = ThemeManager.shared + + let fieldName: String + + private var effectiveColorScheme: ColorScheme { + themeManager.current.colorScheme ?? systemColorScheme + } + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header with back button and title + VStack(spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + .frame(width: 40, height: 40) + .background(Color.appInputFilled) + .clipShape(Circle()) + } + + Spacer() + + Text(fieldName) + .font(.title3.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + + // Invisible placeholder for balance + Color.clear + .frame(width: 40, height: 40) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + // Border bottom + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(height: 1) + } + + // Content placeholder + VStack { + Spacer() + Text("Edit \(fieldName) coming soon...") + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + Spacer() + } + } + } + .navigationBarHidden(true) + .preferredColorScheme(effectiveColorScheme) + } +} + +#Preview { + ProfileTabView() +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/SearchTabView.swift b/apps/ios/Plotwist/Plotwist/Views/Home/SearchTabView.swift new file mode 100644 index 00000000..d0f84869 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Home/SearchTabView.swift @@ -0,0 +1,422 @@ +// +// SearchTabView.swift +// Plotwist +// + +import SwiftUI + +struct SearchTabView: View { + @State private var searchText = "" + @State private var results: [SearchResult] = [] + @State private var popularMovies: [SearchResult] = [] + @State private var popularTVSeries: [SearchResult] = [] + @State private var popularAnimes: [SearchResult] = [] + @State private var popularDoramas: [SearchResult] = [] + @State private var isLoading = false + @State private var isLoadingPopular = false + @State private var isInitialLoad = true + @State private var hasAppeared = false + @State private var strings = L10n.current + @State private var searchTask: Task? + @ObservedObject private var preferencesManager = UserPreferencesManager.shared + + private let cache = SearchDataCache.shared + + private var movies: [SearchResult] { + results.filter { $0.mediaType == "movie" } + } + + private var tvSeries: [SearchResult] { + results.filter { $0.mediaType == "tv" } + } + + private var people: [SearchResult] { + results.filter { $0.mediaType == "person" } + } + + private var isSearching: Bool { + !searchText.isEmpty + } + + var body: some View { + NavigationView { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Search Header + VStack(spacing: 0) { + HStack(spacing: 12) { + HStack(spacing: 12) { + Image(systemName: "magnifyingglass") + .foregroundColor(.appMutedForegroundAdaptive) + + TextField(strings.searchPlaceholder, text: $searchText) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + } + .padding(12) + .background(Color.appInputFilled) + .clipShape(RoundedRectangle(cornerRadius: 12)) + + if !searchText.isEmpty { + Button { + withAnimation(.easeInOut(duration: 0.2)) { + searchText = "" + results = [] + } + } label: { + Text(strings.cancel) + .font(.subheadline) + .foregroundColor(.appForegroundAdaptive) + } + .transition(.opacity.combined(with: .move(edge: .trailing))) + } + } + .animation(.easeInOut(duration: 0.2), value: searchText.isEmpty) + .padding(.horizontal, 24) + .padding(.vertical, 16) + + Rectangle() + .fill(Color.appBorderAdaptive) + .frame(height: 1) + } + + // Results + if isLoading || (isLoadingPopular && isInitialLoad && cache.shouldShowSkeleton) { + ScrollView { + LazyVStack(alignment: .leading, spacing: 24) { + SearchSkeletonSection() + SearchSkeletonSection() + } + .padding(.horizontal, 24) + .padding(.vertical, 24) + } + } else if isSearching { + if results.isEmpty { + Spacer() + Text(strings.noResults) + .foregroundColor(.appMutedForegroundAdaptive) + Spacer() + } else { + ScrollView { + LazyVStack(alignment: .leading, spacing: 24) { + // Preferences Badge + PreferencesBadge() + + if !movies.isEmpty { + SearchSection(title: strings.movies, results: movies) + } + + if !tvSeries.isEmpty { + SearchSection(title: strings.tvSeries, results: tvSeries) + } + + if !people.isEmpty { + SearchSection(title: strings.people, results: people) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 24) + } + } + } else { + // Show popular content with horizontal scroll sections + ScrollView(showsIndicators: false) { + VStack(spacing: 24) { + // Preferences Badge + HStack { + PreferencesBadge() + Spacer() + } + .padding(.horizontal, 24) + .padding(.top, 16) + + HomeSectionView( + title: strings.movies, + items: popularMovies, + mediaType: "movie", + categoryType: .movies, + initialMovieSubcategory: .popular + ) + + HomeSectionView( + title: strings.tvSeries, + items: popularTVSeries, + mediaType: "tv", + categoryType: .tvSeries, + initialTVSeriesSubcategory: .popular + ) + + HomeSectionView( + title: strings.animes, + items: popularAnimes, + mediaType: "tv", + categoryType: .animes + ) + + HomeSectionView( + title: strings.doramas, + items: popularDoramas, + mediaType: "tv", + categoryType: .doramas + ) + } + .padding(.bottom, 80) + } + } + } + } + .navigationBarHidden(true) + } + .onAppear { + if !hasAppeared { + hasAppeared = true + restoreFromCache() + } + } + .task { + await loadPopularContent() + } + .onChange(of: searchText) { newValue in + searchTask?.cancel() + + if !newValue.isEmpty { + isLoading = true // Show skeleton immediately when user types + } else { + isLoading = false + } + + searchTask = Task { + try? await Task.sleep(nanoseconds: 500_000_000) // 500ms debounce + guard !Task.isCancelled else { return } + await performSearch(query: newValue) + } + } + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + Task { + await loadPopularContent(forceRefresh: true) + } + } + .onReceive(NotificationCenter.default.publisher(for: .profileUpdated)) { _ in + Task { + await loadPopularContent(forceRefresh: true) + } + } + .onReceive(NotificationCenter.default.publisher(for: .searchDataCacheInvalidated)) { _ in + Task { + await loadPopularContent(forceRefresh: true) + } + } + } + + private func restoreFromCache() { + if let cachedMovies = cache.popularMovies { + popularMovies = cachedMovies + } + if let cachedTVSeries = cache.popularTVSeries { + popularTVSeries = cachedTVSeries + } + if let cachedAnimes = cache.popularAnimes { + popularAnimes = cachedAnimes + } + if let cachedDoramas = cache.popularDoramas { + popularDoramas = cachedDoramas + } + } + + private func loadPopularContent(forceRefresh: Bool = false) async { + // Check if preferences changed + let currentPreferencesHash = "\(preferencesManager.watchRegion ?? "")-\(preferencesManager.watchProvidersString)" + cache.setPreferencesHash(currentPreferencesHash) + + // Use cache if available and not forcing refresh + if !forceRefresh && cache.isDataAvailable { + restoreFromCache() + isInitialLoad = false + return + } + + isLoadingPopular = true + defer { + isLoadingPopular = false + isInitialLoad = false + } + + let language = Language.current.rawValue + let watchRegion = preferencesManager.watchRegion + let watchProviders = + preferencesManager.hasStreamingServices ? preferencesManager.watchProvidersString : nil + + do { + if preferencesManager.hasStreamingServices { + // Use discover endpoints with watch providers + async let moviesTask = TMDBService.shared.discoverMovies( + language: language, + watchRegion: watchRegion, + withWatchProviders: watchProviders + ) + async let tvTask = TMDBService.shared.discoverTV( + language: language, + watchRegion: watchRegion, + withWatchProviders: watchProviders + ) + async let animesTask = TMDBService.shared.discoverAnimes( + language: language, + watchRegion: watchRegion, + withWatchProviders: watchProviders + ) + async let doramasTask = TMDBService.shared.discoverDoramas( + language: language, + watchRegion: watchRegion, + withWatchProviders: watchProviders + ) + + let (movies, tv, animes, doramas) = try await (moviesTask, tvTask, animesTask, doramasTask) + popularMovies = movies.results + popularTVSeries = tv.results + popularAnimes = animes.results + popularDoramas = doramas.results + + // Save to cache + cache.setPopularMovies(movies.results) + cache.setPopularTVSeries(tv.results) + cache.setPopularAnimes(animes.results) + cache.setPopularDoramas(doramas.results) + } else { + // Use regular popular endpoints + async let moviesTask = TMDBService.shared.getPopularMovies(language: language) + async let tvTask = TMDBService.shared.getPopularTVSeries(language: language) + async let animesTask = TMDBService.shared.getPopularAnimes(language: language) + async let doramasTask = TMDBService.shared.getPopularDoramas(language: language) + + let (movies, tv, animes, doramas) = try await (moviesTask, tvTask, animesTask, doramasTask) + popularMovies = movies.results + popularTVSeries = tv.results + popularAnimes = animes.results + popularDoramas = doramas.results + + // Save to cache + cache.setPopularMovies(movies.results) + cache.setPopularTVSeries(tv.results) + cache.setPopularAnimes(animes.results) + cache.setPopularDoramas(doramas.results) + } + } catch { + popularMovies = [] + popularTVSeries = [] + popularAnimes = [] + popularDoramas = [] + } + } + + private func performSearch(query: String) async { + guard !query.isEmpty else { + results = [] + return + } + + isLoading = true + defer { isLoading = false } + + do { + let response = try await TMDBService.shared.searchMulti( + query: query, + language: Language.current.rawValue + ) + results = response.results + } catch { + results = [] + } + } +} + +// MARK: - Search Section +struct SearchSection: View { + let title: String + let results: [SearchResult] + + private let columns = [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + ] + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.headline) + .foregroundColor(.appForegroundAdaptive) + + LazyVGrid(columns: columns, spacing: 12) { + ForEach(results.prefix(9)) { result in + if result.mediaType != "person" { + NavigationLink { + MediaDetailView( + mediaId: result.id, + mediaType: result.mediaType ?? "movie" + ) + } label: { + PosterCard(result: result) + } + .buttonStyle(.plain) + } else { + PosterCard(result: result) + } + } + } + } + } +} + +// MARK: - Poster Card +struct PosterCard: View { + let result: SearchResult + + var body: some View { + CachedAsyncImage(url: result.imageURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + } + .aspectRatio(2 / 3, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .posterBorder(cornerRadius: 12) + .posterShadow() + } +} + +// MARK: - Skeleton Views +struct SearchSkeletonSection: View { + private let columns = [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + ] + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 80, height: 16) + + LazyVGrid(columns: columns, spacing: 12) { + ForEach(0..<6, id: \.self) { _ in + PosterSkeletonCard() + } + } + } + } +} + +struct PosterSkeletonCard: View { + var body: some View { + RoundedRectangle(cornerRadius: 12) + .fill(Color.appBorderAdaptive) + .aspectRatio(2 / 3, contentMode: .fit) + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/SoundtracksTabView.swift b/apps/ios/Plotwist/Plotwist/Views/Home/SoundtracksTabView.swift new file mode 100644 index 00000000..1ffa4642 --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Home/SoundtracksTabView.swift @@ -0,0 +1,59 @@ +// +// SoundtracksTabView.swift +// Plotwist +// + +import SwiftUI + +struct SoundtracksTabView: View { + @State private var strings = L10n.current + @ObservedObject private var themeManager = ThemeManager.shared + + var body: some View { + ZStack { + Color.appBackgroundAdaptive.ignoresSafeArea() + + VStack(spacing: 0) { + // Header + HStack { + Text(strings.soundtracks) + .font(.title2.bold()) + .foregroundColor(.appForegroundAdaptive) + + Spacer() + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + + // Content - Coming soon placeholder + Spacer() + + VStack(spacing: 16) { + Image(systemName: "music.note.list") + .font(.system(size: 56)) + .foregroundColor(.appMutedForegroundAdaptive) + + Text("Coming soon") + .font(.title3.weight(.medium)) + .foregroundColor(.appForegroundAdaptive) + + Text("Discover soundtracks from your favorite movies and series.") + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .multilineTextAlignment(.center) + .padding(.horizontal, 48) + } + + Spacer() + } + } + .preferredColorScheme(themeManager.current.colorScheme) + .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in + strings = L10n.current + } + } +} + +#Preview { + SoundtracksTabView() +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Reviews/ReviewSectionView.swift b/apps/ios/Plotwist/Plotwist/Views/Reviews/ReviewSectionView.swift new file mode 100644 index 00000000..92022f1e --- /dev/null +++ b/apps/ios/Plotwist/Plotwist/Views/Reviews/ReviewSectionView.swift @@ -0,0 +1,413 @@ +// +// ReviewSectionView.swift +// Plotwist +// + +import SwiftUI + +// MARK: - Review Section +struct ReviewSectionView: View { + let mediaId: Int + let mediaType: String + let refreshId: UUID + var onEmptyStateTapped: (() -> Void)? + var onContentLoaded: ((Bool) -> Void)? + + @State private var reviews: [ReviewListItem] = [] + @State private var isLoading = true + @State private var error: String? + @State private var currentUserId: String? + @State private var selectedReview: ReviewListItem? + @State private var showEditSheet = false + @State private var hasLoaded = false + + private var averageRating: Double { + guard !reviews.isEmpty else { return 0 } + let total = reviews.reduce(0) { $0 + $1.rating } + return total / Double(reviews.count) + } + + private var reviewsWithText: [ReviewListItem] { + reviews.filter { !$0.review.isEmpty } + } + + // Show featured rating (with film strips) only for highly rated content with many reviews + private var isFeaturedRating: Bool { + reviews.count >= 10 && averageRating >= 4.5 + } + + var body: some View { + Group { + if isLoading { + // Loading skeleton + VStack(spacing: 16) { + HStack(spacing: 6) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 16, height: 16) + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 30, height: 18) + Circle() + .fill(Color.appBorderAdaptive) + .frame(width: 4, height: 4) + RoundedRectangle(cornerRadius: 4) + .fill(Color.appBorderAdaptive) + .frame(width: 80, height: 14) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + } + } else if reviews.isEmpty { + // No reviews - show nothing + EmptyView() + } else { + // Has reviews - show content + VStack(spacing: 16) { + // Rating Header + if isFeaturedRating { + // Featured rating display with film strips (10+ reviews AND rating >= 4.5) + HStack(spacing: 8) { + // Left film strip + Image("FilmStrip") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 140) + .shadow(color: Color.black.opacity(0.1), radius: 3, x: 0, y: 3) + .shadow(color: Color.black.opacity(0.06), radius: 6, x: 0, y: 6) + + // Rating content + VStack(spacing: 4) { + // Large rating number + Text(String(format: "%.1f", averageRating)) + .font(.system(size: 56, weight: .semibold, design: .rounded)) + .foregroundColor(.appForegroundAdaptive) + + // Stars + HStack(spacing: 4) { + ForEach(1...5, id: \.self) { index in + Image(systemName: starIcon(for: index)) + .font(.system(size: 14)) + .foregroundColor(starColor(for: index)) + } + } + + // Reviews count + Text( + "\(reviews.count) \(reviews.count == 1 ? L10n.current.reviewSingular.lowercased() : L10n.current.tabReviews.lowercased())" + ) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .padding(.top, 4) + } + + // Right film strip (mirrored) + Image("FilmStrip") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 140) + .scaleEffect(x: -1, y: 1) + .shadow(color: Color.black.opacity(0.1), radius: 3, x: 0, y: 3) + .shadow(color: Color.black.opacity(0.06), radius: 6, x: 0, y: 6) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 24) + } else { + // Simple rating display (star + rating + dot + reviews count) + HStack(spacing: 6) { + Image(systemName: "star.fill") + .font(.system(size: 16)) + .foregroundColor(.appStarYellow) + + Text(String(format: "%.1f", averageRating)) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.appForegroundAdaptive) + + Circle() + .fill(Color.appMutedForegroundAdaptive.opacity(0.5)) + .frame(width: 4, height: 4) + + Text( + "\(reviews.count) \(reviews.count == 1 ? L10n.current.reviewSingular.lowercased() : L10n.current.tabReviews.lowercased())" + ) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + } + + // Horizontal scrolling reviews + if !reviewsWithText.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: 0) { + ForEach(Array(reviewsWithText.enumerated()), id: \.element.id) { index, review in + HStack(alignment: .top, spacing: 0) { + Group { + if review.userId == currentUserId { + ReviewCardView(review: review) + .contentShape(Rectangle()) + .onTapGesture { + selectedReview = review + showEditSheet = true + } + } else { + ReviewCardView(review: review) + } + } + .frame(width: min(UIScreen.main.bounds.width * 0.75, 300)) + .padding(.leading, index == 0 ? 24 : 0) + .padding(.trailing, 24) + + // Vertical divider (except for last item) + if index < reviewsWithText.count - 1 { + Rectangle() + .fill(Color.appBorderAdaptive.opacity(0.5)) + .frame(width: 1) + .frame(height: 140) + .padding(.trailing, 24) + } + } + } + } + } + } + + // See all button (show if 3+ reviews) + if reviews.count >= 3 { + Button(action: { + // TODO: Navigate to all reviews + }) { + Text(L10n.current.seeAll) + .font(.subheadline.weight(.medium)) + .foregroundColor(.appMutedForegroundAdaptive) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(Color.appInputFilled) + .cornerRadius(12) + } + .disabled(true) + .opacity(0.5) + .padding(.horizontal, 24) + } + } + } + } + .task { + await loadCurrentUser() + await loadReviews() + } + .onChange(of: refreshId) { _, _ in + Task { + await loadReviews(forceReload: true) + } + } + .sheet(isPresented: $showEditSheet) { + if let review = selectedReview { + ReviewSheet( + mediaId: mediaId, + mediaType: mediaType, + existingReview: review.toReview(), + onDeleted: { + Task { + await loadReviews(forceReload: true) + } + } + ) + } + } + .onChange(of: showEditSheet) { _, isShowing in + if !isShowing { + // Reload reviews when sheet is dismissed (in case of edit) + Task { + await loadReviews(forceReload: true) + } + } + } + } + + private func loadCurrentUser() async { + do { + let user = try await AuthService.shared.getCurrentUser() + currentUserId = user.id + } catch { + currentUserId = nil + } + } + + private func loadReviews(forceReload: Bool = false) async { + // Skip if already loaded (unless force reload) + guard !hasLoaded || forceReload else { + isLoading = false + onContentLoaded?(!reviews.isEmpty) + return + } + + isLoading = true + error = nil + + do { + let apiMediaType = mediaType == "movie" ? "MOVIE" : "TV_SHOW" + reviews = try await ReviewService.shared.getReviews( + tmdbId: mediaId, + mediaType: apiMediaType + ) + isLoading = false + hasLoaded = true + onContentLoaded?(!reviews.isEmpty) + } catch { + self.error = error.localizedDescription + isLoading = false + hasLoaded = true + onContentLoaded?(false) + } + } + + private func starIcon(for index: Int) -> String { + if Double(index) <= averageRating { + return "star.fill" + } else if Double(index) - 0.5 <= averageRating { + return "star.leadinghalf.filled" + } else { + return "star" + } + } + + private func starColor(for index: Int) -> Color { + if Double(index) <= averageRating || Double(index) - 0.5 <= averageRating { + return .appStarYellow + } else { + return .gray.opacity(0.3) + } + } +} + +// MARK: - Review Card +struct ReviewCardView: View { + let review: ReviewListItem + + private var usernameInitial: String { + review.user.username.first?.uppercased() ?? "?" + } + + private var timeAgo: String { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .abbreviated + + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + if let date = dateFormatter.date(from: review.createdAt) { + return formatter.localizedString(for: date, relativeTo: Date()) + } + return "" + } + + private var userRank: String { + let ranks = ["Cinéfilo", "Crítico", "Entusiasta", "Maratonista", "Expert"] + let index = abs(review.user.id.hashValue) % ranks.count + return ranks[index] + } + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + // Header: Avatar + Username + Rank + HStack(spacing: 12) { + // Avatar + if let avatarUrl = review.user.avatarUrl, + let url = URL(string: avatarUrl) + { + CachedAsyncImage(url: url) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + avatarFallback + } + .frame(width: 40, height: 40) + .clipShape(Circle()) + } else { + avatarFallback + } + + VStack(alignment: .leading, spacing: 2) { + Text(review.user.username) + .font(.subheadline.weight(.medium)) + .foregroundColor(.appForegroundAdaptive) + + Text(userRank) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + } + + // Stars + Time + HStack(spacing: 8) { + HStack(spacing: 2) { + ForEach(1...5, id: \.self) { index in + Image(systemName: ratingIcon(for: index)) + .font(.system(size: 14)) + .foregroundColor(ratingColor(for: index)) + } + } + + Text(timeAgo) + .font(.caption) + .foregroundColor(.appMutedForegroundAdaptive) + } + + // Review text + if !review.review.isEmpty { + Text(review.review) + .font(.subheadline) + .foregroundColor(.appMutedForegroundAdaptive) + .lineLimit(3) + .frame(maxWidth: .infinity, alignment: .leading) + .blur(radius: review.hasSpoilers ? 6 : 0) + .overlay( + review.hasSpoilers + ? Text(L10n.current.containSpoilers) + .font(.caption.weight(.medium)) + .foregroundColor(.appMutedForegroundAdaptive) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.appInputFilled) + .cornerRadius(6) + : nil + ) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var avatarFallback: some View { + Circle() + .fill(Color.appInputFilled) + .frame(width: 40, height: 40) + .overlay( + Text(usernameInitial) + .font(.subheadline.weight(.medium)) + .foregroundColor(.appForegroundAdaptive) + ) + } + + private func ratingIcon(for index: Int) -> String { + let rating = review.rating + if Double(index) <= rating { + return "star.fill" + } else if Double(index) - 0.5 <= rating { + return "star.leadinghalf.filled" + } else { + return "star" + } + } + + private func ratingColor(for index: Int) -> Color { + let rating = review.rating + if Double(index) <= rating || Double(index) - 0.5 <= rating { + return .appStarYellow + } else { + return .appMutedForegroundAdaptive.opacity(0.3) + } + } +} diff --git a/eas.json b/eas.json new file mode 100644 index 00000000..70e502c5 --- /dev/null +++ b/eas.json @@ -0,0 +1,21 @@ +{ + "cli": { + "version": ">= 16.17.4", + "appVersionSource": "remote" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal" + }, + "preview": { + "distribution": "internal" + }, + "production": { + "autoIncrement": true + } + }, + "submit": { + "production": {} + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afa35721..ea920bcf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,17 +24,17 @@ importers: specifier: 2.7.2 version: 2.7.2 - apps/backend: + apps/api: dependencies: '@aws-sdk/client-s3': specifier: ^3.962.0 - version: 3.962.0 + version: 3.966.0 '@aws-sdk/client-sqs': specifier: ^3.962.0 - version: 3.962.0 + version: 3.966.0 '@aws-sdk/lib-storage': specifier: ^3.962.0 - version: 3.962.0(@aws-sdk/client-s3@3.962.0) + version: 3.966.0(@aws-sdk/client-s3@3.966.0) '@fastify/cors': specifier: ^11.2.0 version: 11.2.0 @@ -58,7 +58,7 @@ importers: version: 0.2.5(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) '@react-email/components': specifier: ^1.0.3 - version: 1.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@swc/core': specifier: ^1.15.8 version: 1.15.8 @@ -94,10 +94,10 @@ importers: version: 4.1.0 drizzle-orm: specifier: ^0.45.1 - version: 0.45.1(postgres@3.4.7) + version: 0.45.1(postgres@3.4.8) drizzle-zod: specifier: ^0.8.3 - version: 0.8.3(drizzle-orm@0.45.1(postgres@3.4.7))(zod@4.3.5) + version: 0.8.3(drizzle-orm@0.45.1(postgres@3.4.8))(zod@4.3.5) env-paths: specifier: ^3.0.0 version: 3.0.0 @@ -115,22 +115,22 @@ importers: version: 1.0.0 ioredis: specifier: ^5.8.2 - version: 5.8.2 + version: 5.9.1 node-cron: specifier: ^4.2.1 version: 4.2.1 openai: specifier: ^6.15.0 - version: 6.15.0(ws@8.18.3)(zod@4.3.5) + version: 6.16.0(ws@8.19.0)(zod@4.3.5) pino: specifier: ^10.1.0 - version: 10.1.0 + version: 10.1.1 pino-pretty: specifier: ^13.1.3 version: 13.1.3 postgres: specifier: ^3.4.7 - version: 3.4.7 + version: 3.4.8 puppeteer: specifier: ^24.34.0 version: 24.34.0(typescript@5.9.3) @@ -142,10 +142,10 @@ importers: version: 19.2.3(react@19.2.3) resend: specifier: ^6.6.0 - version: 6.6.0(@react-email/render@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + version: 6.7.0(@react-email/render@2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) stripe: specifier: ^20.1.0 - version: 20.1.0(@types/node@25.0.3) + version: 20.1.2(@types/node@25.0.3) typeid-js: specifier: ^1.2.0 version: 1.2.0 @@ -164,7 +164,7 @@ importers: version: 6.0.0 '@types/ioredis-mock': specifier: ^8.2.6 - version: 8.2.6(ioredis@5.8.2) + version: 8.2.6(ioredis@5.9.1) '@types/node': specifier: ^25.0.3 version: 25.0.3 @@ -179,16 +179,13 @@ importers: version: 0.31.8 ioredis-mock: specifier: ^8.13.1 - version: 8.13.1(@types/ioredis-mock@8.2.6(ioredis@5.8.2))(ioredis@5.8.2) + version: 8.13.1(@types/ioredis-mock@8.2.6(ioredis@5.9.1))(ioredis@5.9.1) localstack: specifier: ^1.0.0 version: 1.0.0 react-email: specifier: ^5.1.1 - version: 5.1.1 - testcontainers: - specifier: ^11.11.0 - version: 11.11.0 + version: 5.2.1 tsup: specifier: ^8.5.1 version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) @@ -200,17 +197,96 @@ importers: version: 5.9.3 unplugin-swc: specifier: ^1.5.9 - version: 1.5.9(@swc/core@1.15.8)(rollup@4.54.0) + version: 1.5.9(@swc/core@1.15.8)(rollup@4.55.1) vite-tsconfig-paths: specifier: ^6.0.3 - version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^4.0.16 - version: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) zod: specifier: ^4.3.5 version: 4.3.5 + apps/mobile: + dependencies: + '@expo-google-fonts/space-grotesk': + specifier: ^0.2.3 + version: 0.2.3 + '@react-navigation/native': + specifier: ^7.1.8 + version: 7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo: + specifier: ~54.0.31 + version: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-dev-client: + specifier: ~6.0.20 + version: 6.0.20(expo@54.0.31) + expo-font: + specifier: ~14.0.10 + version: 14.0.10(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-linking: + specifier: ~8.0.11 + version: 8.0.11(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-router: + specifier: ~6.0.21 + version: 6.0.21(ed0173313472e3a4d9a27b966543cc3e) + expo-splash-screen: + specifier: ~31.0.13 + version: 31.0.13(expo@54.0.31) + expo-status-bar: + specifier: ~3.0.9 + version: 3.0.9(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-system-ui: + specifier: ~6.0.9 + version: 6.0.9(expo@54.0.31)(react-native-web@0.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) + nativewind: + specifier: ^4.1.23 + version: 4.2.1(react-native-reanimated@4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-native: + specifier: 0.81.5 + version: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-css-interop: + specifier: 0.1.22 + version: 0.1.22(react-native-reanimated@4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) + react-native-gesture-handler: + specifier: ~2.28.0 + version: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-reanimated: + specifier: ~4.1.1 + version: 4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-safe-area-context: + specifier: ~5.6.0 + version: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-screens: + specifier: ~4.16.0 + version: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-svg: + specifier: ^15.15.1 + version: 15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-web: + specifier: ~0.21.0 + version: 0.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-native-worklets: + specifier: 0.5.1 + version: 0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + tailwindcss: + specifier: ^3.4.17 + version: 3.4.19(tsx@4.21.0)(yaml@2.8.2) + devDependencies: + '@types/react': + specifier: ~18.3.12 + version: 18.3.27 + typescript: + specifier: ~5.9.2 + version: 5.9.3 + apps/web: dependencies: '@dnd-kit/core': @@ -227,7 +303,7 @@ importers: version: 3.2.2(react@19.2.3) '@formatjs/intl-localematcher': specifier: ^0.7.4 - version: 0.7.4 + version: 0.7.5 '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.70.0(react@19.2.3)) @@ -314,7 +390,7 @@ importers: version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@stripe/stripe-js': specifier: ^8.6.0 - version: 8.6.0 + version: 8.6.1 '@tanstack/react-query': specifier: ^5.90.16 version: 5.90.16(react@19.2.3) @@ -323,7 +399,7 @@ importers: version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-virtual': specifier: ^3.13.16 - version: 3.13.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/mdx': specifier: ^2.0.13 version: 2.0.13 @@ -353,10 +429,10 @@ importers: version: 17.2.3 framer-motion: specifier: ^12.23.26 - version: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 12.25.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) i18next: specifier: ^25.7.3 - version: 25.7.3(typescript@5.9.3) + version: 25.7.4(typescript@5.9.3) jose: specifier: ^6.1.3 version: 6.1.3 @@ -371,25 +447,25 @@ importers: version: 1.0.0 next: specifier: ^16.1.1 - version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: ^4.24.13 - version: 4.24.13(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 4.24.13(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-view-transitions: specifier: ^0.3.5 - version: 0.3.5(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.3.5(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nextjs-toploader: specifier: ^3.9.17 - version: 3.9.17(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 3.9.17(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nprogress: specifier: ^0.2.0 version: 0.2.0 nuqs: specifier: ^2.8.6 - version: 2.8.6(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 2.8.6(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) react: specifier: ^19.2.3 version: 19.2.3 @@ -416,7 +492,7 @@ importers: version: 7.70.0(react@19.2.3) react-i18next: specifier: ^16.5.1 - version: 16.5.1(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + version: 16.5.1(i18next@25.7.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) react-intersection-observer: specifier: ^10.0.0 version: 10.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -443,7 +519,7 @@ importers: version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) stripe: specifier: ^20.1.0 - version: 20.1.0(@types/node@25.0.3) + version: 20.1.2(@types/node@25.0.3) tailwind-variants: specifier: ^3.2.2 version: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18) @@ -492,7 +568,7 @@ importers: version: 3.0.6 '@vitejs/plugin-react': specifier: ^5.1.2 - version: 5.1.2(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.2(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/coverage-istanbul': specifier: ^4.0.16 version: 4.0.16(vitest@4.0.16) @@ -519,7 +595,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.16 - version: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript-config: {} @@ -641,7 +717,7 @@ importers: version: 8.6.0(react@19.2.3) framer-motion: specifier: ^12.23.26 - version: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 12.25.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -665,7 +741,7 @@ importers: version: 7.70.0(react@19.2.3) react-resizable-panels: specifier: ^4.2.1 - version: 4.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 4.3.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) recharts: specifier: ^3.6.0 version: 3.6.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@19.2.3)(react@19.2.3)(redux@5.0.1) @@ -708,7 +784,7 @@ importers: version: 8.5.6 postcss-load-config: specifier: ^6.0.1 - version: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + version: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) tailwindcss: specifier: ^3.4.17 version: 3.4.19(tsx@4.21.0)(yaml@2.8.2) @@ -718,6 +794,14 @@ importers: 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 + '@acemir/cssom@0.9.30': resolution: {integrity: sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg==} @@ -776,145 +860,145 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-s3@3.962.0': - resolution: {integrity: sha512-I2/1McBZCcM3PfM4ck8D6gnZR3K7+yl1fGkwTq/3ThEn9tdLjNwcdgTbPfxfX6LoecLrH9Ekoo+D9nmQ0T261w==} + '@aws-sdk/client-s3@3.966.0': + resolution: {integrity: sha512-IckVv+A6irQyXTiJrNpfi63ZtPuk6/Iu70TnMq2DTRFK/4bD2bOvqL1IHZ2WGmZMoeWd5LI8Fn6pIwdK6g4QJQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-sqs@3.962.0': - resolution: {integrity: sha512-egPwNtyL5Sz3bZOKp4Uk56JBSUcU/BnrWGDMhCcHLWBLavnGstBNe2nPetX/RP1zHO2XRnApuVjusqwcsKrfVA==} + '@aws-sdk/client-sqs@3.966.0': + resolution: {integrity: sha512-qiZphYzjauELP7Dgnz9Ywnk98nzFZuIFY8izEXksd+jRyrUSkoLKmaKnUTOExX/o+XYL64wGPSYwihf46WHu7g==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-sso@3.958.0': - resolution: {integrity: sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg==} + '@aws-sdk/client-sso@3.966.0': + resolution: {integrity: sha512-hQZDQgqRJclALDo9wK+bb5O+VpO8JcjImp52w9KPSz9XveNRgE9AYfklRJd8qT2Bwhxe6IbnqYEino2wqUMA1w==} engines: {node: '>=18.0.0'} - '@aws-sdk/core@3.957.0': - resolution: {integrity: sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==} + '@aws-sdk/core@3.966.0': + resolution: {integrity: sha512-QaRVBHD1prdrFXIeFAY/1w4b4S0EFyo/ytzU+rCklEjMRT7DKGXGoHXTWLGz+HD7ovlS5u+9cf8a/LeSOEMzww==} engines: {node: '>=18.0.0'} - '@aws-sdk/crc64-nvme@3.957.0': - resolution: {integrity: sha512-qSwSfI+qBU9HDsd6/4fM9faCxYJx2yDuHtj+NVOQ6XYDWQzFab/hUdwuKZ77Pi6goLF1pBZhJ2azaC2w7LbnTA==} + '@aws-sdk/crc64-nvme@3.965.0': + resolution: {integrity: sha512-9FbIyJ/Zz1AdEIrb0+Pn7wRi+F/0Y566ooepg0hDyHUzRV3ZXKjOlu3wJH3YwTz2UkdwQmldfUos2yDJps7RyA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-env@3.957.0': - resolution: {integrity: sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog==} + '@aws-sdk/credential-provider-env@3.966.0': + resolution: {integrity: sha512-sxVKc9PY0SH7jgN/8WxhbKQ7MWDIgaJv1AoAKJkhJ+GM5r09G5Vb2Vl8ALYpsy+r8b+iYpq5dGJj8k2VqxoQMg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-http@3.957.0': - resolution: {integrity: sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw==} + '@aws-sdk/credential-provider-http@3.966.0': + resolution: {integrity: sha512-VTJDP1jOibVtc5pn5TNE12rhqOO/n10IjkoJi8fFp9BMfmh3iqo70Ppvphz/Pe/R9LcK5Z3h0Z4EB9IXDR6kag==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-ini@3.962.0': - resolution: {integrity: sha512-h0kVnXLW2d3nxbcrR/Pfg3W/+YoCguasWz7/3nYzVqmdKarGrpJzaFdoZtLgvDSZ8VgWUC4lWOTcsDMV0UNqUQ==} + '@aws-sdk/credential-provider-ini@3.966.0': + resolution: {integrity: sha512-4oQKkYMCUx0mffKuH8LQag1M4Fo5daKVmsLAnjrIqKh91xmCrcWlAFNMgeEYvI1Yy125XeNSaFMfir6oNc2ODA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-login@3.962.0': - resolution: {integrity: sha512-kHYH6Av2UifG3mPkpPUNRh/PuX6adaAcpmsclJdHdxlixMCRdh8GNeEihq480DC0GmfqdpoSf1w2CLmLLPIS6w==} + '@aws-sdk/credential-provider-login@3.966.0': + resolution: {integrity: sha512-wD1KlqLyh23Xfns/ZAPxebwXixoJJCuDbeJHFrLDpP4D4h3vA2S8nSFgBSFR15q9FhgRfHleClycf6g5K4Ww6w==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-node@3.962.0': - resolution: {integrity: sha512-CS78NsWRxLa+nWqeWBEYMZTLacMFIXs1C5WJuM9kD05LLiWL32ksljoPsvNN24Bc7rCSQIIMx/U3KGvkDVZMVg==} + '@aws-sdk/credential-provider-node@3.966.0': + resolution: {integrity: sha512-7QCOERGddMw7QbjE+LSAFgwOBpPv4px2ty0GCK7ZiPJGsni2EYmM4TtYnQb9u1WNHmHqIPWMbZR0pKDbyRyHlQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-process@3.957.0': - resolution: {integrity: sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==} + '@aws-sdk/credential-provider-process@3.966.0': + resolution: {integrity: sha512-q5kCo+xHXisNbbPAh/DiCd+LZX4wdby77t7GLk0b2U0/mrel4lgy6o79CApe+0emakpOS1nPZS7voXA7vGPz4w==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-sso@3.958.0': - resolution: {integrity: sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg==} + '@aws-sdk/credential-provider-sso@3.966.0': + resolution: {integrity: sha512-Rv5aEfbpqsQZzxpX2x+FbSyVFOE3Dngome+exNA8jGzc00rrMZEUnm3J3yAsLp/I2l7wnTfI0r2zMe+T9/nZAQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-web-identity@3.958.0': - resolution: {integrity: sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA==} + '@aws-sdk/credential-provider-web-identity@3.966.0': + resolution: {integrity: sha512-Yv1lc9iic9xg3ywMmIAeXN1YwuvfcClLVdiF2y71LqUgIOupW8B8my84XJr6pmOQuKzZa++c2znNhC9lGsbKyw==} engines: {node: '>=18.0.0'} - '@aws-sdk/lib-storage@3.962.0': - resolution: {integrity: sha512-Ai5gWRQkzsUMQ6NPoZZoiLXoQ6/yPRcR4oracIVjyWcu48TfBpsRgbqY/5zNOM55ag1wPX9TtJJGOhK3TNk45g==} + '@aws-sdk/lib-storage@3.966.0': + resolution: {integrity: sha512-hI+tsvfbIIyA/4Z3uIQYpmsZCe2Nd/FbJEhUhT4AubxABQcJt3LZ9e2vo9ukJAqVh+p1gBgnSpriVxWgknRSDA==} engines: {node: '>=18.0.0'} peerDependencies: - '@aws-sdk/client-s3': ^3.962.0 + '@aws-sdk/client-s3': ^3.966.0 - '@aws-sdk/middleware-bucket-endpoint@3.957.0': - resolution: {integrity: sha512-iczcn/QRIBSpvsdAS/rbzmoBpleX1JBjXvCynMbDceVLBIcVrwT1hXECrhtIC2cjh4HaLo9ClAbiOiWuqt+6MA==} + '@aws-sdk/middleware-bucket-endpoint@3.966.0': + resolution: {integrity: sha512-KMPZ7gtFXErd9pMpXJMBwFlxxlGIaIQrUBfj3ea7rlrNtoVHnSI4qsoldLq5l9/Ho64KoCiICH4+qXjze8JTDQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-expect-continue@3.957.0': - resolution: {integrity: sha512-AlbK3OeVNwZZil0wlClgeI/ISlOt/SPUxBsIns876IFaVu/Pj3DgImnYhpcJuFRek4r4XM51xzIaGQXM6GDHGg==} + '@aws-sdk/middleware-expect-continue@3.965.0': + resolution: {integrity: sha512-UBxVytsmhEmFwkBnt+aV0eAJ7uc+ouNokCqMBrQ7Oc5A77qhlcHfOgXIKz2SxqsiYTsDq+a0lWFM/XpyRWraqA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.957.0': - resolution: {integrity: sha512-iJpeVR5V8se1hl2pt+k8bF/e9JO4KWgPCMjg8BtRspNtKIUGy7j6msYvbDixaKZaF2Veg9+HoYcOhwnZumjXSA==} + '@aws-sdk/middleware-flexible-checksums@3.966.0': + resolution: {integrity: sha512-0/ofXeceTH/flKhg4EGGYr4cDtaLVkR/2RI05J/hxrHIls+iM6j8++GO0TocxmZYK+8B+7XKSaV9LU26nboTUQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-host-header@3.957.0': - resolution: {integrity: sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA==} + '@aws-sdk/middleware-host-header@3.965.0': + resolution: {integrity: sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-location-constraint@3.957.0': - resolution: {integrity: sha512-y8/W7TOQpmDJg/fPYlqAhwA4+I15LrS7TwgUEoxogtkD8gfur9wFMRLT8LCyc9o4NMEcAnK50hSb4+wB0qv6tQ==} + '@aws-sdk/middleware-location-constraint@3.965.0': + resolution: {integrity: sha512-07T1rwAarQs33mVg5U28AsSdLB5JUXu9yBTBmspFGajKVsEahIyntf53j9mAXF1N2KR0bNdP0J4A0kst4t43UQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-logger@3.957.0': - resolution: {integrity: sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ==} + '@aws-sdk/middleware-logger@3.965.0': + resolution: {integrity: sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-recursion-detection@3.957.0': - resolution: {integrity: sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA==} + '@aws-sdk/middleware-recursion-detection@3.965.0': + resolution: {integrity: sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-sdk-s3@3.957.0': - resolution: {integrity: sha512-5B2qY2nR2LYpxoQP0xUum5A1UNvH2JQpLHDH1nWFNF/XetV7ipFHksMxPNhtJJ6ARaWhQIDXfOUj0jcnkJxXUg==} + '@aws-sdk/middleware-sdk-s3@3.966.0': + resolution: {integrity: sha512-9N9zncsY5ydDCRatKdrPZcdCwNWt7TdHmqgwQM52PuA5gs1HXWwLLNDy/51H+9RTHi7v6oly+x9utJ/qypCh2g==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-sdk-sqs@3.957.0': - resolution: {integrity: sha512-3A1V2oSV/NzWukwDBwnf/ng+n+8zU32jRml0lbYiP9PzBgc6D6Y4Z/RCbPp7g+PO8XrCRrZg6QKspO3cLpGnOw==} + '@aws-sdk/middleware-sdk-sqs@3.966.0': + resolution: {integrity: sha512-P0Lr5XPyaIWQidf23PLTE8J8JC+Jp0EaizmCa671szUh5v+HGlReQXF9d7vE7zH3m4cKWjOgLXSH/MAI88eNFA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-ssec@3.957.0': - resolution: {integrity: sha512-qwkmrK0lizdjNt5qxl4tHYfASh8DFpHXM1iDVo+qHe+zuslfMqQEGRkzxS8tJq/I+8F0c6v3IKOveKJAfIvfqQ==} + '@aws-sdk/middleware-ssec@3.965.0': + resolution: {integrity: sha512-dke++CTw26y+a2D1DdVuZ4+2TkgItdx6TeuE0zOl4lsqXGvTBUG4eaIZalt7ZOAW5ys2pbDOk1bPuh4opoD3pQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-user-agent@3.957.0': - resolution: {integrity: sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ==} + '@aws-sdk/middleware-user-agent@3.966.0': + resolution: {integrity: sha512-MvGoy0vhMluVpSB5GaGJbYLqwbZfZjwEZhneDHdPhgCgQqmCtugnYIIjpUw7kKqWGsmaMQmNEgSFf1zYYmwOyg==} engines: {node: '>=18.0.0'} - '@aws-sdk/nested-clients@3.958.0': - resolution: {integrity: sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==} + '@aws-sdk/nested-clients@3.966.0': + resolution: {integrity: sha512-FRzAWwLNoKiaEWbYhnpnfartIdOgiaBLnPcd3uG1Io+vvxQUeRPhQIy4EfKnT3AuA+g7gzSCjMG2JKoJOplDtQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/region-config-resolver@3.957.0': - resolution: {integrity: sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==} + '@aws-sdk/region-config-resolver@3.965.0': + resolution: {integrity: sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg==} engines: {node: '>=18.0.0'} - '@aws-sdk/signature-v4-multi-region@3.957.0': - resolution: {integrity: sha512-t6UfP1xMUigMMzHcb7vaZcjv7dA2DQkk9C/OAP1dKyrE0vb4lFGDaTApi17GN6Km9zFxJthEMUbBc7DL0hq1Bg==} + '@aws-sdk/signature-v4-multi-region@3.966.0': + resolution: {integrity: sha512-VNSpyfKtDiBg/nPwSXDvnjISaDE9mI8zhOK3C4/obqh8lK1V6j04xDlwyIWbbIM0f6VgV1FVixlghtJB79eBqA==} engines: {node: '>=18.0.0'} - '@aws-sdk/token-providers@3.958.0': - resolution: {integrity: sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q==} + '@aws-sdk/token-providers@3.966.0': + resolution: {integrity: sha512-8k5cBTicTGYJHhKaweO4gL4fud1KDnLS5fByT6/Xbiu59AxYM4E/h3ds+3jxDMnniCE3gIWpEnyfM9khtmw2lA==} engines: {node: '>=18.0.0'} - '@aws-sdk/types@3.957.0': - resolution: {integrity: sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==} + '@aws-sdk/types@3.965.0': + resolution: {integrity: sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-arn-parser@3.957.0': - resolution: {integrity: sha512-Aj6m+AyrhWyg8YQ4LDPg2/gIfGHCEcoQdBt5DeSFogN5k9mmJPOJ+IAmNSWmWRjpOxEy6eY813RNDI6qS97M0g==} + '@aws-sdk/util-arn-parser@3.966.0': + resolution: {integrity: sha512-WcCLdKBK2nHhtOPE8du5XjOXaOToxGF3Ge8rgK2jaRpjkzjS0/mO+Jp2H4+25hOne3sP2twBu5BrvD9KoXQ5LQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-endpoints@3.957.0': - resolution: {integrity: sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw==} + '@aws-sdk/util-endpoints@3.965.0': + resolution: {integrity: sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-locate-window@3.957.0': - resolution: {integrity: sha512-nhmgKHnNV9K+i9daumaIz8JTLsIIML9PE/HUks5liyrjUzenjW/aHoc7WJ9/Td/gPZtayxFnXQSJRb/fDlBuJw==} + '@aws-sdk/util-locate-window@3.965.0': + resolution: {integrity: sha512-9LJFand4bIoOjOF4x3wx0UZYiFZRo4oUauxQSiEX2dVg+5qeBOJSjp2SeWykIE6+6frCZ5wvWm2fGLK8D32aJw==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-user-agent-browser@3.957.0': - resolution: {integrity: sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw==} + '@aws-sdk/util-user-agent-browser@3.965.0': + resolution: {integrity: sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ==} - '@aws-sdk/util-user-agent-node@3.957.0': - resolution: {integrity: sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q==} + '@aws-sdk/util-user-agent-node@3.966.0': + resolution: {integrity: sha512-vPPe8V0GLj+jVS5EqFz2NUBgWH35favqxliUOvhp8xBdNRkEjiZm5TqitVtFlxS4RrLY3HOndrWbrP5ejbwl1Q==} engines: {node: '>=18.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -922,14 +1006,17 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.957.0': - resolution: {integrity: sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==} + '@aws-sdk/xml-builder@3.965.0': + resolution: {integrity: sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g==} engines: {node: '>=18.0.0'} - '@aws/lambda-invoke-store@0.2.2': - resolution: {integrity: sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==} + '@aws/lambda-invoke-store@0.2.3': + resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} engines: {node: '>=18.0.0'} + '@babel/code-frame@7.10.4': + resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1033,6 +1120,10 @@ packages: resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} + '@babel/highlight@7.25.9': + resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.28.5': resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} @@ -1068,12 +1159,68 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/plugin-proposal-decorators@7.28.0': + resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} + 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-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} 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.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + 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.27.1': + resolution: {integrity: sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-flow@7.27.1': + resolution: {integrity: sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-assertions@7.27.1': resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} engines: {node: '>=6.9.0'} @@ -1086,12 +1233,64 @@ packages: 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.27.1': resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} 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.27.1': resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} engines: {node: '>=6.9.0'} @@ -1206,6 +1405,12 @@ packages: 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'} @@ -1398,6 +1603,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-runtime@7.28.5': + resolution: {integrity: sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==} + 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'} @@ -1497,9 +1708,6 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@balena/dockerignore@1.0.2': - resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} - '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -1586,8 +1794,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-syntax-patches-for-csstree@1.0.22': - resolution: {integrity: sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==} + '@csstools/css-syntax-patches-for-csstree@1.0.23': + resolution: {integrity: sha512-YEmgyklR6l/oKUltidNVYdjSmLSW88vMsKx0pmiS3r71s8ZZRpd8A0Yf0U+6p/RzElmMnPBv27hNWjDQMSZRtQ==} engines: {node: '>=18'} '@csstools/css-tokenizer@3.0.4': @@ -1632,6 +1840,10 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@egjs/hammerjs@2.0.17': + resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} + engines: {node: '>=0.8.0'} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} @@ -2099,6 +2311,125 @@ packages: '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} + '@expo-google-fonts/space-grotesk@0.2.3': + resolution: {integrity: sha512-UYEMIrzegR02pauH7gVMI7j6cUroTtJug6dH/aQFjMNz0UwZe6GUcrEtJDmsUHJjEZdxbYgHhaiIwswWVo0CMA==} + + '@expo/cli@54.0.21': + resolution: {integrity: sha512-L/FdpyZDsg/Nq6xW6kfiyF9DUzKfLZCKFXEVZcDqCNar6bXxQVotQyvgexRvtUF5nLinuT/UafLOdC3FUALUmA==} + hasBin: true + peerDependencies: + expo: '*' + expo-router: '*' + react-native: '*' + peerDependenciesMeta: + expo-router: + optional: true + react-native: + optional: true + + '@expo/code-signing-certificates@0.0.6': + resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==} + + '@expo/config-plugins@54.0.4': + resolution: {integrity: sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==} + + '@expo/config-types@54.0.10': + resolution: {integrity: sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==} + + '@expo/config@12.0.13': + resolution: {integrity: sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==} + + '@expo/devcert@1.2.1': + resolution: {integrity: sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==} + + '@expo/devtools@0.1.8': + resolution: {integrity: sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ==} + peerDependencies: + react: '*' + react-native: '*' + peerDependenciesMeta: + react: + optional: true + react-native: + optional: true + + '@expo/env@2.0.8': + resolution: {integrity: sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA==} + + '@expo/fingerprint@0.15.4': + resolution: {integrity: sha512-eYlxcrGdR2/j2M6pEDXo9zU9KXXF1vhP+V+Tl+lyY+bU8lnzrN6c637mz6Ye3em2ANy8hhUR03Raf8VsT9Ogng==} + hasBin: true + + '@expo/image-utils@0.8.8': + resolution: {integrity: sha512-HHHaG4J4nKjTtVa1GG9PCh763xlETScfEyNxxOvfTRr8IKPJckjTyqSLEtdJoFNJ1vqiABEjW7tqGhqGibZLeA==} + + '@expo/json-file@10.0.8': + resolution: {integrity: sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ==} + + '@expo/metro-config@54.0.13': + resolution: {integrity: sha512-RRufMCgLR2Za1WGsh02OatIJo5qZFt31yCnIOSfoubNc3Qqe92Z41pVsbrFnmw5CIaisv1NgdBy05DHe7pEyuw==} + peerDependencies: + expo: '*' + peerDependenciesMeta: + expo: + optional: true + + '@expo/metro-runtime@6.1.2': + resolution: {integrity: sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==} + peerDependencies: + expo: '*' + react: '*' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + + '@expo/metro@54.2.0': + resolution: {integrity: sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==} + + '@expo/osascript@2.3.8': + resolution: {integrity: sha512-/TuOZvSG7Nn0I8c+FcEaoHeBO07yu6vwDgk7rZVvAXoeAK5rkA09jRyjYsZo+0tMEFaToBeywA6pj50Mb3ny9w==} + engines: {node: '>=12'} + + '@expo/package-manager@1.9.9': + resolution: {integrity: sha512-Nv5THOwXzPprMJwbnXU01iXSrCp3vJqly9M4EJ2GkKko9Ifer2ucpg7x6OUsE09/lw+npaoUnHMXwkw7gcKxlg==} + + '@expo/plist@0.4.8': + resolution: {integrity: sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==} + + '@expo/prebuild-config@54.0.8': + resolution: {integrity: sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg==} + peerDependencies: + expo: '*' + + '@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/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@15.0.3': + resolution: {integrity: sha512-SBUyYKphmlfUBqxSfDdJ3jAdEVSALS2VUPOUyqn48oZmb2TL/O7t7/PQm5v4NQujYEPLPMTLn9KVw6H7twwbTA==} + peerDependencies: + expo-font: '>=14.0.4' + react: '*' + react-native: '*' + + '@expo/ws-tunnel@1.0.6': + resolution: {integrity: sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==} + + '@expo/xcpretty@4.3.2': + resolution: {integrity: sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==} + hasBin: true + '@faker-js/faker@10.2.0': resolution: {integrity: sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} @@ -2169,28 +2500,14 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@formatjs/fast-memoize@3.0.2': - resolution: {integrity: sha512-YFApUDWFmjpPwAE7VcY7PYVjm6JaLZOAo0UfCQj1/OGi/1QtduG9kIBHmVC551M6AI01qvuP5kjbDebrZOT4Vg==} - - '@formatjs/intl-localematcher@0.7.4': - resolution: {integrity: sha512-AWsSZupIBMU/y04Nj24CjohyNVyfItMJPxSzX5OJwedDEIbGLOHkPxCjAeLeiLF2dw4xmQA8psktdi9MaebBQw==} + '@formatjs/fast-memoize@3.0.3': + resolution: {integrity: sha512-CArYtQKGLAOruCMeq5/RxCg6vUXFx3OuKBdTm30Wn/+gCefehmZ8Y2xSMxMrO2iel7hRyE3HKfV56t3vAU6D4Q==} - '@gerrit0/mini-shiki@3.20.0': - resolution: {integrity: sha512-Wa57i+bMpK6PGJZ1f2myxo3iO+K/kZikcyvH8NIqNNZhQUbDav7V9LQmWOXhf946mz5c1NZ19WMsGYiDKTryzQ==} - - '@grpc/grpc-js@1.14.3': - resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} - engines: {node: '>=12.10.0'} - - '@grpc/proto-loader@0.7.15': - resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} - engines: {node: '>=6'} - hasBin: true + '@formatjs/intl-localematcher@0.7.5': + resolution: {integrity: sha512-7/nd90cn5CT7SVF71/ybUKAcnvBlr9nZlJJp8O8xIZHXFgYOC4SXExZlSdgHv2l6utjw1byidL06QzChvQMHwA==} - '@grpc/proto-loader@0.8.0': - resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} - engines: {node: '>=6'} - hasBin: true + '@gerrit0/mini-shiki@3.21.0': + resolution: {integrity: sha512-9PrsT5DjZA+w3lur/aOIx3FlDeHdyCEFlv9U+fmsVyjPZh61G5SYURQ/1ebe2U63KbDmI2V8IhIUegWb8hjOyg==} '@hookform/resolvers@5.2.2': resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} @@ -2380,9 +2697,6 @@ packages: '@ioredis/as-callback@3.0.0': resolution: {integrity: sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg==} - '@ioredis/commands@1.4.0': - resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} - '@ioredis/commands@1.5.0': resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} @@ -2398,10 +2712,46 @@ packages: 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.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} 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==} @@ -2412,15 +2762,15 @@ packages: 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==} - '@js-sdsl/ordered-map@4.4.2': - resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@jsep-plugin/assignment@1.3.0': resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} engines: {node: '>= 10.16.0'} @@ -2592,46 +2942,12 @@ packages: '@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'} - '@plotwist_app/tmdb@0.2.5': resolution: {integrity: sha512-wIS1l4VbJMUb+e7UEme+85eBU7evcFNhTr4iLvlxlH1baUJmcXF3fOuUr7g2gFzCZ/IabvgqmD7GABCGPlIBGw==} '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.4': - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} - - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.0': - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.0': - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@puppeteer/browsers@2.11.0': resolution: {integrity: sha512-n6oQX6mYkG8TRPuPXmbPidkUbsSRalhmaaVAQxvH1IkQy63cwsH+kOjB3e4cpCDHg0aSvsiX9bQ4s2VB6mGWUQ==} engines: {node: '>=18'} @@ -3105,6 +3421,15 @@ packages: '@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: @@ -3328,8 +3653,8 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc - '@react-email/components@1.0.3': - resolution: {integrity: sha512-RbleOT35XSCWM54Rs76/BgfPA0Son55OH4awBYlkHZgLw0AdbPwobhE7izNDFqY4nHW7+omLfe3CByWbsg/hEw==} + '@react-email/components@1.0.4': + resolution: {integrity: sha512-XpSs/mN0APMD9E3TYZnj8N6kRXqb6WBl9WrE+IHyB4PdgLNqXe7uZ5+5oZkKSE8Tskzw/K2vDJUqSZ2v+sRjUA==} engines: {node: '>=20.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc @@ -3394,8 +3719,8 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc - '@react-email/render@2.0.1': - resolution: {integrity: sha512-eYNL4+SSrV1+58MIcT4znarX4YTMuYBr1uzhI6U8fBFvRMZPryxNOnD7jnZ/Ser3MtJEquQNbXjrAP+RVkfLbg==} + '@react-email/render@2.0.2': + resolution: {integrity: sha512-AGuNo86TP9Y2JBUwFcT+z0frPDML4WLIFlnCi7laCPYJA+43kdim0y+qRNPxRxZkJiUz1JMPnE2M5HaNYhWwIg==} engines: {node: '>=20.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc @@ -3457,6 +3782,115 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-native/assets-registry@0.81.5': + resolution: {integrity: sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==} + engines: {node: '>= 20.19.4'} + + '@react-native/babel-plugin-codegen@0.81.5': + resolution: {integrity: sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ==} + engines: {node: '>= 20.19.4'} + + '@react-native/babel-preset@0.81.5': + resolution: {integrity: sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA==} + engines: {node: '>= 20.19.4'} + peerDependencies: + '@babel/core': '*' + + '@react-native/codegen@0.81.5': + resolution: {integrity: sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g==} + engines: {node: '>= 20.19.4'} + peerDependencies: + '@babel/core': '*' + + '@react-native/community-cli-plugin@0.81.5': + resolution: {integrity: sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw==} + engines: {node: '>= 20.19.4'} + peerDependencies: + '@react-native-community/cli': '*' + '@react-native/metro-config': '*' + peerDependenciesMeta: + '@react-native-community/cli': + optional: true + '@react-native/metro-config': + optional: true + + '@react-native/debugger-frontend@0.81.5': + resolution: {integrity: sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w==} + engines: {node: '>= 20.19.4'} + + '@react-native/dev-middleware@0.81.5': + resolution: {integrity: sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA==} + engines: {node: '>= 20.19.4'} + + '@react-native/gradle-plugin@0.81.5': + resolution: {integrity: sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg==} + engines: {node: '>= 20.19.4'} + + '@react-native/js-polyfills@0.81.5': + resolution: {integrity: sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==} + engines: {node: '>= 20.19.4'} + + '@react-native/normalize-colors@0.74.89': + resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==} + + '@react-native/normalize-colors@0.81.5': + resolution: {integrity: sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==} + + '@react-native/virtualized-lists@0.81.5': + resolution: {integrity: sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw==} + engines: {node: '>= 20.19.4'} + peerDependencies: + '@types/react': ^19.1.0 + react: '*' + react-native: '*' + peerDependenciesMeta: + '@types/react': + optional: true + + '@react-navigation/bottom-tabs@7.9.0': + resolution: {integrity: sha512-024FWdHp3ZsE5rP8tmGI4vh+1z3wg8u8E9Frep8eeGoYo1h9rQhvgofQDGxknmrKsb7t8o8Dim+IZSvl57cPFQ==} + peerDependencies: + '@react-navigation/native': ^7.1.26 + react: '>= 18.2.0' + react-native: '*' + react-native-safe-area-context: '>= 4.0.0' + react-native-screens: '>= 4.0.0' + + '@react-navigation/core@7.13.7': + resolution: {integrity: sha512-k2ABo3250vq1ovOh/iVwXS6Hwr5PVRGXoPh/ewVFOOuEKTvOx9i//OBzt8EF+HokBxS2HBRlR2b+aCOmscRqBw==} + peerDependencies: + react: '>= 18.2.0' + + '@react-navigation/elements@2.9.3': + resolution: {integrity: sha512-3+eyvWiVPIEf6tN9UdduhOEHcTuNe3R5WovgiVkfH9+jApHMTZDc2loePTpY/i2HDJhObhhChpJzO6BVjrpdYQ==} + peerDependencies: + '@react-native-masked-view/masked-view': '>= 0.2.0' + '@react-navigation/native': ^7.1.26 + 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.9.0': + resolution: {integrity: sha512-C/mNPhI0Pnerl7C2cB+6fAkdgSmfKECMERrbyfjx3P6JmEuTC54o+GV1c62FUmlRaRUassVHbtw4EeaY2uLh0g==} + peerDependencies: + '@react-navigation/native': ^7.1.26 + react: '>= 18.2.0' + react-native: '*' + react-native-safe-area-context: '>= 4.0.0' + react-native-screens: '>= 4.0.0' + + '@react-navigation/native@7.1.26': + resolution: {integrity: sha512-RhKmeD0E2ejzKS6z8elAfdfwShpcdkYY8zJzvHYLq+wv183BBcElTeyMLcIX6wIn7QutXeI92Yi21t7aUWfqNQ==} + peerDependencies: + react: '>= 18.2.0' + react-native: '*' + + '@react-navigation/routers@7.5.3': + resolution: {integrity: sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==} + '@reduxjs/toolkit@2.11.2': resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: @@ -3480,134 +3914,158 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.54.0': - resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.54.0': - resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.54.0': - resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.54.0': - resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.54.0': - resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.54.0': - resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.54.0': - resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.54.0': - resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.54.0': - resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.54.0': - resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.54.0': - resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.54.0': - resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.54.0': - resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.54.0': - resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.54.0': - resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.54.0': - resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.54.0': - resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.54.0': - resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.54.0': - resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.54.0': - resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.54.0': - resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.54.0': - resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} cpu: [x64] os: [win32] '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - '@shikijs/engine-oniguruma@3.20.0': - resolution: {integrity: sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==} + '@shikijs/engine-oniguruma@3.21.0': + resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} - '@shikijs/langs@3.20.0': - resolution: {integrity: sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==} + '@shikijs/langs@3.21.0': + resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==} - '@shikijs/themes@3.20.0': - resolution: {integrity: sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==} + '@shikijs/themes@3.21.0': + resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==} - '@shikijs/types@3.20.0': - resolution: {integrity: sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==} + '@shikijs/types@3.21.0': + resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@smithy/abort-controller@4.2.7': resolution: {integrity: sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==} engines: {node: '>=18.0.0'} @@ -3624,8 +4082,8 @@ packages: resolution: {integrity: sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==} engines: {node: '>=18.0.0'} - '@smithy/core@3.20.0': - resolution: {integrity: sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==} + '@smithy/core@3.20.2': + resolution: {integrity: sha512-nc99TseyTwL1bg+T21cyEA5oItNy1XN4aUeyOlXJnvyRW5VSK1oRKRoSM/Iq0KFPuqZMxjBemSZHZCOZbSyBMw==} engines: {node: '>=18.0.0'} '@smithy/credential-provider-imds@4.2.7': @@ -3688,12 +4146,12 @@ packages: resolution: {integrity: sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.1': - resolution: {integrity: sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==} + '@smithy/middleware-endpoint@4.4.3': + resolution: {integrity: sha512-Zb8R35hjBhp1oFhiaAZ9QhClpPHdEDmNDC2UrrB2fqV0oNDUUPH12ovZHB5xi/Rd+pg/BJHOR1q+SfsieSKPQg==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.17': - resolution: {integrity: sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==} + '@smithy/middleware-retry@4.4.19': + resolution: {integrity: sha512-QtisFIjIw2tjMm/ESatjWFVIQb5Xd093z8xhxq/SijLg7Mgo2C2wod47Ib/AHpBLFhwYXPzd7Hp2+JVXfeZyMQ==} engines: {node: '>=18.0.0'} '@smithy/middleware-serde@4.2.8': @@ -3740,8 +4198,8 @@ packages: resolution: {integrity: sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.10.2': - resolution: {integrity: sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==} + '@smithy/smithy-client@4.10.4': + resolution: {integrity: sha512-rHig+BWjhjlHlah67ryaW9DECYixiJo5pQCTEwsJyarRBAwHMMC3iYz5MXXAHXe64ZAMn1NhTUSTFIu1T6n6jg==} engines: {node: '>=18.0.0'} '@smithy/types@4.11.0': @@ -3776,12 +4234,12 @@ packages: resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.16': - resolution: {integrity: sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==} + '@smithy/util-defaults-mode-browser@4.3.18': + resolution: {integrity: sha512-Ao1oLH37YmLyHnKdteMp6l4KMCGBeZEAN68YYe00KAaKFijFELDbRQRm3CNplz7bez1HifuBV0l5uR6eVJLhIg==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.19': - resolution: {integrity: sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==} + '@smithy/util-defaults-mode-node@4.2.21': + resolution: {integrity: sha512-e21ASJDirE96kKXZLcYcnn4Zt0WGOvMYc1P8EK0gQeQ3I8PbJWqBKx9AUr/YeFpDkpYwEu1RsPe4UXk2+QL7IA==} engines: {node: '>=18.0.0'} '@smithy/util-endpoints@3.2.7': @@ -3912,8 +4370,8 @@ packages: resolution: {integrity: sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==} engines: {node: '>=10.8'} - '@stripe/stripe-js@8.6.0': - resolution: {integrity: sha512-EB0/GGgs4hfezzkiMkinlRgWtjz8fSdwVQhwYS7Sg/RQrSvuNOz+ssPjD+lAzqaYTCB0zlbrt0fcqVziLJrufQ==} + '@stripe/stripe-js@8.6.1': + resolution: {integrity: sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA==} engines: {node: '>=12.16'} '@svgr/babel-plugin-add-jsx-attribute@8.0.0': @@ -4126,8 +4584,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.16': - resolution: {integrity: sha512-y4xLKvLu6UZWiGdNcgk3yYlzCznYIV0m8dSyUzr3eAC0dHLos5V74qhUHxutYddFGgGU8sWLkp6H5c2RCrsrXw==} + '@tanstack/react-virtual@3.13.18': + resolution: {integrity: sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4136,8 +4594,8 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.16': - resolution: {integrity: sha512-njazUC8mDkrxWmyZmn/3eXrDcP8Msb3chSr4q6a65RmwdSbMlMCdnOphv6/8mLO7O3Fuza5s4M4DclmvAO5w0w==} + '@tanstack/virtual-core@3.13.18': + resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==} '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} @@ -4243,12 +4701,6 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/docker-modem@3.0.6': - resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} - - '@types/dockerode@3.3.47': - resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} - '@types/es-aggregate-error@1.0.6': resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==} @@ -4261,6 +4713,12 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/hammerjs@2.0.46': + resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -4269,8 +4727,17 @@ packages: peerDependencies: ioredis: '>=5' - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@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/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -4293,12 +4760,6 @@ packages: '@types/node-cron@3.0.11': resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} - '@types/node@18.19.130': - resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - - '@types/node@22.19.3': - resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} - '@types/node@24.10.4': resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} @@ -4308,6 +4769,9 @@ packages: '@types/nprogress@0.2.3': resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -4316,20 +4780,17 @@ packages: '@types/react-simple-maps@3.0.6': resolution: {integrity: sha512-hR01RXt6VvsE41FxDd+Bqm1PPGdKbYjCYVtCgh38YeBPt46z3SwmWPWu2L3EdCAP6bd6VYEgztucihRw1C0Klg==} + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} - '@types/ssh2-streams@0.1.13': - resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} - - '@types/ssh2@0.5.52': - resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} - - '@types/ssh2@1.15.5': - resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -4346,6 +4807,12 @@ packages: '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@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==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -4359,6 +4826,14 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@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 + '@vitejs/plugin-react@5.1.2': resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4413,6 +4888,10 @@ packages: '@vitest/utils@4.0.16': resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + '@xmldom/xmldom@0.8.11': + resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} + engines: {node: '>=10.0.0'} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -4478,10 +4957,21 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + anser@1.4.10: + resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + 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'} @@ -4490,6 +4980,10 @@ packages: 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'} @@ -4509,17 +5003,12 @@ packages: 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==} @@ -4534,6 +5023,9 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -4542,12 +5034,12 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} - asn1@0.2.6: - resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -4567,11 +5059,8 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} - async-lock@1.4.1: - resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} - - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + async-limiter@1.0.1: + resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -4608,6 +5097,20 @@ packages: 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.14: resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: @@ -4623,6 +5126,41 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-react-compiler@1.0.0: + resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + + babel-plugin-react-native-web@0.21.2: + resolution: {integrity: sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==} + + babel-plugin-syntax-hermes-parser@0.29.1: + resolution: {integrity: sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA==} + + 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@54.0.9: + resolution: {integrity: sha512-8J6hRdgEC2eJobjoft6mKJ294cLxmi3khCUy2JJQp4htOYYkllSLUq6vudWJkTJiIuGdVR4bR6xuz2EvJLWHNg==} + peerDependencies: + '@babel/runtime': ^7.20.0 + expo: '*' + react-refresh: '>=0.14.0 <1.0.0' + peerDependenciesMeta: + '@babel/runtime': + optional: true + expo: + 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 + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -4674,31 +5212,33 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.9.11: - resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + baseline-browser-mapping@2.9.14: + resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true basic-ftp@5.1.0: resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==} engines: {node: '>=10.0.0'} - bcrypt-pbkdf@1.0.2: - resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - bcryptjs@3.0.3: resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} hasBin: true + better-opn@3.0.2: + resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} + engines: {node: '>=12.0.0'} + bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + 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'} - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - bn.js@4.12.2: resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} @@ -4708,6 +5248,17 @@ packages: bowser@2.13.1: resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + 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.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -4723,25 +5274,20 @@ packages: 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@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - 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.6.0: resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - - buildcheck@0.0.7: - resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} - engines: {node: '>=10.0.0'} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} @@ -4749,9 +5295,9 @@ packages: peerDependencies: esbuild: '>=0.18' - byline@5.0.0: - resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} - engines: {node: '>=0.10.0'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} @@ -4780,12 +5326,16 @@ packages: 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.30001762: - resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} + caniuse-lite@1.0.30001763: + resolution: {integrity: sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==} canvas-confetti@1.9.4: resolution: {integrity: sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==} @@ -4797,6 +5347,10 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + 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'} @@ -4835,14 +5389,30 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + 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-bidi@12.0.1: resolution: {integrity: sha512-fGg+6jr0xjQhzpy5N4ErZxQ4wF7KLEvhGZXD6EgvZKDhu7iOhZXnZhcDxPJDcwTcrD48NPzOCo84RP2lv3Z+Cg==} peerDependencies: devtools-protocol: '*' + 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==} @@ -4852,6 +5422,10 @@ packages: classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -4871,6 +5445,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -4891,13 +5469,26 @@ packages: collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + 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'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -4908,6 +5499,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@13.1.0: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} @@ -4927,12 +5522,20 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + comment-json@4.5.1: + resolution: {integrity: sha512-taEtr3ozUmOB7it68Jll7s0Pwm+aoiHyXKrEC8SEodL4rNpdfDLqa7PfBlrgFoCNNdR8ImL+muti5IGvktJAAg==} + engines: {node: '>= 6'} + compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - 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==} @@ -4947,6 +5550,10 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + 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} @@ -4997,26 +5604,27 @@ packages: countup.js@2.9.0: resolution: {integrity: sha512-llqrvyXztRFPp6+i8jx25phHWcVWhrHO4Nlt0uAOSKHB8778zzQswa4MU3qKBvkXfJKftRYFJuVHez67lyKdHg==} - cpu-features@0.0.10: - resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} - engines: {node: '>=10.0.0'} - - 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'} + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + + css-in-js-utils@3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + 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-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -5042,8 +5650,8 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - cssstyle@5.3.6: - resolution: {integrity: sha512-legscpSpgSAeGEe0TNcai97DKt9Vd9AsAdOL7Uoetb52Ar/8eJm3LIa39qpv8wWzLFlNG4vVvppQM+teaMPj3A==} + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} engines: {node: '>=20'} csstype@3.2.3: @@ -5168,6 +5776,22 @@ packages: resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} engines: {node: '>=18'} + 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'} @@ -5186,14 +5810,29 @@ packages: decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + 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@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + 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-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -5222,6 +5861,15 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + 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'} @@ -5245,18 +5893,6 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - docker-compose@1.3.0: - resolution: {integrity: sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==} - engines: {node: '>= 6.0.0'} - - docker-modem@5.0.6: - resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} - engines: {node: '>= 8.0'} - - dockerode@4.0.9: - resolution: {integrity: sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==} - engines: {node: '>= 8.0'} - dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -5284,10 +5920,18 @@ packages: resolution: {integrity: sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==} hasBin: true + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + dotenv-expand@12.0.3: resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} engines: {node: '>=12'} + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -5411,6 +6055,9 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -5436,6 +6083,14 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + 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-sniffer@0.2.1: resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} @@ -5462,6 +6117,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + env-editor@0.4.2: + resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==} + engines: {node: '>=8'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -5473,6 +6132,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -5510,9 +6172,6 @@ packages: es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - es6-promise@4.2.8: - resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -5546,6 +6205,14 @@ packages: 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'} @@ -5595,6 +6262,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + 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'} @@ -5609,6 +6280,9 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + exec-async@2.2.0: + resolution: {integrity: sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -5617,6 +6291,166 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + expo-asset@12.0.12: + resolution: {integrity: sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-constants@18.0.13: + resolution: {integrity: sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==} + peerDependencies: + expo: '*' + react-native: '*' + + expo-dev-client@6.0.20: + resolution: {integrity: sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==} + peerDependencies: + expo: '*' + + expo-dev-launcher@6.0.20: + resolution: {integrity: sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==} + peerDependencies: + expo: '*' + + expo-dev-menu-interface@2.0.0: + resolution: {integrity: sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==} + peerDependencies: + expo: '*' + + expo-dev-menu@7.0.18: + resolution: {integrity: sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==} + peerDependencies: + expo: '*' + + expo-file-system@19.0.21: + resolution: {integrity: sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==} + peerDependencies: + expo: '*' + react-native: '*' + + expo-font@14.0.10: + resolution: {integrity: sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-json-utils@0.15.0: + resolution: {integrity: sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==} + + expo-keep-awake@15.0.8: + resolution: {integrity: sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==} + peerDependencies: + expo: '*' + react: '*' + + expo-linking@8.0.11: + resolution: {integrity: sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==} + peerDependencies: + react: '*' + react-native: '*' + + expo-manifests@1.0.10: + resolution: {integrity: sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==} + peerDependencies: + expo: '*' + + expo-modules-autolinking@3.0.24: + resolution: {integrity: sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==} + hasBin: true + + expo-modules-core@3.0.29: + resolution: {integrity: sha512-LzipcjGqk8gvkrOUf7O2mejNWugPkf3lmd9GkqL9WuNyeN2fRwU0Dn77e3ZUKI3k6sI+DNwjkq4Nu9fNN9WS7Q==} + peerDependencies: + react: '*' + react-native: '*' + + expo-router@6.0.21: + resolution: {integrity: sha512-wjTUjrnWj6gRYjaYl1kYfcRnNE4ZAQ0kz0+sQf6/mzBd/OU6pnOdD7WrdAW3pTTpm52Q8sMoeX98tNQEddg2uA==} + peerDependencies: + '@expo/metro-runtime': ^6.1.2 + '@react-navigation/drawer': ^7.5.0 + '@testing-library/react-native': '>= 12.0.0' + expo: '*' + expo-constants: ^18.0.12 + expo-linking: ^8.0.11 + react: '*' + react-dom: '*' + react-native: '*' + react-native-gesture-handler: '*' + react-native-reanimated: '*' + react-native-safe-area-context: '>= 5.4.0' + react-native-screens: '*' + react-native-web: '*' + react-server-dom-webpack: ~19.0.3 || ~19.1.4 || ~19.2.3 + peerDependenciesMeta: + '@react-navigation/drawer': + optional: true + '@testing-library/react-native': + optional: true + react-dom: + optional: true + react-native-gesture-handler: + optional: true + react-native-reanimated: + optional: true + react-native-web: + optional: true + react-server-dom-webpack: + optional: true + + expo-server@1.0.5: + resolution: {integrity: sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA==} + engines: {node: '>=20.16.0'} + + expo-splash-screen@31.0.13: + resolution: {integrity: sha512-1epJLC1cDlwwj089R2h8cxaU5uk4ONVAC+vzGiTZH4YARQhL4Stlz1MbR6yAS173GMosvkE6CAeihR7oIbCkDA==} + peerDependencies: + expo: '*' + + expo-status-bar@3.0.9: + resolution: {integrity: sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw==} + peerDependencies: + react: '*' + react-native: '*' + + expo-system-ui@6.0.9: + resolution: {integrity: sha512-eQTYGzw1V4RYiYHL9xDLYID3Wsec2aZS+ypEssmF64D38aDrqbDgz1a2MSlHLQp2jHXSs3FvojhZ9FVela1Zcg==} + peerDependencies: + expo: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native-web: + optional: true + + expo-updates-interface@2.0.0: + resolution: {integrity: sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==} + peerDependencies: + expo: '*' + + expo@54.0.31: + resolution: {integrity: sha512-kQ3RDqA/a59I7y+oqQGyrPbbYlgPMUdKBOgvFLpoHbD2bCM+F75i4N0mUijy7dG5F/CUCu2qHmGGUCXBbMDkCg==} + 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==} @@ -5644,6 +6478,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.1.1: resolution: {integrity: sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ==} @@ -5701,6 +6538,15 @@ packages: fastseries@1.7.2: resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fbjs-css-vars@1.0.2: + resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} + + fbjs@3.0.5: + resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -5728,10 +6574,22 @@ packages: 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-my-way@9.4.0: resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==} engines: {node: '>=20'} + 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'} @@ -5742,6 +6600,9 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flow-enums-runtime@0.0.6: + resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -5751,6 +6612,9 @@ packages: debug: optional: true + 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'} @@ -5766,8 +6630,8 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.23.26: - resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==} + framer-motion@12.25.0: + resolution: {integrity: sha512-mlWqd0rApIjeyhTCSNCqPYsUAEhkcUukZxH3ke6KbstBRPcxhEpuIjmiUQvB+1E9xkEm5SpNHBgHCapH/QHTWg==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -5780,13 +6644,21 @@ packages: react-dom: optional: true - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + 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'} fs-extra@11.3.3: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} + 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} @@ -5826,9 +6698,9 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} - get-port@7.1.0: - resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} - engines: {node: '>=16'} + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} @@ -5853,6 +6725,10 @@ packages: resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} engines: {node: '>= 14'} + getenv@2.0.0: + resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==} + engines: {node: '>=6'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5861,10 +6737,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - hasBin: true - glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} @@ -5874,6 +6746,14 @@ packages: resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-dirs@0.1.1: + resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} + engines: {node: '>=4'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -5904,6 +6784,10 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} + 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'} @@ -5942,6 +6826,25 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hermes-estree@0.29.1: + resolution: {integrity: sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==} + + hermes-estree@0.32.0: + resolution: {integrity: sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==} + + hermes-parser@0.29.1: + resolution: {integrity: sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==} + + hermes-parser@0.32.0: + resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -5984,8 +6887,11 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - i18next@25.7.3: - resolution: {integrity: sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==} + hyphenate-style-name@1.1.0: + resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + + i18next@25.7.4: + resolution: {integrity: sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -6003,6 +6909,11 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + immer@10.2.0: resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} @@ -6016,15 +6927,29 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + inflected@2.1.0: resolution: {integrity: sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==} + 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==} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + inline-style-prefixer@7.0.1: + resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} + input-otp@1.4.2: resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} peerDependencies: @@ -6042,6 +6967,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis-mock@8.13.1: resolution: {integrity: sha512-Wsi50AU+cMiI32nAgfwpUaJVBtb4iQdVsOHl9M6R3tePCO/8vGsToCVIG82XWAxN4Se55TZoOzVseu+QngFLyw==} engines: {node: '>=12.22'} @@ -6049,8 +6977,8 @@ packages: '@types/ioredis-mock': ^8 ioredis: ^5 - ioredis@5.8.2: - resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} + ioredis@5.9.1: + resolution: {integrity: sha512-BXNqFQ66oOsR82g9ajFFsR8ZKrjVvYCLyeML9IvSMAsP56XH2VXBdZjmI11p65nXXJxTEt1hie3J2QeFJVgrtQ==} engines: {node: '>=12.22.0'} ip-address@10.1.0: @@ -6074,6 +7002,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -6109,6 +7040,11 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -6214,8 +7150,9 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -6227,6 +7164,10 @@ packages: 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'} + istanbul-lib-instrument@6.0.3: resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} engines: {node: '>=10'} @@ -6243,13 +7184,49 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.1.1: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} + 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 @@ -6278,10 +7255,17 @@ packages: 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==} + jsdom@27.4.0: resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -6343,9 +7327,9 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lazystream@1.0.1: - resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} - engines: {node: '>= 0.6.3'} + lan-network@0.1.7: + resolution: {integrity: sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==} + hasBin: true leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -6357,6 +7341,143 @@ packages: light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lighthouse-logger@1.4.2: + resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + 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-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + 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-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + 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-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + 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-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + 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-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + 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-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + 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-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + 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-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + 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-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + 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-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.27.0: + resolution: {integrity: sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -6374,13 +7495,14 @@ packages: localstack@1.0.0: resolution: {integrity: sha512-LJ4lhd8de8OuuRIcmqJVfZxI81e1avXiyFRrlCLt8WnX4HV+v0n6Oslp4rYiGL4uSh8zcEzFmST7cK2QPaeP4g==} + 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.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -6396,6 +7518,9 @@ packages: lodash.omitby@4.6.0: resolution: {integrity: sha512-5OrRcIVR75M288p4nbI2WLAf3ndw2GD9fyNv3Bc15+WCxJDdZ4lYndSxGd7hnG6PVjiJTeJE2dHEGhIuKGicIQ==} + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + lodash.topath@4.5.2: resolution: {integrity: sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==} @@ -6411,6 +7536,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} + log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -6426,9 +7555,6 @@ packages: resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} engines: {node: '>= 0.6.0'} - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -6479,6 +7605,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} @@ -6492,6 +7621,9 @@ packages: engines: {node: '>= 18'} hasBin: true + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -6523,6 +7655,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} @@ -6535,6 +7670,12 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6542,6 +7683,64 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + metro-babel-transformer@0.83.3: + resolution: {integrity: sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==} + engines: {node: '>=20.19.4'} + + metro-cache-key@0.83.3: + resolution: {integrity: sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==} + engines: {node: '>=20.19.4'} + + metro-cache@0.83.3: + resolution: {integrity: sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==} + engines: {node: '>=20.19.4'} + + metro-config@0.83.3: + resolution: {integrity: sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==} + engines: {node: '>=20.19.4'} + + metro-core@0.83.3: + resolution: {integrity: sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==} + engines: {node: '>=20.19.4'} + + metro-file-map@0.83.3: + resolution: {integrity: sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==} + engines: {node: '>=20.19.4'} + + metro-minify-terser@0.83.3: + resolution: {integrity: sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==} + engines: {node: '>=20.19.4'} + + metro-resolver@0.83.3: + resolution: {integrity: sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==} + engines: {node: '>=20.19.4'} + + metro-runtime@0.83.3: + resolution: {integrity: sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==} + engines: {node: '>=20.19.4'} + + metro-source-map@0.83.3: + resolution: {integrity: sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==} + engines: {node: '>=20.19.4'} + + metro-symbolicate@0.83.3: + resolution: {integrity: sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==} + engines: {node: '>=20.19.4'} + hasBin: true + + metro-transform-plugins@0.83.3: + resolution: {integrity: sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==} + engines: {node: '>=20.19.4'} + + metro-transform-worker@0.83.3: + resolution: {integrity: sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==} + engines: {node: '>=20.19.4'} + + metro@0.83.3: + resolution: {integrity: sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==} + engines: {node: '>=20.19.4'} + hasBin: true + micro-cors@0.1.1: resolution: {integrity: sha512-6WqIahA5sbQR1Gjexp1VuWGFDKbZZleJb/gy1khNGk18a6iN1FdTcr3Q8twaxkV5H94RjxIBjirYbWCehpMBFw==} engines: {node: '>=6'} @@ -6650,11 +7849,20 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} hasBin: true + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -6673,10 +7881,6 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - minimatch@6.2.0: resolution: {integrity: sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==} engines: {node: '>=10'} @@ -6692,12 +7896,13 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -6709,16 +7914,19 @@ packages: mnemonist@0.40.3: resolution: {integrity: sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==} - motion-dom@12.23.23: - resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} + motion-dom@12.24.11: + resolution: {integrity: sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A==} - motion-utils@12.23.6: - resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + motion-utils@12.24.10: + resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==} mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -6739,22 +7947,32 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nan@2.24.0: - resolution: {integrity: sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nativewind@4.2.1: + resolution: {integrity: sha512-10uUB2Dlli3MH3NDL5nMHqJHz1A3e/E6mzjTj6cl7hHECClJ7HpE6v+xZL+GXdbwQSnWE+UWMIMsNz7yOQkAJQ==} + 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'} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + nested-error-stacks@2.0.1: + resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} + netmask@2.0.2: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} @@ -6838,6 +8056,13 @@ packages: encoding: optional: true + node-forge@1.3.3: + resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} + engines: {node: '>= 6.13.0'} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-readfiles@0.2.0: resolution: {integrity: sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==} @@ -6851,6 +8076,10 @@ packages: normalize-wheel@1.0.1: resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==} + npm-package-arg@11.0.3: + resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} + engines: {node: ^16.14.0 || >=18.0.0} + npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -6861,6 +8090,9 @@ packages: 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==} + number-flow@0.5.8: resolution: {integrity: sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA==} @@ -6909,6 +8141,10 @@ packages: oauth@0.9.15: resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + ob1@0.83.3: + resolution: {integrity: sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==} + engines: {node: '>=20.19.4'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -6947,9 +8183,25 @@ packages: 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'} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -6958,8 +8210,16 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - openai@6.15.0: - resolution: {integrity: sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g==} + 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@6.16.0: + resolution: {integrity: sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==} hasBin: true peerDependencies: ws: ^8.18.0 @@ -6983,6 +8243,10 @@ packages: openid-client@5.7.1: resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} + ora@3.4.0: + resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} + engines: {node: '>=6'} + ora@8.2.0: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} @@ -6999,14 +8263,26 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + 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'} + pac-proxy-agent@7.2.0: resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} engines: {node: '>= 14'} @@ -7029,6 +8305,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-png@2.1.0: + resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} + engines: {node: '>=10'} + parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -7044,10 +8324,18 @@ packages: 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'} @@ -7055,10 +8343,6 @@ packages: 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.1: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} @@ -7089,6 +8373,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@3.0.1: + resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==} + engines: {node: '>=10'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -7097,9 +8385,6 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} - pino-abstract-transport@2.0.0: - resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} - pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -7110,8 +8395,8 @@ packages: pino-std-serializers@7.0.0: resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} - pino@10.1.0: - resolution: {integrity: sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==} + pino@10.1.1: + resolution: {integrity: sha512-3qqVfpJtRQUCAOs4rTOEwLH6mwJJ/CSAlbis8fKOiMzTtXh0HN/VLsn3UWVTJ7U8DsWmxeNon2IpGb+wORXH4g==} hasBin: true pirates@4.0.7: @@ -7124,6 +8409,14 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + pony-cause@1.1.1: resolution: {integrity: sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==} engines: {node: '>=12.0.0'} @@ -7183,12 +8476,16 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - postgres@3.4.7: - resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + postgres@3.4.8: + resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} engines: {node: '>=12'} preact-render-to-string@5.2.6: @@ -7196,18 +8493,26 @@ packages: peerDependencies: preact: '>=10' - preact@10.28.1: - resolution: {integrity: sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==} + preact@10.28.2: + resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} prettier@3.7.4: resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} hasBin: true + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@3.8.0: resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} @@ -7215,8 +8520,9 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + 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-warning@4.0.1: resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} @@ -7224,14 +8530,16 @@ packages: 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@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + + 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'} @@ -7239,20 +8547,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - - properties-reader@2.3.0: - resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} - engines: {node: '>=14'} - property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - protobufjs@7.5.4: - resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} - engines: {node: '>=12.0.0'} - proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} @@ -7280,19 +8577,35 @@ packages: engines: {node: '>=18'} hasBin: true + qrcode-terminal@0.11.0: + resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} + hasBin: true + qs@6.14.1: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + 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==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-confetti@6.4.0: resolution: {integrity: sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==} engines: {node: '>=16'} @@ -7316,6 +8629,14 @@ packages: peerDependencies: react: '>=16.8.0' + react-devtools-core@6.1.5: + resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -7327,11 +8648,20 @@ packages: react: '>=16.4.0' react-dom: '>=16.4.0' - react-email@5.1.1: - resolution: {integrity: sha512-NUwnOXyxCJtr2xJO4uiEVmBwRg00rgvV2WZe6Z+a2HqGeTTOiLpKoQOvroKGFzUemkQKQlVRFPdPvSK/1uanmw==} + react-email@5.2.1: + resolution: {integrity: sha512-ETejN253u7SXQQIIj+fvqxOUAFTMTnEl9lfVYFIbcGtRSrojAdN6x8eX+mDgvQFSjI7inMXNxmYaqQkx8eoOQw==} engines: {node: '>=20.0.0'} hasBin: true + 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.70.0: resolution: {integrity: sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw==} engines: {node: '>=18.0.0'} @@ -7371,44 +8701,142 @@ packages: peerDependencies: react: '>=16.0.0' - react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + react-native-css-interop@0.1.22: + resolution: {integrity: sha512-Mu01e+H9G+fxSWvwtgWlF5MJBJC4VszTCBXopIpeR171lbeBInHb8aHqoqRPxmJpi3xIHryzqKFOJYAdk7PBxg==} + engines: {node: '>=18'} peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 + react: '>=18' + react-native: '*' + react-native-reanimated: '>=3.6.2' + react-native-safe-area-context: '*' + react-native-svg: '*' + tailwindcss: ~3 peerDependenciesMeta: - '@types/react': + react-native-safe-area-context: optional: true - redux: + react-native-svg: optional: true - react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} - - react-remove-scroll-bar@2.3.8: - resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} - engines: {node: '>=10'} + react-native-css-interop@0.2.1: + resolution: {integrity: sha512-B88f5rIymJXmy1sNC/MhTkb3xxBej1KkuAt7TiT9iM7oXz3RM8Bn+7GUrfR02TvSgKm4cg2XiSuLEKYfKwNsjA==} + engines: {node: '>=18'} peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: '>=18' + react-native: '*' + react-native-reanimated: '>=3.6.2' + react-native-safe-area-context: '*' + react-native-svg: '*' + tailwindcss: ~3 peerDependenciesMeta: - '@types/react': + react-native-safe-area-context: + optional: true + react-native-svg: optional: true - react-remove-scroll@2.7.2: - resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} - engines: {node: '>=10'} + react-native-gesture-handler@2.28.0: + resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==} peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: + react: '*' + react-native: '*' + + react-native-is-edge-to-edge@1.2.1: + resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-reanimated@4.1.6: + resolution: {integrity: sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + react: '*' + react-native: '*' + react-native-worklets: '>=0.5.0' + + react-native-safe-area-context@5.6.2: + resolution: {integrity: sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-screens@4.16.0: + resolution: {integrity: sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-svg@15.15.1: + resolution: {integrity: sha512-ZUD1xwc3Hwo4cOmOLumjJVoc7lEf9oQFlHnLmgccLC19fNm6LVEdtB+Cnip6gEi0PG3wfvVzskViEtrySQP8Fw==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-web@0.21.2: + resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + react-native-worklets@0.5.1: + resolution: {integrity: sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + react: '*' + react-native: '*' + + react-native@0.81.5: + resolution: {integrity: sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==} + engines: {node: '>= 20.19.4'} + hasBin: true + peerDependencies: + '@types/react': ^19.1.0 + react: ^19.1.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: '@types/react': optional: true - react-resizable-panels@4.2.1: - resolution: {integrity: sha512-UYyiZNjd3P12BqQYihojqdVs4ovMBWFO68RQp/O1HGiUKdltNu9hacZEm4PVD/UpYpz5p9V+YBtOZo+CvGKmkQ==} + react-resizable-panels@4.3.3: + resolution: {integrity: sha512-7ZmYcoOiipVwwz8X9O/HiRbm8THM6qnXo7p5dPI6ivzdDoteHo3iXS1pijs8Z4/XU8V1RwhuGgJiZU5G7Zy0KQ==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -7436,6 +8864,10 @@ packages: react: '>=16.14.0' react-dom: '>=16.14.0' + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} @@ -7443,20 +8875,10 @@ packages: 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@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - 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'} @@ -7525,6 +8947,9 @@ packages: 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==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -7560,14 +8985,15 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + requireg@0.2.2: + resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} + engines: {node: '>= 4.0.0'} reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resend@6.6.0: - resolution: {integrity: sha512-d1WoOqSxj5x76JtQMrieNAG1kZkh4NU4f+Je1yq4++JsDpLddhEwnJlNfvkCzvUuZy9ZquWmMMAm2mENd2JvRw==} + resend@6.7.0: + resolution: {integrity: sha512-2ZV0NDZsh4Gh+Nd1hvluZIitmGJ59O4+OxMufymG6Y8uz1Jgt2uS1seSENnkIUlmwg7/dwmfIJC9rAufByz7wA==} engines: {node: '>=20'} peerDependencies: '@react-email/render': '*' @@ -7583,14 +9009,32 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-global@1.0.0: + resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + 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.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} 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'} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -7599,10 +9043,6 @@ packages: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - rettime@0.7.0: resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} @@ -7613,8 +9053,13 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rollup@4.54.0: - resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -7625,9 +9070,6 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -7652,13 +9094,20 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sax@1.4.3: - resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + sax@1.4.4: + resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} + engines: {node: '>=11.0.0'} saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -7672,11 +9121,36 @@ packages: 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.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serialize-error@2.1.0: + resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} + engines: {node: '>=0.10.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -7692,9 +9166,19 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + 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==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -7707,6 +9191,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + should-equal@2.0.0: resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==} @@ -7755,6 +9243,12 @@ packages: resolution: {integrity: sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==} engines: {node: '>=12'} + simple-plist@1.3.1: + resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -7775,6 +9269,10 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slugify@1.6.6: + resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} + engines: {node: '>=8.0.0'} + smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -7817,6 +9315,10 @@ packages: 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'} @@ -7828,29 +9330,44 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - split-ca@1.0.1: - resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + 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==} + sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - ssh-remote-port-forward@1.0.4: - resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} - - ssh2@1.17.0: - resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} - engines: {node: '>=10.16.0'} + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + 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==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + + 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'} @@ -7872,12 +9389,20 @@ packages: stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + stream-buffers@2.2.0: + resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} + engines: {node: '>= 0.10.0'} + streamx@2.23.0: resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -7906,15 +9431,16 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - 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==} stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + 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'} @@ -7931,12 +9457,16 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} - stripe@20.1.0: - resolution: {integrity: sha512-o1VNRuMkY76ZCq92U3EH3/XHm/WHp7AerpzDs4Zyo8uE5mFL4QUcv/2SudWsSnhBSp4moO2+ZoGCZ7mT8crPmQ==} + stripe@20.1.2: + resolution: {integrity: sha512-qU+lQRRJnTxmyvglYBPE24/IepncmywsAg0GDTsTdP2pb+3e3RdREHJZjKgqCmv0phPxN/nmgNPnIPPH8w0P4A==} engines: {node: '>=16'} peerDependencies: '@types/node': '>=16' @@ -7947,6 +9477,9 @@ packages: strnum@2.1.2: resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} + structured-headers@0.4.1: + resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} + stubborn-fs@2.0.0: resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} @@ -7972,15 +9505,30 @@ packages: babel-plugin-macros: optional: true + styleq@0.1.3: + resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true + 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'} @@ -7993,8 +9541,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - svix@1.76.1: - resolution: {integrity: sha512-CRuDWBTgYfDnBLRaZdKp9VuoPcNUq9An14c/k+4YJ15Qc5Grvf66vp0jvTltd4t7OIRj+8lM1DAgvSgvf7hdLw==} + svix@1.84.1: + resolution: {integrity: sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==} swagger2openapi@7.0.8: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} @@ -8038,21 +9586,32 @@ packages: tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} - tar-fs@2.1.4: - resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - tar-fs@3.1.1: resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - testcontainers@11.11.0: - resolution: {integrity: sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==} + tar@7.5.2: + resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} + engines: {node: '>=18'} + + 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.44.1: + resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} + 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.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -8064,8 +9623,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - thread-stream@3.1.0: - resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + + throat@5.0.0: + resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -8099,6 +9662,9 @@ packages: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} + 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'} @@ -8234,8 +9800,17 @@ packages: tween-functions@1.2.0: resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==} - tweetnacl@0.14.5: - resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + 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.3.1: resolution: {integrity: sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==} @@ -8287,11 +9862,15 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + ufo@1.6.2: + resolution: {integrity: sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==} uint8array-extras@1.5.0: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} @@ -8301,17 +9880,15 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.16.0: - resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} + undici@6.23.0: + resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} + engines: {node: '>=18.17'} + + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -8333,6 +9910,10 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -8355,6 +9936,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin-swc@1.5.9: resolution: {integrity: sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==} peerDependencies: @@ -8376,9 +9961,6 @@ packages: urijs@1.19.11: resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -8389,6 +9971,11 @@ packages: '@types/react': optional: true + use-latest-callback@0.2.6: + resolution: {integrity: sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==} + peerDependencies: + react: '>=16.8' + use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -8411,6 +9998,10 @@ packages: resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} engines: {node: '>= 4'} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -8419,10 +10010,18 @@ packages: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true + uuid@7.0.3: + resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + validator@13.15.26: resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} engines: {node: '>= 0.10'} @@ -8446,16 +10045,16 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - vite-tsconfig-paths@6.0.3: - resolution: {integrity: sha512-7bL7FPX/DSviaZGYUKowWF1AiDVWjMjxNbE8lyaVGDezkedWqfGhlnQ4BZXre0ZN5P4kAgIJfAlgFDVyjrCIyg==} + vite-tsconfig-paths@6.0.4: + resolution: {integrity: sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==} peerDependencies: vite: '*' peerDependenciesMeta: vite: optional: true - vite@7.3.0: - resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -8528,6 +10127,9 @@ packages: jsdom: optional: true + 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'} @@ -8536,12 +10138,25 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + 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==} + webdriver-bidi-protocol@0.3.10: resolution: {integrity: sha512-5LAE43jAVLOhB/QqX4bwSiv0Hg1HBfMmOuwBSXHdvg4GMGu9Y0lIq7p4R/yySu6w74WmaR4GM4H9t2IwLW7hgw==} webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@5.0.0: + resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} + engines: {node: '>=8'} + webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -8559,10 +10174,17 @@ packages: engines: {node: '>=18'} deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url-without-unicode@8.0.0-3: + resolution: {integrity: sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==} + engines: {node: '>=10'} + whatwg-url@15.1.0: resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} engines: {node: '>=20'} @@ -8599,6 +10221,9 @@ packages: engines: {node: '>=8'} hasBin: true + wonka@6.3.5: + resolution: {integrity: sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -8614,6 +10239,21 @@ packages: 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'} @@ -8638,10 +10278,38 @@ packages: utf-8-validate: optional: true + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + 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 + + xcode@3.0.1: + resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} + engines: {node: '>=10.0.0'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + 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'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -8659,6 +10327,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -8691,10 +10363,6 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} - zip-stream@6.0.1: - resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} - engines: {node: '>= 14'} - zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -8706,6 +10374,10 @@ packages: snapshots: + '@0no-co/graphql.web@1.2.0(graphql@16.12.0)': + optionalDependencies: + graphql: 16.12.0 + '@acemir/cssom@0.9.30': {} '@alloc/quick-lru@5.2.0': {} @@ -8754,21 +10426,21 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 tslib: 2.8.1 '@aws-crypto/crc32c@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 tslib: 2.8.1 '@aws-crypto/sha1-browser@5.2.0': dependencies: '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-locate-window': 3.957.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-locate-window': 3.965.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -8777,15 +10449,15 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-locate-window': 3.957.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-locate-window': 3.965.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -8794,35 +10466,35 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-s3@3.962.0': + '@aws-sdk/client-s3@3.966.0': dependencies: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/credential-provider-node': 3.962.0 - '@aws-sdk/middleware-bucket-endpoint': 3.957.0 - '@aws-sdk/middleware-expect-continue': 3.957.0 - '@aws-sdk/middleware-flexible-checksums': 3.957.0 - '@aws-sdk/middleware-host-header': 3.957.0 - '@aws-sdk/middleware-location-constraint': 3.957.0 - '@aws-sdk/middleware-logger': 3.957.0 - '@aws-sdk/middleware-recursion-detection': 3.957.0 - '@aws-sdk/middleware-sdk-s3': 3.957.0 - '@aws-sdk/middleware-ssec': 3.957.0 - '@aws-sdk/middleware-user-agent': 3.957.0 - '@aws-sdk/region-config-resolver': 3.957.0 - '@aws-sdk/signature-v4-multi-region': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-endpoints': 3.957.0 - '@aws-sdk/util-user-agent-browser': 3.957.0 - '@aws-sdk/util-user-agent-node': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/credential-provider-node': 3.966.0 + '@aws-sdk/middleware-bucket-endpoint': 3.966.0 + '@aws-sdk/middleware-expect-continue': 3.965.0 + '@aws-sdk/middleware-flexible-checksums': 3.966.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-location-constraint': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-sdk-s3': 3.966.0 + '@aws-sdk/middleware-ssec': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.966.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/signature-v4-multi-region': 3.966.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.966.0 '@smithy/config-resolver': 4.4.5 - '@smithy/core': 3.20.0 + '@smithy/core': 3.20.2 '@smithy/eventstream-serde-browser': 4.2.7 '@smithy/eventstream-serde-config-resolver': 4.3.7 '@smithy/eventstream-serde-node': 4.2.7 @@ -8833,21 +10505,21 @@ snapshots: '@smithy/invalid-dependency': 4.2.7 '@smithy/md5-js': 4.2.7 '@smithy/middleware-content-length': 4.2.7 - '@smithy/middleware-endpoint': 4.4.1 - '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-endpoint': 4.4.3 + '@smithy/middleware-retry': 4.4.19 '@smithy/middleware-serde': 4.2.8 '@smithy/middleware-stack': 4.2.7 '@smithy/node-config-provider': 4.3.7 '@smithy/node-http-handler': 4.4.7 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 '@smithy/url-parser': 4.2.7 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.16 - '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-defaults-mode-browser': 4.3.18 + '@smithy/util-defaults-mode-node': 4.2.21 '@smithy/util-endpoints': 3.2.7 '@smithy/util-middleware': 4.2.7 '@smithy/util-retry': 4.2.7 @@ -8858,44 +10530,44 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sqs@3.962.0': + '@aws-sdk/client-sqs@3.966.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/credential-provider-node': 3.962.0 - '@aws-sdk/middleware-host-header': 3.957.0 - '@aws-sdk/middleware-logger': 3.957.0 - '@aws-sdk/middleware-recursion-detection': 3.957.0 - '@aws-sdk/middleware-sdk-sqs': 3.957.0 - '@aws-sdk/middleware-user-agent': 3.957.0 - '@aws-sdk/region-config-resolver': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-endpoints': 3.957.0 - '@aws-sdk/util-user-agent-browser': 3.957.0 - '@aws-sdk/util-user-agent-node': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/credential-provider-node': 3.966.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-sdk-sqs': 3.966.0 + '@aws-sdk/middleware-user-agent': 3.966.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.966.0 '@smithy/config-resolver': 4.4.5 - '@smithy/core': 3.20.0 + '@smithy/core': 3.20.2 '@smithy/fetch-http-handler': 5.3.8 '@smithy/hash-node': 4.2.7 '@smithy/invalid-dependency': 4.2.7 '@smithy/md5-js': 4.2.7 '@smithy/middleware-content-length': 4.2.7 - '@smithy/middleware-endpoint': 4.4.1 - '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-endpoint': 4.4.3 + '@smithy/middleware-retry': 4.4.19 '@smithy/middleware-serde': 4.2.8 '@smithy/middleware-stack': 4.2.7 '@smithy/node-config-provider': 4.3.7 '@smithy/node-http-handler': 4.4.7 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 '@smithy/url-parser': 4.2.7 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.16 - '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-defaults-mode-browser': 4.3.18 + '@smithy/util-defaults-mode-node': 4.2.21 '@smithy/util-endpoints': 3.2.7 '@smithy/util-middleware': 4.2.7 '@smithy/util-retry': 4.2.7 @@ -8904,41 +10576,41 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.958.0': + '@aws-sdk/client-sso@3.966.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/middleware-host-header': 3.957.0 - '@aws-sdk/middleware-logger': 3.957.0 - '@aws-sdk/middleware-recursion-detection': 3.957.0 - '@aws-sdk/middleware-user-agent': 3.957.0 - '@aws-sdk/region-config-resolver': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-endpoints': 3.957.0 - '@aws-sdk/util-user-agent-browser': 3.957.0 - '@aws-sdk/util-user-agent-node': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.966.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.966.0 '@smithy/config-resolver': 4.4.5 - '@smithy/core': 3.20.0 + '@smithy/core': 3.20.2 '@smithy/fetch-http-handler': 5.3.8 '@smithy/hash-node': 4.2.7 '@smithy/invalid-dependency': 4.2.7 '@smithy/middleware-content-length': 4.2.7 - '@smithy/middleware-endpoint': 4.4.1 - '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-endpoint': 4.4.3 + '@smithy/middleware-retry': 4.4.19 '@smithy/middleware-serde': 4.2.8 '@smithy/middleware-stack': 4.2.7 '@smithy/node-config-provider': 4.3.7 '@smithy/node-http-handler': 4.4.7 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 '@smithy/url-parser': 4.2.7 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.16 - '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-defaults-mode-browser': 4.3.18 + '@smithy/util-defaults-mode-node': 4.2.21 '@smithy/util-endpoints': 3.2.7 '@smithy/util-middleware': 4.2.7 '@smithy/util-retry': 4.2.7 @@ -8947,59 +10619,59 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.957.0': + '@aws-sdk/core@3.966.0': dependencies: - '@aws-sdk/types': 3.957.0 - '@aws-sdk/xml-builder': 3.957.0 - '@smithy/core': 3.20.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/xml-builder': 3.965.0 + '@smithy/core': 3.20.2 '@smithy/node-config-provider': 4.3.7 '@smithy/property-provider': 4.2.7 '@smithy/protocol-http': 5.3.7 '@smithy/signature-v4': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 '@smithy/util-base64': 4.3.0 '@smithy/util-middleware': 4.2.7 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/crc64-nvme@3.957.0': + '@aws-sdk/crc64-nvme@3.965.0': dependencies: '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.957.0': + '@aws-sdk/credential-provider-env@3.966.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.957.0': + '@aws-sdk/credential-provider-http@3.966.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/fetch-http-handler': 5.3.8 '@smithy/node-http-handler': 4.4.7 '@smithy/property-provider': 4.2.7 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 '@smithy/util-stream': 4.5.8 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.962.0': - dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/credential-provider-env': 3.957.0 - '@aws-sdk/credential-provider-http': 3.957.0 - '@aws-sdk/credential-provider-login': 3.962.0 - '@aws-sdk/credential-provider-process': 3.957.0 - '@aws-sdk/credential-provider-sso': 3.958.0 - '@aws-sdk/credential-provider-web-identity': 3.958.0 - '@aws-sdk/nested-clients': 3.958.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/credential-provider-ini@3.966.0': + dependencies: + '@aws-sdk/core': 3.966.0 + '@aws-sdk/credential-provider-env': 3.966.0 + '@aws-sdk/credential-provider-http': 3.966.0 + '@aws-sdk/credential-provider-login': 3.966.0 + '@aws-sdk/credential-provider-process': 3.966.0 + '@aws-sdk/credential-provider-sso': 3.966.0 + '@aws-sdk/credential-provider-web-identity': 3.966.0 + '@aws-sdk/nested-clients': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/credential-provider-imds': 4.2.7 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 @@ -9008,11 +10680,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.962.0': + '@aws-sdk/credential-provider-login@3.966.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/nested-clients': 3.958.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/nested-clients': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/protocol-http': 5.3.7 '@smithy/shared-ini-file-loader': 4.4.2 @@ -9021,15 +10693,15 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.962.0': + '@aws-sdk/credential-provider-node@3.966.0': dependencies: - '@aws-sdk/credential-provider-env': 3.957.0 - '@aws-sdk/credential-provider-http': 3.957.0 - '@aws-sdk/credential-provider-ini': 3.962.0 - '@aws-sdk/credential-provider-process': 3.957.0 - '@aws-sdk/credential-provider-sso': 3.958.0 - '@aws-sdk/credential-provider-web-identity': 3.958.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/credential-provider-env': 3.966.0 + '@aws-sdk/credential-provider-http': 3.966.0 + '@aws-sdk/credential-provider-ini': 3.966.0 + '@aws-sdk/credential-provider-process': 3.966.0 + '@aws-sdk/credential-provider-sso': 3.966.0 + '@aws-sdk/credential-provider-web-identity': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/credential-provider-imds': 4.2.7 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 @@ -9038,21 +10710,21 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.957.0': + '@aws-sdk/credential-provider-process@3.966.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.958.0': + '@aws-sdk/credential-provider-sso@3.966.0': dependencies: - '@aws-sdk/client-sso': 3.958.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/token-providers': 3.958.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/client-sso': 3.966.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/token-providers': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 '@smithy/types': 4.11.0 @@ -9060,11 +10732,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.958.0': + '@aws-sdk/credential-provider-web-identity@3.966.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/nested-clients': 3.958.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/nested-clients': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 '@smithy/types': 4.11.0 @@ -9072,42 +10744,42 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/lib-storage@3.962.0(@aws-sdk/client-s3@3.962.0)': + '@aws-sdk/lib-storage@3.966.0(@aws-sdk/client-s3@3.966.0)': dependencies: - '@aws-sdk/client-s3': 3.962.0 + '@aws-sdk/client-s3': 3.966.0 '@smithy/abort-controller': 4.2.7 - '@smithy/middleware-endpoint': 4.4.1 - '@smithy/smithy-client': 4.10.2 + '@smithy/middleware-endpoint': 4.4.3 + '@smithy/smithy-client': 4.10.4 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: 2.8.1 - '@aws-sdk/middleware-bucket-endpoint@3.957.0': + '@aws-sdk/middleware-bucket-endpoint@3.966.0': dependencies: - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-arn-parser': 3.957.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-arn-parser': 3.966.0 '@smithy/node-config-provider': 4.3.7 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 '@smithy/util-config-provider': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-expect-continue@3.957.0': + '@aws-sdk/middleware-expect-continue@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.957.0': + '@aws-sdk/middleware-flexible-checksums@3.966.0': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/crc64-nvme': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/crc64-nvme': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/is-array-buffer': 4.2.0 '@smithy/node-config-provider': 4.3.7 '@smithy/protocol-http': 5.3.7 @@ -9117,43 +10789,43 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.957.0': + '@aws-sdk/middleware-host-header@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-location-constraint@3.957.0': + '@aws-sdk/middleware-location-constraint@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.957.0': + '@aws-sdk/middleware-logger@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.957.0': + '@aws-sdk/middleware-recursion-detection@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 - '@aws/lambda-invoke-store': 0.2.2 + '@aws-sdk/types': 3.965.0 + '@aws/lambda-invoke-store': 0.2.3 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.957.0': + '@aws-sdk/middleware-sdk-s3@3.966.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-arn-parser': 3.957.0 - '@smithy/core': 3.20.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-arn-parser': 3.966.0 + '@smithy/core': 3.20.2 '@smithy/node-config-provider': 4.3.7 '@smithy/protocol-http': 5.3.7 '@smithy/signature-v4': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 '@smithy/util-config-provider': 4.2.0 '@smithy/util-middleware': 4.2.7 @@ -9161,66 +10833,66 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-sqs@3.957.0': + '@aws-sdk/middleware-sdk-sqs@3.966.0': dependencies: - '@aws-sdk/types': 3.957.0 - '@smithy/smithy-client': 4.10.2 + '@aws-sdk/types': 3.965.0 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 '@smithy/util-hex-encoding': 4.2.0 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-ssec@3.957.0': + '@aws-sdk/middleware-ssec@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.957.0': + '@aws-sdk/middleware-user-agent@3.966.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-endpoints': 3.957.0 - '@smithy/core': 3.20.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@smithy/core': 3.20.2 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.958.0': + '@aws-sdk/nested-clients@3.966.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/middleware-host-header': 3.957.0 - '@aws-sdk/middleware-logger': 3.957.0 - '@aws-sdk/middleware-recursion-detection': 3.957.0 - '@aws-sdk/middleware-user-agent': 3.957.0 - '@aws-sdk/region-config-resolver': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-endpoints': 3.957.0 - '@aws-sdk/util-user-agent-browser': 3.957.0 - '@aws-sdk/util-user-agent-node': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.966.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.966.0 '@smithy/config-resolver': 4.4.5 - '@smithy/core': 3.20.0 + '@smithy/core': 3.20.2 '@smithy/fetch-http-handler': 5.3.8 '@smithy/hash-node': 4.2.7 '@smithy/invalid-dependency': 4.2.7 '@smithy/middleware-content-length': 4.2.7 - '@smithy/middleware-endpoint': 4.4.1 - '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-endpoint': 4.4.3 + '@smithy/middleware-retry': 4.4.19 '@smithy/middleware-serde': 4.2.8 '@smithy/middleware-stack': 4.2.7 '@smithy/node-config-provider': 4.3.7 '@smithy/node-http-handler': 4.4.7 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 '@smithy/url-parser': 4.2.7 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.16 - '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-defaults-mode-browser': 4.3.18 + '@smithy/util-defaults-mode-node': 4.2.21 '@smithy/util-endpoints': 3.2.7 '@smithy/util-middleware': 4.2.7 '@smithy/util-retry': 4.2.7 @@ -9229,28 +10901,28 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.957.0': + '@aws-sdk/region-config-resolver@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/config-resolver': 4.4.5 '@smithy/node-config-provider': 4.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.957.0': + '@aws-sdk/signature-v4-multi-region@3.966.0': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/middleware-sdk-s3': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/protocol-http': 5.3.7 '@smithy/signature-v4': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.958.0': + '@aws-sdk/token-providers@3.966.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/nested-clients': 3.958.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.966.0 + '@aws-sdk/nested-clients': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 '@smithy/types': 4.11.0 @@ -9258,49 +10930,53 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.957.0': + '@aws-sdk/types@3.965.0': dependencies: '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/util-arn-parser@3.957.0': + '@aws-sdk/util-arn-parser@3.966.0': dependencies: tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.957.0': + '@aws-sdk/util-endpoints@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 '@smithy/url-parser': 4.2.7 '@smithy/util-endpoints': 3.2.7 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.957.0': + '@aws-sdk/util-locate-window@3.965.0': dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.957.0': + '@aws-sdk/util-user-agent-browser@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 bowser: 2.13.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.957.0': + '@aws-sdk/util-user-agent-node@3.966.0': dependencies: - '@aws-sdk/middleware-user-agent': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/middleware-user-agent': 3.966.0 + '@aws-sdk/types': 3.965.0 '@smithy/node-config-provider': 4.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.957.0': + '@aws-sdk/xml-builder@3.965.0': dependencies: '@smithy/types': 4.11.0 fast-xml-parser: 5.2.5 tslib: 2.8.1 - '@aws/lambda-invoke-store@0.2.2': {} + '@aws/lambda-invoke-store@0.2.3': {} + + '@babel/code-frame@7.10.4': + dependencies: + '@babel/highlight': 7.25.9 '@babel/code-frame@7.27.1': dependencies: @@ -9456,6 +11132,13 @@ snapshots: '@babel/template': 7.27.2 '@babel/types': 7.28.5 + '@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.28.5': dependencies: '@babel/types': 7.28.5 @@ -9495,70 +11178,174 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5)': + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.5)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.5)': + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-export-default-from@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.28.5)': + '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) @@ -9640,6 +11427,12 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -9851,6 +11644,18 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-runtime@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.5) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.5) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.5) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -10044,8 +11849,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@balena/dockerignore@1.0.2': {} - '@bcoe/v8-coverage@1.0.2': {} '@biomejs/biome@2.3.11': @@ -10105,7 +11908,7 @@ snapshots: dependencies: '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-syntax-patches-for-csstree@1.0.22': {} + '@csstools/css-syntax-patches-for-csstree@1.0.23': {} '@csstools/css-tokenizer@3.0.4': {} @@ -10147,6 +11950,10 @@ snapshots: '@drizzle-team/brocli@0.10.2': {} + '@egjs/hammerjs@2.0.17': + dependencies: + '@types/hammerjs': 2.0.46 + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 @@ -10388,6 +12195,308 @@ snapshots: '@exodus/schemasafe@1.3.0': {} + '@expo-google-fonts/space-grotesk@0.2.3': {} + + '@expo/cli@54.0.21(expo-router@6.0.21)(expo@54.0.31)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))': + dependencies: + '@0no-co/graphql.web': 1.2.0(graphql@16.12.0) + '@expo/code-signing-certificates': 0.0.6 + '@expo/config': 12.0.13 + '@expo/config-plugins': 54.0.4 + '@expo/devcert': 1.2.1 + '@expo/env': 2.0.8 + '@expo/image-utils': 0.8.8 + '@expo/json-file': 10.0.8 + '@expo/metro': 54.2.0 + '@expo/metro-config': 54.0.13(expo@54.0.31) + '@expo/osascript': 2.3.8 + '@expo/package-manager': 1.9.9 + '@expo/plist': 0.4.8 + '@expo/prebuild-config': 54.0.8(expo@54.0.31) + '@expo/schema-utils': 0.1.8 + '@expo/spawn-async': 1.7.2 + '@expo/ws-tunnel': 1.0.6 + '@expo/xcpretty': 4.3.2 + '@react-native/dev-middleware': 0.81.5 + '@urql/core': 5.2.0(graphql@16.12.0) + '@urql/exchange-retry': 1.3.2(@urql/core@5.2.0(graphql@16.12.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 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-server: 1.0.5 + freeport-async: 2.0.0 + getenv: 2.0.0 + glob: 13.0.0 + lan-network: 0.1.7 + minimatch: 9.0.5 + node-forge: 1.3.3 + npm-package-arg: 11.0.3 + ora: 3.4.0 + picomatch: 3.0.1 + 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.11 + resolve-from: 5.0.0 + resolve.exports: 2.0.3 + semver: 7.7.3 + send: 0.19.2 + slugify: 1.6.6 + source-map-support: 0.5.21 + stacktrace-parser: 0.1.11 + structured-headers: 0.4.1 + tar: 7.5.2 + terminal-link: 2.1.1 + undici: 6.23.0 + wrap-ansi: 7.0.0 + ws: 8.19.0 + optionalDependencies: + expo-router: 6.0.21(ed0173313472e3a4d9a27b966543cc3e) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - bufferutil + - graphql + - supports-color + - utf-8-validate + + '@expo/code-signing-certificates@0.0.6': + dependencies: + node-forge: 1.3.3 + + '@expo/config-plugins@54.0.4': + dependencies: + '@expo/config-types': 54.0.10 + '@expo/json-file': 10.0.8 + '@expo/plist': 0.4.8 + '@expo/sdk-runtime-versions': 1.0.0 + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + glob: 13.0.0 + resolve-from: 5.0.0 + semver: 7.7.3 + slash: 3.0.0 + slugify: 1.6.6 + xcode: 3.0.1 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + + '@expo/config-types@54.0.10': {} + + '@expo/config@12.0.13': + dependencies: + '@babel/code-frame': 7.10.4 + '@expo/config-plugins': 54.0.4 + '@expo/config-types': 54.0.10 + '@expo/json-file': 10.0.8 + deepmerge: 4.3.1 + getenv: 2.0.0 + glob: 13.0.0 + require-from-string: 2.0.2 + resolve-from: 5.0.0 + resolve-workspace-root: 2.0.1 + semver: 7.7.3 + slugify: 1.6.6 + sucrase: 3.35.1 + transitivePeerDependencies: + - supports-color + + '@expo/devcert@1.2.1': + dependencies: + '@expo/sudo-prompt': 9.3.2 + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + + '@expo/devtools@0.1.8(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + chalk: 4.1.2 + optionalDependencies: + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + + '@expo/env@2.0.8': + 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.15.4': + dependencies: + '@expo/spawn-async': 1.7.2 + arg: 5.0.2 + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + glob: 13.0.0 + ignore: 5.3.2 + minimatch: 9.0.5 + p-limit: 3.1.0 + resolve-from: 5.0.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + '@expo/image-utils@0.8.8': + 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 + resolve-global: 1.0.0 + semver: 7.7.3 + temp-dir: 2.0.0 + unique-string: 2.0.0 + + '@expo/json-file@10.0.8': + dependencies: + '@babel/code-frame': 7.10.4 + json5: 2.2.3 + + '@expo/metro-config@54.0.13(expo@54.0.31)': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@expo/config': 12.0.13 + '@expo/env': 2.0.8 + '@expo/json-file': 10.0.8 + '@expo/metro': 54.2.0 + '@expo/spawn-async': 1.7.2 + browserslist: 4.28.1 + chalk: 4.1.2 + debug: 4.4.3 + dotenv: 16.4.7 + dotenv-expand: 11.0.7 + getenv: 2.0.0 + glob: 13.0.0 + hermes-parser: 0.29.1 + jsc-safe-url: 0.2.4 + lightningcss: 1.30.2 + minimatch: 9.0.5 + postcss: 8.4.49 + resolve-from: 5.0.0 + optionalDependencies: + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@expo/metro-runtime@6.1.2(expo@54.0.31)(react-dom@18.3.1(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + anser: 1.4.10 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + pretty-format: 29.7.0 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + '@expo/metro@54.2.0': + dependencies: + metro: 0.83.3 + metro-babel-transformer: 0.83.3 + metro-cache: 0.83.3 + metro-cache-key: 0.83.3 + metro-config: 0.83.3 + metro-core: 0.83.3 + metro-file-map: 0.83.3 + metro-minify-terser: 0.83.3 + metro-resolver: 0.83.3 + metro-runtime: 0.83.3 + metro-source-map: 0.83.3 + metro-symbolicate: 0.83.3 + metro-transform-plugins: 0.83.3 + metro-transform-worker: 0.83.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@expo/osascript@2.3.8': + dependencies: + '@expo/spawn-async': 1.7.2 + exec-async: 2.2.0 + + '@expo/package-manager@1.9.9': + dependencies: + '@expo/json-file': 10.0.8 + '@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.4.8': + dependencies: + '@xmldom/xmldom': 0.8.11 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + '@expo/prebuild-config@54.0.8(expo@54.0.31)': + dependencies: + '@expo/config': 12.0.13 + '@expo/config-plugins': 54.0.4 + '@expo/config-types': 54.0.10 + '@expo/image-utils': 0.8.8 + '@expo/json-file': 10.0.8 + '@react-native/normalize-colors': 0.81.5 + debug: 4.4.3 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + resolve-from: 5.0.0 + semver: 7.7.3 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + + '@expo/schema-utils@0.1.8': {} + + '@expo/sdk-runtime-versions@1.0.0': {} + + '@expo/spawn-async@1.7.2': + dependencies: + cross-spawn: 7.0.6 + + '@expo/sudo-prompt@9.3.2': {} + + '@expo/vector-icons@15.0.3(expo-font@14.0.10(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + expo-font: 14.0.10(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + + '@expo/ws-tunnel@1.0.6': {} + + '@expo/xcpretty@4.3.2': + dependencies: + '@babel/code-frame': 7.10.4 + chalk: 4.1.2 + find-up: 5.0.0 + js-yaml: 4.1.1 + '@faker-js/faker@10.2.0': {} '@fastify/accept-negotiator@2.0.1': {} @@ -10443,7 +12552,7 @@ snapshots: '@fastify/redis@7.1.0': dependencies: fastify-plugin: 5.1.0 - ioredis: 5.8.2 + ioredis: 5.9.1 transitivePeerDependencies: - supports-color @@ -10499,42 +12608,23 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@formatjs/fast-memoize@3.0.2': + '@formatjs/fast-memoize@3.0.3': dependencies: tslib: 2.8.1 - '@formatjs/intl-localematcher@0.7.4': + '@formatjs/intl-localematcher@0.7.5': dependencies: - '@formatjs/fast-memoize': 3.0.2 + '@formatjs/fast-memoize': 3.0.3 tslib: 2.8.1 - '@gerrit0/mini-shiki@3.20.0': + '@gerrit0/mini-shiki@3.21.0': dependencies: - '@shikijs/engine-oniguruma': 3.20.0 - '@shikijs/langs': 3.20.0 - '@shikijs/themes': 3.20.0 - '@shikijs/types': 3.20.0 + '@shikijs/engine-oniguruma': 3.21.0 + '@shikijs/langs': 3.21.0 + '@shikijs/themes': 3.21.0 + '@shikijs/types': 3.21.0 '@shikijs/vscode-textmate': 10.0.2 - '@grpc/grpc-js@1.14.3': - dependencies: - '@grpc/proto-loader': 0.8.0 - '@js-sdsl/ordered-map': 4.4.2 - - '@grpc/proto-loader@0.7.15': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.5.4 - yargs: 17.7.2 - - '@grpc/proto-loader@0.8.0': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.5.4 - yargs: 17.7.2 - '@hookform/resolvers@5.2.2(react-hook-form@7.70.0(react@19.2.3))': dependencies: '@standard-schema/utils': 0.3.0 @@ -10685,8 +12775,6 @@ snapshots: '@ioredis/as-callback@3.0.0': {} - '@ioredis/commands@1.4.0': {} - '@ioredis/commands@1.5.0': {} '@isaacs/balanced-match@4.0.1': {} @@ -10704,8 +12792,75 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@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.3': {} + '@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': 25.0.3 + 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': 25.0.3 + 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.8 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.28.5 + '@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': 25.0.3 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -10718,6 +12873,11 @@ snapshots: '@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': @@ -10725,8 +12885,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@js-sdsl/ordered-map@4.4.2': {} - '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': dependencies: jsep: 1.4.0 @@ -10988,9 +13146,6 @@ snapshots: '@pinojs/redact@0.4.0': {} - '@pkgjs/parseargs@0.11.0': - optional: true - '@plotwist_app/tmdb@0.2.5(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2)': dependencies: axios: 1.13.2 @@ -11009,29 +13164,6 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.4': {} - - '@protobufjs/eventemitter@1.1.0': {} - - '@protobufjs/fetch@1.1.0': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/inquire@1.1.0': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.0': {} - '@puppeteer/browsers@2.11.0': dependencies: debug: 4.4.3 @@ -11145,6 +13277,18 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) @@ -11157,6 +13301,12 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 @@ -11177,6 +13327,12 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-context@1.1.2(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-context@1.1.2(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 @@ -11189,6 +13345,28 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -11211,12 +13389,31 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-direction@1.1.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -11245,12 +13442,29 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) @@ -11283,6 +13497,13 @@ snapshots: dependencies: react: 19.2.3 + '@radix-ui/react-id@1.1.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.2.3)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) @@ -11406,6 +13627,16 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -11416,6 +13647,16 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) @@ -11426,6 +13667,15 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) @@ -11472,6 +13722,23 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -11563,6 +13830,20 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-slot@1.2.0(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-slot@1.2.3(@types/react@19.2.7)(react@19.2.3)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) @@ -11592,6 +13873,22 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -11674,12 +13971,26 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.7)(react@19.2.3)': dependencies: '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.7)(react@19.2.3) @@ -11688,6 +13999,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.7)(react@19.2.3)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) @@ -11695,6 +14013,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.7)(react@19.2.3)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) @@ -11709,6 +14034,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 @@ -11767,7 +14098,7 @@ snapshots: dependencies: react: 19.2.3 - '@react-email/components@1.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@react-email/components@1.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@react-email/body': 0.2.1(react@19.2.3) '@react-email/button': 0.2.1(react@19.2.3) @@ -11784,7 +14115,7 @@ snapshots: '@react-email/link': 0.0.13(react@19.2.3) '@react-email/markdown': 0.0.18(react@19.2.3) '@react-email/preview': 0.0.14(react@19.2.3) - '@react-email/render': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-email/render': 2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-email/row': 0.0.13(react@19.2.3) '@react-email/section': 0.0.17(react@19.2.3) '@react-email/tailwind': 2.0.3(@react-email/body@0.2.1(react@19.2.3))(@react-email/button@0.2.1(react@19.2.3))(@react-email/code-block@0.2.1(react@19.2.3))(@react-email/code-inline@0.0.6(react@19.2.3))(@react-email/container@0.0.16(react@19.2.3))(@react-email/heading@0.0.16(react@19.2.3))(@react-email/hr@0.0.12(react@19.2.3))(@react-email/img@0.0.12(react@19.2.3))(@react-email/link@0.0.13(react@19.2.3))(@react-email/preview@0.0.14(react@19.2.3))(@react-email/text@0.1.6(react@19.2.3))(react@19.2.3) @@ -11834,7 +14165,7 @@ snapshots: dependencies: react: 19.2.3 - '@react-email/render@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@react-email/render@2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: html-to-text: 9.0.5 prettier: 3.7.4 @@ -11870,6 +14201,190 @@ snapshots: dependencies: react: 19.2.3 + '@react-native/assets-registry@0.81.5': {} + + '@react-native/babel-plugin-codegen@0.81.5(@babel/core@7.28.5)': + dependencies: + '@babel/traverse': 7.28.5 + '@react-native/codegen': 0.81.5(@babel/core@7.28.5) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@react-native/babel-preset@0.81.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.5) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-block-scoping': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-logical-assignment-operators': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.5) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-runtime': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.5) + '@babel/template': 7.27.2 + '@react-native/babel-plugin-codegen': 0.81.5(@babel/core@7.28.5) + babel-plugin-syntax-hermes-parser: 0.29.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.5) + react-refresh: 0.14.2 + transitivePeerDependencies: + - supports-color + + '@react-native/codegen@0.81.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + glob: 7.2.3 + hermes-parser: 0.29.1 + invariant: 2.2.4 + nullthrows: 1.1.1 + yargs: 17.7.2 + + '@react-native/community-cli-plugin@0.81.5': + dependencies: + '@react-native/dev-middleware': 0.81.5 + debug: 4.4.3 + invariant: 2.2.4 + metro: 0.83.3 + metro-config: 0.83.3 + metro-core: 0.83.3 + semver: 7.7.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/debugger-frontend@0.81.5': {} + + '@react-native/dev-middleware@0.81.5': + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@react-native/debugger-frontend': 0.81.5 + chrome-launcher: 0.15.2 + chromium-edge-launcher: 0.2.0 + connect: 3.7.0 + debug: 4.4.3 + 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.81.5': {} + + '@react-native/js-polyfills@0.81.5': {} + + '@react-native/normalize-colors@0.74.89': {} + + '@react-native/normalize-colors@0.81.5': {} + + '@react-native/virtualized-lists@0.81.5(@types/react@18.3.27)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + + '@react-navigation/bottom-tabs@7.9.0(@react-navigation/native@7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-navigation/elements': 2.9.3(@react-navigation/native@7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-navigation/native': 7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + color: 4.2.3 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + sf-symbols-typescript: 2.2.0 + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + + '@react-navigation/core@7.13.7(react@18.3.1)': + dependencies: + '@react-navigation/routers': 7.5.3 + escape-string-regexp: 4.0.0 + fast-deep-equal: 3.1.3 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 18.3.1 + react-is: 19.2.3 + use-latest-callback: 0.2.6(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + + '@react-navigation/elements@2.9.3(@react-navigation/native@7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-navigation/native': 7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + color: 4.2.3 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + use-latest-callback: 0.2.6(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + + '@react-navigation/native-stack@7.9.0(@react-navigation/native@7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-navigation/elements': 2.9.3(@react-navigation/native@7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-navigation/native': 7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + color: 4.2.3 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + sf-symbols-typescript: 2.2.0 + warn-once: 0.1.1 + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + + '@react-navigation/native@7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-navigation/core': 7.13.7(react@18.3.1) + escape-string-regexp: 4.0.0 + fast-deep-equal: 3.1.3 + nanoid: 3.3.11 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + use-latest-callback: 0.2.6(react@18.3.1) + + '@react-navigation/routers@7.5.3': + dependencies: + nanoid: 3.3.11 + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': dependencies: '@standard-schema/spec': 1.1.0 @@ -11884,78 +14399,87 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.53': {} - '@rollup/pluginutils@5.3.0(rollup@4.54.0)': + '@rollup/pluginutils@5.3.0(rollup@4.55.1)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.54.0 + rollup: 4.55.1 + + '@rollup/rollup-android-arm-eabi@4.55.1': + optional: true + + '@rollup/rollup-android-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.55.1': + optional: true - '@rollup/rollup-android-arm-eabi@4.54.0': + '@rollup/rollup-darwin-x64@4.55.1': optional: true - '@rollup/rollup-android-arm64@4.54.0': + '@rollup/rollup-freebsd-arm64@4.55.1': optional: true - '@rollup/rollup-darwin-arm64@4.54.0': + '@rollup/rollup-freebsd-x64@4.55.1': optional: true - '@rollup/rollup-darwin-x64@4.54.0': + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': optional: true - '@rollup/rollup-freebsd-arm64@4.54.0': + '@rollup/rollup-linux-arm-musleabihf@4.55.1': optional: true - '@rollup/rollup-freebsd-x64@4.54.0': + '@rollup/rollup-linux-arm64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + '@rollup/rollup-linux-arm64-musl@4.55.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.54.0': + '@rollup/rollup-linux-loong64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.54.0': + '@rollup/rollup-linux-loong64-musl@4.55.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.54.0': + '@rollup/rollup-linux-ppc64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-loong64-gnu@4.54.0': + '@rollup/rollup-linux-ppc64-musl@4.55.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.54.0': + '@rollup/rollup-linux-riscv64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.54.0': + '@rollup/rollup-linux-riscv64-musl@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.54.0': + '@rollup/rollup-linux-s390x-gnu@4.55.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.54.0': + '@rollup/rollup-linux-x64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.54.0': + '@rollup/rollup-linux-x64-musl@4.55.1': optional: true - '@rollup/rollup-linux-x64-musl@4.54.0': + '@rollup/rollup-openbsd-x64@4.55.1': optional: true - '@rollup/rollup-openharmony-arm64@4.54.0': + '@rollup/rollup-openharmony-arm64@4.55.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.54.0': + '@rollup/rollup-win32-arm64-msvc@4.55.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.54.0': + '@rollup/rollup-win32-ia32-msvc@4.55.1': optional: true - '@rollup/rollup-win32-x64-gnu@4.54.0': + '@rollup/rollup-win32-x64-gnu@4.55.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.54.0': + '@rollup/rollup-win32-x64-msvc@4.55.1': optional: true '@selderee/plugin-htmlparser2@0.11.0': @@ -11963,26 +14487,36 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 - '@shikijs/engine-oniguruma@3.20.0': + '@shikijs/engine-oniguruma@3.21.0': dependencies: - '@shikijs/types': 3.20.0 + '@shikijs/types': 3.21.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.20.0': + '@shikijs/langs@3.21.0': dependencies: - '@shikijs/types': 3.20.0 + '@shikijs/types': 3.21.0 - '@shikijs/themes@3.20.0': + '@shikijs/themes@3.21.0': dependencies: - '@shikijs/types': 3.20.0 + '@shikijs/types': 3.21.0 - '@shikijs/types@3.20.0': + '@shikijs/types@3.21.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 '@shikijs/vscode-textmate@10.0.2': {} + '@sinclair/typebox@0.27.8': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + '@smithy/abort-controller@4.2.7': dependencies: '@smithy/types': 4.11.0 @@ -12006,7 +14540,7 @@ snapshots: '@smithy/util-middleware': 4.2.7 tslib: 2.8.1 - '@smithy/core@3.20.0': + '@smithy/core@3.20.2': dependencies: '@smithy/middleware-serde': 4.2.8 '@smithy/protocol-http': 5.3.7 @@ -12110,9 +14644,9 @@ snapshots: '@smithy/types': 4.11.0 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.1': + '@smithy/middleware-endpoint@4.4.3': dependencies: - '@smithy/core': 3.20.0 + '@smithy/core': 3.20.2 '@smithy/middleware-serde': 4.2.8 '@smithy/node-config-provider': 4.3.7 '@smithy/shared-ini-file-loader': 4.4.2 @@ -12121,12 +14655,12 @@ snapshots: '@smithy/util-middleware': 4.2.7 tslib: 2.8.1 - '@smithy/middleware-retry@4.4.17': + '@smithy/middleware-retry@4.4.19': dependencies: '@smithy/node-config-provider': 4.3.7 '@smithy/protocol-http': 5.3.7 '@smithy/service-error-classification': 4.2.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 '@smithy/util-middleware': 4.2.7 '@smithy/util-retry': 4.2.7 @@ -12200,10 +14734,10 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/smithy-client@4.10.2': + '@smithy/smithy-client@4.10.4': dependencies: - '@smithy/core': 3.20.0 - '@smithy/middleware-endpoint': 4.4.1 + '@smithy/core': 3.20.2 + '@smithy/middleware-endpoint': 4.4.3 '@smithy/middleware-stack': 4.2.7 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 @@ -12248,20 +14782,20 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.16': + '@smithy/util-defaults-mode-browser@4.3.18': dependencies: '@smithy/property-provider': 4.2.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.19': + '@smithy/util-defaults-mode-node@4.2.21': dependencies: '@smithy/config-resolver': 4.4.5 '@smithy/credential-provider-imds': 4.2.7 '@smithy/node-config-provider': 4.3.7 '@smithy/property-provider': 4.2.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.4 '@smithy/types': 4.11.0 tslib: 2.8.1 @@ -12494,7 +15028,7 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@stripe/stripe-js@8.6.0': {} + '@stripe/stripe-js@8.6.1': {} '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': dependencies: @@ -12675,15 +15209,15 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@tanstack/react-virtual@3.13.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-virtual@3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/virtual-core': 3.13.16 + '@tanstack/virtual-core': 3.13.18 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.16': {} + '@tanstack/virtual-core@3.13.18': {} '@testing-library/dom@10.4.1': dependencies: @@ -12799,17 +15333,6 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/docker-modem@3.0.6': - dependencies: - '@types/node': 25.0.3 - '@types/ssh2': 1.15.5 - - '@types/dockerode@3.3.47': - dependencies: - '@types/docker-modem': 3.0.6 - '@types/node': 25.0.3 - '@types/ssh2': 1.15.5 - '@types/es-aggregate-error@1.0.6': dependencies: '@types/node': 25.0.3 @@ -12822,13 +15345,29 @@ snapshots: '@types/geojson@7946.0.16': {} + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 25.0.3 + + '@types/hammerjs@2.0.46': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 - '@types/ioredis-mock@8.2.6(ioredis@5.8.2)': + '@types/ioredis-mock@8.2.6(ioredis@5.9.1)': + dependencies: + ioredis: 5.9.1 + + '@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: - ioredis: 5.8.2 + '@types/istanbul-lib-report': 3.0.3 '@types/json-schema@7.0.15': {} @@ -12852,14 +15391,6 @@ snapshots: '@types/node-cron@3.0.11': {} - '@types/node@18.19.130': - dependencies: - undici-types: 5.26.5 - - '@types/node@22.19.3': - dependencies: - undici-types: 6.21.0 - '@types/node@24.10.4': dependencies: undici-types: 7.16.0 @@ -12870,6 +15401,13 @@ snapshots: '@types/nprogress@0.2.3': {} + '@types/prop-types@15.7.15': {} + + '@types/react-dom@19.2.3(@types/react@18.3.27)': + dependencies: + '@types/react': 18.3.27 + optional: true + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -12881,26 +15419,20 @@ snapshots: '@types/geojson': 7946.0.16 '@types/react': 19.2.7 - '@types/react@19.2.7': + '@types/react@18.3.27': dependencies: + '@types/prop-types': 15.7.15 csstype: 3.2.3 - '@types/sax@1.2.7': - dependencies: - '@types/node': 25.0.3 - - '@types/ssh2-streams@0.1.13': + '@types/react@19.2.7': dependencies: - '@types/node': 25.0.3 + csstype: 3.2.3 - '@types/ssh2@0.5.52': + '@types/sax@1.2.7': dependencies: '@types/node': 25.0.3 - '@types/ssh2-streams': 0.1.13 - '@types/ssh2@1.15.5': - dependencies: - '@types/node': 18.19.130 + '@types/stack-utils@2.0.3': {} '@types/statuses@2.0.6': {} @@ -12912,6 +15444,12 @@ snapshots: '@types/use-sync-external-store@0.0.6': {} + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + '@types/yauzl@2.10.3': dependencies: '@types/node': 25.0.3 @@ -12924,7 +15462,19 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@urql/core@5.2.0(graphql@16.12.0)': + dependencies: + '@0no-co/graphql.web': 1.2.0(graphql@16.12.0) + wonka: 6.3.5 + transitivePeerDependencies: + - graphql + + '@urql/exchange-retry@1.3.2(@urql/core@5.2.0(graphql@16.12.0))': + dependencies: + '@urql/core': 5.2.0(graphql@16.12.0) + wonka: 6.3.5 + + '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -12932,7 +15482,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -12949,7 +15499,7 @@ snapshots: magicast: 0.5.1 obug: 2.1.1 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -12966,7 +15516,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -12979,14 +15529,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.16(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.16(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.16 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.7(@types/node@25.0.3)(typescript@5.9.3) - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.16': dependencies: @@ -13014,13 +15564,15 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/utils@4.0.16': dependencies: '@vitest/pretty-format': 4.0.16 tinyrainbow: 3.0.3 + '@xmldom/xmldom@0.8.11': {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -13069,12 +15621,24 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + anser@1.4.10: {} + ansi-colors@4.1.3: {} + 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 @@ -13090,30 +15654,11 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - 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.17.21 - normalize-path: 3.0.0 - readable-stream: 4.7.0 + arg@5.0.2: {} - archiver@7.0.1: + argparse@1.0.10: 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.1.7 - zip-stream: 6.0.1 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - arg@5.0.2: {} + sprintf-js: 1.0.3 argparse@2.0.1: {} @@ -13130,6 +15675,8 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 + array-timsort@1.0.3: {} + array-union@2.1.0: {} arraybuffer.prototype.slice@1.0.4: @@ -13142,6 +15689,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + asap@2.0.6: {} + asn1.js@5.4.1: dependencies: bn.js: 4.12.2 @@ -13149,10 +15698,6 @@ snapshots: minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 - asn1@0.2.6: - dependencies: - safer-buffer: 2.1.2 - assertion-error@2.0.1: {} ast-types@0.13.4: @@ -13169,9 +15714,7 @@ snapshots: async-function@1.0.0: {} - async-lock@1.4.1: {} - - async@3.2.6: {} + async-limiter@1.0.1: {} asynckit@0.4.0: {} @@ -13185,7 +15728,7 @@ snapshots: autoprefixer@10.4.23(postcss@8.5.6): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001762 + caniuse-lite: 1.0.30001763 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 @@ -13210,6 +15753,36 @@ snapshots: b4a@1.7.3: {} + babel-jest@29.7.0(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + '@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.28.5) + 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.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + 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.27.2 + '@babel/types': 7.28.5 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.5): dependencies: '@babel/compat-data': 7.28.5 @@ -13234,6 +15807,79 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-react-compiler@1.0.0: + dependencies: + '@babel/types': 7.28.5 + + babel-plugin-react-native-web@0.21.2: {} + + babel-plugin-syntax-hermes-parser@0.29.1: + dependencies: + hermes-parser: 0.29.1 + + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.28.5): + dependencies: + '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.5) + transitivePeerDependencies: + - '@babel/core' + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.5) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.5) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) + + babel-preset-expo@54.0.9(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.31)(react-refresh@0.14.2): + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.5) + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.28.5) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.5) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-runtime': 7.28.5(@babel/core@7.28.5) + '@babel/preset-react': 7.28.5(@babel/core@7.28.5) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + '@react-native/babel-preset': 0.81.5(@babel/core@7.28.5) + babel-plugin-react-compiler: 1.0.0 + babel-plugin-react-native-web: 0.21.2 + babel-plugin-syntax-hermes-parser: 0.29.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.5) + debug: 4.4.3 + react-refresh: 0.14.2 + resolve-from: 5.0.0 + optionalDependencies: + '@babel/runtime': 7.28.4 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + babel-preset-jest@29.6.3(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -13279,27 +15925,23 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.9.11: {} + baseline-browser-mapping@2.9.14: {} basic-ftp@5.1.0: {} - bcrypt-pbkdf@1.0.2: - dependencies: - tweetnacl: 0.14.5 - bcryptjs@3.0.3: {} + better-opn@3.0.2: + dependencies: + open: 8.4.2 + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 - binary-extensions@2.3.0: {} + big-integer@1.6.52: {} - bl@4.1.0: - dependencies: - buffer: 5.6.0 - inherits: 2.0.4 - readable-stream: 3.6.2 + binary-extensions@2.3.0: {} bn.js@4.12.2: {} @@ -13307,6 +15949,18 @@ snapshots: bowser@2.13.1: {} + 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.12: dependencies: balanced-match: 1.0.2 @@ -13322,15 +15976,17 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.11 - caniuse-lite: 1.0.30001762 + baseline-browser-mapping: 2.9.14 + caniuse-lite: 1.0.30001763 electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) - buffer-crc32@0.2.13: {} + bser@2.1.1: + dependencies: + node-int64: 0.4.0 - buffer-crc32@1.0.0: {} + buffer-crc32@0.2.13: {} buffer-from@1.1.2: {} @@ -13339,20 +15995,17 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - buffer@6.0.3: + buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - buildcheck@0.0.7: - optional: true - bundle-require@5.1.0(esbuild@0.27.2): dependencies: esbuild: 0.27.2 load-tsconfig: 0.2.5 - byline@5.0.0: {} + bytes@3.1.2: {} cac@6.7.14: {} @@ -13379,9 +16032,11 @@ snapshots: camelcase-css@2.0.1: {} + camelcase@5.3.1: {} + camelcase@6.3.0: {} - caniuse-lite@1.0.30001762: {} + caniuse-lite@1.0.30001763: {} canvas-confetti@1.9.4: {} @@ -13389,6 +16044,12 @@ snapshots: chai@6.2.2: {} + 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 @@ -13426,7 +16087,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.16.0 + undici: 7.18.2 whatwg-mimetype: 4.0.0 chokidar@3.6.0: @@ -13445,7 +16106,16 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@1.1.4: {} + chownr@3.0.0: {} + + chrome-launcher@0.15.2: + dependencies: + '@types/node': 25.0.3 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + transitivePeerDependencies: + - supports-color chromium-bidi@12.0.1(devtools-protocol@0.0.1534754): dependencies: @@ -13453,6 +16123,21 @@ snapshots: mitt: 3.0.1 zod: 3.25.76 + chromium-edge-launcher@0.2.0: + dependencies: + '@types/node': 25.0.3 + 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 @@ -13463,6 +16148,10 @@ snapshots: classnames@2.5.1: {} + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -13479,6 +16168,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone@1.0.4: {} + clsx@2.1.1: {} cluster-key-slot@1.1.2: {} @@ -13501,12 +16192,28 @@ snapshots: collapse-white-space@2.1.0: {} + 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 + colorette@2.0.20: {} combined-stream@1.0.8: @@ -13515,6 +16222,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@12.1.0: {} + commander@13.1.0: {} commander@14.0.2: {} @@ -13525,15 +16234,29 @@ snapshots: commander@7.2.0: {} + comment-json@4.5.1: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + compare-versions@6.1.1: {} - compress-commons@6.0.2: + compressible@2.0.18: 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 + 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: {} @@ -13553,6 +16276,15 @@ snapshots: confbox@0.2.2: {} + 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: {} content-disposition@1.0.1: {} @@ -13594,18 +16326,11 @@ snapshots: countup.js@2.9.0: {} - cpu-features@0.0.10: - dependencies: - buildcheck: 0.0.7 - nan: 2.24.0 - optional: true - - crc-32@1.2.2: {} - - crc32-stream@6.0.0: + cross-fetch@3.2.0: dependencies: - crc-32: 1.2.2 - readable-stream: 4.7.0 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding cross-spawn@7.0.6: dependencies: @@ -13613,6 +16338,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-random-string@2.0.0: {} + + css-in-js-utils@3.1.0: + dependencies: + hyphenate-style-name: 1.1.0 + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -13621,6 +16352,11 @@ snapshots: 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-tree@2.2.1: dependencies: mdn-data: 2.0.28 @@ -13644,10 +16380,10 @@ snapshots: dependencies: css-tree: 2.2.1 - cssstyle@5.3.6: + cssstyle@5.3.7: dependencies: '@asamuzakjp/css-color': 4.1.1 - '@csstools/css-syntax-patches-for-csstree': 1.0.22 + '@csstools/css-syntax-patches-for-csstree': 1.0.23 css-tree: 3.1.0 lru-cache: 11.2.4 @@ -13774,6 +16510,14 @@ snapshots: debounce@2.2.0: {} + 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 @@ -13786,14 +16530,24 @@ snapshots: dependencies: character-entities: 2.0.2 + decode-uri-component@0.2.2: {} + + deep-extend@0.6.0: {} + deepmerge@4.3.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-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -13816,6 +16570,10 @@ snapshots: dequal@2.0.3: {} + destroy@1.2.0: {} + + detect-libc@1.0.3: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -13834,31 +16592,6 @@ snapshots: dlv@1.1.3: {} - docker-compose@1.3.0: - dependencies: - yaml: 2.8.2 - - docker-modem@5.0.6: - dependencies: - debug: 4.4.3 - readable-stream: 3.6.2 - split-ca: 1.0.1 - ssh2: 1.17.0 - transitivePeerDependencies: - - supports-color - - dockerode@4.0.9: - dependencies: - '@balena/dockerignore': 1.0.2 - '@grpc/grpc-js': 1.14.3 - '@grpc/proto-loader': 0.7.15 - docker-modem: 5.0.6 - protobufjs: 7.5.4 - tar-fs: 2.1.4 - uuid: 10.0.0 - transitivePeerDependencies: - - supports-color - dom-accessibility-api@0.5.16: {} dom-serializer@2.0.0: @@ -13895,10 +16628,16 @@ snapshots: dotenv-expand: 12.0.3 minimist: 1.2.8 + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.4.7 + dotenv-expand@12.0.3: dependencies: dotenv: 16.6.1 + dotenv@16.4.7: {} + dotenv@16.6.1: {} dotenv@17.2.3: {} @@ -13912,13 +16651,13 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.45.1(postgres@3.4.7): + drizzle-orm@0.45.1(postgres@3.4.8): optionalDependencies: - postgres: 3.4.7 + postgres: 3.4.8 - drizzle-zod@0.8.3(drizzle-orm@0.45.1(postgres@3.4.7))(zod@4.3.5): + drizzle-zod@0.8.3(drizzle-orm@0.45.1(postgres@3.4.8))(zod@4.3.5): dependencies: - drizzle-orm: 0.45.1(postgres@3.4.7) + drizzle-orm: 0.45.1(postgres@3.4.8) zod: 4.3.5 dunder-proto@1.0.1: @@ -13935,6 +16674,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + ee-first@1.1.1: {} + electron-to-chromium@1.5.267: {} embla-carousel-react@8.6.0(react@19.2.3): @@ -13955,6 +16696,10 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + encoding-sniffer@0.2.1: dependencies: iconv-lite: 0.6.3 @@ -13991,6 +16736,8 @@ snapshots: entities@6.0.1: {} + env-editor@0.4.2: {} + env-paths@2.2.1: {} env-paths@3.0.0: {} @@ -13999,6 +16746,10 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 @@ -14094,8 +16845,6 @@ snapshots: es6-promise@3.3.1: {} - es6-promise@4.2.8: {} - esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -14204,6 +16953,10 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} escodegen@2.1.0: @@ -14257,6 +17010,8 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + event-target-shim@5.0.1: {} eventemitter3@5.0.1: {} @@ -14269,6 +17024,8 @@ snapshots: events@3.3.0: {} + exec-async@2.2.0: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -14283,6 +17040,215 @@ snapshots: expect-type@1.3.0: {} + expo-asset@12.0.12(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + '@expo/image-utils': 0.8.8 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-constants: 18.0.13(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - supports-color + + expo-constants@18.0.13(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)): + dependencies: + '@expo/config': 12.0.13 + '@expo/env': 2.0.8 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - supports-color + + expo-dev-client@6.0.20(expo@54.0.31): + dependencies: + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-dev-launcher: 6.0.20(expo@54.0.31) + expo-dev-menu: 7.0.18(expo@54.0.31) + expo-dev-menu-interface: 2.0.0(expo@54.0.31) + expo-manifests: 1.0.10(expo@54.0.31) + expo-updates-interface: 2.0.0(expo@54.0.31) + transitivePeerDependencies: + - supports-color + + expo-dev-launcher@6.0.20(expo@54.0.31): + dependencies: + ajv: 8.17.1 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-dev-menu: 7.0.18(expo@54.0.31) + expo-manifests: 1.0.10(expo@54.0.31) + transitivePeerDependencies: + - supports-color + + expo-dev-menu-interface@2.0.0(expo@54.0.31): + dependencies: + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + + expo-dev-menu@7.0.18(expo@54.0.31): + dependencies: + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-dev-menu-interface: 2.0.0(expo@54.0.31) + + expo-file-system@19.0.21(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)): + dependencies: + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + + expo-font@14.0.10(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + fontfaceobserver: 2.3.0 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + + expo-json-utils@0.15.0: {} + + expo-keep-awake@15.0.8(expo@54.0.31)(react@18.3.1): + dependencies: + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react: 18.3.1 + + expo-linking@8.0.11(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + expo-constants: 18.0.13(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) + invariant: 2.2.4 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - expo + - supports-color + + expo-manifests@1.0.10(expo@54.0.31): + dependencies: + '@expo/config': 12.0.13 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-json-utils: 0.15.0 + transitivePeerDependencies: + - supports-color + + expo-modules-autolinking@3.0.24: + dependencies: + '@expo/spawn-async': 1.7.2 + chalk: 4.1.2 + commander: 7.2.0 + require-from-string: 2.0.2 + resolve-from: 5.0.0 + + expo-modules-core@3.0.29(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + invariant: 2.2.4 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + + expo-router@6.0.21(ed0173313472e3a4d9a27b966543cc3e): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.31)(react-dom@18.3.1(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@expo/schema-utils': 0.1.8 + '@radix-ui/react-slot': 1.2.0(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-navigation/bottom-tabs': 7.9.0(@react-navigation/native@7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-navigation/native': 7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-navigation/native-stack': 7.9.0(@react-navigation/native@7.1.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-constants: 18.0.13(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) + expo-linking: 8.0.11(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-server: 1.0.5 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 18.3.1 + react-fast-compare: 3.2.2 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.2.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@18.3.1) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-reanimated: 4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-web: 0.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + + expo-server@1.0.5: {} + + expo-splash-screen@31.0.13(expo@54.0.31): + dependencies: + '@expo/prebuild-config': 54.0.8(expo@54.0.31) + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - supports-color + + expo-status-bar@3.0.9(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + + expo-system-ui@6.0.9(expo@54.0.31)(react-native-web@0.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)): + dependencies: + '@react-native/normalize-colors': 0.81.5 + debug: 4.4.3 + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + react-native-web: 0.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - supports-color + + expo-updates-interface@2.0.0(expo@54.0.31): + dependencies: + expo: 54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + + expo@54.0.31(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@expo/cli': 54.0.21(expo-router@6.0.21)(expo@54.0.31)(graphql@16.12.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) + '@expo/config': 12.0.13 + '@expo/config-plugins': 54.0.4 + '@expo/devtools': 0.1.8(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@expo/fingerprint': 0.15.4 + '@expo/metro': 54.2.0 + '@expo/metro-config': 54.0.13(expo@54.0.31) + '@expo/vector-icons': 15.0.3(expo-font@14.0.10(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@ungap/structured-clone': 1.3.0 + babel-preset-expo: 54.0.9(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.31)(react-refresh@0.14.2) + expo-asset: 12.0.12(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-constants: 18.0.13(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) + expo-file-system: 19.0.21(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) + expo-font: 14.0.10(expo@54.0.31)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-keep-awake: 15.0.8(expo@54.0.31)(react@18.3.1) + expo-modules-autolinking: 3.0.24 + expo-modules-core: 3.0.29(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + pretty-format: 29.7.0 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-refresh: 0.14.2 + whatwg-url-without-unicode: 8.0.0-3 + optionalDependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.31)(react-dom@18.3.1(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - expo-router + - graphql + - supports-color + - utf-8-validate + + exponential-backoff@3.1.3: {} + exsolve@1.0.8: {} extend@3.0.2: {} @@ -14313,6 +17279,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.1.1: dependencies: '@fastify/merge-json-schemas': 0.2.1 @@ -14374,7 +17342,7 @@ snapshots: fast-json-stringify: 6.1.1 find-my-way: 9.4.0 light-my-request: 6.6.0 - pino: 10.1.0 + pino: 10.1.1 process-warning: 5.0.0 rfdc: 1.4.1 secure-json-parse: 4.1.0 @@ -14395,6 +17363,24 @@ snapshots: reusify: 1.1.0 xtend: 4.0.2 + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fbjs-css-vars@1.0.2: {} + + fbjs@3.0.5: + dependencies: + cross-fetch: 3.2.0 + fbjs-css-vars: 1.0.2 + loose-envify: 1.4.0 + object-assign: 4.1.1 + promise: 7.3.1 + setimmediate: 1.0.5 + ua-parser-js: 1.0.41 + transitivePeerDependencies: + - encoding + fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -14419,12 +17405,31 @@ snapshots: 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-my-way@9.4.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 safe-regex2: 5.0.0 + 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 @@ -14434,12 +17439,16 @@ snapshots: dependencies: magic-string: 0.30.21 mlly: 1.8.0 - rollup: 4.54.0 + rollup: 4.55.1 flatted@3.3.3: {} + flow-enums-runtime@0.0.6: {} + follow-redirects@1.15.11: {} + fontfaceobserver@2.3.0: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -14459,16 +17468,18 @@ snapshots: fraction.js@5.3.4: {} - framer-motion@12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + framer-motion@12.25.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - motion-dom: 12.23.23 - motion-utils: 12.23.6 + motion-dom: 12.24.11 + motion-utils: 12.24.10 tslib: 2.8.1 optionalDependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - fs-constants@1.0.0: {} + freeport-async@2.0.0: {} + + fresh@0.5.2: {} fs-extra@11.3.3: dependencies: @@ -14476,6 +17487,8 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true @@ -14515,7 +17528,7 @@ snapshots: get-nonce@1.0.1: {} - get-port@7.1.0: {} + get-package-type@0.1.0: {} get-proto@1.0.1: dependencies: @@ -14546,6 +17559,8 @@ snapshots: transitivePeerDependencies: - supports-color + getenv@2.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -14554,15 +17569,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - glob@11.1.0: dependencies: foreground-child: 3.3.1 @@ -14578,6 +17584,19 @@ snapshots: minipass: 7.1.2 path-scurry: 2.0.1 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-dirs@0.1.1: + dependencies: + ini: 1.3.8 + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -14606,6 +17625,8 @@ snapshots: has-bigints@1.1.0: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -14675,6 +17696,26 @@ snapshots: help-me@5.0.0: {} + hermes-estree@0.29.1: {} + + hermes-estree@0.32.0: {} + + hermes-parser@0.29.1: + dependencies: + hermes-estree: 0.29.1 + + hermes-parser@0.32.0: + dependencies: + hermes-estree: 0.32.0 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 19.2.3 + + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.8.0 @@ -14737,7 +17778,9 @@ snapshots: human-signals@2.1.0: {} - i18next@25.7.3(typescript@5.9.3): + hyphenate-style-name@1.1.0: {} + + i18next@25.7.4(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 optionalDependencies: @@ -14751,6 +17794,10 @@ snapshots: ignore@5.3.2: {} + image-size@1.2.1: + dependencies: + queue: 6.0.2 + immer@10.2.0: {} immer@11.1.3: {} @@ -14762,12 +17809,25 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + imurmurhash@0.1.4: {} + inflected@2.1.0: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} + ini@1.3.8: {} + inline-style-parser@0.2.7: {} + inline-style-prefixer@7.0.1: + dependencies: + css-in-js-utils: 3.1.0 + input-otp@1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 @@ -14783,19 +17843,23 @@ snapshots: internmap@2.0.3: {} - ioredis-mock@8.13.1(@types/ioredis-mock@8.2.6(ioredis@5.8.2))(ioredis@5.8.2): + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ioredis-mock@8.13.1(@types/ioredis-mock@8.2.6(ioredis@5.9.1))(ioredis@5.9.1): dependencies: '@ioredis/as-callback': 3.0.0 '@ioredis/commands': 1.5.0 - '@types/ioredis-mock': 8.2.6(ioredis@5.8.2) + '@types/ioredis-mock': 8.2.6(ioredis@5.9.1) fengari: 0.1.5 fengari-interop: 0.1.4(fengari@0.1.5) - ioredis: 5.8.2 + ioredis: 5.9.1 semver: 7.7.3 - ioredis@5.8.2: + ioredis@5.9.1: dependencies: - '@ioredis/commands': 1.4.0 + '@ioredis/commands': 1.5.0 cluster-key-slot: 1.1.2 debug: 4.4.3 denque: 2.1.0 @@ -14826,6 +17890,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.4: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -14866,6 +17932,8 @@ snapshots: is-decimal@2.0.1: {} + is-docker@2.2.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -14954,7 +18022,9 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - isarray@1.0.0: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 isarray@2.0.5: {} @@ -14962,6 +18032,16 @@ snapshots: istanbul-lib-coverage@3.2.2: {} + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.28.5 @@ -14991,15 +18071,83 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jackspeak@3.4.3: + jackspeak@4.1.1: dependencies: '@isaacs/cliui': 8.0.2 + + 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': 25.0.3 + 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': 25.0.3 + 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: - '@pkgjs/parseargs': 0.11.0 + fsevents: 2.3.3 - jackspeak@4.1.1: + jest-message-util@29.7.0: dependencies: - '@isaacs/cliui': 8.0.2 + '@babel/code-frame': 7.27.1 + '@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': 25.0.3 + jest-util: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 25.0.3 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + 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': 25.0.3 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jimp-compact@0.16.1: {} jiti@1.21.7: {} @@ -15017,16 +18165,23 @@ snapshots: 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: {} + jsdom@27.4.0: dependencies: '@acemir/cssom': 0.9.30 '@asamuzakjp/dom-selector': 6.7.6 '@exodus/bytes': 1.8.0 - cssstyle: 5.3.6 + cssstyle: 5.3.7 data-urls: 6.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 6.0.0 @@ -15041,7 +18196,7 @@ snapshots: webidl-conversions: 8.0.1 whatwg-mimetype: 4.0.0 whatwg-url: 15.1.0 - ws: 8.18.3 + ws: 8.19.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - '@exodus/crypto' @@ -15093,9 +18248,7 @@ snapshots: kleur@3.0.3: {} - lazystream@1.0.1: - dependencies: - readable-stream: 2.3.8 + lan-network@0.1.7: {} leac@0.6.0: {} @@ -15107,6 +18260,107 @@ snapshots: process-warning: 4.0.1 set-cookie-parser: 2.7.2 + lighthouse-logger@1.4.2: + dependencies: + debug: 2.6.9 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.27.0: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.27.0: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.27.0: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.27.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.27.0: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.27.0: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.27.0: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.27.0: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.27.0: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.27.0: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + 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 + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -15119,12 +18373,14 @@ snapshots: localstack@1.0.0: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 - lodash.camelcase@4.3.0: {} - lodash.debounce@4.0.8: {} lodash.defaults@4.2.0: {} @@ -15135,6 +18391,8 @@ snapshots: lodash.omitby@4.6.0: {} + lodash.throttle@4.1.1: {} + lodash.topath@4.5.2: {} lodash.uniq@4.5.0: {} @@ -15145,6 +18403,10 @@ snapshots: lodash@4.17.21: {} + log-symbols@2.2.0: + dependencies: + chalk: 2.4.2 + log-symbols@6.0.0: dependencies: chalk: 5.6.2 @@ -15159,8 +18421,6 @@ snapshots: loglevel@1.9.2: {} - long@5.3.2: {} - longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -15207,6 +18467,10 @@ snapshots: dependencies: semver: 7.7.3 + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + markdown-extensions@2.0.0: {} markdown-it@14.1.0: @@ -15220,6 +18484,8 @@ snapshots: marked@15.0.12: {} + marky@1.3.0: {} + math-intrinsics@1.1.0: {} mdast-util-from-markdown@2.0.2: @@ -15321,6 +18587,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdn-data@2.0.14: {} + mdn-data@2.0.28: {} mdn-data@2.0.30: {} @@ -15329,10 +18597,189 @@ snapshots: mdurl@2.0.0: {} + memoize-one@5.2.1: {} + + memoize-one@6.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} + metro-babel-transformer@0.83.3: + dependencies: + '@babel/core': 7.28.5 + flow-enums-runtime: 0.0.6 + hermes-parser: 0.32.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-cache-key@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-cache@0.83.3: + dependencies: + exponential-backoff: 3.1.3 + flow-enums-runtime: 0.0.6 + https-proxy-agent: 7.0.6 + metro-core: 0.83.3 + transitivePeerDependencies: + - supports-color + + metro-config@0.83.3: + dependencies: + connect: 3.7.0 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.83.3 + metro-cache: 0.83.3 + metro-core: 0.83.3 + metro-runtime: 0.83.3 + yaml: 2.8.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-core@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.83.3 + + metro-file-map@0.83.3: + 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.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.44.1 + + metro-resolver@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-runtime@0.83.3: + dependencies: + '@babel/runtime': 7.28.4 + flow-enums-runtime: 0.0.6 + + metro-source-map@0.83.3: + dependencies: + '@babel/traverse': 7.28.5 + '@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.5' + '@babel/types': 7.28.5 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.83.3 + nullthrows: 1.1.1 + ob1: 0.83.3 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-symbolicate@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.83.3 + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-transform-plugins@0.83.3: + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-worker@0.83.3: + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + flow-enums-runtime: 0.0.6 + metro: 0.83.3 + metro-babel-transformer: 0.83.3 + metro-cache: 0.83.3 + metro-cache-key: 0.83.3 + metro-minify-terser: 0.83.3 + metro-source-map: 0.83.3 + metro-transform-plugins: 0.83.3 + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro@0.83.3: + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + 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.32.0 + 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.83.3 + metro-cache: 0.83.3 + metro-cache-key: 0.83.3 + metro-config: 0.83.3 + metro-core: 0.83.3 + metro-file-map: 0.83.3 + metro-resolver: 0.83.3 + metro-runtime: 0.83.3 + metro-source-map: 0.83.3 + metro-symbolicate: 0.83.3 + metro-transform-plugins: 0.83.3 + metro-transform-worker: 0.83.3 + 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 + micro-cors@0.1.1: {} micromark-core-commonmark@2.0.3: @@ -15558,8 +19005,12 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@1.6.0: {} + mime@3.0.0: {} + mimic-fn@1.2.0: {} + mimic-fn@2.1.0: {} mimic-function@5.0.1: {} @@ -15574,10 +19025,6 @@ snapshots: dependencies: brace-expansion: 1.1.12 - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.2 - minimatch@6.2.0: dependencies: brace-expansion: 2.0.2 @@ -15590,9 +19037,11 @@ snapshots: minipass@7.1.2: {} - mitt@3.0.1: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 - mkdirp-classic@0.5.3: {} + mitt@3.0.1: {} mkdirp@1.0.4: {} @@ -15601,20 +19050,22 @@ snapshots: acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 - ufo: 1.6.1 + ufo: 1.6.2 mnemonist@0.40.3: dependencies: obliterator: 2.0.5 - motion-dom@12.23.23: + motion-dom@12.24.11: dependencies: - motion-utils: 12.23.6 + motion-utils: 12.24.10 - motion-utils@12.23.6: {} + motion-utils@12.24.10: {} mrmime@2.0.1: {} + ms@2.0.0: {} + ms@2.1.3: {} msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3): @@ -15650,28 +19101,43 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.24.0: - optional: true - nanoid@3.3.11: {} + nativewind@4.2.1(react-native-reanimated@4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + comment-json: 4.5.1 + debug: 4.4.3 + react-native-css-interop: 0.2.1(react-native-reanimated@4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) + 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: {} + negotiator@1.0.0: {} + nested-error-stacks@2.0.1: {} + netmask@2.0.2: {} - next-auth@4.24.13(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-auth@4.24.13(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) oauth: 0.9.15 openid-client: 5.7.1 - preact: 10.28.1 - preact-render-to-string: 5.2.6(preact@10.28.1) + preact: 10.28.2 + preact-render-to-string: 5.2.6(preact@10.28.2) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) uuid: 8.3.2 @@ -15681,18 +19147,18 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next-view-transitions@0.3.5(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-view-transitions@0.3.5(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - next: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 16.1.1 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.11 - caniuse-lite: 1.0.30001762 + baseline-browser-mapping: 2.9.14 + caniuse-lite: 1.0.30001763 postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -15706,14 +19172,15 @@ snapshots: '@next/swc-linux-x64-musl': 16.1.1 '@next/swc-win32-arm64-msvc': 16.1.1 '@next/swc-win32-x64-msvc': 16.1.1 + babel-plugin-react-compiler: 1.0.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextjs-toploader@3.9.17(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + nextjs-toploader@3.9.17(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - next: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nprogress: 0.2.0 prop-types: 15.8.1 react: 19.2.3 @@ -15744,6 +19211,10 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-forge@1.3.3: {} + + node-int64@0.4.0: {} + node-readfiles@0.2.0: dependencies: es6-promise: 3.3.1 @@ -15754,6 +19225,13 @@ snapshots: normalize-wheel@1.0.1: {} + npm-package-arg@11.0.3: + dependencies: + hosted-git-info: 7.0.2 + proc-log: 4.2.0 + semver: 7.7.3 + validate-npm-package-name: 5.0.1 + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 @@ -15764,16 +19242,18 @@ snapshots: dependencies: boolbase: 1.0.0 + nullthrows@1.1.1: {} + number-flow@0.5.8: dependencies: esm-env: 1.2.2 - nuqs@2.8.6(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): + nuqs@2.8.6(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.3 optionalDependencies: - next: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nypm@0.6.2: dependencies: @@ -15816,6 +19296,10 @@ snapshots: oauth@0.9.15: {} + ob1@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + object-assign@4.1.1: {} object-hash@2.2.0: {} @@ -15843,10 +19327,24 @@ snapshots: 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 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -15855,9 +19353,20 @@ snapshots: dependencies: mimic-function: 5.0.1 - openai@6.15.0(ws@8.18.3)(zod@4.3.5): + 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@6.16.0(ws@8.19.0)(zod@4.3.5): optionalDependencies: - ws: 8.18.3 + ws: 8.19.0 zod: 4.3.5 openapi-types@12.1.3: {} @@ -15875,6 +19384,15 @@ snapshots: object-hash: 2.2.0 oidc-token-hash: 5.2.0 + 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 + ora@8.2.0: dependencies: chalk: 5.6.2 @@ -15931,14 +19449,24 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + 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: {} + pac-proxy-agent@7.2.0: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 @@ -15980,6 +19508,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-png@2.1.0: + dependencies: + pngjs: 3.4.0 + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -16002,17 +19534,16 @@ snapshots: 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.2 - path-scurry@2.0.1: dependencies: lru-cache: 11.2.4 @@ -16034,14 +19565,12 @@ snapshots: picomatch@2.3.1: {} + picomatch@3.0.1: {} + picomatch@4.0.3: {} pify@2.3.0: {} - pino-abstract-transport@2.0.0: - dependencies: - split2: 4.2.0 - pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -16064,19 +19593,19 @@ snapshots: pino-std-serializers@7.0.0: {} - pino@10.1.0: + pino@10.1.1: dependencies: '@pinojs/redact': 0.4.0 atomic-sleep: 1.0.0 on-exit-leak-free: 2.1.2 - pino-abstract-transport: 2.0.0 + pino-abstract-transport: 3.0.0 pino-std-serializers: 7.0.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.0 - thread-stream: 3.1.0 + thread-stream: 4.0.0 pirates@4.0.7: {} @@ -16092,6 +19621,14 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.11 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pngjs@3.4.0: {} + pony-cause@1.1.1: {} possible-typed-array-names@1.1.0: {} @@ -16149,43 +19686,63 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.4.49: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - postgres@3.4.7: {} + postgres@3.4.8: {} - preact-render-to-string@5.2.6(preact@10.28.1): + preact-render-to-string@5.2.6(preact@10.28.2): dependencies: - preact: 10.28.1 + preact: 10.28.2 pretty-format: 3.8.0 - preact@10.28.1: {} + preact@10.28.2: {} prettier@3.7.4: {} + pretty-bytes@5.6.0: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 ansi-styles: 5.2.0 react-is: 19.2.3 + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 19.2.3 + pretty-format@3.8.0: {} prismjs@1.30.0: {} - process-nextick-args@2.0.1: {} + proc-log@4.2.0: {} process-warning@4.0.1: {} process-warning@5.0.0: {} - process@0.11.10: {} - progress@2.0.3: {} + promise@7.3.1: + dependencies: + asap: 2.0.6 + + promise@8.3.0: + dependencies: + asap: 2.0.6 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -16197,33 +19754,8 @@ snapshots: object-assign: 4.1.1 react-is: 19.2.3 - proper-lockfile@4.1.2: - dependencies: - graceful-fs: 4.2.11 - retry: 0.12.0 - signal-exit: 3.0.7 - - properties-reader@2.3.0: - dependencies: - mkdirp: 1.0.4 - property-information@7.1.0: {} - protobufjs@7.5.4: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 - '@types/node': 25.0.3 - long: 5.3.2 - proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 @@ -16256,7 +19788,7 @@ snapshots: devtools-protocol: 0.0.1534754 typed-query-selector: 2.12.0 webdriver-bidi-protocol: 0.3.10 - ws: 8.18.3 + ws: 8.19.0 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -16282,16 +19814,36 @@ snapshots: - typescript - utf-8-validate + qrcode-terminal@0.11.0: {} + qs@6.14.1: dependencies: side-channel: 1.1.0 - querystringify@2.2.0: {} + 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: {} + range-parser@1.2.1: {} + + 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-confetti@6.4.0(react@19.2.3): dependencies: react: 19.2.3 @@ -16313,6 +19865,20 @@ snapshots: date-fns-jalali: 4.1.0-0 react: 19.2.3 + react-devtools-core@6.1.5: + dependencies: + shell-quote: 1.8.3 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -16325,7 +19891,7 @@ snapshots: react-dom: 19.2.3(react@19.2.3) tslib: 2.8.1 - react-email@5.1.1: + react-email@5.2.1: dependencies: '@babel/parser': 7.28.5 '@babel/traverse': 7.28.5 @@ -16345,36 +19911,202 @@ snapshots: socket.io: 4.8.3 tsconfig-paths: 4.2.0 transitivePeerDependencies: - - bufferutil + - bufferutil + - supports-color + - utf-8-validate + + react-fast-compare@3.2.2: {} + + react-freeze@1.0.4(react@18.3.1): + dependencies: + react: 18.3.1 + + react-hook-form@7.70.0(react@19.2.3): + dependencies: + react: 19.2.3 + + react-i18next@16.5.1(i18next@25.7.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.28.4 + html-parse-stringify: 3.0.1 + i18next: 25.7.4(typescript@5.9.3) + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + typescript: 5.9.3 + + react-intersection-observer@10.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + + react-is@19.2.3: {} + + react-masonry-css@1.0.16(react@19.2.3): + dependencies: + react: 19.2.3 + + react-native-css-interop@0.1.22(react-native-reanimated@4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + debug: 4.4.3 + lightningcss: 1.30.2 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-reanimated: 4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + semver: 7.7.3 + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) + optionalDependencies: + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-svg: 15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - supports-color + + react-native-css-interop@0.2.1(react-native-reanimated@4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + debug: 4.4.3 + lightningcss: 1.27.0 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-reanimated: 4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + semver: 7.7.3 + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) + optionalDependencies: + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-svg: 15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: - supports-color - - utf-8-validate - react-hook-form@7.70.0(react@19.2.3): + react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): dependencies: - react: 19.2.3 + '@egjs/hammerjs': 2.0.17 + hoist-non-react-statics: 3.3.2 + invariant: 2.2.4 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) - react-i18next@16.5.1(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): + react-native-is-edge-to-edge@1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.28.4 - html-parse-stringify: 3.0.1 - i18next: 25.7.3(typescript@5.9.3) - react: 19.2.3 - use-sync-external-store: 1.6.0(react@19.2.3) - optionalDependencies: - react-dom: 19.2.3(react@19.2.3) - typescript: 5.9.3 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) - react-intersection-observer@10.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-native-reanimated@4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): dependencies: - react: 19.2.3 - optionalDependencies: - react-dom: 19.2.3(react@19.2.3) + '@babel/core': 7.28.5 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-worklets: 0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + semver: 7.7.2 - react-is@19.2.3: {} + react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) - react-masonry-css@1.0.16(react@19.2.3): + react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): dependencies: - react: 19.2.3 + react: 18.3.1 + react-freeze: 1.0.4(react@18.3.1) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + warn-once: 0.1.1 + + react-native-svg@15.15.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + css-select: 5.2.2 + css-tree: 1.1.3 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + warn-once: 0.1.1 + + react-native-web@0.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@react-native/normalize-colors': 0.74.89 + fbjs: 3.0.5 + inline-style-prefixer: 7.0.1 + memoize-one: 6.0.0 + nullthrows: 1.1.1 + postcss-value-parser: 4.2.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styleq: 0.1.3 + transitivePeerDependencies: + - encoding + + react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.5) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + convert-source-map: 2.0.0 + react: 18.3.1 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native/assets-registry': 0.81.5 + '@react-native/codegen': 0.81.5(@babel/core@7.28.5) + '@react-native/community-cli-plugin': 0.81.5 + '@react-native/gradle-plugin': 0.81.5 + '@react-native/js-polyfills': 0.81.5 + '@react-native/normalize-colors': 0.81.5 + '@react-native/virtualized-lists': 0.81.5(@types/react@18.3.27)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-jest: 29.7.0(@babel/core@7.28.5) + babel-plugin-syntax-hermes-parser: 0.29.1 + base64-js: 1.5.1 + commander: 12.1.0 + 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.83.3 + metro-source-map: 0.83.3 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 18.3.1 + react-devtools-core: 6.1.5 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.26.0 + semver: 7.7.3 + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + ws: 6.2.3 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 18.3.27 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1): dependencies: @@ -16385,8 +20117,18 @@ snapshots: '@types/react': 19.2.7 redux: 5.0.1 + react-refresh@0.14.2: {} + react-refresh@0.18.0: {} + react-remove-scroll-bar@2.3.8(@types/react@18.3.27)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.27)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 + react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3): dependencies: react: 19.2.3 @@ -16395,6 +20137,17 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + react-remove-scroll@2.7.2(@types/react@18.3.27)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.27)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.27)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.27)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + react-remove-scroll@2.7.2(@types/react@19.2.7)(react@19.2.3): dependencies: react: 19.2.3 @@ -16406,7 +20159,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 - react-resizable-panels@4.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-resizable-panels@4.3.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -16421,6 +20174,14 @@ snapshots: react-dom: 19.2.3(react@19.2.3) topojson-client: 3.1.0 + react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 + react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.2.3): dependencies: get-nonce: 1.0.1 @@ -16436,40 +20197,22 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + react@19.2.3: {} 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@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 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.6 - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -16560,6 +20303,8 @@ snapshots: regenerate@1.4.2: {} + regenerator-runtime@0.13.11: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -16620,28 +20365,49 @@ snapshots: require-from-string@2.0.2: {} - requires-port@1.0.0: {} + requireg@0.2.2: + dependencies: + nested-error-stacks: 2.0.1 + rc: 1.2.8 + resolve: 1.7.1 reselect@5.1.1: {} - resend@6.6.0(@react-email/render@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): + resend@6.7.0(@react-email/render@2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): dependencies: - svix: 1.76.1 + svix: 1.84.1 optionalDependencies: - '@react-email/render': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-email/render': 2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) resolve-from@4.0.0: {} resolve-from@5.0.0: {} + resolve-global@1.0.0: + dependencies: + global-dirs: 0.1.1 + resolve-pkg-maps@1.0.0: {} + resolve-workspace-root@2.0.1: {} + + resolve.exports@2.0.3: {} + resolve@1.22.11: dependencies: is-core-module: 2.16.1 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 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -16649,40 +20415,45 @@ snapshots: ret@0.5.0: {} - retry@0.12.0: {} - rettime@0.7.0: {} reusify@1.1.0: {} rfdc@1.4.1: {} - rollup@4.54.0: + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.55.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.54.0 - '@rollup/rollup-android-arm64': 4.54.0 - '@rollup/rollup-darwin-arm64': 4.54.0 - '@rollup/rollup-darwin-x64': 4.54.0 - '@rollup/rollup-freebsd-arm64': 4.54.0 - '@rollup/rollup-freebsd-x64': 4.54.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 - '@rollup/rollup-linux-arm-musleabihf': 4.54.0 - '@rollup/rollup-linux-arm64-gnu': 4.54.0 - '@rollup/rollup-linux-arm64-musl': 4.54.0 - '@rollup/rollup-linux-loong64-gnu': 4.54.0 - '@rollup/rollup-linux-ppc64-gnu': 4.54.0 - '@rollup/rollup-linux-riscv64-gnu': 4.54.0 - '@rollup/rollup-linux-riscv64-musl': 4.54.0 - '@rollup/rollup-linux-s390x-gnu': 4.54.0 - '@rollup/rollup-linux-x64-gnu': 4.54.0 - '@rollup/rollup-linux-x64-musl': 4.54.0 - '@rollup/rollup-openharmony-arm64': 4.54.0 - '@rollup/rollup-win32-arm64-msvc': 4.54.0 - '@rollup/rollup-win32-ia32-msvc': 4.54.0 - '@rollup/rollup-win32-x64-gnu': 4.54.0 - '@rollup/rollup-win32-x64-msvc': 4.54.0 + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 fsevents: 2.3.3 run-parallel@1.2.0: @@ -16697,8 +20468,6 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 - safe-buffer@5.1.2: {} - safe-buffer@5.2.1: {} safe-push-apply@1.0.0: @@ -16722,12 +20491,18 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.4.3: {} + sax@1.4.4: {} saxes@6.0.0: dependencies: xmlchars: 2.2.0 + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + scheduler@0.26.0: {} + scheduler@0.27.0: {} secure-json-parse@4.1.0: {} @@ -16738,8 +20513,43 @@ snapshots: semver@6.3.1: {} + semver@7.6.3: {} + + semver@7.7.2: {} + semver@7.7.3: {} + 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 + + serialize-error@2.1.0: {} + + 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 + + server-only@0.0.1: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: @@ -16764,8 +20574,14 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} + sf-symbols-typescript@2.2.0: {} + + shallowequal@1.1.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -16803,6 +20619,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.3: {} + should-equal@2.0.0: dependencies: should-type: 1.4.0 @@ -16867,6 +20685,16 @@ snapshots: dependencies: jsep: 1.4.0 + simple-plist@1.3.1: + dependencies: + bplist-creator: 0.1.0 + bplist-parser: 0.3.1 + plist: 3.1.0 + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.29 @@ -16886,10 +20714,12 @@ snapshots: '@types/node': 24.10.4 '@types/sax': 1.2.7 arg: 5.0.2 - sax: 1.4.3 + sax: 1.4.4 slash@3.0.0: {} + slugify@1.6.6: {} + smart-buffer@4.2.0: {} snake-case@3.0.4: @@ -16956,35 +20786,43 @@ snapshots: 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: {} space-separated-tokens@2.0.2: {} - split-ca@1.0.1: {} + split-on-first@1.1.0: {} split2@4.2.0: {} - sprintf-js@1.1.3: {} + sprintf-js@1.0.3: {} - ssh-remote-port-forward@1.0.4: - dependencies: - '@types/ssh2': 0.5.52 - ssh2: 1.17.0 + sprintf-js@1.1.3: {} - ssh2@1.17.0: + stack-utils@2.0.6: dependencies: - asn1: 0.2.6 - bcrypt-pbkdf: 1.0.2 - optionalDependencies: - cpu-features: 0.0.10 - nan: 2.24.0 + escape-string-regexp: 2.0.0 stackback@0.0.2: {} + stackframe@1.3.4: {} + + stacktrace-parser@0.1.11: + dependencies: + type-fest: 0.7.1 + standard-as-callback@2.1.0: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + + statuses@1.5.0: {} + statuses@2.0.2: {} std-env@3.10.0: {} @@ -17009,6 +20847,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + stream-buffers@2.2.0: {} + streamx@2.23.0: dependencies: events-universal: 1.0.1 @@ -17020,6 +20860,8 @@ snapshots: strict-event-emitter@0.5.1: {} + strict-uri-encode@2.0.0: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -17063,10 +20905,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -17076,6 +20914,10 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -17088,9 +20930,11 @@ snapshots: strip-final-newline@2.0.0: {} + strip-json-comments@2.0.1: {} + strip-json-comments@5.0.3: {} - stripe@20.1.0(@types/node@25.0.3): + stripe@20.1.2(@types/node@25.0.3): dependencies: qs: 6.14.1 optionalDependencies: @@ -17098,6 +20942,8 @@ snapshots: strnum@2.1.2: {} + structured-headers@0.4.1: {} + stubborn-fs@2.0.0: dependencies: stubborn-utils: 1.0.2 @@ -17119,6 +20965,8 @@ snapshots: optionalDependencies: '@babel/core': 7.28.5 + styleq@0.1.3: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -17129,10 +20977,23 @@ snapshots: tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 + 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: {} svg-parser@2.0.4: {} @@ -17147,13 +21008,9 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - svix@1.76.1: + svix@1.84.1: dependencies: - '@stablelib/base64': 1.0.1 - '@types/node': 22.19.3 - es6-promise: 4.2.8 - fast-sha256: 1.3.0 - url-parse: 1.5.10 + standardwebhooks: 1.0.0 uuid: 10.0.0 swagger2openapi@7.0.8: @@ -17228,13 +21085,6 @@ snapshots: tailwindcss@4.1.18: {} - tar-fs@2.1.4: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.3 - tar-stream: 2.2.0 - tar-fs@3.1.1: dependencies: pump: 3.0.3 @@ -17247,14 +21097,6 @@ snapshots: - bare-buffer - react-native-b4a - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - tar-stream@3.1.7: dependencies: b4a: 1.7.3 @@ -17264,28 +21106,33 @@ snapshots: - bare-abort-controller - react-native-b4a - testcontainers@11.11.0: + tar@7.5.2: dependencies: - '@balena/dockerignore': 1.0.2 - '@types/dockerode': 3.3.47 - archiver: 7.0.1 - async-lock: 1.4.1 - byline: 5.0.0 - debug: 4.4.3 - docker-compose: 1.3.0 - dockerode: 4.0.9 - get-port: 7.1.0 - proper-lockfile: 4.1.2 - properties-reader: 2.3.0 - ssh-remote-port-forward: 1.0.4 - tar-fs: 3.1.1 - tmp: 0.2.5 - undici: 7.16.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - supports-color + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + temp-dir@2.0.0: {} + + terminal-link@2.1.1: + dependencies: + ansi-escapes: 4.3.2 + supports-hyperlinks: 2.3.0 + + terser@5.44.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 text-decoder@1.2.3: dependencies: @@ -17301,10 +21148,12 @@ snapshots: dependencies: any-promise: 1.3.0 - thread-stream@3.1.0: + thread-stream@4.0.0: dependencies: real-require: 0.2.0 + throat@5.0.0: {} + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -17328,6 +21177,8 @@ snapshots: tmp@0.2.5: {} + tmpl@1.0.5: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -17391,7 +21242,7 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) resolve-from: 5.0.0 - rollup: 4.54.0 + rollup: 4.55.1 source-map: 0.7.6 sucrase: 3.35.1 tinyexec: 0.3.2 @@ -17443,7 +21294,11 @@ snapshots: tween-functions@1.2.0: {} - tweetnacl@0.14.5: {} + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@0.7.1: {} type-fest@5.3.1: dependencies: @@ -17494,7 +21349,7 @@ snapshots: typedoc@0.28.15(typescript@5.9.3): dependencies: - '@gerrit0/mini-shiki': 3.20.0 + '@gerrit0/mini-shiki': 3.21.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 @@ -17507,9 +21362,11 @@ snapshots: typescript@5.9.3: {} + ua-parser-js@1.0.41: {} + uc.micro@2.1.0: {} - ufo@1.6.1: {} + ufo@1.6.2: {} uint8array-extras@1.5.0: {} @@ -17520,13 +21377,11 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@5.26.5: {} - - undici-types@6.21.0: {} - undici-types@7.16.0: {} - undici@7.16.0: {} + undici@6.23.0: {} + + undici@7.18.2: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -17549,6 +21404,10 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -17578,9 +21437,11 @@ snapshots: universalify@2.0.1: {} - unplugin-swc@1.5.9(@swc/core@1.15.8)(rollup@4.54.0): + unpipe@1.0.0: {} + + unplugin-swc@1.5.9(@swc/core@1.15.8)(rollup@4.55.1): dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.54.0) + '@rollup/pluginutils': 5.3.0(rollup@4.55.1) '@swc/core': 1.15.8 load-tsconfig: 0.2.5 unplugin: 2.3.11 @@ -17604,10 +21465,12 @@ snapshots: urijs@1.19.11: {} - url-parse@1.5.10: + use-callback-ref@1.3.3(@types/react@18.3.27)(react@18.3.1): dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3): dependencies: @@ -17616,6 +21479,18 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + use-latest-callback@0.2.6(react@18.3.1): + dependencies: + react: 18.3.1 + + use-sidecar@1.1.3(@types/react@18.3.27)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 + use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.3): dependencies: detect-node-es: 1.1.0 @@ -17624,6 +21499,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store@1.6.0(react@19.2.3): dependencies: react: 19.2.3 @@ -17632,16 +21511,31 @@ snapshots: utility-types@3.11.0: {} + utils-merge@1.0.1: {} + uuid@10.0.0: {} uuid@13.0.0: {} + uuid@7.0.3: {} + uuid@8.3.2: {} + validate-npm-package-name@5.0.1: {} + validator@13.15.26: {} vary@1.1.2: {} + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -17678,36 +21572,38 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.54.0 + rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 25.0.3 fsevents: 2.3.3 jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.1 tsx: 4.21.0 yaml: 2.8.2 - vitest@4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.16(@types/node@25.0.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.16(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -17724,7 +21620,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.0.3 @@ -17743,16 +21639,30 @@ snapshots: - tsx - yaml + vlq@1.0.1: {} + void-elements@3.1.0: {} w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + warn-once@0.1.1: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + webdriver-bidi-protocol@0.3.10: {} webidl-conversions@3.0.1: {} + webidl-conversions@5.0.0: {} + webidl-conversions@8.0.1: {} webpack-bundle-analyzer@4.10.1: @@ -17780,8 +21690,16 @@ snapshots: dependencies: iconv-lite: 0.6.3 + whatwg-fetch@3.6.20: {} + whatwg-mimetype@4.0.0: {} + whatwg-url-without-unicode@8.0.0-3: + dependencies: + buffer: 5.7.1 + punycode: 2.3.1 + webidl-conversions: 5.0.0 + whatwg-url@15.1.0: dependencies: tr46: 6.0.0 @@ -17844,6 +21762,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wonka@6.3.5: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -17864,12 +21784,37 @@ snapshots: 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.18.3: {} + ws@8.19.0: {} + + xcode@3.0.1: + dependencies: + simple-plist: 1.3.1 + uuid: 7.0.3 + xml-name-validator@5.0.0: {} + xml2js@0.6.0: + dependencies: + sax: 1.4.4 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xmlbuilder@15.1.1: {} + xmlchars@2.2.0: {} xtend@4.0.2: {} @@ -17880,6 +21825,8 @@ snapshots: yallist@4.0.0: {} + yallist@5.0.0: {} + yaml@1.10.2: {} yaml@2.8.2: {} @@ -17907,12 +21854,6 @@ snapshots: yoctocolors@2.1.2: {} - 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: {} zod@4.3.5: {}