diff --git a/AlloVoisinsSwiftUI/AlloVoisinsSwiftUIApp.swift b/AlloVoisinsSwiftUI/AlloVoisinsSwiftUIApp.swift index 56ad891..33e7031 100644 --- a/AlloVoisinsSwiftUI/AlloVoisinsSwiftUIApp.swift +++ b/AlloVoisinsSwiftUI/AlloVoisinsSwiftUIApp.swift @@ -12,7 +12,7 @@ struct AlloVoisinsSwiftUIApp: App { var body: some Scene { WindowGroup { NavigationStack { - MarketingSupportSelectionView() + EmptyView() } } } diff --git a/AlloVoisinsSwiftUI/Core/Models/SQFormFieldError.swift b/AlloVoisinsSwiftUI/Core/Models/SQFormFieldError.swift new file mode 100644 index 0000000..389b6da --- /dev/null +++ b/AlloVoisinsSwiftUI/Core/Models/SQFormFieldError.swift @@ -0,0 +1,34 @@ +// +// SQFormFieldError.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/05/2025. +// + +enum SQFormFieldError: Equatable { + case none + case empty + case siret + case invalidPhoneNumber + case confirmation + case minCharacters(Int) + case api(String) + case custom(String) + + var isInError: Bool { + self != .none + } + + var message: String { + switch self { + case .none: return "" + case .empty: return "Ce champ est obligatoire." + case .siret: return "Le numéro SIRET doit être composé de 14 chiffres." + case .invalidPhoneNumber: return "Le numéro de téléphone n'est pas valide." + case .confirmation: return "Cette condition est obligatoire." + case let .minCharacters(count): return "Ce champ doit comporter au minimum \(count) caractères." + case let .api(message): return message + case let .custom(message): return message + } + } +} diff --git a/AlloVoisinsSwiftUI/Core/Views/Modals/Components/OnlyForPremierView.swift b/AlloVoisinsSwiftUI/Core/Views/Modals/Components/OnlyForPremierView.swift index b0f5384..f1944f8 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Modals/Components/OnlyForPremierView.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Modals/Components/OnlyForPremierView.swift @@ -15,9 +15,10 @@ struct OnlyForPremierView: View { SQText("Réservé aux abonnés Premier", size: 18, font: .bold) SQText("Seuls les abonnés Premier peuvent profiter de cette fonctionnalité.") } - SQButton( "Découvrir l’abonnement Premier", color: Color.sqOrange(50), textColor: .white) { - + SQButton( "Découvrir l’abonnement Premier") { + } + .colorScheme(.orange) } .frame(maxWidth: .infinity) } diff --git a/AlloVoisinsSwiftUI/Core/Views/Modals/OnlyForPremierModal.swift b/AlloVoisinsSwiftUI/Core/Views/Modals/OnlyForPremierModal.swift index 79a57d8..4903c9b 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Modals/OnlyForPremierModal.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Modals/OnlyForPremierModal.swift @@ -58,9 +58,10 @@ struct OnlyForPremierModal: View { SQText("Essai gratuit de 14 jours", size: 18, font: .bold) SQText("à partir de 29,99 € / mois") } - SQButton("Je m'abonne", color: .sqOrange(50), textColor: .white) { - + SQButton("Je m'abonne") { + } + .colorScheme(.orange) } .padding(.horizontal, 16) .padding(.vertical, 24) diff --git a/AlloVoisinsSwiftUI/Core/Views/Modals/RegulatedProfessionEditProfilModal.swift b/AlloVoisinsSwiftUI/Core/Views/Modals/RegulatedProfessionEditProfilModal.swift index 1fbc2c7..37bb70f 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Modals/RegulatedProfessionEditProfilModal.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Modals/RegulatedProfessionEditProfilModal.swift @@ -48,7 +48,7 @@ struct RegulatedProfessionEditProfilModal: View { Spacer() VStack { - SQButton("fermer", color: .sqNeutral(100), textColor: .white) {} + SQButton("fermer") {} .disabled(true) } .padding(.horizontal, 16) diff --git a/AlloVoisinsSwiftUI/Core/Views/Modals/RegulatedProfessionModal.swift b/AlloVoisinsSwiftUI/Core/Views/Modals/RegulatedProfessionModal.swift index 2a06e36..7da9032 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Modals/RegulatedProfessionModal.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Modals/RegulatedProfessionModal.swift @@ -24,7 +24,7 @@ struct RegulatedProfessionModal: View { Spacer() VStack { - SQButton("J'ai compris", color: .sqNeutral(100), textColor: .white) {} + SQButton("J'ai compris") {} } .padding(.horizontal, 16) .padding(.vertical, 24) diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQButton.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQButton.swift index 096a46c..613bda7 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQButton.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQButton.swift @@ -9,16 +9,17 @@ import Lottie import SwiftUI struct SQButton: View { - var title: String - var color: Color = .sqNeutral(10) - var textColor: Color = .black - var textSize: CGFloat = 16 - var font: SQTextFont = .demiBold - var icon: SQIcon? - var isFull: Bool = true - @Binding var isLoading: Bool + let title: String let action: () -> Void + @State private var type: SQButtonType = .solid + @State private var colorScheme: SQButtonColorScheme = .neutral + @State private var textSize: CGFloat = 16 + @State private var font: SQTextFont = .demiBold + @State private var icon: SQIcon? = nil + @State private var isLoading: Bool = false + @State private var isDisabled: Bool = false + private var textWidth: CGFloat { let font = UIFont.systemFont(ofSize: textSize) let attributes = [NSAttributedString.Key.font: font] @@ -26,21 +27,25 @@ struct SQButton: View { return size.width + (icon != nil ? 24 : 0) } - init(_ title: String, color: Color = .sqNeutral(10), textColor: Color = .black, textSize: CGFloat = 16, font: SQTextFont = .demiBold, icon: SQIcon? = nil, isFull: Bool = true, isLoading: Binding = .constant(false), action: @escaping () -> Void) { + init(_ title: String, action: @escaping () -> Void) { self.title = title - self.color = color - self.textColor = textColor - self.textSize = textSize - self.font = font - self.icon = icon - self.isFull = isFull - self._isLoading = isLoading self.action = action } + private var buttonStyle: SQButtonStyle { + SQButtonStyle( + type: type, + colorScheme: colorScheme, + isLoading: isLoading, + isDisabled: isDisabled + ) + } + var body: some View { Button(action: { - self.action() + if !isLoading && !isDisabled { + action() + } }, label: { HStack { if isLoading { @@ -50,56 +55,347 @@ struct SQButton: View { } else { if icon != nil { icon + .foregroundColor(buttonStyle.textColor) } - SQText(title, size: textSize, font: font, textColor: textColor) + SQText(title, size: textSize, font: font, textColor: buttonStyle.textColor) } } .frame(minWidth: textWidth) .padding(.horizontal, 30) .padding(.vertical, 12) .frame(height: 40, alignment: .center) - .background(isFull ? color : .clear) + .background(buttonStyle.backgroundColor) .cornerRadius(100) .overlay( RoundedRectangle(cornerRadius: 100) .inset(by: 0.5) - .stroke(color.opacity(isFull ? 0 : 1), lineWidth: 1) + .stroke(buttonStyle.borderColor, lineWidth: 1) ) }) - .buttonStyle(PlainButtonStyle()) + .disabled(isLoading || isDisabled) } } +// MARK: - Modifiers extension SQButton { - func sqStyle(_ theme: SQButtonStyle = .neutral) -> some View { - modifier(SQButtonModifier(theme: theme)) + func buttonType(_ type: SQButtonType) -> SQButton { + var copy = self + copy._type = State(initialValue: type) + return copy + } + + func colorScheme(_ scheme: SQButtonColorScheme) -> SQButton { + var copy = self + copy._colorScheme = State(initialValue: scheme) + return copy + } + + func textSize(_ size: CGFloat) -> SQButton { + var copy = self + copy._textSize = State(initialValue: size) + return copy + } + + func font(_ font: SQTextFont) -> SQButton { + var copy = self + copy._font = State(initialValue: font) + return copy + } + + func icon(_ icon: SQIcon?) -> SQButton { + var copy = self + copy._icon = State(initialValue: icon) + return copy + } + + func loading(_ isLoading: Bool) -> SQButton { + var copy = self + copy._isLoading = State(initialValue: isLoading) + return copy + } + + func disabled(_ isDisabled: Bool) -> SQButton { + var copy = self + copy._isDisabled = State(initialValue: isDisabled) + return copy } } -struct SQButtonModifier: ViewModifier { - let theme: SQButtonStyle +// MARK: - Properties - func body(content: Content) -> some View { - content - } +enum SQButtonType: String, CaseIterable { + case solid + case line + case light + case glass } -enum SQButtonStyle { - case royal +enum SQButtonColorScheme: String, CaseIterable { case neutral - case purple + case green + case pink + case blue + case yellow + case gold case orange + case red + case grape + case forest + case royal + + var baseColor: Color { + switch self { + case .neutral: + return .sqNeutral(100) + case .green: + return .sqGreen(60) + case .pink: + return .sqPink(60) + case .blue: + return .sqBlue(50) + case .yellow: + return .sqYellow(50) + case .gold: + return .sqGold(50) + case .orange: + return .sqOrange(50) + case .red: + return .sqRed(60) + case .grape: + return .sqGrape(80) + case .forest: + return .sqForest(100) + case .royal: + return .sqRoyal(60) + } + } + + var mediumColor: Color { + switch self { + case .neutral: + return .sqNeutral(30) + case .green: + return .sqGreen(30) + case .pink: + return .sqPink(30) + case .blue: + return .sqBlue(30) + case .yellow: + return .sqYellow(30) + case .gold: + return .sqGold(30) + case .orange: + return .sqOrange(30) + case .red: + return .sqRed(10) + case .grape: + return .sqGrape(30) + case .forest: + return .sqForest(50) + case .royal: + return .sqRoyal(30) + } + } + + var lightColor: Color { + switch self { + case .neutral: + return .sqNeutral(15) + case .green: + return .sqGreen(10) + case .pink: + return .sqPink(10) + case .blue: + return .sqBlue(10) + case .yellow: + return .sqYellow(10) + case .gold: + return .sqGold(10) + case .orange: + return .sqOrange(10) + case .red: + return .sqRed(10) + case .grape: + return .sqGrape(10) + case .forest: + return .sqForest(10) + case .royal: + return .sqRoyal(10) + } + } +} + +struct SQButtonStyle { + let type: SQButtonType + let colorScheme: SQButtonColorScheme + let isLoading: Bool + let isDisabled: Bool + + var backgroundColor: Color { + if isDisabled { + switch type { + case .solid: + return colorScheme.mediumColor + case .line: + return .clear + case .light: + return colorScheme.lightColor + case .glass: + return .clear + } + } + + if isLoading { + switch type { + case .solid: + return colorScheme.mediumColor + case .line: + return .clear + case .light: + return colorScheme.lightColor + case .glass: + return .clear + } + } + + switch type { + case .solid: + return colorScheme.baseColor + case .line: + return .clear + case .light: + return colorScheme.lightColor + case .glass: + return .clear + } + } + + var borderColor: Color { + switch type { + + case .solid: + if isDisabled { + return colorScheme.lightColor.opacity(0.5) + } + + if isLoading { + return colorScheme.lightColor.opacity(0.8) + } + case .line: + if isDisabled { + return colorScheme.mediumColor + } + + if isLoading { + return colorScheme.mediumColor + } + + return colorScheme.baseColor + + case .light: + if isDisabled { + return colorScheme.lightColor.opacity(0.5) + } + + if isLoading { + return colorScheme.lightColor.opacity(0.8) + } + + return colorScheme.lightColor + + case .glass: + return .clear + } + + return colorScheme.baseColor + } + + var textColor: Color { + if isDisabled { + switch type { + case .solid: + return .white + case .line, .light, .glass: + return colorScheme.mediumColor + } + } + + switch type { + case .solid: + return .white + case .line, .light, .glass: + return colorScheme.baseColor + } + } } #Preview { - VStack(spacing: 16) { - SQButton("C'est parti !", color: .sqNeutral(100), isFull: false) {} - SQButton("C'est parti !") {} - SQButton("C'est parti !", color: .sqOrange(50), textColor: .white) {} - .disabled(true) - SQButton("Imprimer", color: .sqNeutral(100), textColor: .white, icon: SQIcon(.print, color: .white)) {} - .disabled(true) - SQButton("C'est parti !", isLoading: .constant(true)) {} + VStack(spacing: 32) { + HStack { + VStack(spacing: 16) { + SQButton("C'est parti !") {} + .colorScheme(.royal) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.line) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.light) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.glass) + } + VStack(spacing: 16) { + SQButton("C'est parti !") {} + .colorScheme(.royal) + .loading(true) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.line) + .loading(true) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.light) + .loading(true) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.glass) + .loading(true) + } + } + + HStack { + VStack(spacing: 16) { + SQButton("C'est parti !") {} + .colorScheme(.royal) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.line) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.light) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.glass) + } + VStack(spacing: 16) { + SQButton("C'est parti !") {} + .colorScheme(.royal) + .disabled(true) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.line) + .disabled(true) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.light) + .disabled(true) + SQButton("C'est parti !") {} + .colorScheme(.royal) + .buttonType(.glass) + .disabled(true) + } + } } } diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQCheckbox.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQCheckbox.swift index 494d3e5..306c3ca 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQCheckbox.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQCheckbox.swift @@ -10,22 +10,22 @@ import SwiftUI struct SQCheckbox: View { var text: String @Binding var isChecked: Bool - var errorText: String? - @Binding var isInError: Bool + var error: Binding + var alignment: VerticalAlignment = .top - init(_ text: String, isChecked: Binding, errorText: String? = nil, isInError: Binding = .constant(false)) { + init(_ text: String, isChecked: Binding, error: Binding = .constant(.none), alignment: VerticalAlignment = .top) { self.text = text self._isChecked = isChecked - self.errorText = errorText - self._isInError = isInError + self.error = error + self.alignment = alignment } var body: some View { VStack(alignment: .leading, spacing: 4) { - HStack(alignment: .top) { + HStack(alignment: alignment) { if isChecked { SQImage("checked_neutral", height: 20) - } else if isInError { + } else if error.wrappedValue.isInError { SQImage("checkbox_unchecked_error", height: 20) } else { SQImage("checkbox_unchecked", height: 20) @@ -35,20 +35,21 @@ struct SQCheckbox: View { .onTapGesture { isChecked.toggle() - if isInError && isChecked { - isInError = false + if error.wrappedValue.isInError && isChecked { + error.wrappedValue = .none } } HStack { SQIcon(.circle_exclamation, customSize: 13, type: .solid, color: .sqSemanticRed) - SQText("Cette condition est obligatoire.", size: 12, font: .demiBold, textColor: .sqSemanticRed) + SQText(error.wrappedValue.message, size: 12, font: .demiBold, textColor: .sqSemanticRed) } - .isHidden(hidden: !isInError) + .isHidden(hidden: !error.wrappedValue.isInError) } } } #Preview { SQCheckbox("Je comprends que j’engage ma responsabilité sur l’exhaustivité et l’authenticité des informations renseignées ci-dessus.", isChecked: .constant(false)) + SQCheckbox("Je comprends que j’engage ma responsabilité sur l’exhaustivité et l’authenticité des informations renseignées ci-dessus.", isChecked: .constant(false), error: .constant(.empty)) } diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQCircleButton.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQCircleButton.swift new file mode 100644 index 0000000..64be373 --- /dev/null +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQCircleButton.swift @@ -0,0 +1,96 @@ +// +// SQCircleButton.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 05/06/2025. +// + +import SwiftUI + +enum SQCircleButtonSize { + case s + case m + case l + + var vertical: CGFloat { + switch self { + case .s: + return 12 + case .m: + return 12 + case .l: + return 16 + } + } + + var horizontal: CGFloat { + switch self { + case .s: + return 30 + case .m: + return 30 + case .l: + return 25 + } + } +} + +struct SQCircleButton: View { + let icon: SQIcon + let text: String + var color: Color + var size: SQCircleButtonSize + var isFull: Bool + let action: () -> Void + + init(_ icon: SQIcon, text: String = "", color: Color = .white, size: SQCircleButtonSize = .m, isFull: Bool = true, action: @escaping () -> Void) { + self.icon = icon + self.text = text + self.color = color + self.size = size + self.isFull = isFull + self.action = action + } + + var body: some View { + Button { + self.action() + } label: { + VStack(spacing: 2) { + icon + .padding(.horizontal, size.horizontal) + .padding(.vertical, size.vertical) + .cornerRadius(100) + .background { + Circle() + .fill(isFull ? color : .white) + .overlay( + Circle() + .stroke(isFull ? Color.white : color, lineWidth: 1) + ) + } + .shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 4) + + if !text.isEmpty { + SQText(text, font: .demiBold, textColor: .sqNeutral(80)) + } + } + } + } +} + +#Preview { + SQCircleButton( + SQIcon(.arrow_right, size: .l) + ) {} + SQCircleButton( + SQIcon(.arrow_left, size: .l) + ) {} + SQCircleButton( + SQIcon(.plus, size: .l, color: .sqNeutral(80)), + text: "Sélectionner", + color: .sqNeutral(80), + size: .l, + isFull: false + ) {} +} diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQColorPicker.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQColorPicker.swift index 89934bc..0d86426 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQColorPicker.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQColorPicker.swift @@ -71,7 +71,7 @@ struct SQColorPicker: View { UIColorPickerViewController_SwiftUI(selectedColor: $selectedColor) .background(Color.white) .sqNavigationBar(title: "Choisir une couleur personnalisée") - SQButton("Valider", color: .sqNeutral(100), textColor: .white) { + SQButton("Valider") { showColorPicker.toggle() } } diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQIcon.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQIcon.swift index 468f443..139c518 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQIcon.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQIcon.swift @@ -147,6 +147,7 @@ enum SQIconName: String { case pen_to_square case phone_flip case phone + case phone_hangup case play_store_brand case plus case print @@ -182,6 +183,7 @@ enum SQIconName: String { case user case users case video + case video_slash case whatsapp_brand case xmark case x_twitter_brand diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQNavigationBar.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQNavigationBar.swift index 60b0ad1..c2ba03d 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQNavigationBar.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQNavigationBar.swift @@ -28,18 +28,18 @@ struct SQNavigationBar: ViewModifier { Button(action: { backAction?() }) { - style.closeIcon + style.leadingIcon } } } - } else { - if showBackButton { - ToolbarItem(placement: .navigationBarLeading) { - Button(action: { - dismiss() - }) { - style.closeIcon - } + } + + if style == .modal || style == .booster { + ToolbarItem(placement: .topBarTrailing) { + Button { + dismiss() + } label: { + style.trailingIcon } } } @@ -58,30 +58,35 @@ extension View { enum SQNavigationBarStyle { case white + case modal case booster case boosterFreeTrialResiliation var backgroundColor: Color { switch self { - case .white: return .white - case .booster: return .sqRoyal(60) - case .boosterFreeTrialResiliation: return .sqRoyal(60) + case .white, .modal: return .white + case .booster, .boosterFreeTrialResiliation: return .sqRoyal(60) } } var foregroundColor: Color { switch self { - case .white: return .sqNeutral(100) - case .booster: return .white - case .boosterFreeTrialResiliation: return .white + case .white, .modal: return .sqNeutral(100) + case .booster, .boosterFreeTrialResiliation: return .white } } - var closeIcon: SQIcon { + var leadingIcon: SQIcon { switch self { - case .white: return SQIcon(.chevron_left, size: .l, color: foregroundColor) - case .booster: return SQIcon(.xmark, size: .l, color: foregroundColor) - case .boosterFreeTrialResiliation: return SQIcon(.chevron_left, size: .l, color: foregroundColor) + default: return SQIcon(.chevron_left, size: .l, color: foregroundColor) + } + } + + var trailingIcon: SQIcon { + switch self { + case .modal, .booster: return .init(.xmark, size: .l, color: foregroundColor) + default: + return .init(.xmark, size: .l, color: .clear) } } } diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQProgressIndicator.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQProgressIndicator.swift new file mode 100644 index 0000000..d041744 --- /dev/null +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQProgressIndicator.swift @@ -0,0 +1,28 @@ +// +// SQProgressIndicator.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 02/06/2025. +// + +import SwiftUI + +struct SQProgressIndicator: View { + @Binding var isActivated: Bool + + init(_ isActivated: Binding) { + self._isActivated = isActivated + } + + var body: some View { + Rectangle() + .foregroundColor(.clear) + .frame(maxWidth: .infinity, minHeight: 4, maxHeight: 4) + .background(isActivated ? Color.sqSemanticGreen : Color.sqNeutral(20)) + .cornerRadius(8) + } +} + +#Preview { + SQProgressIndicator(.constant(true)) +} diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQRadio.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQRadio.swift index 1b96bb8..727a5ea 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQRadio.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQRadio.swift @@ -12,29 +12,45 @@ enum SQRadioOrientation { case vertical } +struct SQRadioOption: Identifiable { + let id: Int = UUID().hashValue + let title: String + var desc: String? = nil + + init(title: String, desc: String? = nil) { + self.title = title + self.desc = desc + } +} + struct SQRadio: View { let title: String? + let titleSize: CGFloat + var radioTextSize: CGFloat = 16 var orientation: SQRadioOrientation = .vertical - let options: [String] + let options: [SQRadioOption] + var error: Binding @Binding var selectedIndex: Int? - init(title: String? = nil, orientation: SQRadioOrientation = .vertical, options: [String], selectedIndex: Binding) { + init(title: String? = nil, titleSize: CGFloat = 24, radioTextSize: CGFloat = 16, orientation: SQRadioOrientation = .vertical, options: [SQRadioOption], selectedIndex: Binding, error: Binding = .constant(.none)) { self.title = title + self.titleSize = titleSize + self.radioTextSize = radioTextSize self.orientation = orientation self.options = options + self.error = error self._selectedIndex = selectedIndex } var body: some View { - VStack(spacing: 16) { + VStack(alignment: orientation == .vertical ? .leading : .center, spacing: 16) { if let title = title { - SQText(title, size: 24, font: .bold) + SQText(title, size: titleSize, font: .bold) .multilineTextAlignment(.center) } Group { if orientation == .horizontal { - HorizontalRadioButtons(options: options, selectedIndex: $selectedIndex) - .background(Color.blue) + HorizontalRadioButtons(options: options, titleSize: radioTextSize, selectedIndex: $selectedIndex, isInError: error.wrappedValue.isInError) .frame(maxWidth: .infinity) } else { @@ -43,94 +59,131 @@ struct SQRadio: View { } } } + + if error.wrappedValue != .none { + SQText(error.wrappedValue.message, size: 12, font: .demiBold, textColor: Color.sqSemanticRed) + } } } private var radioButtons: some View { ForEach(Array(options.enumerated()), id: \.offset) { index, option in RadioButton( - title: option, + title: option.title, + desc: option.desc, + titleSize: radioTextSize, isSelected: Binding( get: { selectedIndex == index }, set: { _ in selectedIndex = index } ), + isInError: error.wrappedValue.isInError, orientation: orientation ) } } -} -private struct HorizontalRadioButtons: View { - let options: [String] - @Binding var selectedIndex: Int? + private struct HorizontalRadioButtons: View { + let options: [SQRadioOption] + let titleSize: CGFloat + @Binding var selectedIndex: Int? + var isInError: Bool - var body: some View { - GeometryReader { geometry in - HStack(alignment: .top, spacing: 0) { - Spacer() - ForEach(Array(options.enumerated()), id: \.offset) { index, option in - RadioButton( - title: option, - isSelected: Binding( - get: { selectedIndex == index }, - set: { _ in selectedIndex = index } - ), - orientation: .horizontal - ) - .frame(width: geometry.size.width / CGFloat(options.count)) - } - Spacer() - } - } - .frame(height: 80) - } -} - -private struct RadioButton: View { - let title: String - @Binding var isSelected: Bool - let orientation: SQRadioOrientation - - var body: some View { - Group { - if orientation == .horizontal { - VStack(spacing: 8) { - radioCircle - SQText(title) - .multilineTextAlignment(.center) - } - } else { - HStack(spacing: 8) { - radioCircle - SQText(title) + var body: some View { + GeometryReader { geometry in + HStack(alignment: .top, spacing: 0) { + Spacer() + ForEach(Array(options.enumerated()), id: \.offset) { index, option in + RadioButton( + title: option.title, + desc: option.desc, + titleSize: titleSize, + isSelected: Binding( + get: { selectedIndex == index }, + set: { _ in selectedIndex = index } + ), + isInError: isInError, + orientation: .horizontal + ) + .frame(width: geometry.size.width / CGFloat(options.count)) + } + Spacer() } } - } - .contentShape(Rectangle()) // Make the entire area tappable - .onTapGesture { - isSelected.toggle() + .frame(height: 80) } } - private var radioCircle: some View { - ZStack { - Circle() - .stroke(isSelected ? Color.sqNeutral(100) : Color.sqNeutral(30), lineWidth: 1) - .frame(width: 20, height: 20) + private struct RadioButton: View { + let title: String + let desc: String? + let titleSize: CGFloat + @Binding var isSelected: Bool + var isInError: Bool + let orientation: SQRadioOrientation - if isSelected { + var body: some View { + Group { + if orientation == .horizontal { + VStack(spacing: 8) { + radioCircle + SQText(title) + .multilineTextAlignment(.center) + } + } else { + VStack(alignment: .leading) { + HStack(spacing: 8) { + radioCircle + SQText(title) + } + + if let desc = desc { + SQText(desc, size: 12) + } + } + } + } + .contentShape(Rectangle()) // Make the entire area tappable + .onTapGesture { + isSelected.toggle() + } + } + + private var radioCircle: some View { + ZStack { Circle() - .inset(by: 3) - .stroke(Color.sqNeutral(100), lineWidth: 6) + .stroke(isInError ? Color.sqSemanticRed : + (isSelected ? + Color.sqNeutral(100) : + Color.sqNeutral(30)), lineWidth: 1) .frame(width: 20, height: 20) + + if isSelected { + Circle() + .inset(by: 3) + .stroke(isInError ? Color.sqSemanticRed : Color.sqNeutral(100), lineWidth: 6) + .frame(width: 20, height: 20) + } } } } } #Preview { - VStack(spacing: 32) { - SQRadio(title: "Vertical", options: ["Option 1", "Option 2", "Option 3"], selectedIndex: .constant(0)) - SQRadio(title: "Horizontal", orientation: .horizontal, options: ["1\n Non Jamais", "2", "3", "4", "5\nOui"], selectedIndex: .constant(2)) + VStack(alignment: .leading, spacing: 32) { + SQRadio(title: "Vertical", options: [ + SQRadioOption(title: "Option 1", desc: "L’utilisateur semble proposer un service relevant d’une activité réglementée sans disposer des autorisations requises."), + SQRadioOption(title: "Option 2", desc: "L’utilisateur semble évoquer ou proposer des activités interdites par la loi (trafic d’animaux, substances illicites, armes, etc.)."), + SQRadioOption(title: "Option 3", desc: "L’utilisateur semble fournir de fausses informations dans son profil."), + SQRadioOption(title: "Option 4", desc: "L’utilisateur semble avoir usurpé mon identité ou l’identité d’un autre utilisateur (SIRET, numéro de téléphone...).") + ], selectedIndex: .constant(0), error: .constant(.custom("Merci de détailler votre signalement"))) + + SQRadio(title: "Horizontal", orientation: .horizontal, options: [ + SQRadioOption(title: "1\n Non Jamais"), + SQRadioOption(title: "2"), + SQRadioOption(title: "3"), + SQRadioOption(title: "4"), + SQRadioOption(title: "5\nOui") + ], selectedIndex: .constant(2)) } + .padding() } diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQRowDivider.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQRowDivider.swift new file mode 100644 index 0000000..e364ddf --- /dev/null +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQRowDivider.swift @@ -0,0 +1,33 @@ +// +// SQRowDivider.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 05/06/2025. +// + +import SwiftUI + +struct SQRowDivider: View { + var body: some View { + Rectangle() + .frame(height: 16) + .foregroundColor(Color.sqNeutral(10)) + } +} + +struct SQDivider: View { + var backgroundColor: Color + + init(_ backgroundColor: Color = Color.sqNeutral(20)) { + self.backgroundColor = backgroundColor + } + + var body: some View { + Divider() + .overlay(backgroundColor) + } +} + +#Preview { + SQRowDivider() +} diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextEditor.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextEditor.swift index bfcc982..59930ae 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextEditor.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextEditor.swift @@ -11,12 +11,11 @@ struct SQTextEditor: View { var label: String var placeholder: String var type: SQTextFieldType = .text - var errorText: String + var error: Binding var icon: SQIcon? var isDisabled: Bool = false var isOptional: Bool = false var tooltipText: String? - @Binding var isInError: Bool var minCharacters: Int? var maxCharacters: Int? @Binding var text: String @@ -30,13 +29,12 @@ struct SQTextEditor: View { init(_ label: String, placeholder: String, type: SQTextFieldType = .text, - errorText: String = "", + error: Binding = .constant(.none), text: Binding, icon: SQIcon? = nil, isDisabled: Bool = false, isOptional: Bool = false, tooltipText: String? = nil, - isInError: Binding = .constant(false), minCharacters: Int? = nil, maxCharacters: Int? = nil, infoAction: (() -> Void)? = nil, @@ -45,13 +43,12 @@ struct SQTextEditor: View { self.label = label self.placeholder = placeholder self.type = type - self.errorText = errorText + self.error = error self._text = text self.icon = icon self.isDisabled = isDisabled self.isOptional = isOptional self.tooltipText = tooltipText - self._isInError = isInError self.minCharacters = minCharacters self.maxCharacters = maxCharacters self.infoAction = infoAction @@ -72,7 +69,7 @@ struct SQTextEditor: View { set: { self.text = String($0.prefix(self.maxCharacters ?? Int.max)) } )) .onChange(of: self.text, perform: { _ in - self.isInError = false + error.wrappedValue = .none }) .font(.sq(.medium)) .foregroundStyle(Color.sqNeutral(100)) @@ -84,13 +81,37 @@ struct SQTextEditor: View { .overlay( RoundedRectangle(cornerRadius: 8) .inset(by: 0.5) - .stroke(isInError ? .sqSemanticRed : isFocused ? accentColor : isDisabled ? Color.sqNeutral(20) : Color.sqNeutral(30), lineWidth: 1) + .stroke(error.wrappedValue.isInError ? .sqSemanticRed : isFocused ? accentColor : isDisabled ? Color.sqNeutral(20) : Color.sqNeutral(30), lineWidth: 1) ) + HStack(spacing: 16) { + if error.wrappedValue.isInError { + HStack(spacing: 8) { + SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed) + SQText(error.wrappedValue.message, size: 12, font: .demiBold, textColor: .sqSemanticRed) + } + } + Spacer() + if !characterCountText.isEmpty { + SQText(characterCountText, size: 12, textColor: .sqNeutral(50)) + } + } } } + + private var characterCountText: String { + let count = text.count + if let min = minCharacters, count < min { + return "\(count)/\(min)" + } else if let max = maxCharacters { + return count >= (max * 4 / 5) ? "\(count)/\(max)" : "\(count)" + } + return "" + } } #Preview { SQTextEditor("Zone de texte", placeholder: "Ex : Pro Solutions propose une gamme de services complète pour tout type de dépannage électroménager. Bénéficiez de nos 10 ans d’expérience ! Devis gratuit", text: .constant("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) .padding() + SQTextEditor("Zone de texte", placeholder: "Ex : Pro Solutions propose une gamme de services complète pour tout type de dépannage électroménager. Bénéficiez de nos 10 ans d’expérience ! Devis gratuit", error: .constant(.empty), text: .constant("Lorem ipsum dolor sit amet, consectetur"), minCharacters: 50) + .padding() } diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextField.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextField.swift index fbf5eae..7f36865 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextField.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextField.swift @@ -16,12 +16,12 @@ struct SQTextField: View { var label: String var placeholder: String var type: SQTextFieldType = .text - var errorText: String var icon: SQIcon? var isDisabled: Bool = false var isOptional: Bool = false var tooltipText: String? - @Binding var isInError: Bool + var helperText: String? + var error: Binding var minCharacters: Int? var maxCharacters: Int? @Binding var text: String @@ -35,13 +35,13 @@ struct SQTextField: View { init(_ label: String, placeholder: String, type: SQTextFieldType = .text, - errorText: String = "", text: Binding, icon: SQIcon? = nil, isDisabled: Bool = false, isOptional: Bool = false, tooltipText: String? = nil, - isInError: Binding = .constant(false), + helperText: String? = nil, + error: Binding = .constant(.none), minCharacters: Int? = nil, maxCharacters: Int? = nil, infoAction: (() -> Void)? = nil, @@ -50,13 +50,13 @@ struct SQTextField: View { self.label = label self.placeholder = placeholder self.type = type - self.errorText = errorText self._text = text self.icon = icon self.isDisabled = isDisabled self.isOptional = isOptional self.tooltipText = tooltipText - self._isInError = isInError + self.helperText = helperText + self.error = error self.minCharacters = minCharacters self.maxCharacters = maxCharacters self.infoAction = infoAction @@ -98,18 +98,14 @@ struct SQTextField: View { // TextField HStack(spacing: 4) { if type == .phoneNumber { - TextField(placeholder, text: Binding( - get: { formatPhoneNumber(self.text) }, - set: { - let filtered = $0.filter { $0.isNumber } - let truncated = String(filtered.prefix(10)) - self.text = truncated - self.isInError = !isValidPhoneNumber(truncated) + TextField("", text: $text) + .placeholder(when: text.isEmpty) { + SQText(placeholder, textColor: .sqNeutral(50)) + } + .onReceive(self.text.publisher.collect()) { + let formatted = formatPhoneNumber(String($0.prefix(15))) + self.text = formatted } - )) - .onReceive(self.text.publisher.collect()) { - self.text = String($0.prefix(10)) - } .keyboardType(.numberPad) .font(.sq(.medium)) .foregroundStyle(Color.sqNeutral(100)) @@ -120,7 +116,7 @@ struct SQTextField: View { set: { self.text = String($0.prefix(self.maxCharacters ?? Int.max)) } )) .onChange(of: self.text, perform: { _ in - self.isInError = false + self.error.wrappedValue = .none }) .font(.sq(.medium)) .foregroundStyle(Color.sqNeutral(100)) @@ -135,26 +131,24 @@ struct SQTextField: View { } } .padding(16) - .background { - RoundedRectangle(cornerRadius: 8) - .fill(Color.white) - } .foregroundStyle(Color.sqNeutral()) .background(isDisabled ? Color.sqNeutral(10) : .clear) .overlay( RoundedRectangle(cornerRadius: 8) .inset(by: 0.5) - .stroke(isInError ? .sqSemanticRed : isFocused ? accentColor : isDisabled ? Color.sqNeutral(20) : Color.sqNeutral(30), lineWidth: 1) + .stroke(error.wrappedValue.isInError ? .sqSemanticRed : isFocused ? accentColor : isDisabled ? Color.sqNeutral(20) : Color.sqNeutral(30), lineWidth: 1) ) .focused($isFocused) .disabled(isDisabled) HStack(spacing: 16) { // HStack error - if isInError { + if error.wrappedValue.isInError { HStack(spacing: 4) { SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed) - SQText(errorText, size: 12, textColor: .sqSemanticRed) + SQText(error.wrappedValue.message, size: 12, textColor: .sqSemanticRed) } + } else if helperText != nil { + SQText(helperText!, size: 12, textColor: .sqNeutral(50)) } Spacer() if !characterCountText.isEmpty { @@ -214,7 +208,7 @@ struct SQTextField: View { SQTextField("Label name", placeholder: "Placeholder", text: .constant("Bonjour, "), minCharacters: 20) SQTextField("Label name", placeholder: "Placeholder", text: .constant(""), isOptional: true) SQTextField("Label name", placeholder: "Placeholder", text: .constant(""), isDisabled: true) - SQTextField("Label name", placeholder: "Placeholder", errorText: "Champ invalide", text: .constant(""), isInError: .constant(true)) + SQTextField("Label name", placeholder: "Placeholder", text: .constant(""), error: .constant(.empty)) SQTextField("Téléphone", placeholder: "01 23 45 67 89", type: .phoneNumber, text: .constant("")) SQTextField( "Numéro de RCS", diff --git a/AlloVoisinsSwiftUI/Features/Booster/Views/Components/BoosterPromotionView.swift b/AlloVoisinsSwiftUI/Features/Booster/Views/Components/BoosterPromotionView.swift index 7695143..87d519c 100644 --- a/AlloVoisinsSwiftUI/Features/Booster/Views/Components/BoosterPromotionView.swift +++ b/AlloVoisinsSwiftUI/Features/Booster/Views/Components/BoosterPromotionView.swift @@ -17,9 +17,10 @@ struct BoosterPromotionView: View { } BoosterStatsView() - SQButton("Activer l’option Booster", color: .sqRoyal(), textColor: .white) { - + SQButton("Activer l’option Booster") { + } + .colorScheme(.royal) } .padding() .foregroundColor(.sqRoyal()) diff --git a/AlloVoisinsSwiftUI/Features/DebugLand/Views/ConfigPrestationSearchView.swift b/AlloVoisinsSwiftUI/Features/DebugLand/Views/ConfigPrestationSearchView.swift index 4d5bc76..e335076 100644 --- a/AlloVoisinsSwiftUI/Features/DebugLand/Views/ConfigPrestationSearchView.swift +++ b/AlloVoisinsSwiftUI/Features/DebugLand/Views/ConfigPrestationSearchView.swift @@ -43,7 +43,7 @@ struct ConfigPrestationSearchView: View { .padding(.horizontal) Spacer() SQFooter { - SQButton("Afficher la vue", color: .sqNeutral(100), textColor: .white) { + SQButton("Afficher la vue") { showSearchView.toggle() } } diff --git a/AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift b/AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift index a24d24a..2005786 100644 --- a/AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift +++ b/AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift @@ -58,14 +58,14 @@ struct DebugLandView: View { HStack(spacing: 8) { VStack { SQTextField("Visiter le profil :", placeholder: "UserID", text: $userId) - SQButton("Visiter le profil", color: .sqNeutral(100), textColor: .white) { + SQButton("Visiter le profil") { } } VStack { SQTextField("Se connecter sur :", placeholder: "UserID", text: $userId) .keyboardType(.numberPad) - SQButton("Se connecter", color: .sqNeutral(100), textColor: .white) { + SQButton("Se connecter") { } } diff --git a/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardColorSelectionView.swift b/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardColorSelectionView.swift index f468afa..246e4c7 100644 --- a/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardColorSelectionView.swift +++ b/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardColorSelectionView.swift @@ -34,7 +34,7 @@ struct CardColorSelectionView: View { } .padding() SQFooter { - SQButton("Continuer", color: .sqNeutral(100), textColor: .white) { + SQButton("Continuer") { self.goToNext.toggle() } } diff --git a/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardFormView.swift b/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardFormView.swift index d36ea06..93b742c 100644 --- a/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardFormView.swift +++ b/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardFormView.swift @@ -60,9 +60,9 @@ struct CardFormView: View { } .frame(maxWidth: .infinity) } - SQTextField("Titre", placeholder: "Ex : Pro Solutions", errorText: "", text: $title) - SQTextField("Sous-titre", placeholder: "Ex : Martin Dupont", errorText: "", text: $subtitle, isOptional: true) - SQTextField("Métier", placeholder: "Ex : Dépannage électroménager", errorText: "", text: $job, isOptional: true) + SQTextField("Titre", placeholder: "Ex : Pro Solutions", text: $title) + SQTextField("Sous-titre", placeholder: "Ex : Martin Dupont", text: $subtitle, isOptional: true) + SQTextField("Métier", placeholder: "Ex : Dépannage électroménager", text: $job, isOptional: true) VStack(alignment: .leading, spacing: 0) { Toggle(isOn: $showRating) { SQText("Afficher ma note AlloVoisins", font: .demiBold) @@ -83,17 +83,18 @@ struct CardFormView: View { .foregroundColor(Color.sqNeutral(10)) VStack(alignment: .leading, spacing: 16) { - SQTextField("Numéro de téléphone", placeholder: "Ex : 06 12 34 56 78", errorText: "", text: $phoneNumber) - SQTextField("Adresse complète", placeholder: "Ex : 1 rue de la gare, 67000 Strasbourg", errorText: "", text: $address) + SQTextField("Numéro de téléphone", placeholder: "Ex : 06 12 34 56 78", text: $phoneNumber) + SQTextField("Adresse complète", placeholder: "Ex : 1 rue de la gare, 67000 Strasbourg", text: $address) } .padding() } } SQFooter { - SQButton("Aperçu", color: .sqNeutral(100), textColor: .sqNeutral(100), icon: SQIcon(.eye, color: .sqNeutral(100)), isFull: false) {} - .sqStyle() - SQButton("Imprimer", color: .sqNeutral(100), textColor: .white, icon: SQIcon(.print, color: .white)) {} - .sqStyle() + SQButton("Aperçu") {} + .icon(SQIcon(.eye, color: .sqNeutral(100))) + .buttonType(.line) + SQButton("Imprimer") {} + .icon(SQIcon(.print, color: .white)) } } .confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: { diff --git a/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardPrintView.swift b/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardPrintView.swift index 16f661f..42300ce 100644 --- a/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardPrintView.swift +++ b/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardPrintView.swift @@ -17,7 +17,7 @@ struct CardPrintView: View { .frame(height: 100) .frame(maxWidth: .infinity) SQText("Nous vous recommandons : \n • Une impression haute qualité, en couleur \n • L’utilisation d’un papier d’au moins
200 g/m2 ") - SQButton("Imprimer", color: .sqNeutral(100), textColor: .white) {} + SQButton("Imprimer") {} .frame(maxWidth: .infinity) Spacer() } diff --git a/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardTemplateSelectionView.swift b/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardTemplateSelectionView.swift index c2d285e..0392e77 100644 --- a/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardTemplateSelectionView.swift +++ b/AlloVoisinsSwiftUI/Features/Marketing/Views/Cards/CardTemplateSelectionView.swift @@ -67,7 +67,7 @@ struct CardTemplateSelectionView: View { } } SQFooter { - SQButton("Continuer", color: .sqNeutral(100), textColor: .white) { + SQButton("Continuer") { if selectedTemplate != nil { goToNext.toggle() } diff --git a/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerColorSelectionView.swift b/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerColorSelectionView.swift index e1cb123..d72a429 100644 --- a/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerColorSelectionView.swift +++ b/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerColorSelectionView.swift @@ -34,7 +34,7 @@ struct FlyerColorSelectionView: View { } .padding() SQFooter { - SQButton("Continuer", color: .sqNeutral(100), textColor: .white) { + SQButton("Continuer") { self.goToNext.toggle() } } diff --git a/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerFormView.swift b/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerFormView.swift index fcb28bc..1d2a74b 100644 --- a/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerFormView.swift +++ b/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerFormView.swift @@ -62,9 +62,9 @@ struct FlyerFormView: View { } .frame(maxWidth: .infinity) } - SQTextField("Titre", placeholder: "Ex : Pro Solutions", errorText: "", text: $title) - SQTextField("Sous-titre", placeholder: "Ex : Martin Dupont", errorText: "", text: $subtitle, isOptional: true) - SQTextField("Métier", placeholder: "Ex : Dépannage électroménager", errorText: "", text: $job, isOptional: true) + SQTextField("Titre", placeholder: "Ex : Pro Solutions", text: $title) + SQTextField("Sous-titre", placeholder: "Ex : Martin Dupont", text: $subtitle, isOptional: true) + SQTextField("Métier", placeholder: "Ex : Dépannage électroménager", text: $job, isOptional: true) VStack(alignment: .leading, spacing: 0) { Toggle(isOn: $showRating) { SQText("Afficher ma note AlloVoisins", font: .demiBold) @@ -87,11 +87,11 @@ struct FlyerFormView: View { VStack(alignment: .leading, spacing: 16) { SQTextEditor("Zone de text", placeholder: "Ex : Pro Solutions propose une gamme de services complète pour tout type de dépannage électroménager. Bénéficiez de nos 10 ans d’expérience ! Devis gratuit", text: .constant("")) - SQTextField("Prestation 1", placeholder: "Ex : Réparation lave-vaisselle", errorText: "", text: $job, isOptional: true) - SQTextField("Prestation 2", placeholder: "Ex : Réparation machine à laver", errorText: "", text: $job, isOptional: true) - SQTextField("Prestation 3", placeholder: "Ex : Réparation four", errorText: "", text: $job, isOptional: true) - SQTextField("Prestation 4", placeholder: "Ex : Réparation outillage", errorText: "", text: $job, isOptional: true) - SQTextField("Prestation 5", placeholder: "Ex : Dépannage électroménager", errorText: "", text: $job, isOptional: true) + SQTextField("Prestation 1", placeholder: "Ex : Réparation lave-vaisselle", text: $job, isOptional: true) + SQTextField("Prestation 2", placeholder: "Ex : Réparation machine à laver", text: $job, isOptional: true) + SQTextField("Prestation 3", placeholder: "Ex : Réparation four", text: $job, isOptional: true) + SQTextField("Prestation 4", placeholder: "Ex : Réparation outillage", text: $job, isOptional: true) + SQTextField("Prestation 5", placeholder: "Ex : Dépannage électroménager", text: $job, isOptional: true) } .padding() @@ -100,8 +100,8 @@ struct FlyerFormView: View { .foregroundColor(Color.sqNeutral(10)) VStack(alignment: .leading, spacing: 16) { - SQTextField("Numéro de téléphone", placeholder: "Ex : 06 12 34 56 78", errorText: "", text: $phoneNumber) - SQTextField("Adresse complète", placeholder: "Ex : 1 rue de la gare, 67000 Strasbourg", errorText: "", text: $address) + SQTextField("Numéro de téléphone", placeholder: "Ex : 06 12 34 56 78", text: $phoneNumber) + SQTextField("Adresse complète", placeholder: "Ex : 1 rue de la gare, 67000 Strasbourg", text: $address) } .padding() @@ -112,30 +112,31 @@ struct FlyerFormView: View { VStack(alignment: .leading, spacing: 16) { SQText("Mentions légales obligatoires", size: 18, font: .bold) - SQTextField("Dénomination sociale", placeholder: "Ex : Pro solutions", errorText: "", text: $job, isOptional: true) - SQTextField("Adresse du siège social", placeholder: "Ex : 16 rue de la Redoute, 67500 Haguenau", errorText: "", text: $job, isOptional: true) + SQTextField("Dénomination sociale", placeholder: "Ex : Pro solutions", text: $job, isOptional: true) + SQTextField("Adresse du siège social", placeholder: "Ex : 16 rue de la Redoute, 67500 Haguenau", text: $job, isOptional: true) VStack(spacing: 0) { - SQTextField("Numéro SIRET", placeholder: "Ex : 12345678901234", errorText: "", text: $job, isOptional: true) + SQTextField("Numéro SIRET", placeholder: "Ex : 12345678901234", text: $job, isOptional: true) SQText("Le numéro SIRET se compose de 14 chiffres : les 9 chiffres du SIREN + 5 chiffres propres à chaque établissement (NIC).", size: 12, textColor: .sqNeutral(50)) } - SQTextField("Numéro de RCS", placeholder: "Ex : RCS STRASBOURG B 123456789", errorText: "", text: $job, isOptional: true) - SQTextField("Statut juridique", placeholder: "Ex : SARL", errorText: "", text: $job, isOptional: true) - SQTextField("Montant du capital social (€)", placeholder: "Ex : 1 000,00", errorText: "", text: $job, isOptional: true) - SQTextField("Autre champ relatif à votre activité", placeholder: "Ex : Pour votre santé, mangez 5 fruits et légumes par jour", errorText: "", text: $job, isOptional: true) - SQCheckbox("Je comprends que j’engage ma responsabilité sur l’exhaustivité et l’authenticité des informations renseignées ci-dessus.", isChecked: $authentConfirm, isInError: $confirmIsInError) + SQTextField("Numéro de RCS", placeholder: "Ex : RCS STRASBOURG B 123456789", text: $job, isOptional: true) + SQTextField("Statut juridique", placeholder: "Ex : SARL", text: $job, isOptional: true) + SQTextField("Montant du capital social (€)", placeholder: "Ex : 1 000,00", text: $job, isOptional: true) + SQTextField("Autre champ relatif à votre activité", placeholder: "Ex : Pour votre santé, mangez 5 fruits et légumes par jour", text: $job, isOptional: true) + SQCheckbox("Je comprends que j’engage ma responsabilité sur l’exhaustivité et l’authenticité des informations renseignées ci-dessus.", isChecked: $authentConfirm, error: .constant(.none)) } .padding() } } SQFooter { - SQButton("Aperçu", color: .sqNeutral(100), textColor: .sqNeutral(100), icon: SQIcon(.eye, color: .sqNeutral(100)), isFull: false) {} - .sqStyle() - SQButton("Imprimer", color: .sqNeutral(100), textColor: .white, icon: SQIcon(.print, color: .white)) { + SQButton("Aperçu") {} + .icon(SQIcon(.eye, color: .sqNeutral(100))) + .buttonType(.line) + SQButton("Imprimer") { if authentConfirm == false { self.confirmIsInError = true } } - .sqStyle() + .icon(SQIcon(.print, color: .white)) } } .confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: { diff --git a/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerTemplateSelectionView.swift b/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerTemplateSelectionView.swift index 2e7a8a8..636d7dc 100644 --- a/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerTemplateSelectionView.swift +++ b/AlloVoisinsSwiftUI/Features/Marketing/Views/Flyers/FlyerTemplateSelectionView.swift @@ -88,7 +88,7 @@ struct FlyerTemplateSelectionView: View { } } SQFooter { - SQButton("Continuer", color: .sqNeutral(100), textColor: .white) { + SQButton("Continuer") { if selectedTemplate != nil { goToNext.toggle() } diff --git a/AlloVoisinsSwiftUI/Features/Neighbors/Views/Components/MoreNeighborsSelectedBadge.swift b/AlloVoisinsSwiftUI/Features/Neighbors/Views/Components/MoreNeighborsSelectedBadge.swift new file mode 100644 index 0000000..44ce600 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Neighbors/Views/Components/MoreNeighborsSelectedBadge.swift @@ -0,0 +1,25 @@ +// +// MoreNeighborsSelectedBadge.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 02/06/2025. +// + +import SwiftUI + +struct MoreNeighborsSelectedBadge: View { + @Binding var selectedNeighbors: Int + + var body: some View { + HStack(alignment: .center, spacing: 10) { + SQText("+\(selectedNeighbors)", font: .bold, textColor: .sqNeutral(50)) + } + .frame(width: 32, height: 32, alignment: .center) + .background(Color.sqNeutral(20)) + .cornerRadius(80) + } +} + +#Preview { + MoreNeighborsSelectedBadge(selectedNeighbors: .constant(6)) +} diff --git a/AlloVoisinsSwiftUI/Features/Neighbors/Views/Components/NeighborsSelectionFooter.swift b/AlloVoisinsSwiftUI/Features/Neighbors/Views/Components/NeighborsSelectionFooter.swift new file mode 100644 index 0000000..363dabf --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Neighbors/Views/Components/NeighborsSelectionFooter.swift @@ -0,0 +1,90 @@ +// +// NeighborsSelectionFooter.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 02/06/2025. +// + +import SwiftUI + +struct NeighborsSelectionFooter: View { + var inProfile: Bool = true + + var body: some View { + VStack(spacing: 0) { + if inProfile { + VStack { + HStack(alignment: .top) { + SQCircleButton( + SQIcon(.arrow_left, size: .l) + ) {} + Spacer() + VStack { + SQCircleButton( + SQIcon(.plus, size: .l), + color: .sqNeutral(), + size: .l, + isFull: false + ) {} + SQText("Sélectionner", font: .demiBold) + } + Spacer() + SQCircleButton( + SQIcon(.arrow_right, size: .l) + ) {} + } + + SQDivider() + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background( + LinearGradient( + stops: [ + Gradient.Stop(color: Color.sqNeutral(10).opacity(0), location: 0.00), + Gradient.Stop(color: Color.sqNeutral(10), location: 0.70), + ], + startPoint: UnitPoint(x: 0.5, y: 0), + endPoint: UnitPoint(x: 0.5, y: 1) + ) + ) + } + + VStack(alignment: .leading, spacing: 8) { + SQText("Nous vous recommandons de sélectionner 3 offreurs :", size: 13, font: .bold) + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + SQImage("neighbor_avatar", height: 32) + SQImage("neighbor_avatar", height: 32) + SQImage("neighbor_avatar", height: 32) + MoreNeighborsSelectedBadge(selectedNeighbors: .constant(4)) + } + HStack { + VStack(alignment: .leading) { + HStack { + SQText("0 offreur sélectionné") + Spacer() + } + HStack(spacing: 4) { + SQProgressIndicator(.constant(true)) + SQProgressIndicator(.constant(true)) + SQProgressIndicator(.constant(false)) + } + .frame(maxWidth: .infinity) + } + Spacer() + SQButton("Continuer") { + + } + } + } + } + .padding() + .background(Color.sqNeutral(10)) + } + } +} + +#Preview { + NeighborsSelectionFooter() +} diff --git a/AlloVoisinsSwiftUI/Features/Neighbors/Views/Components/NeihborsCard.swift b/AlloVoisinsSwiftUI/Features/Neighbors/Views/Components/NeihborsCard.swift new file mode 100644 index 0000000..0f7daa8 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Neighbors/Views/Components/NeihborsCard.swift @@ -0,0 +1,48 @@ +// +// NeihborsCard.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 05/06/2025. +// + +import SwiftUI + +struct NeihborsCard: View { + var body: some View { + VStack(alignment: .leading) { + HStack(spacing: 8) { + AVAvatar(model: AVAvatarModel(avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", onlineStatus: .online(), sizing: .L)) + VStack(alignment: .leading) { + SQText("Julien L.", font: .demiBold) + SQText("Julien, Petits Jobs À Nantes", size: 12, font: .demiBold) + SQText("Nantes (Gloriette-Feydeaux) - 3,1 km", size: 12, textColor: .sqNeutral(80)) + SQText("En ligne", size: 12, textColor: .sqNeutral(80)) + } + Spacer() + } + + HStack { + SQImage("ladame", height: 100) + SQImage("ladame", height: 100) + } + + HStack { + SQIcon(.star, type: .solid, color: .sqGold(50)) + SQText("4,5/5", font: .bold) + SQText("(35 avis)") + } + } + .frame(height: 247) + .padding() + .overlay( + RoundedRectangle(cornerRadius: 8) + .inset(by: 0.5) + .stroke(Color.sqNeutral(20), lineWidth: 1) + ) + } +} + +#Preview { + NeihborsCard() + .padding() +} diff --git a/AlloVoisinsSwiftUI/Features/Profile/Views/Components/AVProfileReportCell.swift b/AlloVoisinsSwiftUI/Features/Profile/Views/Components/AVProfileReportCell.swift new file mode 100644 index 0000000..6a85c44 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Profile/Views/Components/AVProfileReportCell.swift @@ -0,0 +1,23 @@ +// +// AVProfileReportCell.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 21/05/2025. +// + +import SwiftUI + +struct AVProfileReportCell: View { + var body: some View { + HStack(spacing: 16) { + SQIcon(.flag) + SQText("Signaler un profil") + Spacer() + } + .padding(16) + } +} + +#Preview { + AVProfileReportCell() +} diff --git a/AlloVoisinsSwiftUI/Features/Reporting/Views/ReportConfirmationView.swift b/AlloVoisinsSwiftUI/Features/Reporting/Views/ReportConfirmationView.swift new file mode 100644 index 0000000..cf79bbb --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Reporting/Views/ReportConfirmationView.swift @@ -0,0 +1,43 @@ +// +// ReportConfirmationView.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 21/05/2025. +// + +import SwiftUI + +struct ReportConfirmationView: View { + var body: some View { + VStack(spacing: 16) { + VStack { + SQIcon(.check, customSize: 64, type: .solid, color: .sqSemanticGreen) + SQText("Votre signalement a bien été pris en compte.", size: 18, font: .bold) + } + + VStack(alignment: .leading, spacing: 16) { + SQText("Merci de participer à améliorer la communauté. Nous nous efforçons de traiter votre signalement dans les plus brefs délais et nous vous informerons de la suite qui sera donnée.", font: .demiBold) + VStack(alignment: .leading, spacing: 4) { + SQText("Souhaitez-vous bloquer cet utilisateur ?", font: .demiBold) + HStack { + Toggle(isOn: .constant(true)) { + SQText("En bloquant cet utilisateur, il ne pourra plus vous contacter.") + } + } + } + } + Spacer() + SQButton("Confirmer") { + + } + } + .padding() + .sqNavigationBar(title: "Signaler un profil", style: .modal) + } +} + +#Preview { + NavigationView { + ReportConfirmationView() + } +} diff --git a/AlloVoisinsSwiftUI/Features/Reporting/Views/ReportFinalizationView.swift b/AlloVoisinsSwiftUI/Features/Reporting/Views/ReportFinalizationView.swift new file mode 100644 index 0000000..346352b --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Reporting/Views/ReportFinalizationView.swift @@ -0,0 +1,46 @@ +// +// ReportFinalizationView.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 21/05/2025. +// + +import SwiftUI + +struct ReportFinalizationView: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + SQText("Votre signalement", font: .bold) + HStack(alignment: .top) { + SQText("Motif :", font: .demiBold) + SQText("Image non conforme") + } + Divider() + HStack(alignment: .top) { + SQText("Sous-motif :", font: .demiBold) + SQText("Photo de profil ou couverture inappropriée") + } + Divider() + VStack { + SQTextEditor("Explication", placeholder: "", error: .constant(.none), text: .constant(""), minCharacters: 50) + } + Spacer() + VStack(spacing: 0) { + SQCheckbox("Je certifie que toutes les informations renseignées sont exactes et complètes et que je les ai fournies en toute bonne foi.", isChecked: .constant(false), error: .constant(.none), alignment: .center) + SQCheckbox("Je comprends que tout signalement abusif pourra faire l’objet de sanctions.", isChecked: .constant(true), error: .constant(.none), alignment: .center) + SQButton("Signaler") {} + .colorScheme(.red) + } + } + } + .padding() + .sqNavigationBar(title: "Signaler un profil", style: .modal) + } +} + +#Preview { + NavigationView { + ReportFinalizationView() + } +} diff --git a/AlloVoisinsSwiftUI/Features/Reporting/Views/ReportReasonsView.swift b/AlloVoisinsSwiftUI/Features/Reporting/Views/ReportReasonsView.swift new file mode 100644 index 0000000..8aed71b --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Reporting/Views/ReportReasonsView.swift @@ -0,0 +1,39 @@ +// +// ReportReasonsView.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 21/05/2025. +// + +import SwiftUI + +struct ReportReasonsView: View { + var body: some View { + VStack { + VStack(alignment: .leading, spacing: 16) { + SQText("Seules les interactions ayant eu lieu sur AlloVoisins pourront être prises en compte.") + SQRadio(title: "Sélectionner un motif :", + titleSize: 16, + options: [ + SQRadioOption(title: "Comportement inapproprié"), + SQRadioOption(title: "Comportement suspect ou frauduleux"), + SQRadioOption(title: "Litige avec ce membre"), + SQRadioOption(title: "Spammeur / Démarcheur"), + ], + selectedIndex: .constant(1), + error: .constant(.custom("Merci de détailler votre signalement."))) + } + + Spacer() + SQButton("Continuer") {} + } + .padding(16) + .sqNavigationBar(title: "Signaler un profil", style: .modal) + } +} + +#Preview { + NavigationView { + ReportReasonsView() + } +} diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/AlloVoisinReputationScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/AlloVoisinReputationScreen.swift index fa4750b..3490336 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/AlloVoisinReputationScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/AlloVoisinReputationScreen.swift @@ -75,7 +75,7 @@ struct AlloVoisinReputationScreen: View { .padding(16) .background(Color.white) .cornerRadius(8) - SQButton("Conserver mon abonnement", color: .sqNeutral(100), textColor: .white) { + SQButton("Conserver mon abonnement") { navigateToNext = true } } @@ -83,9 +83,11 @@ struct AlloVoisinReputationScreen: View { .background(Color.sqBlue(10)) .cornerRadius(8) - SQButton("J’ai compris, mais je souhaite résilier", color: .white, textSize: 13) { + SQButton("J’ai compris, mais je souhaite résilier") { navigateToNext = true } + .buttonType(.glass) + .textSize(13) } .navigationDestination(isPresented: $navigateToNext) { if let screen = viewModel.nextPromotionalScreen { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/AskIfWillComeBackScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/AskIfWillComeBackScreen.swift index 61b8ad2..aefe7c9 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/AskIfWillComeBackScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/AskIfWillComeBackScreen.swift @@ -14,7 +14,13 @@ struct AskIfWillComeBackScreen: View { var body: some View { VStack { - SQRadio(title: "Pensez-vous redevenir abonné Premier ?", orientation: .horizontal, options: ["1\nNon,Jamais", "2", "3", "4", "5\nOui, sûrement"], selectedIndex: $selectedIndex) + SQRadio(title: "Pensez-vous redevenir abonné Premier ?", orientation: .horizontal, options: [ + SQRadioOption(title: "1\nNon,Jamais"), + SQRadioOption(title: "2"), + SQRadioOption(title: "3"), + SQRadioOption(title: "4"), + SQRadioOption(title: "5\nOui, sûrement") + ], selectedIndex: $selectedIndex) } } } diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ContinueAsParticularScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ContinueAsParticularScreen.swift index 18768d9..cb261d2 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ContinueAsParticularScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ContinueAsParticularScreen.swift @@ -18,12 +18,14 @@ struct ContinueAsParticularScreen: View { SQText("Continuez de proposer vos services en tant que particulier pour arrondir vos fins de mois. À partir de 9,99 € / mois (sans engagement).", font: .demiBold) .multilineTextAlignment(.center) VStack { - SQButton("Changer de statut", color: .sqNeutral(100), textColor: .white) { + SQButton("Changer de statut") { navigateToNext = true } - SQButton("J’ai compris, mais je souhaite résilier", color: .white) { + SQButton("J’ai compris, mais je souhaite résilier") + { navigateToNext = true } + .buttonType(.glass) } } .navigationDestination(isPresented: $navigateToNext) { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/GenericResiliationScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/GenericResiliationScreen.swift index de7b9e8..5991a51 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/GenericResiliationScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/GenericResiliationScreen.swift @@ -44,15 +44,15 @@ enum ResiliationScreenType { } } - var buttonColor: Color { + var buttonColor: SQButtonColorScheme { switch self { case .alloVoisinReputation, .continueAsParticular, .profileCompletion, .softwarePresentation: - return Color.sqNeutral(100) - case .getMoreRatings: return Color.sqGold(50) - case .moreTime: return Color.sqBlue(50) - case .onlyProRequests, .personalizedSupport, .webinaire: return Color.sqGrape(80) - case .resizePerimeter, .statusChange: return Color.sqOrange(50) - default: return Color.sqNeutral(100) + return .neutral + case .getMoreRatings: return .gold + case .moreTime: return .blue + case .onlyProRequests, .personalizedSupport, .webinaire: return .grape + case .resizePerimeter, .statusChange: return .orange + default: return .neutral } } @@ -97,13 +97,16 @@ struct GenericResiliationScreen: View { .background(content.screenType.mainColor) .cornerRadius(8) - SQButton(content.screenType.buttonTitle, color: content.screenType.buttonColor, textColor: .white, action: buttonAction) + SQButton(content.screenType.buttonTitle, action: buttonAction) + .colorScheme(content.screenType.buttonColor) } if content.screenType != .statusChange { - SQButton(content.screenType.cancelButtonTitle, color: .white, textSize: 13) { + SQButton(content.screenType.cancelButtonTitle) { navigateToNext = true } + .buttonType(.glass) + .textSize(13) } } .navigationDestination(isPresented: $navigateToNext) { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/GetMoreRatingsScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/GetMoreRatingsScreen.swift index cd214b9..297649c 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/GetMoreRatingsScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/GetMoreRatingsScreen.swift @@ -30,17 +30,20 @@ struct GetMoreRatingsScreen: View { SQText("Vous pouvez recueillir des avis auprès de vos clients hors AlloVoisins pour faire décoller votre activité.") .multilineTextAlignment(.center) } - SQButton("Recueillir des avis", color: .sqGold(50), textColor: .white) { + SQButton("Recueillir des avis") { navigateToNext = true } + .colorScheme(.gold) } .padding(16) .background(Color.sqGold(10)) .cornerRadius(8) - SQButton("J’ai compris, mais je souhaite résilier", color: .white, textSize: 13) { + SQButton("J’ai compris, mais je souhaite résilier") { navigateToNext = true } + .buttonType(.glass) + .textSize(13) } .navigationDestination(isPresented: $navigateToNext) { if let screen = viewModel.nextPromotionalScreen { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/MoreTimeScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/MoreTimeScreen.swift index e8b05e8..3c15dda 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/MoreTimeScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/MoreTimeScreen.swift @@ -22,17 +22,20 @@ struct MoreTimeScreen: View { SQText("d’essai supplémentaires pour découvrir toutes les fonctionnalités de l’abonnement Premier.") .multilineTextAlignment(.center) } - SQButton("Je prolonge ma période d’essai", color: .sqBlue(50), textColor: .white) { + SQButton("Je prolonge ma période d’essai") { navigateToNext = true } + .colorScheme(.blue) } .padding(16) .background(Color.sqBlue(10)) .cornerRadius(8) - SQButton("Non merci, je souhaite résilier", color: .white, textSize: 13) { + SQButton("Non merci, je souhaite résilier") { navigateToNext = true } + .buttonType(.glass) + .textSize(13) } .navigationDestination(isPresented: $navigateToNext) { if let screen = viewModel.nextPromotionalScreen { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/OnlyProRequestsScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/OnlyProRequestsScreen.swift index 2155070..bb8eb17 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/OnlyProRequestsScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/OnlyProRequestsScreen.swift @@ -30,17 +30,20 @@ struct OnlyProRequestsScreen: View { SQText("Sur le menu abonnement, vous pouvez filtrer les demandes réservées aux pros.") .multilineTextAlignment(.center) } - SQButton("Voir les demandes", color: .sqGrape(80), textColor: .white) { + SQButton("Voir les demandes") { navigateToNext = true } + .colorScheme(.grape) } .padding(16) .background(Color.sqGrape(10)) .cornerRadius(8) - SQButton("J’ai compris, mais je souhaite résilier", color: .white, textSize: 13) { + SQButton("J’ai compris, mais je souhaite résilier") { navigateToNext = true } + .buttonType(.glass) + .textSize(13) } .navigationDestination(isPresented: $navigateToNext) { if let screen = viewModel.nextPromotionalScreen { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/PersonalizedSupportScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/PersonalizedSupportScreen.swift index 5745bea..7596f09 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/PersonalizedSupportScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/PersonalizedSupportScreen.swift @@ -25,17 +25,20 @@ struct PersonalizedSupportScreen: View { SQText("Notre équipe dédiée aux professionnels est disponible pour répondre à toutes vos questions par téléphone.") .multilineTextAlignment(.center) } - SQButton("Je souhaite être appelé", color: .sqGrape(80), textColor: .white) { + SQButton("Je souhaite être appelé") { navigateToNext = true } + .colorScheme(.grape) } .padding(16) .background(Color.sqGrape(10)) .cornerRadius(8) - SQButton("Non merci, je souhaite résilier", color: .white, textSize: 13) { + SQButton("Non merci, je souhaite résilier") { navigateToNext = true } + .buttonType(.glass) + .textSize(13) } .navigationDestination(isPresented: $navigateToNext) { if let screen = viewModel.nextPromotionalScreen { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ProfileCompletionScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ProfileCompletionScreen.swift index d3ce202..0ee2bfd 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ProfileCompletionScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ProfileCompletionScreen.swift @@ -54,7 +54,7 @@ struct ProfileCompletionScreen: View { SQText("90%", size: 32, font: .bold) SQText("des demandeurs comparent systématiquement les profils des offreurs pour faire leur choix.") .multilineTextAlignment(.center) - SQButton("Je complète mon profil", color: .sqNeutral(100), textColor: .white) { + SQButton("Je complète mon profil") { navigateToNext = true } } @@ -62,9 +62,11 @@ struct ProfileCompletionScreen: View { .background(Color.sqNeutral(10)) .cornerRadius(8) - SQButton("J’ai compris, mais je souhaite résilier", color: .white, textSize: 13) { + SQButton("J’ai compris, mais je souhaite résilier") { navigateToNext = true } + .buttonType(.glass) + .textSize(13) } .navigationDestination(isPresented: $navigateToNext) { if let screen = viewModel.nextPromotionalScreen { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationCheckStepsScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationCheckStepsScreen.swift index c615e73..c5f5da6 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationCheckStepsScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationCheckStepsScreen.swift @@ -42,10 +42,9 @@ struct ResiliationCheckStepsScreen: View { } HStack { - SQButton("Annuler", color: .sqNeutral(100), isFull: false) { - - } - SQButton("Continuer", color: .sqNeutral(100), textColor: .white) { + SQButton("Annuler") {} + .buttonType(.glass) + SQButton("Continuer") { navigateToReasonScreen = true } .disabled(!allStepsSelected) diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationConfirmationScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationConfirmationScreen.swift index 6cff302..cdb5f9f 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationConfirmationScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationConfirmationScreen.swift @@ -17,7 +17,7 @@ struct ResiliationConfirmationScreen: View { .scaledToFit() .frame(height: 200) SQText("Votre résiliation a été prise en compte. Elle sera effective le JJ/MM/AAAA.", size: 18, font: .bold) - SQButton("Terminer", color: .sqNeutral(100), textColor: .white) { + SQButton("Terminer") { } .padding() diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationReasonScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationReasonScreen.swift index 8909122..ade38fd 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationReasonScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResiliationReasonScreen.swift @@ -18,17 +18,18 @@ struct ResiliationReasonScreen: View { ScrollView { VStack(spacing: 16) { SQRadio(title: "Aidez-nous à nous améliorer : précisez le motif de votre résiliation", - options: viewModel.resiliationReasons.map { $0.text }, + options: viewModel.resiliationReasons.map { SQRadioOption(title: $0.text) }, selectedIndex: $selectedIndex) if selectedIndex == viewModel.resiliationReasons.count - 1 { SQTextField("", placeholder: "Précisez-nous le motif de votre résiliation", text: $resiliationOtherMotif) } HStack { - SQButton("Annuler", color: .sqNeutral(100), isFull: false) { + SQButton("Annuler") { dismiss() } - SQButton("Continuer", color: .sqNeutral(100), textColor: .white) { + .buttonType(.glass) + SQButton("Continuer") { if let index = selectedIndex { let selectedReason = viewModel.resiliationReasons[index] viewModel.setSelectedReason(selectedReason) diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResizePerimeterScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResizePerimeterScreen.swift index 6addfa1..7115e8b 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResizePerimeterScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/ResizePerimeterScreen.swift @@ -25,17 +25,20 @@ struct ResizePerimeterScreen: View { SQText("Si vous le souhaitez, vous pouvez modifier à tout moment votre abonnement en élargissant votre périmètre d’intervention.") .multilineTextAlignment(.center) } - SQButton("Modifier mon périmètre", color: .sqOrange(50), textColor: .white) { + SQButton("Modifier mon périmètre") { navigateToNext = true } + .colorScheme(.orange) } .padding(16) .background(Color.sqOrange(10)) .cornerRadius(8) - SQButton("J’ai compris, mais je souhaite résilier", color: .white, textSize: 13) { + SQButton("J’ai compris, mais je souhaite résilier") { navigateToNext = true } + .buttonType(.glass) + .textSize(13) } .navigationDestination(isPresented: $navigateToNext) { if let screen = viewModel.nextPromotionalScreen { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/SoftwarePresentationScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/SoftwarePresentationScreen.swift index 3e07d6f..ded51e8 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/SoftwarePresentationScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/SoftwarePresentationScreen.swift @@ -39,12 +39,14 @@ struct SoftwarePresentationScreen: View { SQText("Inclus, sans surcoût", size: 13, font: .demiBold) } VStack { - SQButton("Découvrir le logiciel", color: .sqNeutral(100), textColor: .white) { + SQButton("Découvrir le logiciel") { navigateToNext = true } - SQButton("J’ai compris, mais je souhaite résilier", color: .white, textSize: 13) { + SQButton("J’ai compris, mais je souhaite résilier") { navigateToNext = true } + .buttonType(.glass) + .textSize(13) } } .navigationDestination(isPresented: $navigateToNext) { diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/StatusChangeScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/StatusChangeScreen.swift index 4015441..e40da24 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/StatusChangeScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/StatusChangeScreen.swift @@ -21,9 +21,10 @@ struct StatusChangeScreen: View { SQText("Si vous le souhaitez, vous pourrez vous abonner à l’abonnement Premier en tant que particulier.") .multilineTextAlignment(.center) } - SQButton("Résilier et changer de statut", color: .sqOrange(50), textColor: .white) { + SQButton("Résilier et changer de statut") { navigateToNext = true } + .colorScheme(.orange) } .padding(16) .background(Color.sqOrange(10)) diff --git a/AlloVoisinsSwiftUI/Features/Resiliation/Views/WebinaireScreen.swift b/AlloVoisinsSwiftUI/Features/Resiliation/Views/WebinaireScreen.swift index 8173e3c..96372a2 100644 --- a/AlloVoisinsSwiftUI/Features/Resiliation/Views/WebinaireScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Resiliation/Views/WebinaireScreen.swift @@ -33,8 +33,9 @@ struct WebinaireScreen: View { ResiliationConfirmationScreen() } } label: { - SQButton("Je m’inscris", color: .sqGrape(80), textColor: .white) { + SQButton("Je m’inscris") { } + .colorScheme(.grape) } } .padding(16) @@ -48,8 +49,10 @@ struct WebinaireScreen: View { ResiliationConfirmationScreen() } } label: { - SQButton("Non merci, je souhaite résilier", color: .white, textSize: 13) { + SQButton("Non merci, je souhaite résilier") { } + .buttonType(.glass) + .textSize(13) } } .navigationDestination(isPresented: $navigateToNext) { diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Models/Pricing.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/Pricing.swift index 88cadd2..ff5a329 100644 --- a/AlloVoisinsSwiftUI/Features/Subscriptions/Models/Pricing.swift +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/Pricing.swift @@ -23,12 +23,12 @@ enum Pricing: String, CaseIterable, Identifiable { } } - var footerButtonColor: Color { + var footerButtonColor: SQButtonColorScheme { switch self { case .standard: - return .sqNeutral(100) + return .neutral case .premier, .premierPro: - return .sqOrange(50) + return .orange } } diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/NewPerimeterCellView.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/NewPerimeterCellView.swift index 8b18a64..1749a70 100644 --- a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/NewPerimeterCellView.swift +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/NewPerimeterCellView.swift @@ -14,7 +14,9 @@ struct NewPerimeterCellView: View { SQText("Vous souhaitez ajouter un nouveau périmètre ?", size: 24, font: .bold) .multilineTextAlignment(.center) SQText("Si vous souhaitez couvrir une zone géographique différente de votre périmètre actuel, souscrivez à un nouvel abonnement pour proposer vos services dans ce nouveau secteur.") - SQButton("Souscrire à un nouveau périmètre", color: .sqOrange(50), textColor: .sqOrange(50), isFull: false) {} + SQButton("Souscrire à un nouveau périmètre") {} + .buttonType(.line) + .colorScheme(.orange) SQText("À partir de 9,99€ / mois, sans engagement", size: 13) } .padding(.horizontal, 32) diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeFooter.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeFooter.swift index b0e1e95..af515d2 100644 --- a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeFooter.swift +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeFooter.swift @@ -16,9 +16,11 @@ struct PricingSubscribeFooter: View { if !pricing.footerSecondaryText.isEmpty { SQText(pricing.footerSecondaryText, size: 13, textColor: pricing.footerTextColor) } - SQButton("Continuer", color: pricing.footerButtonColor, textColor: .white) { + SQButton("Continuer") { } + .colorScheme(pricing.footerButtonColor) + if !pricing.footerTertiaryText.isEmpty { SQText(pricing.footerTertiaryText, size: 12, textColor: .sqOrange(70)) } diff --git a/AlloVoisinsSwiftUI/Features/Visio/Views/VisioPermissionsModal.swift b/AlloVoisinsSwiftUI/Features/Visio/Views/VisioPermissionsModal.swift new file mode 100644 index 0000000..5f89d86 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Visio/Views/VisioPermissionsModal.swift @@ -0,0 +1,55 @@ +// +// VisioPermissionsModal.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 19/05/2025. +// + +import SwiftUI + +struct VisioPermissionsModal: View { + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 16) { + SQText("Appels vocaux et vidéo", size: 18, font: .bold) + Spacer() + Image("new") + SQText("NOUVEAU", size: 9, font: .bold, textColor: .white) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .background { + RoundedRectangle(cornerRadius: 4) + .fill(Color.sqSemanticRed) + } + Button { + + } label: { + SQIcon(.xmark, size: .m, color: .sqNeutral(100)) + } + } + .padding() + + Divider() + .overlay { + Color.sqNeutral(20) + } + + VStack { + Image("visio_permissions") + SQText("Nouveau ! Échangez en appel vocal ou vidéo avec les autres utilisateurs, depuis la messagerie, sans avoir à communiquer votre numéro de téléphone !") + SQButton("Autoriser l'accès au micro") { + } + .padding() + } + .padding() + } + } +} + +#Preview { + EmptyView() + .sheet(isPresented: .constant(true)) { + VisioPermissionsModal() + } + .presentationDetents([.height(300)]) +} diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Icons/solid/video_slash_solid.imageset/Contents.json b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Icons/solid/video_slash_solid.imageset/Contents.json new file mode 100644 index 0000000..0269cf1 --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Icons/solid/video_slash_solid.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Name=video-slash, Size=32.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Icons/solid/video_slash_solid.imageset/Name=video-slash, Size=32.svg b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Icons/solid/video_slash_solid.imageset/Name=video-slash, Size=32.svg new file mode 100644 index 0000000..8894cf2 --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Icons/solid/video_slash_solid.imageset/Name=video-slash, Size=32.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/ladame.imageset/07df87a0478c154775ee348712373fc2.jpeg b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/ladame.imageset/07df87a0478c154775ee348712373fc2.jpeg new file mode 100644 index 0000000..f6af67f Binary files /dev/null and b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/ladame.imageset/07df87a0478c154775ee348712373fc2.jpeg differ diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/ladame.imageset/Contents.json b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/ladame.imageset/Contents.json new file mode 100644 index 0000000..f991c17 --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/ladame.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "07df87a0478c154775ee348712373fc2.jpeg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/lemonsieur.imageset/3eef4f9f219f60bfe2aaa7c4076f35b9.jpeg b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/lemonsieur.imageset/3eef4f9f219f60bfe2aaa7c4076f35b9.jpeg new file mode 100644 index 0000000..676418b Binary files /dev/null and b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/lemonsieur.imageset/3eef4f9f219f60bfe2aaa7c4076f35b9.jpeg differ diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/lemonsieur.imageset/Contents.json b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/lemonsieur.imageset/Contents.json new file mode 100644 index 0000000..84a9aed --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/lemonsieur.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "3eef4f9f219f60bfe2aaa7c4076f35b9.jpeg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/neighbor_avatar.imageset/Avatar.svg b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/neighbor_avatar.imageset/Avatar.svg new file mode 100644 index 0000000..b0338fc --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/neighbor_avatar.imageset/Avatar.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/neighbor_avatar.imageset/Contents.json b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/neighbor_avatar.imageset/Contents.json new file mode 100644 index 0000000..e5ed0d7 --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/neighbor_avatar.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Avatar.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/visio_permissions.imageset/Contents.json b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/visio_permissions.imageset/Contents.json new file mode 100644 index 0000000..e723929 --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/visio_permissions.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "illustration.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/visio_permissions.imageset/illustration.svg b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/visio_permissions.imageset/illustration.svg new file mode 100644 index 0000000..d0e38c0 --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/visio_permissions.imageset/illustration.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + +