diff --git a/Screenshot 2026-09-15 at 20.50.41.png b/Screenshot 2026-09-15 at 20.50.41.png new file mode 100644 index 0000000..9c5010e Binary files /dev/null and b/Screenshot 2026-09-15 at 20.50.41.png differ diff --git a/medmap.xcodeproj/project.pbxproj b/medmap.xcodeproj/project.pbxproj index 9360a08..1ae8d95 100644 --- a/medmap.xcodeproj/project.pbxproj +++ b/medmap.xcodeproj/project.pbxproj @@ -35,9 +35,22 @@ 53AF1523303F8BCD00326308 /* medmapUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = medmapUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 531881B93059F606003A4FAC /* Exceptions for "medmap" folder in "medmap" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 53AF1509303F8BCB00326308 /* medmap */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 53AF150C303F8BCB00326308 /* medmap */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 531881B93059F606003A4FAC /* Exceptions for "medmap" folder in "medmap" target */, + ); path = medmap; sourceTree = ""; }; @@ -417,6 +430,7 @@ DEVELOPMENT_TEAM = KNLHKLUVSG; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = medmap/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -450,6 +464,7 @@ DEVELOPMENT_TEAM = KNLHKLUVSG; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = medmap/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/medmap/Assets.xcassets/bluetoothIcon.imageset/Contents.json b/medmap/Assets.xcassets/bluetoothIcon.imageset/Contents.json new file mode 100644 index 0000000..55814bc --- /dev/null +++ b/medmap/Assets.xcassets/bluetoothIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bluetoothIcon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/medmap/Assets.xcassets/bluetoothIcon.imageset/bluetoothIcon.svg b/medmap/Assets.xcassets/bluetoothIcon.imageset/bluetoothIcon.svg new file mode 100644 index 0000000..3e86d16 --- /dev/null +++ b/medmap/Assets.xcassets/bluetoothIcon.imageset/bluetoothIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/medmap/Assets.xcassets/timerIcon.imageset/Contents.json b/medmap/Assets.xcassets/timerIcon.imageset/Contents.json new file mode 100644 index 0000000..47ae739 --- /dev/null +++ b/medmap/Assets.xcassets/timerIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "timerIcon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/medmap/Assets.xcassets/timerIcon.imageset/timerIcon.svg b/medmap/Assets.xcassets/timerIcon.imageset/timerIcon.svg new file mode 100644 index 0000000..9db0d2f --- /dev/null +++ b/medmap/Assets.xcassets/timerIcon.imageset/timerIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/medmap/Extensions/Color+Extensions.swift b/medmap/Extensions/Color+Extensions.swift index 262951d..a27769e 100644 --- a/medmap/Extensions/Color+Extensions.swift +++ b/medmap/Extensions/Color+Extensions.swift @@ -47,7 +47,7 @@ extension Color { static let appBackground = Color(hex: "#4078BC") static let appBackgroundDark = Color(hex: "##4078BC") - + static let appBackgroundLight = Color(hex: "#4078BC") diff --git a/medmap/Extensions/Font+CustomFonts.swift b/medmap/Extensions/Font+CustomFonts.swift index 71448db..87b74b4 100755 --- a/medmap/Extensions/Font+CustomFonts.swift +++ b/medmap/Extensions/Font+CustomFonts.swift @@ -11,10 +11,20 @@ extension Font { static let interRegular = "Inter Regular" static let interBold = "Inter Bold" + + + static let lineSeedBold = "LINE Seed JP Bold" + static let lineSeedRegular = " LINE Seed JP Regular" + + static let redHatSemibold = "Red Hat Display SemiBold" static let redHat = "Red Hat Display Regular" - + + + static let lineSeedBold22 = Font.custom(lineSeedBold, size: 22) + + static let lineSeedBold20 = Font.custom(lineSeedBold, size: 20) static let jakartaRegular24 = Font.custom(jakartaRegularName, size: 24) static let jakartaRegular64 = Font.custom(jakartaRegularName, size: 64) static let jakartaRegular60 = Font.custom(jakartaRegularName, size: 60) diff --git a/medmap/Info.plist b/medmap/Info.plist new file mode 100644 index 0000000..2a4de02 --- /dev/null +++ b/medmap/Info.plist @@ -0,0 +1,13 @@ + + + + + UIAppFonts + + LINESeedJP-Bold.ttf + LINESeedJP-ExtraBold.ttf + LINESeedJP-Regular.ttf + LINESeedJP-Thin.ttf + + + diff --git a/medmap/Resources/Fonts/LINESeedJP-Bold.ttf b/medmap/Resources/Fonts/LINESeedJP-Bold.ttf new file mode 100644 index 0000000..7f5664a Binary files /dev/null and b/medmap/Resources/Fonts/LINESeedJP-Bold.ttf differ diff --git a/medmap/Resources/Fonts/LINESeedJP-ExtraBold.ttf b/medmap/Resources/Fonts/LINESeedJP-ExtraBold.ttf new file mode 100644 index 0000000..bbddd39 Binary files /dev/null and b/medmap/Resources/Fonts/LINESeedJP-ExtraBold.ttf differ diff --git a/medmap/Resources/Fonts/LINESeedJP-Regular.ttf b/medmap/Resources/Fonts/LINESeedJP-Regular.ttf new file mode 100644 index 0000000..655c30d Binary files /dev/null and b/medmap/Resources/Fonts/LINESeedJP-Regular.ttf differ diff --git a/medmap/Resources/Fonts/LINESeedJP-Thin.ttf b/medmap/Resources/Fonts/LINESeedJP-Thin.ttf new file mode 100644 index 0000000..b910523 Binary files /dev/null and b/medmap/Resources/Fonts/LINESeedJP-Thin.ttf differ diff --git a/medmap/ViewModels/LoginViewModel.swift b/medmap/ViewModels/LoginViewModel.swift index 5b6a334..cbe3cfa 100644 --- a/medmap/ViewModels/LoginViewModel.swift +++ b/medmap/ViewModels/LoginViewModel.swift @@ -9,8 +9,8 @@ import Combine @MainActor final class LoginViewModel: ViewModel { - @Published var email = "" - @Published var password = "" + @Published var email = "katia.carvalho564@mail.pt" + @Published var password = "2fuckpprod" @Published private(set) var errorMessage: String? @Published private(set) var authenticatedPatient: LoginResponse? diff --git a/medmap/Views/Components/RadialSegmentedRingView.swift b/medmap/Views/Components/RadialSegmentedRingView.swift new file mode 100644 index 0000000..ad97da6 --- /dev/null +++ b/medmap/Views/Components/RadialSegmentedRingView.swift @@ -0,0 +1,64 @@ +// +// RadialSegmentedRingView.swift +// medmap +// +// Created by RSA on 15/09/2026. +// + +import SwiftUI + +/// A single colored arc segment of a `RadialSegmentedRingView`. +struct RadialRingSegment: Identifiable { + let id = UUID() + let percentage: Double + let color: Color +} + +/// Renders a ring made of proportional colored arc segments, with a small gap between each segment. +struct RadialSegmentedRingView: View { + + let segments: [RadialRingSegment] + var lineWidth: CGFloat = 6 + var gapDegrees: Double = 6 + + private var total: Double { + max(segments.reduce(0) { $0 + $1.percentage }, 0.0001) + } + + var body: some View { + ZStack { + ForEach(Array(segments.enumerated()), id: \.element.id) { index, segment in + let (start, end) = arcRange(for: index) + Circle() + .trim(from: start, to: end) + .stroke(segment.color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) + .rotationEffect(.degrees(-90)) + } + } + } + + /// Computes the normalized (0...1) start/end trim values for the segment at `index`, leaving a small gap between segments. + private func arcRange(for index: Int) -> (CGFloat, CGFloat) { + let gapFraction = gapDegrees / 360 + var start = 0.0 + for i in 0.. some View { - VStack(alignment: .leading, spacing: 12) { - Text(message) - .font(.jakartaRegular16) - .foregroundStyle(.red) - Button("Tentar novamente") { - Task { await viewModel.loadDashboard() } - } - .font(.jakartaBold16) - .foregroundStyle(Color.appBackground) - } - .padding(.horizontal, 20) - } + private func errorState(_ message: String) -> some View { + VStack(alignment: .leading, spacing: 12) { + Text(message) + .font(.jakartaRegular16) + .foregroundStyle(.red) + Button("Tentar novamente") { + Task { await viewModel.loadDashboard() } + } + .font(.jakartaBold16) + .foregroundStyle(Color.appBackground) + } + .padding(.horizontal, 20) + } - private func decodedImage(from base64String: String) -> Image? { - guard let data = Data(base64Encoded: base64String), let uiImage = UIImage(data: data) else { - return nil - } - return Image(uiImage: uiImage) - } + private func decodedImage(from base64String: String) -> Image? { + guard let data = Data(base64Encoded: base64String), let uiImage = UIImage(data: data) else { + return nil + } + return Image(uiImage: uiImage) + } } private struct MedicationCardView: View { - let card: MedicationCard + let card: MedicationCard - var body: some View { - VStack(spacing: 14) { - medicationImage - .frame(height: 140) - .frame(maxWidth: .infinity) - .background(Color.appSecondary) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(alignment: .topTrailing) { - Image(systemName: "info.circle.fill") - .foregroundStyle(Color.appBackground) - .padding(8) - .background(.white) - .clipShape(Circle()) - .padding(8) - } + var body: some View { + VStack(spacing: 14) { + medicationImage + .frame(height: 140) + .frame(maxWidth: .infinity) + .background(Color.appSecondary) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay(alignment: .topTrailing) { + Image(systemName: "info.circle.fill") + .foregroundStyle(Color.appBackground) + .padding(8) + .background(.white) + .clipShape(Circle()) + .padding(8) + } - Text(card.name) - .font(.jakartaBold16) - .foregroundStyle(Color.appAccent) - .multilineTextAlignment(.center) + Text(card.name) + .font(.lineSeedBold20) + .foregroundStyle(Color.appAccent) + .multilineTextAlignment(.center) - Text(card.tag) - .font(.jakartaMedium14) - .foregroundStyle(.white) - .padding(.horizontal, 14) - .padding(.vertical, 6) - .background(Color.appBackground) - .clipShape(Capsule()) + Text(card.tag) + .font(.jakartaMedium14) + .foregroundStyle(.white) + .padding(.horizontal, 14) + .padding(.vertical, 6) + .background(Color.appBackground) + .clipShape(Capsule()) - if let nextDoseTime = card.nextDoseTime { - Group { - Text("Próxima toma às ") + - Text("\(nextDoseTime)h").fontWeight(.bold) - } - .font(.jakartaRegular14) - .foregroundStyle(Color.appBackground) - } + if let nextDoseTime = card.nextDoseTime { + Group { + Text("Próxima toma às ") + + Text("\(nextDoseTime)h").fontWeight(.bold) + } + .font(.jakartaRegular14) + .foregroundStyle(Color.appBackground) + } - Divider() + Divider() - doseTimesGrid - } - .padding(16) - .background(.white) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .shadow(color: .black.opacity(0.06), radius: 8, y: 4) - } + doseTimesGrid - @ViewBuilder - private var medicationImage: some View { - if let photoBase64 = card.photoBase64, - let data = Data(base64Encoded: photoBase64), - let uiImage = UIImage(data: data) { - Image(uiImage: uiImage) - .resizable() - .scaledToFill() - .clipped() - } else { - Image(systemName: "pills.fill") - .font(.system(size: 40)) - .foregroundStyle(Color.appBackground.opacity(0.5)) - } - } + Spacer(minLength: 0) + } + .padding(16) + .frame(maxHeight: .infinity) + .background(Color.appBackgroundLight.opacity(0.11)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .black.opacity(0.06), radius: 8, y: 4) + } - private var doseTimesGrid: some View { - LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) { - ForEach(card.doseTimes, id: \.self) { time in - HStack(spacing: 6) { - Image(systemName: time == card.nextDoseTime ? "bell.fill" : "clock") - .font(.system(size: 13)) - Text("\(time)") - .font(time == card.nextDoseTime ? .jakartaBold14 : .jakartaRegular14) - } - .foregroundStyle( - time == card.nextDoseTime - ? Color.appBackground - : Color.appTextPrimary.opacity(0.4) - ) - } - } - } + @ViewBuilder + private var medicationImage: some View { + if let photoBase64 = card.photoBase64, + let data = Data(base64Encoded: photoBase64), + let uiImage = UIImage(data: data) { + Image(uiImage: uiImage) + .resizable() + .scaledToFill() + .clipped() + } else { + Image(systemName: "pills.fill") + .font(.system(size: 40)) + .foregroundStyle(Color.appBackground.opacity(0.5)) + } + } + + private var doseTimesGrid: some View { + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) { + ForEach(card.doseTimes, id: \.self) { time in + HStack(spacing: 6) { + Image(systemName: time == card.nextDoseTime ? "bell.fill" : "clock") + .font(.system(size: 13)) + Text("\(time)") + .font(time == card.nextDoseTime ? .jakartaBold14 : .jakartaRegular14) + } + .foregroundStyle( + time == card.nextDoseTime + ? Color.appBackground + : Color.appTextPrimary.opacity(0.4) + ) + } + } + } } #Preview { - DashboardView() + DashboardView() } diff --git a/medmap/Views/FloatingCircle.swift b/medmap/Views/FloatingCircle.swift new file mode 100644 index 0000000..8185afe --- /dev/null +++ b/medmap/Views/FloatingCircle.swift @@ -0,0 +1,154 @@ +// +// FloatingCircle.swift +// medmap +// +// Created by RSA on 15/09/2026. +// + + +import SwiftUI + +// MARK: - Model + +private struct FloatingCircle: Identifiable { + let id = UUID() + var position: CGPoint + var velocity: CGVector + let radius: CGFloat + let color: Color +} + +// MARK: - Background View + +/// A background of colored, blurred circles that bounce around the screen +/// with simple physics (constant velocity + reflection off the edges). +/// +/// Usage: +/// ```swift +/// ZStack { +/// BouncingCirclesBackground(numberOfCircles: 8, blurStrength: 60) +/// YourContent() +/// } +/// ``` +struct BouncingCirclesBackground: View { + + /// How many circles to animate. + var numberOfCircles: Int = 6 + + /// Gaussian blur radius applied to the whole layer of circles. + /// Higher = softer, more "ambient glow" look. + var blurStrength: CGFloat = 40 + + /// Palette the circles are drawn from (cycled if there are more circles than colors). + var colors: [Color] = [.pink, .purple, .blue, .orange, .mint, .yellow, .red, .teal] + + /// Range for circle radii (before scaling). + var radiusRange: ClosedRange = 60...140 + + /// Range for movement speed, in points per second. + var speedRange: ClosedRange = 25...65 + + @State private var circles: [FloatingCircle] = [] + @State private var lastTimestamp: Date = Date() + + var body: some View { + GeometryReader { geo in + TimelineView(.animation) { timeline in + Canvas { context, size in + for circle in circles { + let rect = CGRect( + x: circle.position.x - circle.radius, + y: circle.position.y - circle.radius, + width: circle.radius * 2, + height: circle.radius * 2 + ) + context.fill(Path(ellipseIn: rect), with: .color(circle.color)) + } + } + .blur(radius: blurStrength) + .onChange(of: timeline.date) { newDate in + let dt = min(newDate.timeIntervalSince(lastTimestamp), 1 / 30) // clamp to avoid big jumps + lastTimestamp = newDate + step(dt: CGFloat(dt), in: geo.size) + } + } + .onAppear { + if circles.isEmpty { + circles = makeInitialCircles(in: geo.size) + } + lastTimestamp = Date() + } + } + .ignoresSafeArea() + .drawingGroup() // renders via Metal, much smoother with blur + many shapes + } + + // MARK: - Setup + + private func makeInitialCircles(in size: CGSize) -> [FloatingCircle] { + (0.. 0, size.width > 0, size.height > 0 else { return } + + for i in circles.indices { + var c = circles[i] + + var newX = c.position.x + c.velocity.dx * dt + var newY = c.position.y + c.velocity.dy * dt + + // Bounce off left/right edges + if newX - c.radius < 0 { + newX = c.radius + c.velocity.dx = abs(c.velocity.dx) + } else if newX + c.radius > size.width { + newX = size.width - c.radius + c.velocity.dx = -abs(c.velocity.dx) + } + + // Bounce off top/bottom edges + if newY - c.radius < 0 { + newY = c.radius + c.velocity.dy = abs(c.velocity.dy) + } else if newY + c.radius > size.height { + newY = size.height - c.radius + c.velocity.dy = -abs(c.velocity.dy) + } + + c.position = CGPoint(x: newX, y: newY) + circles[i] = c + } + } +} + +// MARK: - Preview + +#Preview { + ZStack { + BouncingCirclesBackground(numberOfCircles: 8, blurStrength: 60) + + VStack { + Text("Your content goes here") + .font(.title.bold()) + .foregroundStyle(.white) + .padding() + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16)) + } + } +} \ No newline at end of file diff --git a/medmap/Views/LoginView.swift b/medmap/Views/LoginView.swift index 64a72ad..92b5e62 100644 --- a/medmap/Views/LoginView.swift +++ b/medmap/Views/LoginView.swift @@ -30,7 +30,8 @@ struct LoginView: View { Image("splashLogo") .resizable() .scaledToFit() - .frame(width: 120, height: 76) + + .frame(width: 150, height: 180) .accessibilityHidden(true) Spacer() @@ -38,9 +39,9 @@ struct LoginView: View { .padding(.top, 32) .padding(.horizontal, 32) - Spacer() + Spacer() - VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading) { Text("Aceda aqui à sua conta") .font(.jakartaBold24) .foregroundStyle(.white) @@ -96,7 +97,7 @@ struct LoginView: View { .padding(.horizontal, 32) Spacer() - + Spacer() VStack(spacing: 8) { Button("Não tem conta? Criar conta") { viewModel.showSignup() @@ -107,8 +108,13 @@ struct LoginView: View { Text("MedMap") .font(.jakartaRegular12) .foregroundStyle(.white.opacity(0.6)) + + + Image("corporateLogos") + + } - .padding(.bottom, 24) + .padding(.bottom, 12) } } diff --git a/medmap/Views/SplashScreenView.swift b/medmap/Views/SplashScreenView.swift index f13f2c8..8dd2c71 100644 --- a/medmap/Views/SplashScreenView.swift +++ b/medmap/Views/SplashScreenView.swift @@ -44,7 +44,7 @@ struct SplashScreenView: View { .ignoresSafeArea() .task { do { - try await Task.sleep(for: .seconds(3)) + try await Task.sleep(for: .seconds(2)) } catch { return }