# Project export: Tide Party

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: Discover and collect local marine life while learning how to protect our oceans.
- Devpost: https://devpost.com/software/tide-party-icnodv
- GitHub: https://github.com/omrynskyi/TideParty
- Video: https://www.youtube.com/embed/1b-NM6MSwWQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Education Hacks; [MLH] Best Use of Auth0)
- Team: 2 GitHub contributor(s) — Oleg Mrynskyi (25 commits), Anthony (25 commits)

## Devpost submission (written by the team)

### Inspiration

Hey! We're TideParty, but our names are Oleg and Anthony. We LOVE tide pooling in Santa Cruz. It’s a relaxing activity that many people enjoy, and Santa Cruz is an ideal place for it thanks to its rocky cliffs and abundant marine life. However, when people go tide pooling, they often don’t know what they’re looking at. We built this app to help people identify marine organisms in real time, learn about them, and gain a deeper appreciation for ocean conservation.

### What it does

Tide Party is an interactive mobile app that turns tide pooling into a fun, educational experience: Discover and navigate to local tide pooling spots Identify marine life in real time by pointing your camera at a sea creature Take photos and collect the creatures you find Learn detailed information about each species Earn badges by collecting creatures and answering quiz questions Join party rooms with friends and race to find the most creatures

### How we built it

We built the app using SwiftUI for the frontend and Firebase for backend storage and real-time features. For marine life recognition, we trained a custom machine learning model using PyTorch and converted it into an Apple-compatible Core ML model for on-device inference.

### Challenges we ran into

Training the model was our biggest challenge. Due to the limited available image data, we relied heavily on image augmentation to improve performance. Collecting and labeling data was also difficult, and early versions of the model suffered from underfitting. After extensive preprocessing and training, we were able to build a generalized model capable of accurately classifying Santa Cruz tide pool marine life with 94% accuracy.

### Accomplishments we're proud of

We’re proud that we were able to fully realize our original vision, from real-time classification to multiplayer features. Achieving high model accuracy, building a polished UI, and designing a friendly mascot to enhance the user experience were major wins for us.

### What we learned

We learned how to work quickly and effectively under pressure, how to design thoughtful UI/UX animations and interactions in Swift, and how to train and deploy a machine learning model within a real mobile application.

### What's next

Next, we want to release Tide Party to real users. It would be especially impactful for school field trips, where students can compete to discover marine life together. We also plan to: Expand and improve our tide pool location map Train a more robust model to classify additional species Support multiple creature detections in a single frame Add online leaderboards and public party rooms Partner with charities focused on local ocean conservation

## README (from the GitHub repository)

# TideParty 🌊

TideParty is an iOS application designed to enhance the tide pooling experience through AI-powered creature identification, gamified discovery, and safety education.

## ✨ Key Features

### 🔍 AI Scanner & Discovery

- **Real-time Identification**: Uses CoreML (`TidePoolIdentifier.mlpackage`) to identify sea creatures in real-time.
- **Discovery Results**: Shows detailed information about captured creatures.
- **"Let's Learn"**: Educational fact sheets powered by Generative AI (Cerebras/Gemini).
- **Interactive Quizzes**: Test your knowledge about the creatures you find to earn XP.

### 🏠 Landing & Dashboard

- **Dynamic Dashboard**: Displays current weather conditions and forecast.
- **AI Insights**: "Otto's Insights" provides daily tide pooling tips and fun facts.
- **Donate Card**: Direct links to ocean conservation charities (Ocean Conservancy, Surfrider, etc.).

### 🤝 Party Mode

- **Group Experience**: Join parties with friends to explore together.
- **Gamification**: Earn XP, level up, and compete on leaderboards.

### 👤 User Account & Badges

- **Profile**: Track your exploration stats.
- **Badge System**: Unlock badges for milestones (e.g., finding 5 starfish, visiting 10 spots).
- **Customization**: Set unlocked badges as your profile icon.

### 📍 Spots & Navigation

- **Tide Pool Map**: Locate nearby tide pools.
- **Conditions**: View tide charts and weather for specific spots.

### 🛡️ Safety & Education

- **T.I.D.E. Code**: Onboarding pledge to ensure respectful interaction with marine life.
- **Safety First**: Mandatory safety pledge confirmation on every app launch.

## 📂 Project Structure

The project is organized into the following main directories:

### `TideParty/Features/`

Contains the UI and specific logic for each feature area:

