Add Views

This commit is contained in:
Victor Bodinaud
2025-06-30 11:25:36 +02:00
parent eb99d76108
commit 08666a6818
64 changed files with 1334 additions and 261 deletions

View File

@@ -12,7 +12,7 @@ struct AlloVoisinsSwiftUIApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
MarketingSupportSelectionView()
EmptyView()
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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 labonnement Premier", color: Color.sqOrange(50), textColor: .white) {
SQButton( "Découvrir labonnement Premier") {
}
.colorScheme(.orange)
}
.frame(maxWidth: .infinity)
}

View File

@@ -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)

View File

@@ -48,7 +48,7 @@ struct RegulatedProfessionEditProfilModal: View {
Spacer()
VStack {
SQButton("fermer", color: .sqNeutral(100), textColor: .white) {}
SQButton("fermer") {}
.disabled(true)
}
.padding(.horizontal, 16)

View File

@@ -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)

View File

@@ -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<Bool> = .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: 32) {
HStack {
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) {}
.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("Imprimer", color: .sqNeutral(100), textColor: .white, icon: SQIcon(.print, color: .white)) {}
SQButton("C'est parti !") {}
.colorScheme(.royal)
.buttonType(.line)
.disabled(true)
SQButton("C'est parti !", isLoading: .constant(true)) {}
SQButton("C'est parti !") {}
.colorScheme(.royal)
.buttonType(.light)
.disabled(true)
SQButton("C'est parti !") {}
.colorScheme(.royal)
.buttonType(.glass)
.disabled(true)
}
}
}
}

View File

@@ -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<SQFormFieldError>
var alignment: VerticalAlignment = .top
init(_ text: String, isChecked: Binding<Bool>, errorText: String? = nil, isInError: Binding<Bool> = .constant(false)) {
init(_ text: String, isChecked: Binding<Bool>, error: Binding<SQFormFieldError> = .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 jengage ma responsabilité sur lexhaustivité et lauthenticité des informations renseignées ci-dessus.", isChecked: .constant(false))
SQCheckbox("Je comprends que jengage ma responsabilité sur lexhaustivité et lauthenticité des informations renseignées ci-dessus.", isChecked: .constant(false), error: .constant(.empty))
}

View File

@@ -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
) {}
}

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -28,18 +28,18 @@ struct SQNavigationBar: ViewModifier {
Button(action: {
backAction?()
}) {
style.closeIcon
style.leadingIcon
}
}
}
} else {
if showBackButton {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
}
if style == .modal || style == .booster {
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
}) {
style.closeIcon
}
} 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)
}
}
}

View File

@@ -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<Bool>) {
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))
}

View File

