Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Beacon.swiftpm/Components/ResourceCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,24 @@ struct ResourceCard: View {
let resource: Resource

var body: some View {
VStack(spacing: 12) {
VStack(spacing: 8) {
Image(systemName: resource.icon)
.font(.system(size: 30))
.font(.system(size: 32))
.foregroundStyle(.blue)

Text(resource.title)
.font(.headline)
.multilineTextAlignment(.center)
.lineLimit(2)

Text(resource.subtitle)
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.lineLimit(1)
}
.frame(maxWidth: .infinity)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.ultraThinMaterial)
.cornerRadius(20)
}
Expand Down
82 changes: 59 additions & 23 deletions Beacon.swiftpm/Data/ResourcesData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,75 @@ import Foundation

let resourcesData: [Resource] = [
Resource(
title: "First Aid",
subtitle: "Bleeding, CPR",
icon: "cross.case.fill",
details: "Apply pressure to bleeding wounds. If unconscious and not breathing, begin CPR if trained."
title: "Cyclone Preparedness",
subtitle: "Securing Home & Supplies",
icon: "hurricane",
details: "Before a cyclone hits, secure all loose outdoor items that could become projectiles. Board up windows or close storm shutters. Gather a 72-hour supply of food and water (at least 1 gallon per person per day).\n\nEnsure your mobile devices are fully charged and have backup power banks ready. Keep important documents in a waterproof container and know your local evacuation routes.\n\nReference: Ready.gov (Federal Emergency Management Agency)"
),
Resource(
title: "Cyclone Safety",
subtitle: "Shelter tips",
icon: "hurricane",
details: "Move to interior rooms. Stay away from windows. Keep emergency kit nearby."
title: "During a Cyclone",
subtitle: "Shelter in Place Safely",
icon: "wind",
details: "Stay indoors and away from all windows, skylights, and glass doors. Retreat to a small, windowless interior room, closet, or hallway on the lowest level that is not prone to flooding.\n\nDo not go outside during the 'eye' of the storm. The calm is temporary, and severe winds will return rapidly from the opposite direction.\n\nReference: National Weather Service (NWS) / NOAA"
),
Resource(
title: "Water Safety",
subtitle: "Purification",
title: "Water Purification",
subtitle: "Making Water Safe to Drink",
icon: "drop.fill",
details: "Boil water for 1 minute or use purification tablets before drinking."
details: "During floods, assume all tap and ground water is contaminated. Boiling is the safest method: bring water to a rolling boil for 1 full minute (3 minutes at high elevations).\n\nIf boiling isn't possible, use unscented liquid household bleach (5-9% sodium hypochlorite). Add 8 drops per gallon of clear water (16 drops if cloudy), stir, and let stand for 30 minutes before drinking.\n\nReference: Centers for Disease Control and Prevention (CDC) & EPA"
),
Resource(
title: "Severe Bleeding",
subtitle: "Stop the Bleed",
icon: "bandage.fill",
details: "Apply firm, continuous direct pressure to the wound using a clean cloth or your hands. Do not lift the cloth to check the wound; simply add more layers if blood seeps through.\n\nIf bleeding is on a limb and cannot be controlled by pressure, apply a tourniquet 2 to 3 inches above the wound (never over a joint). Tighten until bleeding stops completely, secure it, and note the time of application.\n\nReference: American College of Surgeons (Stop the Bleed Program)"
),
Resource(
title: "Basic CPR (Adult)",
subtitle: "Hands-Only Resuscitation",
icon: "heart.text.square.fill",
details: "If an adult collapses and is unresponsive and not breathing normally, call for emergency help immediately.\n\nPlace the heel of one hand in the center of the chest, place the other hand on top, and interlace your fingers. Push hard and fast—at least 2 inches deep at a rate of 100 to 120 pushes a minute. Do not stop until help arrives or an AED is ready to use.\n\nReference: American Heart Association (AHA)"
),
Resource(
title: "Downed Power Lines",
subtitle: "Post-Storm Electrical Safety",
icon: "bolt.trianglebadge.exclamationmark.fill",
details: "Always assume that fallen power lines are live and extremely dangerous. Stay at least 35 feet away (about the length of a school bus).\n\nNever drive over downed power lines. If a line falls on your car while you are in it, stay inside the vehicle and call for help. Remember that floodwaters can carry electrical currents from downed lines over long distances.\n\nReference: Occupational Safety and Health Administration (OSHA)"
),
Resource(
title: "Power Saving",
subtitle: "Extend battery",
icon: "battery.100",
details: "Enable Low Power Mode. Reduce screen brightness. Close background apps."
title: "Treating Shock",
subtitle: "Post-Trauma Care",
icon: "bolt.heart.fill",
details: "Shock often follows severe trauma or blood loss. Lay the person flat on their back. If there is no suspicion of a head, neck, back, or leg injury, elevate their legs about 12 inches.\n\nKeep the person warm with a blanket or jacket to prevent hypothermia. Loosen restrictive clothing. Do not give the person anything to eat or drink, as they may vomit or choke.\n\nReference: Mayo Clinic First Aid Guidelines"
),
Resource(
title: "SOS Signals",
subtitle: "Get attention",
icon: "wave.3.right",
details: "Use whistle, flashlight flashes, or reflective surfaces to signal rescuers."
title: "Carbon Monoxide Safety",
subtitle: "Generator & Stove Rules",
icon: "smoke.fill",
details: "Carbon Monoxide (CO) is an odorless, colorless gas that can kill rapidly. Never use a generator, camp stove, or charcoal grill indoors, in a garage, or within 20 feet of windows and doors.\n\nSymptoms of CO poisoning include headache, dizziness, weakness, nausea, vomiting, and confusion. If you suspect CO poisoning, move to fresh air immediately and seek medical help.\n\nReference: Centers for Disease Control and Prevention (CDC)"
),
Resource(
title: "Snake Bites",
subtitle: "Floods & Displaced Wildlife",
icon: "exclamationmark.triangle.fill",
details: "Floods and heavy rains displace snakes. If bitten, keep the victim calm and still to slow the spread of venom. Position the affected area at or slightly below heart level.\n\nDo NOT attempt to suck out the venom, do NOT cut the wound, and do NOT apply a tourniquet or ice. Wash the area gently with soap and water, and seek immediate medical attention.\n\nReference: World Health Organization (WHO) & CDC"
),
Resource(
title: "Fractures & Sprains",
subtitle: "Immobilizing Injuries",
icon: "cross.case.fill",
details: "If you suspect a broken bone, do not try to realign it or push a bone back in. Stop any bleeding first by applying pressure around (not directly on) the fracture.\n\nImmobilize the injured area in the exact position you found it. Create a rigid splint using rolled up magazines, boards, or thick cardboard, tying it securely with cloth above and below the injured joint.\n\nReference: American Red Cross"
),
Resource(
title: "Drowning Response",
subtitle: "Safe Water Rescues",
icon: "drop.fill",
details: "Use the 'Reach or Throw, Don't Go' rule. Never jump into fast-moving floodwaters to save someone, as you will likely become a victim yourself.\n\nReach out with a long pole, branch, or rope, or throw a buoyant object like a life jacket or cooler. Once the person is on dry land, check for breathing and begin CPR immediately if they are unresponsive.\n\nReference: American Red Cross Water Safety"
),
Resource(
title: "Evacuation",
subtitle: "When to move",
icon: "figure.walk",
details: "Evacuate if instructed by authorities. Follow marked safe routes."
title: "Evacuation Go-Bag",
subtitle: "The 72-Hour Survival Kit",
icon: "bag.fill",
details: "Your emergency Go-Bag should be packed well before a disaster. It must include: a 3-day supply of non-perishable food and water, a flashlight, extra batteries, and a battery-powered or hand-crank radio.\n\nAdditionally, pack a first aid kit, a whistle to signal for help, necessary medications, dust masks, and copies of important personal documents in a waterproof bag.\n\nReference: FEMA / Ready.gov"
)
]
71 changes: 46 additions & 25 deletions Beacon.swiftpm/Views/BeaconView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,63 @@ struct BeaconView: View {
@EnvironmentObject var vm: BeaconViewModel
@EnvironmentObject var locationManager: LocationManager

@State private var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic)

var body: some View {
ZStack(alignment: .bottom) {
Map(position: $cameraPosition) {

Map {
if let myLoc = locationManager.location {
Annotation("Me",
coordinate: myLoc.coordinate) {
Circle()
.fill(.blue)
.frame(width: 20, height: 20)
}
// Yourself
if let myLoc = locationManager.location {
Annotation("Me", coordinate: myLoc.coordinate) {
Circle()
.fill(.blue)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(.white, lineWidth: 2))
.shadow(radius: 2)
}

// Peers
ForEach(vm.peers) { peer in
Annotation(peer.name,
coordinate: CLLocationCoordinate2D(
latitude: peer.latitude,
longitude: peer.longitude
)) {
}

// Peers
ForEach(vm.peers) { peer in
Annotation(peer.name, coordinate: CLLocationCoordinate2D(
latitude: peer.latitude,
longitude: peer.longitude
)) {
ZStack {
Circle()
.fill(peer.status == .help ? .red : .orange)
.frame(width: 18, height: 18)
.overlay(Circle().stroke(.white, lineWidth: 2))
.shadow(radius: 2)

if peer.status == .help {
VStack(spacing: -2) {
Text("HELP")
.font(.caption2.bold())
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.red)
.foregroundStyle(.white)
.clipShape(Capsule())
.shadow(radius: 2)

Image(systemName: "arrowtriangle.down.fill")
.font(.system(size: 8))
.foregroundStyle(.red)
}
.offset(y: -28)
}
}
}
}
.mapControls {
MapUserLocationButton()
MapCompass()
}
.ignoresSafeArea()
}
.mapControls {
MapUserLocationButton()
MapCompass()
}
.safeAreaInset(edge: .bottom) {
// Control Panel
VStack(spacing: 12) {

Button {
vm.toggleHelp()
} label: {
Expand All @@ -63,7 +85,6 @@ struct BeaconView: View {
Text("Nearby People: \(vm.peers.count)")
.font(.subheadline)
.foregroundStyle(.secondary)

}
.padding()
.background(.ultraThinMaterial)
Expand Down
42 changes: 28 additions & 14 deletions Beacon.swiftpm/Views/ResourceDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,34 @@ struct ResourceDetailView: View {
let resource: Resource

var body: some View {
VStack(spacing: 20) {
Image(systemName: resource.icon)
.font(.system(size: 60))
.foregroundStyle(.blue)

Text(resource.title)
.font(.largeTitle.bold())

Text(resource.details)
.multilineTextAlignment(.center)
.padding()

Spacer()
ScrollView {
VStack(spacing: 24) {
// Header Icon
Image(systemName: resource.icon)
.font(.system(size: 64))
.foregroundStyle(.blue)
.padding(.top, 30)

// Titles
VStack(spacing: 8) {
Text(resource.title)
.font(.largeTitle.bold())
.multilineTextAlignment(.center)

Text(resource.subtitle)
.font(.title3)
.foregroundStyle(.secondary)
}

// Details Text
Text(resource.details)
.multilineTextAlignment(.leading)
.padding(.vertical)

Spacer()
}
.padding(.horizontal, 24)
}
.padding()
.presentationDragIndicator(.visible)
}
}
44 changes: 34 additions & 10 deletions Beacon.swiftpm/Views/ResourcesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,51 @@ import SwiftUI
struct ResourcesView: View {

@State private var selectedResource: Resource?
@State private var searchText = ""

let columns = [
GridItem(.flexible()),
GridItem(.flexible())
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16)
]

var filteredResources: [Resource] {
if searchText.isEmpty {
return resourcesData
} else {
return resourcesData.filter { resource in
resource.title.localizedCaseInsensitiveContains(searchText) ||
resource.subtitle.localizedCaseInsensitiveContains(searchText) ||
resource.details.localizedCaseInsensitiveContains(searchText)
}
}
}

var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {

ForEach(resourcesData) { resource in
ResourceCard(resource: resource)
.onTapGesture {
selectedResource = resource
Group {
if filteredResources.isEmpty {
ContentUnavailableView.search(text: searchText)
} else {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(filteredResources) { resource in
Button {
selectedResource = resource
} label: {
ResourceCard(resource: resource)
.frame(height: 140)
}
.buttonStyle(.plain)
.transition(.scale(scale: 0.9).combined(with: .opacity))
}
}
.padding()
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: searchText)
}
}
.padding()
}
.navigationTitle("Resources")
.searchable(text: $searchText, prompt: "search")
.sheet(item: $selectedResource) { res in
ResourceDetailView(resource: res)
}
Expand Down