- **Landing/**: Main home screen (`LandingView.swift`), AI Insights (`AIInsightView.swift`).
- **Scanner/**: Camera logic (`ScannerView.swift`), ML model (`TidePoolIdentifier.mlpackage`), and result pages (`DiscoveryResultView.swift`).
- **Account/**: User profile, badge grid, and settings (`AccountView.swift`).
- **Onboarding/**: Login/Signup flow (`OnboardingViews.swift`) and T.I.D.E. code education.
- **Safety/**: App launch safety pledge (`SafetyPledgeView.swift`).
- **Party/**: Group session logic and views.
- **Spots/**: Map and location-based features (`SpotsListView.swift`, `SpotsMapView.swift`).

### `TideParty/Services/`

Core application services managed as singletons:

- `AuthManager.swift`: Firebase authentication.
- `UserStatsService.swift`: Manages user progress, badges, and XP.
- `PartyService.swift`: Handles realtime coordination for party mode.
- `LocationManager.swift`: CoreLocation wrapper.
- `CameraManager.swift`: AVFoundation setup.

### `TideParty/Data/`

Data models and external data services:

- **Models/**: Swift structs for `Badge`, `TideSpot`, `Weather`, etc.
- **Services/**:
  - `CerebrasService.swift` / `GenAIService.swift`: Interfaces for AI text generation.
  - `WeatherService.swift`: Fetches weather data.
  - `TideService.swift`: Fetches tide data.

### `TideParty/Components/`

Reusable UI components used across multiple features.

- `DonateCard.swift`: Conservation donation promotion card.

### `TideParty/Assets.xcassets/`

Contains all images, icons, and color sets (e.g., `MainBlue`, `OttoBook`, `OttoMonacle`, Badge icons).

## 🛠️ Tech Stack

- **SwiftUI**: Main UI framework.
- **CoreML**: On-device machine learning for image recognition.
- **Firebase**: Backend for Authentication and Firestore (database).
- **AVFoundation**: Camera handling.
- **MapKit**: Location and mapping.

## 🚀 Getting Started

1. Open `TideParty.xcodeproj` in Xcode.
2. Ensure you have a valid development team selected.
3. Build and run on a simulator or device (requires iOS 16+).


## Detected evidence (automated analysis)

Indexed codebase: 52 recognized source files, 306 KB.
- Swift (language) — detected in the code
- Firebase (technology) — claimed on Devpost, not found in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- Python (language) — claimed on Devpost, not found in the code
- PyTorch (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (96 of 96)

```
.gitignore
Hackathon.md
README.md
TideParty.xcodeproj/project.pbxproj
TideParty.xcodeproj/project.xcworkspace/contents.xcworkspacedata
TideParty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
TideParty.xcodeproj/project.xcworkspace/xcuserdata/anthonylamas.xcuserdatad/UserInterfaceState.xcuserstate
TideParty.xcodeproj/xcuserdata/anthonylamas.xcuserdatad/xcschemes/xcschememanagement.plist
TideParty.xcodeproj/xcuserdata/oleg.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
TideParty.xcodeproj/xcuserdata/oleg.xcuserdatad/xcschemes/xcschememanagement.plist
TideParty/App/GenerativeAI-Info.plist
TideParty/Assets.xcassets/AccentColor.colorset/Contents.json
TideParty/Assets.xcassets/AppIcon.appiconset/Contents.json
TideParty/Assets.xcassets/ClownfishBadge.imageset/Contents.json
TideParty/Assets.xcassets/Contents.json
TideParty/Assets.xcassets/CrabBadge.imageset/Contents.json
TideParty/Assets.xcassets/EelBadge.imageset/Contents.json
TideParty/Assets.xcassets/Flag.imageset/Contents.json
TideParty/Assets.xcassets/JellyfishBadge.imageset/Contents.json
TideParty/Assets.xcassets/LocalsFavoriteBG.colorset/Contents.json
TideParty/Assets.xcassets/MainBlue.colorset/Contents.json
TideParty/Assets.xcassets/MusselsBadge.imageset/Contents.json
TideParty/Assets.xcassets/OctopusBadge.imageset/Contents.json
TideParty/Assets.xcassets/OtterBadge.imageset/Contents.json
TideParty/Assets.xcassets/OttoBook.imageset/Contents.json
TideParty/Assets.xcassets/OttoCheckeredFlag.imageset/Contents.json
TideParty/Assets.xcassets/OttoChill.imageset/Contents.json
TideParty/Assets.xcassets/OttoFloatie.imageset/Contents.json
TideParty/Assets.xcassets/OttoMonacle.imageset/Contents.json
TideParty/Assets.xcassets/OttoPuffer.imageset/Contents.json
TideParty/Assets.xcassets/OttoWaving.imageset/Contents.json
TideParty/Assets.xcassets/SardineBadge.imageset/Contents.json
TideParty/Assets.xcassets/SeaAnenomeBadge.imageset/Contents.json
TideParty/Assets.xcassets/SeahorseBadge.imageset/Contents.json
TideParty/Assets.xcassets/SecondaryBlue.colorset/Contents.json
TideParty/Assets.xcassets/SharkBadge.imageset/Contents.json
TideParty/Assets.xcassets/SpongeBadge.imageset/Contents.json
TideParty/Assets.xcassets/StarfishBadge.imageset/Contents.json
TideParty/Assets.xcassets/TurtleBadge.imageset/Contents.json
TideParty/Assets.xcassets/WhaleBadge.imageset/Contents.json
TideParty/Components/DonateCard.swift
TideParty/Core/DesignSystem/DesignSystem.swift
TideParty/Data/Models/Badge.swift
TideParty/Data/Models/CerebrasModels.swift
TideParty/Data/Models/Models.swift
TideParty/Data/Models/PartyModels.swift
TideParty/Data/Models/TideModels.swift
TideParty/Data/Models/TideSpot.swift
TideParty/Data/Models/WeatherModels.swift
TideParty/Data/Services/AIServiceProtocol.swift
TideParty/Data/Services/CerebrasService.swift
TideParty/Data/Services/GenAIService.swift
TideParty/Data/Services/Services.swift
TideParty/Data/Services/TideService.swift
TideParty/Data/Services/WeatherService.swift
TideParty/Features/.gitignore
TideParty/Features/Account/AccountView.swift
TideParty/Features/Landing/Components/AIInsightView.swift
TideParty/Features/Landing/Components/HomeMapCard.swift
TideParty/Features/Landing/Components/TideGraphView.swift
TideParty/Features/Landing/LandingView.swift
TideParty/Features/Landing/LandingViewModel.swift
TideParty/Features/Onboarding/OnboardingContainerView.swift
TideParty/Features/Onboarding/OnboardingViewModel.swift
TideParty/Features/Onboarding/OnboardingViews.swift
TideParty/Features/Party/Components/CodeInputView.swift
TideParty/Features/Party/Components/PlayerProgressBar.swift
TideParty/Features/Party/PartyJoinView.swift
TideParty/Features/Party/PartyRaceView.swift
TideParty/Features/Party/PartyViewModel.swift
TideParty/Features/Party/RaceWinView.swift
TideParty/Features/Party/TidePartySection.swift
TideParty/Features/Safety/SafetyPledgeView.swift
TideParty/Features/Scanner/CameraPreview.swift
TideParty/Features/Scanner/DiscoveryResultView.swift
TideParty/Features/Scanner/marine_labels.json
TideParty/Features/Scanner/ScannerView.swift
TideParty/Features/Scanner/ScannerViewModel.swift
TideParty/Features/Scanner/ScannerWaveView.swift
TideParty/Features/Spots/Components/CarouselView.swift
TideParty/Features/Spots/Components/SpotCardView.swift
TideParty/Features/Spots/SpotCluster.swift
TideParty/Features/Spots/SpotsContainerView.swift
TideParty/Features/Spots/SpotsListView.swift
TideParty/Features/Spots/SpotsMapView.swift
TideParty/Features/Spots/SpotsViewModel.swift
TideParty/imagenet22k_labels.json
TideParty/Info.plist
TideParty/question_ex.json
TideParty/Services/AuthManager.swift
TideParty/Services/CameraManager.swift
TideParty/Services/LocationManager.swift
TideParty/Services/PartyService.swift
TideParty/Services/UserStatsService.swift
TideParty/TidePartyApp.swift
TideProfileIcons/OttoWaving.psd
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Fix .gitignore entry for GoogleService-Info.plist
- xp change
- Update .gitignore to exclude GoogleService-Info.plist
- Fix broken code structure in OnboardingViewModel
- Enhance auth logic with robust retry-on-fail strategy
- Sanitize email input during authentication
- all the icons
- Fix email login and ensure Firebase user creation
- removed refresh
- Move and update README.md to project root with correct file paths
- Add party race win screen with podium, fix XP display timing, improve tide trend calculation, add AI insights refresh button
- Add project documentation
- Style DiscoveryResultView with OttoBook and updated card designs
- header
- Redesign AIInsightView with Otto mascot and collapsible UI
- party style fix
- Update DiscoveryResultView
- Add DonateCard to landing page with ocean conservation charity links
- Merge branch 'main' of remote; resolve DiscoveryResultView conflict
- added party page

## Key source files (fetched from GitHub, selected and truncated for size)

### Hackathon.md

```markdown
# Tide Party

Discover and collect local marine life while learning how to protect our oceans.

## Inspiration

Hey! We're TideParty, but our names are Oleg and Anthony. We LOVE tide pooling in Santa Cruz. It’s a relaxing activity that many people enjoy, and Santa Cruz is an ideal place for it thanks to its rocky cliffs and abundant marine life. However, when people go tide pooling, they often don’t know what they’re looking at. We built this app to help people identify marine organisms in real time, learn about them, and gain a deeper appreciation for ocean conservation.

## What it does

Tide Party is an interactive mobile app that turns tide pooling into a fun, educational experience:

- Discover and navigate to local tide pooling spots
- Identify marine life in real time by pointing your camera at a sea creature
- Take photos and collect the creatures you find
- Learn detailed information about each species
- Earn badges by collecting creatures and answering quiz questions
- Join party rooms with friends and race to find the most creatures

## How we built it

We built the app using **SwiftUI** for the frontend and **Firebase** for backend storage and real-time features. For marine life recognition, we trained a custom machine learning model using **PyTorch** and converted it into an Apple-compatible Core ML model for on-device inference.

## Challenges we ran into

Training the model was our biggest challenge. Due to the limited available image data, we relied heavily on image augmentation to improve performance. Collecting and labeling data was also difficult, and early versions of the model suffered from underfitting. After extensive preprocessing and training, we were able to build a generalized model capable of accurately classifying Santa Cruz tide pool marine life with **94% accuracy**.

## Accomplishments that we’re proud of

We’re proud that we were able to fully realize our original vision, from real-time classification to multiplayer features. Achieving high model accuracy, building a polished UI, and designing a friendly mascot to enhance the user experience were major wins for us.

## What we learned

We learned how to work quickly and effectively under pressure, how to design thoughtful UI/UX animations and interactions in Swift, and how to train and deploy a machine learning model within a real mobile application.

## What’s next for Tide Party

Next, we want to release Tide Party to real users. It would be especially impactful for school field trips, where students can compete to discover marine life together. We also plan to:

- Expand and improve our tide pool location map
- Train a more robust model to classify additional species
- Support multiple creature detections in a single frame
- Add online leaderboards and public party rooms
- Partner with charities focused on local ocean conservation

```

### TideParty/TidePartyApp.swift

```swift
import SwiftUI
import FirebaseCore

enum Route {
    case spots
    case scanner
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        FirebaseApp.configure()
        return true
    }
}

@main
struct TidePartyApp: App {
    @StateObject private var authManager = AuthManager.shared
    @State private var navigationPath = NavigationPath()
    @State private var hasPledgedSafety = false
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    
    var body: some Scene {
        WindowGroup {
            Group {
                if authManager.isLoading {
                    // Show loading while checking auth state
                    ZStack {
                        Color("MainBlue").ignoresSafeArea()
                        ProgressView()
                            .tint(.white)
                            .scaleEffect(1.5)
                    }
                } else if authManager.currentUser != nil {
                    // Logged in - show safety pledge first, then main content
                    if !hasPledgedSafety {
                        SafetyPledgeView(onConfirm: {
                            withAnimation {
                                hasPledgedSafety = true
                            }
                        })
                    } else {
                        NavigationStack(path: $navigationPath) {
                            LandingView(
                                onFindSpots: {
                                    navigationPath.append(Route.spots)
                                },
                                onOpenCamera: {
                                    navigationPath.append(Route.scanner)
                                }
                            )
                            .navigationDestination(for: Route.self) { route in
                                switch route {
                                case .spots:
                                    SpotsContainerView(onOpenCamera: {
                                        navigationPath.append(Route.scanner)
                                    })
                                case .scanner:
                                    ScannerView()
                                        .navigationBarBackButtonHidden(true)
                                }
                            }
                        }
                    }
                } else {
                    OnboardingContainerView()
                }
            }
        }
    }
}

```

### TideParty/Services/AuthManager.swift

```swift
import SwiftUI
import Combine
import FirebaseAuth

@MainActor
class AuthManager: ObservableObject {
    static let shared = AuthManager()
    
    @Published var currentUser: User?
    @Published var isLoading: Bool = true
    
    private var authStateHandle: AuthStateDidChangeListenerHandle?
    
    private init() {
        authStateHandle = Auth.auth().addStateDidChangeListener { [weak self] _, user in
            Task { @MainActor in
                self?.currentUser = user
                self?.isLoading = false
            }
        }
    }
    
    deinit {
        if let handle = authStateHandle {
            Auth.auth().removeStateDidChangeListener(handle)
        }
    }
    
    func signOut() throws {
        try Auth.auth().signOut()
    }
}

```

### TideParty/Services/LocationManager.swift

```swift
import Foundation
import CoreLocation
import MapKit
import Combine

class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    static let shared = LocationManager()
    
    private let manager = CLLocationManager()
    
    @Published var userLocation: CLLocation?
    @Published var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 36.9741, longitude: -122.0308), // Santa Cruz default
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )
    @Published var authorizationStatus: CLAuthorizationStatus = .notDetermined
    
    private override init() {
        super.init()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.requestWhenInUseAuthorization()
        manager.startUpdatingLocation()
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        DispatchQueue.main.async {
            self.userLocation = location
            self.region = MKCoordinateRegion(
                center: location.coordinate,
                span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
            )
        }
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        DispatchQueue.main.async {
            self.authorizationStatus = manager.authorizationStatus
            if manager.authorizationStatus == .authorizedWhenInUse ||
               manager.authorizationStatus == .authorizedAlways {
                manager.startUpdatingLocation()
            }
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("Location error: \(error.localizedDescription)")
    }
}

```

### TideParty/Services/CameraManager.swift

```swift
import AVFoundation
import SwiftUI
import Combine

class CameraManager: NSObject, ObservableObject {
    @Published var permissionGranted = false
    @Published var error: String?
    
    let session = AVCaptureSession()
    private let sessionQueue = DispatchQueue(label: "com.tideparty.cameraSessionQueue")
    private let videoOutput = AVCaptureVideoDataOutput()
    
    // Pass visual stream to the UI
    var previewLayer: AVCaptureVideoPreviewLayer {
        let layer = AVCaptureVideoPreviewLayer(session: session)
        layer.videoGravity = .resizeAspectFill
        return layer
    }
    
    override init() {
        super.init()
        checkPermission()
        sessionQueue.async { [weak self] in
            self?.configureSession()
            self?.session.startRunning()
        }
    }
    
    func checkPermission() {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            permissionGranted = true
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                DispatchQueue.main.async {
                    self?.permissionGranted = granted
                }
            }
        default:
            permissionGranted = false
            error = "Camera access is denied. Please enable it in Settings."
        }
    }
    
    private func configureSession() {
        guard permissionGranted else { return }
        session.beginConfiguration()
        defer { session.commitConfiguration() }
        
        session.sessionPreset = .high
        
        // Input
        guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
            DispatchQueue.main.async { self.error = "No back camera found" }
            return
        }
        
        do {
            let input = try AVCaptureDeviceInput(device: device)
            if session.canAddInput(input) {
                session.addInput(input)
            }
        } catch {
            DispatchQueue.main.async { self.error = "Error setting up camera input: \(error.localizedDescription)" }
            return
        }
        
        // Output (for Vision processing later)
        if session.canAddOutput(videoOutput) {
            session.addOutput(videoOutput)
            videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)]
            // We will set the delegate later when we build the Vision service
            // videoOutput.setSampleBufferDelegate(self, queue: sessionQueue)
        }
    }
}

```

### TideParty/Components/DonateCard.swift

```swift
//
//  DonateCard.swift
//  TideParty
//
//  Card encouraging donations to ocean conservation charities
//

import SwiftUI

struct DonateCard: View {
    var body: some View {
        VStack(spacing: 0) {
            // Header with wave-like gradient
            ZStack(alignment: .leading) {
                LinearGradient(
                    colors: [Color("MainBlue"), Color("MainBlue").opacity(0.8)],
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
                
                HStack(spacing: 12) {
                    ZStack {
                        Circle()
                            .fill(Color.white)
                            .frame(width: 44, height: 44)
                        Image(systemName: "heart.fill")
                            .font(.system(size: 24))
                            .foregroundColor(.red)
                    }
                    
                    VStack(alignment: .leading, spacing: 2) {
                        Text("Protect Our Oceans")
                            .font(.system(size: 18, weight: .bold))
                            .foregroundColor(.white)
                        Text("Support Marine Conservation")
                            .font(.system(size: 13))
                            .foregroundColor(.white.opacity(0.85))
                    }
                    
                    Spacer()
                    
                    Image(systemName: "water.waves")
                        .font(.system(size: 24))
                        .foregroundColor(.white.opacity(0.4))
                }
                .padding(16)
            }
            .frame(height: 76)
            
            // Charity buttons section
            VStack(spacing: 10) {
                HStack(spacing: 10) {
                    CharityButton(
                        name: "Ocean Conservancy",
                        url: "https://oceanconservancy.org/donate/"
                    )
                    
                    CharityButton(
                        name: "Surfrider",
                        url: "https://www.surfrider.org/donate"
                    )
                }
                
                HStack(spacing: 10) {
                    CharityButton(
                        name: "Marine Conservation",
                        url: "https://marine-conservation.org/donate/"
                    )
                    
                    CharityButton(
                        name: "Sea Shepherd",
                        url: "https://www.seashepherd.org/donate/"
                    )
                }
            }
            .padding(12)
            .background(Color("MainBlue").opacity(0.05))
        }
        .clipShape(RoundedRectangle(cornerRadius: 20))
        .overlay(
            RoundedRectangle(cornerRadius: 20)
                .stroke(Color("MainBlue").opacity(0.15), lineWidth: 1)
        )
    }
}

struct CharityButton: View {
    let name: String
    let url: String
    
    var body: some View {
        Link(destination: URL(string: url)!) {
            Text(name)
                .font(.system(size: 12, weight: .semibold))
                .foregroundColor(Color("MainBlue"))
                .lineLimit(1)
                .padding(.horizontal, 12)
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity)
                .background(Color.white)
                .cornerRadius(12)
                .shadow(color: Color("MainBlue").opacity(0.1), radius: 4, x: 0, y: 2)
        }
    }
}

#Preview {
    DonateCard()
        .padding()
}

```

### TideParty/Services/UserStatsService.swift

```swift
//
//  UserStatsService.swift
//  TideParty
//
//  Service for managing user statistics in Firebase Firestore
//

import Foundation
import FirebaseFirestore
import FirebaseAuth
import Combine

@MainActor
class UserStatsService: ObservableObject {
    static let shared = UserStatsService()
    
    private let db = Firestore.firestore()
    
    @Published var displayName: String = ""
    @Published var creatureCounts: [String: Int] = [:]  // creature name -> catch count
    @Published var locationsVisited: Int = 0
    @Published var quizCorrect: Int = 0
    @Published var selectedBadgeId: Int = 0  // ID of the badge the user selected as profile icon
    
    private init() {
        // Load from UserDefaults on init
        displayName = UserDefaults.standard.string(forKey: "displayName") ?? ""
        if let counts = UserDefaults.standard.dictionary(forKey: "creatureCounts") as? [String: Int] {
            creatureCounts = counts
        }
        selectedBadgeId = UserDefaults.standard.integer(forKey: "selectedBadgeId")
    }
    
    // MARK: - User ID Helper
    private var userId: String? {
        Auth.auth().currentUser?.uid
    }
    
    // MARK: - Create User Stats Document
    func createUserStats(displayName: String) async throws {
        guard let userId = userId else {
            throw NSError(domain: "UserStatsService", code: -1, userInfo: [NSLocalizedDescriptionKey: "No user logged in"])
        }
        
        // Save locally
        self.displayName = displayName
        UserDefaults.standard.set(displayName, forKey: "displayName")
        
        // Create Firestore document
        let data: [String: Any] = [
            "displayName": displayName,
            "creatureCounts": [:],
            "locationsVisited": 0,
            "quizCorrect": 0,
            "selectedBadgeId": 0,
            "createdAt": FieldValue.serverTimestamp()
        ]
        
        try await db.collection("users").document(userId).setData(data, merge: true)
        print("✅ Created user stats document for \(userId)")
    }
    
    // MARK: - Fetch User Stats
    @discardableResult
    func fetchStats() async throws -> Bool {
        guard let userId = userId else { return false }
        
        let doc = try await db.collection("users").document(userId).getDocument()
        
        if doc.exists, let data = doc.data() {
            self.displayName = data["displayName"] as? String ?? ""
            if let counts = data["creatureCounts"] as? [String: Int] {
                self.creatureCounts = counts
            }
            self.locationsVisited = data["locationsVisited"] as? Int ?? 0
            self.quizCorrect = data["quizCorrect"] as? Int ?? 0
            self.selectedBadgeId = data["selectedBadgeId"] as? Int ?? 0
            
            // Cache locally
            UserDefaults.standard.set(displayName, forKey: "displayName")
            UserDefaults.standard.set(creatureCounts, forKey: "creatureCounts")
            UserDefaults.standard.set(selectedBadgeId, forKey: "selectedBadgeId")
            
            return true
        }
        
        return false
    }
    
    // MARK: - Capture Creature
    /// Returns the new catch count for this creature (1 = first catch, 2+ = repeat)
    @discardableResult
    func captureCreature(name: String) async throws -> Int {
        guard let userId = userId else { return 0 }
        
        // Increment local count
        let newCount = (creatureCounts[name] ?? 0) + 1
        creatureCounts[name] = newCount
        UserDefaults.standard.set(creatureCounts, forKey: "creatureCounts")
        
        // Update Firestore
        try await db.collection("users").document(userId).updateData([
            "creatureCounts.\(name)": newCount
        ])
        
        if newCount == 1 {
            print("✅ First catch of \(name)! (unique creatures: \(uniqueCreatureCount))")
        } else {
            print("🔄 Caught \(name) again! (count: \(newCount))")
        }
        
        return newCount
    }
    
    // MARK: - Increment Quiz Score
    func incrementQuizCorrect() {
        guard let userId = userId else { return }
        
        quizCorrect += 1
        UserDefaults.standard.set(quizCorrect, forKey: "quizCorrect") // Update local immediately
        
        // Update Firestore
        db.collection("users").document(userId).updateData([
            "quizCorrect": FieldValue.increment(Int64(1))
        ]) { error in
            if let error = error {
                print("Error updating quiz score: \(error)")
            } else {
                print("✅ Quiz score incremented!")
            }
        }
    }
    
    // MARK: - Set Selected Badge
    func setSelectedBadge(_ badgeId: Int) {
        guard let userId = userId else { return }
        
        selectedBadgeId = badgeId
        UserDefaults.standard.set(badgeId, forKey: "selectedBadgeId")
        
        // Update Firestore
        db.collection("users").document(userId).updateData([
            "selectedBadgeId": badgeId
        ]) { error in
            if let error = error {
                print("Error updating selected badge: \(error)")
            } else {
                print("✅ Selected badge updated to \(badgeId)!")
            }
        }
    }
    
    // MARK: - Helpers
    
    /// Number of unique creatures caught
    var uniqueCreatureCount: Int {
        creatureCounts.count
    }
    
    /// Get catch count for a specific creature
    func getCatchCount(for name: String) -> Int {
        creatureCounts[name] ?? 0
    }
    
    /// Check if creature has been caught before
    func isCreatureCaptured(_ name: String) -> Bool {
        creatureCounts[name] != nil
    }
    
    // MARK: - Next Badge Logic
    
    struct NextBadge {
        let title: String // e.g., "Creature Expert Badge"
        let subtitle: String // e.g., "2 unique creatures away"
        let progress: Double // 0.0 to 1.0
        let icon: String // "tortoise.fill
[truncated — 3495 more characters]
```

### TideParty/Services/PartyService.swift

```swift
//
//  PartyService.swift
//  TideParty
//
//  Service for managing multiplayer party operations with Firebase Firestore
//

import Foundation
import FirebaseFirestore
import FirebaseAuth

@MainActor
class PartyService {
    static let shared = PartyService()
    
    private let db = Firestore.firestore()
    private var partyListener: ListenerRegistration?
    
    private init() {}
    
    // MARK: - User ID Helper
    
    private var userId: String? {
        Auth.auth().currentUser?.uid
    }
    
    // MARK: - Create Party
    
    /// Creates a new party and returns the 4-digit join code
    func createParty(mode: GameMode, target: Int, locationId: String? = nil, locationName: String? = nil) async throws -> String {
        guard let userId = userId else {
            throw NSError(domain: "PartyService", code: -1, userInfo: [NSLocalizedDescriptionKey: "No user logged in"])
        }
        
        // Generate unique code
        let code = try await generateUniqueCode()
        
        // Get current user data for first player
        let player = await getCurrentPlayerData()
        
        // Create party document
        var party = Party(
            code: code,
            hostId: userId,
            locationId: locationId,
            locationName: locationName,
            gameMode: mode,
            targetValue: target,
            players: [player]
        )
        
        // Encode to Firestore-compatible format using Firestore's encoder
        let encoder = Firestore.Encoder()
        let data = try encoder.encode(party)
        
        try await db.collection("parties").document(code).setData(data)
        print("✅ Created party with code: \(code)")
        
        return code
    }
    
    // MARK: - Join Party
    
    /// Joins an existing party with the given code
    func joinParty(code: String) async throws {
        guard let userId = userId else {
            throw NSError(domain: "PartyService", code: -1, userInfo: [NSLocalizedDescriptionKey: "No user logged in"])
        }
        
        let partyRef = db.collection("parties").document(code)
        
        try await db.runTransaction { transaction, errorPointer in
            let partyDoc: DocumentSnapshot
            do {
                partyDoc = try transaction.getDocument(partyRef)
            } catch let error as NSError {
                errorPointer?.pointee = error
                return nil
            }
            
            guard partyDoc.exists else {
                let error = NSError(domain: "PartyService", code: 404, userInfo: [NSLocalizedDescriptionKey: "Party not found"])
                errorPointer?.pointee = error
                return nil
            }
            
            guard let data = partyDoc.data(),
                  let statusString = data["status"] as? String,
                  let status = PartyStatus(rawValue: statusString) else {
                let error = NSError(domain: "PartyService", code: -2, userInfo: [NSLocalizedDescriptionKey: "Invalid party data"])
                errorPointer?.pointee = error
                return nil
            }
            
            // Check if party is joinable
            if status != .waiting && status != .active {
                let error = NSError(domain: "PartyService", code: -3, userInfo: [NSLocalizedDescriptionKey: "Party has already finished"])
                errorPointer?.pointee = error
                return nil
            }
            
            // Check if user already in party
            if let players = data["players"] as? [[String: Any]],
               players.contains(where: { ($0["id"] as? String) == userId }) {
                // Already in party - this is okay, just return
                return nil
            }
            
            // Add player to party
            let player = self.getCurrentPlayerDataSync()
            transaction.updateData([
                "players": FieldValue.arrayUnion([self.playerToDict(player)])
            ], forDocument: partyRef)
            
            return nil
        }
        
        print("✅ Joined party: \(code)")
    }
    
    // MARK: - Listen to Party
    
    /// Sets up real-time listener for party updates
    func listenToParty(code: String, completion: @escaping (Result<Party, Error>) -> Void) {
        // Remove any existing listener
        stopListening()
        
        partyListener = db.collection("parties").document(code).addSnapshotListener { snapshot, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let snapshot = snapshot, snapshot.exists else {
                let error = NSError(domain: "PartyService", code: 404, userInfo: [NSLocalizedDescriptionKey: "Party not found"])
                completion(.failure(error))
                return
            }
            
            do {
                let party = try snapshot.data(as: Party.self)
                completion(.success(party))
            } catch {
                completion(.failure(error))
            }
        }
    }
    
    /// Stops the active party listener
    func stopListening() {
        partyListener?.remove()
        partyListener = nil
    }
    
    /// Updates player's score when they catch a creature (with transaction safety)
    func updateScore(code: String, creatureId: String) async throws -> Int {
        guard let userId = userId else {
            throw NSError(domain: "PartyService", code: -1, userInfo: [NSLocalizedDescriptionKey: "No user logged in"])
        }
        
        let partyRef = db.collection("parties").document(code)
        
        let result = try await db.runTransaction { (transaction, errorPointer) -> Any? in
            let partyDoc: DocumentSnapshot
            do {
                partyDoc = try transaction.getDocument(partyRef)
            } catch let error as NSError {
                errorPointer?.pointee =
[truncated — 8019 more characters]
```

### TideParty/Data/Models/Models.swift

```swift
import Foundation

struct WeatherData {
    let temp: Int
    let condition: String // "Sunny", "Cloudy", etc.
    let isSunny: Bool
    var forecastTemp: Int? // Added for +2h forecast
}

struct TideData {
    let height: Double
    let trend: String // "Rising" or "Falling"
    let nextLowTide: Date
}

```

### TideParty/Data/Models/TideModels.swift

```swift
import Foundation

// MARK: - API Response
struct TideResponse: Codable {
    let predictions: [TidePrediction]
}

struct TidePrediction: Codable {
    let t: String // Timestamp "yyyy-MM-dd HH:mm"
    let v: String // Value (Height) in feet
    let type: String // "H" or "L"
}

// MARK: - Domain Model
struct TideCurvePoint: Identifiable {
    let id = UUID()
    let date: Date
    let height: Double
    let type: String? // "H" or "L" or nil for interpolated
}

```

[41 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]