@@ -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<SQFormFieldError>
@Binding var selectedIndex: Int?
init(title: String? = nil, orientation: SQRadioOrientation = .vertical, options: [String], selectedIndex: Binding<Int?>) {
init(title: String? = nil, titleSize: CGFloat = 24, radioTextSize: CGFloat = 16, orientation: SQRadioOrientation = .vertical, options: [SQRadioOption], selectedIndex: Binding<Int?>, error: Binding<SQFormFieldError> = .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,26 +59,34 @@ 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]
private struct HorizontalRadioButtons: View {
let options: [SQRadioOption]
let titleSize: CGFloat
@Binding var selectedIndex: Int?
var isInError: Bool
var body: some View {
GeometryReader { geometry in
@@ -70,11 +94,14 @@ private struct HorizontalRadioButtons: View {
Spacer()
ForEach(Array(options.enumerated()), id: \.offset) { index, option in
RadioButton(
title: option,
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))
@@ -84,11 +111,14 @@ private struct HorizontalRadioButtons: View {
}
.frame(height: 80)
}
}
}
private struct RadioButton: View {
private struct RadioButton: View {
let title: String
let desc: String?
let titleSize: CGFloat
@Binding var isSelected: Bool
var isInError: Bool
let orientation: SQRadioOrientation
var body: some View {
@@ -100,10 +130,16 @@ private struct RadioButton: View {
.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
@@ -115,22 +151,39 @@ private struct RadioButton: View {
private var radioCircle: some View {
ZStack {
Circle()
.stroke(isSelected ? Color.sqNeutral(100) : Color.sqNeutral(30), lineWidth: 1)
.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(Color.sqNeutral(100), lineWidth: 6)
.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: "Lutilisateur semble proposer un service relevant dune activité réglementée sans disposer des autorisations requises."),
SQRadioOption(title: "Option 2", desc: "Lutilisateur semble évoquer ou proposer des activités interdites par la loi (trafic danimaux, substances illicites, armes, etc.)."),
SQRadioOption(title: "Option 3", desc: "Lutilisateur semble fournir de fausses informations dans son profil."),
SQRadioOption(title: "Option 4", desc: "Lutilisateur semble avoir usurpé mon identité ou lidentité dun 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()
}

View File

@@ -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()
}

View File

@@ -11,12 +11,11 @@ struct SQTextEditor: View {
var label: String
var placeholder: String
var type: SQTextFieldType = .text
var errorText: String
var error: Binding<SQFormFieldError>
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<SQFormFieldError> = .constant(.none),
text: Binding<String>,
icon: SQIcon? = nil,
isDisabled: Bool = false,
isOptional: Bool = false,
tooltipText: String? = nil,
isInError: Binding<Bool> = .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 dexpé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 dexpérience ! Devis gratuit", error: .constant(.empty), text: .constant("Lorem ipsum dolor sit amet, consectetur"), minCharacters: 50)
.padding()
}

View File

@@ -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<SQFormFieldError>
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<String>,
icon: SQIcon? = nil,
isDisabled: Bool = false,
isOptional: Bool = false,
tooltipText: String? = nil,
isInError: Binding<Bool> = .constant(false),
helperText: String? = nil,
error: Binding<SQFormFieldError> = .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,17 +98,13 @@ 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()) {
self.text = String($0.prefix(10))
let formatted = formatPhoneNumber(String($0.prefix(15)))
self.text = formatted
}
.keyboardType(.numberPad)
.font(.sq(.medium))
@@ -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",

View File

@@ -17,9 +17,10 @@ struct BoosterPromotionView: View {
}
BoosterStatsView()
SQButton("Activer loption Booster", color: .sqRoyal(), textColor: .white) {
SQButton("Activer loption Booster") {
}
.colorScheme(.royal)
}
.padding()
.foregroundColor(.sqRoyal())

View File

@@ -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()
}
}

View File

@@ -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") {
}
}

View File

@@ -34,7 +34,7 @@ struct CardColorSelectionView: View {
}
.padding()
SQFooter {
SQButton("Continuer", color: .sqNeutral(100), textColor: .white) {
SQButton("Continuer") {
self.goToNext.toggle()
}
}

View File

@@ -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: {

View File

@@ -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 • Lutilisation dun papier dau moins200 g/m2 ")
SQButton("Imprimer", color: .sqNeutral(100), textColor: .white) {}
SQButton("Imprimer") {}
.frame(maxWidth: .infinity)
Spacer()
}

View File

@@ -67,7 +67,7 @@ struct CardTemplateSelectionView: View {
}
}
SQFooter {
SQButton("Continuer", color: .sqNeutral(100), textColor: .white) {
SQButton("Continuer") {
if selectedTemplate != nil {
goToNext.toggle()
}

View File

@@ -34,7 +34,7 @@ struct FlyerColorSelectionView: View {
}
.padding()
SQFooter {
SQButton("Continuer", color: .sqNeutral(100), textColor: .white) {
SQButton("Continuer") {
self.goToNext.toggle()
}
}

View File

@@ -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 dexpé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 jengage ma responsabilité sur lexhaustivité et lauthenticité 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 jengage ma responsabilité sur lexhaustivité et lauthenticité 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: {

View File

@@ -88,7 +88,7 @@ struct FlyerTemplateSelectionView: View {
}
}
SQFooter {
SQButton("Continuer", color: .sqNeutral(100), textColor: .white) {
SQButton("Continuer") {
if selectedTemplate != nil {
goToNext.toggle()
}

View File

@@ -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))
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -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 lobjet 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()
}
}

View File

@@ -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()
}
}

View File

@@ -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("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
SQButton("Jai compris, mais je souhaite résilier") {
navigateToNext = true
}
.buttonType(.glass)
.textSize(13)
}
.navigationDestination(isPresented: $navigateToNext) {
if let screen = viewModel.nextPromotionalScreen {

View File

@@ -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)
}
}
}

View File

@@ -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("Jai compris, mais je souhaite résilier", color: .white) {
SQButton("Jai compris, mais je souhaite résilier")
{
navigateToNext = true
}
.buttonType(.glass)
}
}
.navigationDestination(isPresented: $navigateToNext) {

View File

@@ -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<Content: ResiliationContentView>: 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) {

View File

@@ -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("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
SQButton("Jai compris, mais je souhaite résilier") {
navigateToNext = true
}
.buttonType(.glass)
.textSize(13)
}
.navigationDestination(isPresented: $navigateToNext) {
if let screen = viewModel.nextPromotionalScreen {

View File

@@ -22,17 +22,20 @@ struct MoreTimeScreen: View {
SQText("dessai supplémentaires pour découvrir toutes les fonctionnalités de labonnement Premier.")
.multilineTextAlignment(.center)
}
SQButton("Je prolonge ma période dessai", color: .sqBlue(50), textColor: .white) {
SQButton("Je prolonge ma période dessai") {
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 {

View File

@@ -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("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
SQButton("Jai compris, mais je souhaite résilier") {
navigateToNext = true
}
.buttonType(.glass)
.textSize(13)
}
.navigationDestination(isPresented: $navigateToNext) {
if let screen = viewModel.nextPromotionalScreen {

View File

@@ -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 {

View File

@@ -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("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
SQButton("Jai compris, mais je souhaite résilier") {
navigateToNext = true
}
.buttonType(.glass)
.textSize(13)
}
.navigationDestination(isPresented: $navigateToNext) {
if let screen = viewModel.nextPromotionalScreen {

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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 dintervention.")
.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("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
SQButton("Jai compris, mais je souhaite résilier") {
navigateToNext = true
}
.buttonType(.glass)
.textSize(13)
}
.navigationDestination(isPresented: $navigateToNext) {
if let screen = viewModel.nextPromotionalScreen {

View File

@@ -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("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
SQButton("Jai compris, mais je souhaite résilier") {
navigateToNext = true
}
.buttonType(.glass)
.textSize(13)
}
}
.navigationDestination(isPresented: $navigateToNext) {

View File

@@ -21,9 +21,10 @@ struct StatusChangeScreen: View {
SQText("Si vous le souhaitez, vous pourrez vous abonner à labonnement 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))

View File

@@ -33,8 +33,9 @@ struct WebinaireScreen: View {
ResiliationConfirmationScreen()
}
} label: {
SQButton("Je minscris", color: .sqGrape(80), textColor: .white) {
SQButton("Je minscris") {
}
.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) {

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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)])
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Name=video-slash, Size=32.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,10 @@
<svg width="40" height="32" viewBox="0 0 40 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2376_165)">
<path d="M2.42535 0.318888C1.77535 -0.193612 0.831601 -0.0748623 0.319101 0.575138C-0.193399 1.22514 -0.0746487 2.16889 0.575351 2.68139L37.5754 31.6814C38.2254 32.1939 39.1691 32.0751 39.6816 31.4251C40.1941 30.7751 40.0754 29.8314 39.4254 29.3189L34.0254 25.0876L34.8879 25.6626C35.5004 26.0689 36.2878 26.1126 36.9441 25.7626C37.6003 25.4126 38.0004 24.7376 38.0004 24.0001V8.00014C38.0004 7.26264 37.5941 6.58764 36.9441 6.23764C36.2941 5.88764 35.5066 5.92514 34.8879 6.33764L28.8879 10.3376L28.0004 10.9314V12.0001V20.0001V20.3626L26.0004 18.7939V8.00014C26.0004 5.79389 24.2066 4.00014 22.0004 4.00014H7.1191L2.42535 0.318888ZM25.4379 26.0439L2.0191 7.59389C2.0066 7.72514 2.00035 7.86264 2.00035 8.00014V24.0001C2.00035 26.2064 3.7941 28.0001 6.00035 28.0001H22.0004C23.4629 28.0001 24.7441 27.2126 25.4379 26.0439Z" fill="#172433"/>
</g>
<defs>
<clipPath id="clip0_2376_165">
<rect width="40" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "07df87a0478c154775ee348712373fc2.jpeg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "3eef4f9f219f60bfe2aaa7c4076f35b9.jpeg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<rect x="0.5" y="0.5" width="33" height="33" rx="16.5" stroke="#374A61" stroke-dasharray="4 4"/>
<path d="M17 16.9997C19.7404 16.9997 21.9583 14.7817 21.9583 12.0413C21.9583 9.30098 19.7404 7.08301 17 7.08301C14.2596 7.08301 12.0417 9.30098 12.0417 12.0413C12.0417 14.7817 14.2596 16.9997 17 16.9997ZM20.4 18.4163H20.0326C19.1117 18.859 18.0891 19.1247 17 19.1247C15.9109 19.1247 14.8927 18.859 13.9674 18.4163H13.6C10.7844 18.4163 8.5 20.7007 8.5 23.5163V24.7913C8.5 25.9645 9.45182 26.9163 10.625 26.9163H23.375C24.5482 26.9163 25.5 25.9645 25.5 24.7913V23.5163C25.5 20.7007 23.2156 18.4163 20.4 18.4163Z" fill="#778BA3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 751 B

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Avatar.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "illustration.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
<svg width="99" height="79" viewBox="0 0 99 79" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M43.4785 3.52671C25.5808 4.62837 16.9441 15.2318 11.7413 21.2909C6.6804 27.1847 0.988698 50.2094 0.121628 56.8193C-1.79931 71.4632 19.4877 75.0884 31.8589 75.2721C35.7321 76.0524 51.6494 76.8764 82.1529 68.5244C112.329 60.262 94.8611 36.5764 84.234 25.5599C77.9906 19.0876 61.3763 2.42506 43.4785 3.52671Z" fill="#F2F8FF"/>
<path d="M45.2568 26.792L41.5287 16.7954C41.3066 16.1997 40.6435 15.8969 40.0478 16.1191C39.4521 16.3412 39.1493 17.0043 39.3715 17.6L43.0996 27.5965C43.3217 28.1923 43.9847 28.4951 44.5805 28.2729C45.1762 28.0507 45.479 27.3877 45.2568 26.792Z" fill="#249EE6"/>
<path d="M39.386 29.0835L34.4791 24.5891C34.0102 24.1597 33.282 24.1916 32.8526 24.6605L32.8511 24.6621C32.4216 25.1309 32.4536 25.8591 32.9224 26.2886L37.8294 30.783C38.2982 31.2124 39.0264 31.1805 39.4559 30.7116L39.4574 30.71C39.8868 30.2412 39.8549 29.513 39.386 29.0835Z" fill="#249EE6"/>
<path d="M51.1969 26.0451L52.179 19.4638C52.2728 18.835 51.8391 18.2491 51.2103 18.1553L51.2081 18.155C50.5793 18.0611 49.9934 18.4948 49.8996 19.1236L48.9175 25.7049C48.8237 26.3337 49.2573 26.9196 49.8862 27.0134L49.8884 27.0138C50.5172 27.1076 51.103 26.6739 51.1969 26.0451Z" fill="#249EE6"/>
<g clip-path="url(#clip0_2476_6083)">
<path d="M34.5082 42.1113L37.7249 58.8042C38.021 60.3407 37.0154 61.8264 35.4789 62.1225L18.786 65.3392C17.2494 65.6353 15.7638 64.6298 15.4677 63.0932L12.251 46.4003C11.9549 44.8638 12.9604 43.3781 14.497 43.082L31.1899 39.8653C32.7259 39.5693 34.2121 40.5747 34.5082 42.1113ZM45.8099 40.8652L48.6793 55.7555C48.9641 57.2335 47.4397 58.4224 46.0466 57.8131L38.8503 54.6619L37.0923 45.5388L42.6009 39.935C43.6723 38.8503 45.5262 39.393 45.8099 40.8652Z" fill="#0982C9"/>
</g>
<g clip-path="url(#clip1_2476_6083)">
<path d="M83.2322 52.231L81.8061 57.8447C81.6056 58.6383 80.8923 59.1809 80.0711 59.1627C65.9489 58.8495 54.7164 47.1094 55.0285 32.9866C55.0467 32.166 55.6202 31.4764 56.4219 31.3121L62.0931 30.1353C62.919 29.963 63.7492 30.4081 64.0735 31.1939L66.5433 37.3248C66.8319 38.0469 66.6081 38.8759 65.9955 39.3541L62.9283 41.7073C64.7447 45.624 67.8255 48.844 71.6592 50.8328L74.1932 47.8758C74.6928 47.2831 75.5372 47.0929 76.2457 47.4186L82.2616 50.1562C82.9836 50.5566 83.4406 51.418 83.2322 52.231Z" fill="#0982C9"/>
</g>
<defs>
<clipPath id="clip0_2476_6083">
<rect width="34" height="30.2222" fill="white" transform="translate(11 39.9082) rotate(-10.9073)"/>
</clipPath>
<clipPath id="clip1_2476_6083">
<rect width="28.6641" height="28.6668" fill="white" transform="translate(55.0959 29.9365) rotate(1.26602)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB