New views

This commit is contained in:
Victor Bodinaud
2025-12-10 16:31:08 +01:00
parent 08666a6818
commit e305b1697a
139 changed files with 3743 additions and 725 deletions

View File

@@ -199,7 +199,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
@@ -256,7 +256,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;

View File

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

View File

@@ -0,0 +1,49 @@
//
// BaseNavigationManager.swift
// Allovoisins
//
// Created by Victor on 06/12/2024.
// Copyright © 2024 AlloVoisins. All rights reserved.
//
import SwiftUI
protocol NavigationRoute: Hashable {}
class BaseNavigationManager<Route: NavigationRoute>: ObservableObject {
@Published var navigationPath = NavigationPath()
@Published private var routeStack: [Route] = []
@Published var shouldDismissToRoot: Bool = false
@Published var shouldDismiss: Bool = false
var currentRoute: Route? {
routeStack.last
}
var canGoBack: Bool {
!routeStack.isEmpty && !navigationPath.isEmpty
}
func navigateTo(_ route: Route) {
navigationPath.append(route)
routeStack.append(route)
}
func goBack() {
if canGoBack {
navigationPath.removeLast()
routeStack.removeLast()
} else {
shouldDismiss = true
}
}
func resetNavigation() {
navigationPath = NavigationPath()
routeStack = []
}
func dismissToRoot() {
shouldDismissToRoot = true
}
}

View File

@@ -33,8 +33,11 @@ struct MessageNotificationView: View {
AVAvatar(model: AVAvatarModel(avatarString: notification.avatarUrl, onlineStatus: .online(), sizing: .M)) AVAvatar(model: AVAvatarModel(avatarString: notification.avatarUrl, onlineStatus: .online(), sizing: .M))
.frame(width: 48, height: 48) .frame(width: 48, height: 48)
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText(notification.displayName, font: .demiBold) SQText(notification.displayName)
SQText(notification.message, size: 14, textColor: .sqNeutral(70)) .sqFont(.semiBold)
SQText(notification.message)
.sqSize(14)
.sqColor(.sqNeutral(70))
.lineLimit(1) .lineLimit(1)
} }
Spacer() Spacer()

View File

@@ -12,13 +12,15 @@ struct OnlyForPremierView: View {
var body: some View { var body: some View {
VStack(spacing: 8) { VStack(spacing: 8) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
SQText("Réservé aux abonnés Premier", size: 18, font: .bold) SQText("Réservé aux abonnés Premier")
.sqSize(18)
.sqFont(.bold)
SQText("Seuls les abonnés Premier peuvent profiter de cette fonctionnalité.") SQText("Seuls les abonnés Premier peuvent profiter de cette fonctionnalité.")
} }
SQButton( "Découvrir labonnement Premier") { SQButton( "Découvrir labonnement Premier") {
} }
.colorScheme(.orange) .sqColorScheme(.orange)
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }

View File

@@ -27,26 +27,33 @@ struct OnlyForPremierModal: View {
Color.white Color.white
VStack(spacing: 48) { VStack(spacing: 48) {
VStack(spacing: 24) { VStack(spacing: 24) {
SQText("Vous avez déjà répondu à 4 demandes de services ce mois-ci", size: 24, font: .bold) SQText("Vous avez déjà répondu à 4 demandes de services ce mois-ci")
.sqSize(24)
.sqFont(.bold)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQText("Augmentez votre chiffre daffaires en bénéficiant de tous les avantages inclus dans labonnement Premier.", font: .bold) SQText("Augmentez votre chiffre daffaires en bénéficiant de tous les avantages inclus dans labonnement Premier.")
.sqFont(.bold)
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50)) SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
SQText("Répondez aux demandes en illimité", font: .demiBold) SQText("Répondez aux demandes en illimité")
.sqFont(.semiBold)
} }
HStack { HStack {
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50)) SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
SQText("Augmentez votre visibilité sur Google", font: .demiBold) SQText("Augmentez votre visibilité sur Google")
.sqFont(.semiBold)
} }
HStack { HStack {
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50)) SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
SQText("Affichez votre numéro de téléphone sur votre profil", font: .demiBold) SQText("Affichez votre numéro de téléphone sur votre profil")
.sqFont(.semiBold)
} }
HStack { HStack {
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50)) SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
SQText("Gérez vos devis, factures, encaissements...", font: .demiBold) SQText("Gérez vos devis, factures, encaissements...")
.sqFont(.semiBold)
} }
} }
} }
@@ -55,13 +62,15 @@ struct OnlyForPremierModal: View {
Spacer() Spacer()
VStack(spacing: 16) { VStack(spacing: 16) {
VStack { VStack {
SQText("Essai gratuit de 14 jours", size: 18, font: .bold) SQText("Essai gratuit de 14 jours")
.sqSize(18)
.sqFont(.bold)
SQText("à partir de 29,99 € / mois") SQText("à partir de 29,99 € / mois")
} }
SQButton("Je m'abonne") { SQButton("Je m'abonne") {
} }
.colorScheme(.orange) .sqColorScheme(.orange)
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 24) .padding(.vertical, 24)

View File

@@ -12,34 +12,38 @@ struct RegulatedProfessionEditProfilModal: View {
var body: some View { var body: some View {
VStack { VStack {
VStack(spacing: 24) { VStack(spacing: 24) {
SQText("Profession réglementée", size: 32, font: .bold) SQText("Profession réglementée")
.sqSize(32)
.sqFont(.bold)
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQText("Vous ne pouvez pas enregistrer votre profil car des éléments indiquent que vous proposez vos services en déménagement avec véhicule. Lactivité de déménageur avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent lexercer.", font: .medium) SQText("Vous ne pouvez pas enregistrer votre profil car des éléments indiquent que vous proposez vos services en déménagement avec véhicule. Lactivité de déménageur avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent lexercer.")
SQText("Aussi, vous nêtes pas autorisé à proposer vos services de déménageur avec véhicule, ni à en faire mention sur votre profil.", font: .medium) SQText("Aussi, vous nêtes pas autorisé à proposer vos services de déménageur avec véhicule, ni à en faire mention sur votre profil.")
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
SQText("Nous vous invitons à corriger les éléments suivants :", size: 16, font: .demiBold) SQText("Nous vous invitons à corriger les éléments suivants :")
.sqFont(.semiBold)
HStack { HStack {
SQIcon(.xmark, size: .m, type: .solid, color: .red) SQIcon(.xmark, size: .m, type: .solid, color: .red)
SQText("Titre du profil", font: .medium) SQText("Titre du profil")
} }
HStack { HStack {
SQIcon(.xmark, size: .m, type: .solid, color: .red) SQIcon(.xmark, size: .m, type: .solid, color: .red)
SQText("Présentation profil", font: .medium) SQText("Présentation profil")
} }
HStack { HStack {
SQIcon(.xmark, size: .m, type: .solid, color: .red) SQIcon(.xmark, size: .m, type: .solid, color: .red)
SQText("Photo de couverture", font: .medium) SQText("Photo de couverture")
} }
HStack { HStack {
SQIcon(.xmark, size: .m, type: .solid, color: .red) SQIcon(.xmark, size: .m, type: .solid, color: .red)
SQText("Photo de profil", font: .medium) SQText("Photo de profil")
} }
HStack { HStack {
SQIcon(.xmark, size: .m, type: .solid, color: .red) SQIcon(.xmark, size: .m, type: .solid, color: .red)
SQText("Photos de réalisation", font: .medium) SQText("Photos de réalisation")
} }
} }
SQText("À défaut, ces éléments seront supprimés de votre profil en septembre.", font: .demiBold) SQText("À défaut, ces éléments seront supprimés de votre profil en septembre.")
.sqFont(.semiBold)
Text("[Retrouvez ici](https://www.codingwithrashid.com) les informations utiles pour devenir déménageur agréé.") Text("[Retrouvez ici](https://www.codingwithrashid.com) les informations utiles pour devenir déménageur agréé.")
// SQText("[Retrouvez ici](https://www.codingwithrashid.com) les informations utiles pour devenir déménageur agréé.", font: .medium) // SQText("[Retrouvez ici](https://www.codingwithrashid.com) les informations utiles pour devenir déménageur agréé.", font: .medium)
} }

View File

@@ -12,12 +12,15 @@ struct RegulatedProfessionModal: View {
var body: some View { var body: some View {
VStack() { VStack() {
VStack(spacing: 16) { VStack(spacing: 16) {
SQText("Attention au cadre juridique", size: 24, font: .bold) SQText("Attention au cadre juridique")
.sqSize(24)
.sqFont(.bold)
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQText("Lactivité de déménagement avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent lexercer.") SQText("Lactivité de déménagement avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent lexercer.")
SQText("Vous pouvez proposer vos services pour participer à un déménagement, mais vous nêtes pas autorisé à mettre à disposition un véhicule.") SQText("Vous pouvez proposer vos services pour participer à un déménagement, mais vous nêtes pas autorisé à mettre à disposition un véhicule.")
SQText("Vous trouverez plus d'informations sur les activités réglementées dans la FAQ.") SQText("Vous trouverez plus d'informations sur les activités réglementées dans la FAQ.")
SQText("Le non-respect de ce cadre légal pourra entraîner la suspension de votre compte.", font: .demiBold) SQText("Le non-respect de ce cadre légal pourra entraîner la suspension de votre compte.")
.sqFont(.semiBold)
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
} }

View File

@@ -12,7 +12,8 @@ struct NeighborBanner: View {
var body: some View { var body: some View {
HStack(spacing: 8) { HStack(spacing: 8) {
SQIcon(.lightbulb_on, size: .xl, color: .sqGreen(80)) SQIcon(.lightbulb_on, size: .xl, color: .sqGreen(80))
SQText("Nous vous recommandons de sélectionner 3 offreurs.", size: 16, font: .demiBold) SQText("Nous vous recommandons de sélectionner 3 offreurs.")
.sqFont(.semiBold)
} }
.foregroundColor(.sqGreen(80)) .foregroundColor(.sqGreen(80))
.padding(16) .padding(16)

View File

@@ -12,12 +12,15 @@ struct SQAddressModifyLine: View {
let attributedText = NSAttributedString("Modifier") let attributedText = NSAttributedString("Modifier")
HStack { HStack {
SQText("à 8 allée Baco, 44000 Nantes", size: 14) SQText("à 8 allée Baco, 44000 Nantes")
.sqSize(14)
Spacer() Spacer()
Button { Button {
print("Modifier") print("Modifier")
} label: { } label: {
SQText("Modifier", size: 14, font: .bold) SQText("Modifier")
.sqSize(14)
.sqFont(.bold)
.underline(true, pattern: .solid) .underline(true, pattern: .solid)
} }
} }

View File

@@ -15,10 +15,14 @@ struct SQButton: View {
@State private var type: SQButtonType = .solid @State private var type: SQButtonType = .solid
@State private var colorScheme: SQButtonColorScheme = .neutral @State private var colorScheme: SQButtonColorScheme = .neutral
@State private var textSize: CGFloat = 16 @State private var textSize: CGFloat = 16
@State private var font: SQTextFont = .demiBold @State private var font: SQTextFont = .semiBold
@State private var icon: SQIcon? = nil @State private var icon: SQIcon? = nil
@State private var isLoading: Bool = false @State private var isPressed: Bool = false
@State private var isDisabled: Bool = false @State private var isLarge: Bool = false
@Binding var isLoading: Bool
@Environment(\.isEnabled) private var isEnabled
private var textWidth: CGFloat { private var textWidth: CGFloat {
let font = UIFont.systemFont(ofSize: textSize) let font = UIFont.systemFont(ofSize: textSize)
@@ -27,8 +31,12 @@ struct SQButton: View {
return size.width + (icon != nil ? 24 : 0) return size.width + (icon != nil ? 24 : 0)
} }
init(_ title: String, action: @escaping () -> Void) { init(_ title: String,
isLoading: Binding<Bool> = .constant(false),
action: @escaping () -> Void)
{
self.title = title self.title = title
self._isLoading = isLoading
self.action = action self.action = action
} }
@@ -37,13 +45,14 @@ struct SQButton: View {
type: type, type: type,
colorScheme: colorScheme, colorScheme: colorScheme,
isLoading: isLoading, isLoading: isLoading,
isDisabled: isDisabled isDisabled: !isEnabled,
isPressed: isPressed
) )
} }
var body: some View { var body: some View {
Button(action: { Button(action: {
if !isLoading && !isDisabled { if !isLoading, !isEnabled {
action() action()
} }
}, label: { }, label: {
@@ -58,10 +67,14 @@ struct SQButton: View {
.foregroundColor(buttonStyle.textColor) .foregroundColor(buttonStyle.textColor)
} }
SQText(title, size: textSize, font: font, textColor: buttonStyle.textColor) SQText(title)
.sqSize(textSize)
.sqFont(font)
.sqColor(buttonStyle.textColor)
} }
} }
.frame(minWidth: textWidth) .frame(minWidth: textWidth)
.frame(maxWidth: isLarge ? .infinity : nil, alignment: .center)
.padding(.horizontal, 30) .padding(.horizontal, 30)
.padding(.vertical, 12) .padding(.vertical, 12)
.frame(height: 40, alignment: .center) .frame(height: 40, alignment: .center)
@@ -73,51 +86,57 @@ struct SQButton: View {
.stroke(buttonStyle.borderColor, lineWidth: 1) .stroke(buttonStyle.borderColor, lineWidth: 1)
) )
}) })
.disabled(isLoading || isDisabled) .buttonStyle(PressedButtonStyle(isPressed: $isPressed))
}
}
struct PressedButtonStyle: ButtonStyle {
@Binding var isPressed: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed) { newValue in
isPressed = newValue
}
} }
} }
// MARK: - Modifiers // MARK: - Modifiers
extension SQButton { extension SQButton {
func buttonType(_ type: SQButtonType) -> SQButton { func sqButtonType(_ type: SQButtonType) -> SQButton {
var copy = self var copy = self
copy._type = State(initialValue: type) copy._type = State(initialValue: type)
return copy return copy
} }
func colorScheme(_ scheme: SQButtonColorScheme) -> SQButton { func sqColorScheme(_ scheme: SQButtonColorScheme) -> SQButton {
var copy = self var copy = self
copy._colorScheme = State(initialValue: scheme) copy._colorScheme = State(initialValue: scheme)
return copy return copy
} }
func textSize(_ size: CGFloat) -> SQButton { func sqTextSize(_ size: CGFloat) -> SQButton {
var copy = self var copy = self
copy._textSize = State(initialValue: size) copy._textSize = State(initialValue: size)
return copy return copy
} }
func font(_ font: SQTextFont) -> SQButton { func sqFont(_ font: SQTextFont) -> SQButton {
var copy = self var copy = self
copy._font = State(initialValue: font) copy._font = State(initialValue: font)
return copy return copy
} }
func icon(_ icon: SQIcon?) -> SQButton { func sqIcon(_ icon: SQIcon?) -> SQButton {
var copy = self var copy = self
copy._icon = State(initialValue: icon) copy._icon = State(initialValue: icon)
return copy return copy
} }
func loading(_ isLoading: Bool) -> SQButton { func sqLarge(_ isLarge: Bool = true) -> SQButton {
var copy = self var copy = self
copy._isLoading = State(initialValue: isLoading) copy._isLarge = State(initialValue: isLarge)
return copy
}
func disabled(_ isDisabled: Bool) -> SQButton {
var copy = self
copy._isDisabled = State(initialValue: isDisabled)
return copy return copy
} }
} }
@@ -143,6 +162,7 @@ enum SQButtonColorScheme: String, CaseIterable {
case grape case grape
case forest case forest
case royal case royal
case white
var baseColor: Color { var baseColor: Color {
switch self { switch self {
@@ -168,6 +188,8 @@ enum SQButtonColorScheme: String, CaseIterable {
return .sqForest(100) return .sqForest(100)
case .royal: case .royal:
return .sqRoyal(60) return .sqRoyal(60)
case .white:
return .white
} }
} }
@@ -195,6 +217,8 @@ enum SQButtonColorScheme: String, CaseIterable {
return .sqForest(50) return .sqForest(50)
case .royal: case .royal:
return .sqRoyal(30) return .sqRoyal(30)
case .white:
return .white
} }
} }
@@ -222,6 +246,8 @@ enum SQButtonColorScheme: String, CaseIterable {
return .sqForest(10) return .sqForest(10)
case .royal: case .royal:
return .sqRoyal(10) return .sqRoyal(10)
case .white:
return .white
} }
} }
} }
@@ -231,8 +257,22 @@ struct SQButtonStyle {
let colorScheme: SQButtonColorScheme let colorScheme: SQButtonColorScheme
let isLoading: Bool let isLoading: Bool
let isDisabled: Bool let isDisabled: Bool
let isPressed: Bool
var backgroundColor: Color { var backgroundColor: Color {
if isPressed && !isDisabled && !isLoading {
switch type {
case .solid:
return colorScheme.baseColor.opacity(0.8)
case .line:
return .clear
case .light:
return colorScheme.lightColor
case .glass:
return .clear
}
}
if isDisabled { if isDisabled {
switch type { switch type {
case .solid: case .solid:
@@ -273,8 +313,11 @@ struct SQButtonStyle {
var borderColor: Color { var borderColor: Color {
switch type { switch type {
case .solid: case .solid:
if isPressed && !isDisabled && !isLoading {
return colorScheme.baseColor.opacity(0.8)
}
if isDisabled { if isDisabled {
return colorScheme.lightColor.opacity(0.5) return colorScheme.lightColor.opacity(0.5)
} }
@@ -282,7 +325,12 @@ struct SQButtonStyle {
if isLoading { if isLoading {
return colorScheme.lightColor.opacity(0.8) return colorScheme.lightColor.opacity(0.8)
} }
case .line: case .line:
if isPressed && !isDisabled && !isLoading {
return colorScheme.baseColor.opacity(0.8)
}
if isDisabled { if isDisabled {
return colorScheme.mediumColor return colorScheme.mediumColor
} }
@@ -294,6 +342,10 @@ struct SQButtonStyle {
return colorScheme.baseColor return colorScheme.baseColor
case .light: case .light:
if isPressed && !isDisabled && !isLoading {
return colorScheme.lightColor.opacity(0.8)
}
if isDisabled { if isDisabled {
return colorScheme.lightColor.opacity(0.5) return colorScheme.lightColor.opacity(0.5)
} }
@@ -312,6 +364,15 @@ struct SQButtonStyle {
} }
var textColor: Color { var textColor: Color {
if isPressed && !isDisabled && !isLoading {
switch type {
case .solid:
return .white
case .line, .light, .glass:
return colorScheme.baseColor.opacity(0.8)
}
}
if isDisabled { if isDisabled {
switch type { switch type {
case .solid: case .solid:
@@ -335,67 +396,37 @@ struct SQButtonStyle {
HStack { HStack {
VStack(spacing: 16) { VStack(spacing: 16) {
SQButton("C'est parti !") {} SQButton("C'est parti !") {}
.colorScheme(.royal) .sqColorScheme(.royal)
SQButton("C'est parti !") {} SQButton("C'est parti !") {}
.colorScheme(.royal) .sqColorScheme(.royal)
.buttonType(.line) .sqButtonType(.line)
SQButton("C'est parti !") {} SQButton("C'est parti !") {}
.colorScheme(.royal) .sqColorScheme(.royal)
.buttonType(.light) .sqButtonType(.light)
SQButton("C'est parti !") {} SQButton("C'est parti !") {}
.colorScheme(.royal) .sqColorScheme(.royal)
.buttonType(.glass) .sqButtonType(.glass)
} }
VStack(spacing: 16) { VStack(spacing: 16) {
SQButton("C'est parti !") {} SQButton("C'est parti !", isLoading: .constant(true)) {}
.colorScheme(.royal) .sqColorScheme(.royal)
.loading(true) SQButton("C'est parti !", isLoading: .constant(true)) {}
SQButton("C'est parti !") {} .sqColorScheme(.royal)
.colorScheme(.royal) .sqButtonType(.line)
.buttonType(.line) SQButton("C'est parti !", isLoading: .constant(true)) {}
.loading(true) .sqColorScheme(.royal)
SQButton("C'est parti !") {} .sqButtonType(.light)
.colorScheme(.royal) SQButton("C'est parti !", isLoading: .constant(true)) {}
.buttonType(.light) .sqColorScheme(.royal)
.loading(true) .sqButtonType(.glass)
SQButton("C'est parti !") {}
.colorScheme(.royal)
.buttonType(.glass)
.loading(true)
} }
} }
HStack { SQButton("Continuer avec Apple") {}
VStack(spacing: 16) { .sqButtonType(.line)
SQButton("C'est parti !") {} .sqIcon(SQIcon(.apple_brand))
.colorScheme(.royal) .sqLarge()
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)
}
}
} }
.padding()
} }

View File

@@ -42,7 +42,10 @@ struct SQCheckbox: View {
HStack { HStack {
SQIcon(.circle_exclamation, customSize: 13, type: .solid, color: .sqSemanticRed) SQIcon(.circle_exclamation, customSize: 13, type: .solid, color: .sqSemanticRed)
SQText(error.wrappedValue.message, size: 12, font: .demiBold, textColor: .sqSemanticRed) SQText(error.wrappedValue.message)
.sqSize(12)
.sqFont(.semiBold)
.sqColor(.sqSemanticRed)
} }
.isHidden(hidden: !error.wrappedValue.isInError) .isHidden(hidden: !error.wrappedValue.isInError)
} }

View File

@@ -37,7 +37,10 @@ fileprivate struct SQChip: View {
} }
var body: some View { var body: some View {
SQText(option.label, size: 14, font: .demiBold, textColor: isSelected ? .white : Color.sqNeutral(100)) SQText(option.label)
.sqSize(14)
.sqFont(.semiBold)
.sqColor(isSelected ? .white : Color.sqNeutral(100))
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.vertical, 8) .padding(.vertical, 8)
.background(isSelected ? option.bgColor : Color.clear) .background(isSelected ? option.bgColor : Color.clear)

View File

@@ -72,7 +72,9 @@ struct SQCircleButton: View {
.shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 4) .shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 4)
if !text.isEmpty { if !text.isEmpty {
SQText(text, font: .demiBold, textColor: .sqNeutral(80)) SQText(text)
.sqFont(.semiBold)
.sqColor(.sqNeutral(80))
} }
} }
} }

View File

@@ -35,7 +35,9 @@ struct SQColorPicker: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText(title, size: 18, font: .bold) SQText(title)
.sqSize(18)
.sqFont(.bold)
SQText(subtitle ?? "") SQText(subtitle ?? "")
.lineLimit(2) .lineLimit(2)
.lineSpacing(1) .lineSpacing(1)

View File

@@ -0,0 +1,143 @@
//
// SQDatePicker.swift
// Allovoisins
//
// Created by gabin Warnier de wailly on 13/12/2024.
// Copyright © 2024 AlloVoisins. All rights reserved.
//
import SwiftUI
struct SQDatePickerView: View {
var label: String
var placeholder: String
var error: Binding<SQFormFieldError>?
var isDisabled: Bool
var dateRange: ClosedRange<Date>?
@Binding private var selectedDate: Date?
@State private var showDatePicker: Bool = false
init(_ label: String,
placeholder: String,
selectedDate: Binding<Date?> = .constant(Date()),
error: Binding<SQFormFieldError>? = nil,
isDisabled: Bool = false,
dateRange: ClosedRange<Date>? = nil)
{
self.label = label
self.placeholder = placeholder
self._selectedDate = selectedDate
self.error = error
self.isDisabled = isDisabled
self.dateRange = dateRange
}
var body: some View {
ZStack {
VStack(alignment: .leading, spacing: 4) {
SQText(label)
.sqFont(.semiBold)
.sqColor(.sqNeutral(80))
// TextField
HStack(spacing: 4) {
TextField("", text: .constant(formattedDate(selectedDate)))
.font(.sq(.medium))
.foregroundStyle(Color.sqNeutral(isDisabled ? 60 : 100))
.tint(.sqNeutral(80))
.placeholder(when: selectedDate == nil) {
SQText(placeholder)
.sqColor(.sqNeutral(50))
}
.onChange(of: self.selectedDate) { _ in
error?.wrappedValue = .none
}
.submitLabel(.done)
.disabled(true)
SQIcon(.calendar, color: isDisabled ? .sqNeutral(40) : .sqNeutral(100))
}
.padding(16)
.foregroundStyle(Color.sqNeutral())
.background(isDisabled ? Color.sqNeutral(10) : .clear)
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(error?.wrappedValue.isInError ?? false ? .sqSemanticRed : isDisabled ? Color.sqNeutral(20) : Color.sqNeutral(30), lineWidth: 1)
)
HStack(spacing: 16) {
if let error = error?.wrappedValue, error.isInError {
HStack(spacing: 4) {
SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed)
SQText(error.message)
.sqSize(12)
.sqFont(.semiBold)
.sqColor(.sqSemanticRed)
}
}
}
}
}
.contentShape(.rect)
.onTapGesture {
if isDisabled == false {
showDatePicker.toggle()
}
}
.sheet(isPresented: $showDatePicker) {
BottomSheetDatePicker(label: label, selectedDate: $selectedDate, dateRange: dateRange)
}
}
private func formattedDate(_ date: Date?) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .short
guard let dateNotNil = date else { return "" }
return formatter.string(from: dateNotNil)
}
}
struct BottomSheetDatePicker: View {
var label: String
@Binding var selectedDate: Date?
var dateRange: ClosedRange<Date>? = nil
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
SQText(label)
.sqFont(.semiBold)
.padding()
DatePicker(
"",
selection: Binding(
get: { selectedDate ?? Date() },
set: { selectedDate = $0 }
),
in: dateRange ?? Date.distantPast ... Date.distantFuture,
displayedComponents: .date
)
.datePickerStyle(GraphicalDatePickerStyle())
.labelsHidden()
Spacer()
SQButton("Valider") {
if selectedDate == nil {
if let range = dateRange {
// Utiliser la borne inférieur (hier) qui est la date la plus récente autorisée
selectedDate = range.lowerBound
} else {
selectedDate = Date()
}
}
presentationMode.wrappedValue.dismiss()
}
}
.padding()
.presentationDetents([.medium])
}
}
#Preview {
SQDatePickerView("Date de naissance", placeholder: "01/01/1970", selectedDate: .constant(Date()))
}

View File

@@ -21,7 +21,8 @@ struct SQImagePicker: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("Images", font: .demiBold) SQText("Images")
.sqFont(.semiBold)
LazyVGrid(columns: columns, alignment: .leading) { LazyVGrid(columns: columns, alignment: .leading) {
ForEach(0 ..< nbOfImages) { _ in ForEach(0 ..< nbOfImages) { _ in
@@ -98,7 +99,9 @@ struct SQImagePickerSelectorSheet: View {
SQIcon(.xmark, size: .l) SQIcon(.xmark, size: .l)
} }
SQText("Sélectionner une photo", size: 19, font: .bold) SQText("Sélectionner une photo")
.sqSize(19)
.sqFont(.bold)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
Spacer() Spacer()
} }

View File

@@ -19,7 +19,9 @@ struct SQNavigationBar: ViewModifier {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
SQText(title, size: 18, font: .bold) SQText(title)
.sqSize(18)
.sqFont(.bold)
.foregroundColor(style.foregroundColor) .foregroundColor(style.foregroundColor)
} }
if backAction != nil { if backAction != nil {

View File

@@ -59,14 +59,14 @@ struct MenuPicker: UIViewRepresentable {
let options: [PickerOption] let options: [PickerOption]
@Binding var isPresenting: Bool @Binding var isPresenting: Bool
func makeUIView(context: Context) -> UIButton { func makeUIView(context _: Context) -> UIButton {
let button = UIButton(type: .custom) let button = UIButton(type: .custom)
button.setTitle("", for: .normal) button.setTitle("", for: .normal)
updateMenu(button) updateMenu(button)
return button return button
} }
func updateUIView(_ uiView: UIButton, context: Context) { func updateUIView(_ uiView: UIButton, context _: Context) {
updateMenu(uiView) updateMenu(uiView)
if isPresenting { if isPresenting {
uiView.sendActions(for: .menuActionTriggered) uiView.sendActions(for: .menuActionTriggered)
@@ -106,7 +106,7 @@ struct SQPickerPreview: View {
let options = [ let options = [
PickerOption(text: "1 mois"), PickerOption(text: "1 mois"),
PickerOption(text: "2 mois"), PickerOption(text: "2 mois"),
PickerOption(text: "3 mois") PickerOption(text: "3 mois"),
] ]
_selectedDuration = State(initialValue: options[0]) _selectedDuration = State(initialValue: options[0])
durationOptions = options durationOptions = options
@@ -119,8 +119,5 @@ struct SQPickerPreview: View {
} }
#Preview { #Preview {
ZStack { SQPickerPreview()
Color.black
SQPickerPreview()
}
} }

View File

@@ -10,19 +10,62 @@ import SwiftUI
struct SQProgressIndicator: View { struct SQProgressIndicator: View {
@Binding var isActivated: Bool @Binding var isActivated: Bool
@State private var colorScheme: SQProgressIndicatorColorScheme = .neutral
init(_ isActivated: Binding<Bool>) { init(_ isActivated: Binding<Bool>) {
self._isActivated = isActivated self._isActivated = isActivated
} }
var body: some View { var body: some View {
Rectangle() Rectangle()
.foregroundColor(.clear) .foregroundColor(.clear)
.frame(maxWidth: .infinity, minHeight: 4, maxHeight: 4) .frame(maxWidth: .infinity, minHeight: 4, maxHeight: 4)
.background(isActivated ? Color.sqSemanticGreen : Color.sqNeutral(20)) .background(isActivated ? colorScheme.baseColor : Color.sqNeutral(20))
.cornerRadius(8) .cornerRadius(8)
}
}
// MARK: - Modifiers
extension SQProgressIndicator {
func colorScheme(_ scheme: SQProgressIndicatorColorScheme) -> SQProgressIndicator {
var copy = self
copy._colorScheme = State(initialValue: scheme)
return copy
}
}
enum SQProgressIndicatorColorScheme: String, CaseIterable {
case neutral
case green
var baseColor: Color {
switch self {
case .neutral:
return .sqNeutral(80)
case .green:
return .sqSemanticGreen
}
} }
} }
#Preview { #Preview {
SQProgressIndicator(.constant(true)) VStack {
HStack {
SQProgressIndicator(.constant(true))
SQProgressIndicator(.constant(false))
SQProgressIndicator(.constant(false))
}
HStack {
SQProgressIndicator(.constant(true))
.colorScheme(.green)
SQProgressIndicator(.constant(true))
.colorScheme(.green)
SQProgressIndicator(.constant(false))
.colorScheme(.green)
SQProgressIndicator(.constant(false))
.colorScheme(.green)
}
}
.padding()
} }

View File

@@ -45,7 +45,9 @@ struct SQRadio: View {
var body: some View { var body: some View {
VStack(alignment: orientation == .vertical ? .leading : .center, spacing: 16) { VStack(alignment: orientation == .vertical ? .leading : .center, spacing: 16) {
if let title = title { if let title = title {
SQText(title, size: titleSize, font: .bold) SQText(title)
.sqSize(titleSize)
.sqFont(.bold)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
Group { Group {
@@ -61,7 +63,10 @@ struct SQRadio: View {
} }
if error.wrappedValue != .none { if error.wrappedValue != .none {
SQText(error.wrappedValue.message, size: 12, font: .demiBold, textColor: Color.sqSemanticRed) SQText(error.wrappedValue.message)
.sqSize(12)
.sqFont(.semiBold)
.sqColor(.sqSemanticRed)
} }
} }
} }
@@ -137,7 +142,8 @@ struct SQRadio: View {
} }
if let desc = desc { if let desc = desc {
SQText(desc, size: 12) SQText(desc)
.sqSize(12)
} }
} }
} }

View File

@@ -1,37 +0,0 @@
//
// SQTabBar.swift
//
//
// Created by Victor on 18/07/2024.
//
import SwiftUI
struct SQTabBar: View {
@Binding var selection: Int
var body: some View {
HStack {
TabBarButton(imageName: "house", text: "Accueil")
}
}
}
private struct TabBarButton: View {
let imageName: String
let text: String
var body: some View {
VStack {
Image(systemName: imageName)
.renderingMode(.template)
.tint(.black)
.fontWeight(.bold)
Text(text)
}
}
}
#Preview {
SQTabBar(selection: .constant(1))
}

View File

@@ -1,61 +0,0 @@
//
// SQText.swift
//
//
// Created by Victor on 12/06/2024.
//
import SwiftUI
enum SQTextFont: String {
case medium = "TTChocolates-Medium"
case mediumItalic = "TTChocolates-MediumIt"
case demiBold = "TTChocolates-DemiBold"
case bold = "TTChocolates-Bold"
case boldItalic = "TTChocolates-Bold-Italic"
}
struct SQText: View {
var text: String
var attributedText: AttributedString?
var size: CGFloat
var font: SQTextFont
var textColor: Color
init(_ text: String, size: CGFloat = 16, font: SQTextFont = .medium, textColor: Color = .sqNeutral(90)) {
self.text = text
self.attributedText = nil
self.size = size
self.font = font
self.textColor = textColor
}
init(_ attributedText: AttributedString, size: CGFloat = 16, textColor: Color = .sqNeutral(90)) {
self.text = ""
self.attributedText = attributedText
self.size = size
self.font = .medium
self.textColor = textColor
}
var body: some View {
if let attributedText = attributedText {
Text(attributedText)
.foregroundStyle(textColor)
} else {
Text(text)
.font(.custom(font.rawValue, size: size))
.foregroundStyle(textColor)
}
}
}
#Preview {
VStack(spacing: 10) {
SQText("Hello world!", font: .medium)
SQText("Hello world!", font: .mediumItalic)
SQText("Hello world!", size: 18, font: .demiBold)
SQText("Hello world!", font: .bold)
SQText("Hello world!", size: 18, font: .boldItalic)
}
}

View File

@@ -0,0 +1,88 @@
//
// SQText.swift
// Allovoisins
//
// Created by Victor on 13/06/2024.
// Copyright © 2024 AlloVoisins. All rights reserved.
//
import SwiftUI
// MARK: - SQTextFont
enum SQTextFont: String {
case medium = "TTChocolates-Medium"
case mediumItalic = "TTChocolates-MediumIt"
case semiBold = "TTChocolates-DemiBold"
case bold = "TTChocolates-Bold"
case boldItalic = "TTChocolates-Bold-Italic"
}
// MARK: - SQText
/// Text component with custom Sequoia typography.
///
/// Use View Modifiers to configure the text:
/// ```swift
/// SQText("Hello world!")
/// .sqSize(18)
/// .sqFont(.bold)
/// .sqColor(.sqNeutral(100))
/// ```
struct SQText: View {
private let text: String
private let attributedText: AttributedString?
@Environment(\.sqTextConfiguration) private var config
/// Creates a text view with plain string.
///
/// - Parameter text: The string to display
init(_ text: String) {
self.text = text
self.attributedText = nil
}
/// Creates a text view with attributed string.
///
/// - Parameter attributedText: The attributed string to display
init(_ attributedText: AttributedString) {
self.text = ""
self.attributedText = attributedText
}
var body: some View {
if let attributedText = attributedText {
Text(attributedText)
.font(.custom(config.font.rawValue, fixedSize: config.size))
.foregroundStyle(config.textColor)
} else {
Text(text)
.font(.custom(config.font.rawValue, fixedSize: config.size))
.foregroundStyle(config.textColor)
}
}
}
// MARK: - Preview
#Preview {
VStack(spacing: 10) {
SQText("Hello world!")
.sqFont(.medium)
SQText("Hello world!")
.sqFont(.mediumItalic)
SQText("Hello world!")
.sqSize(18)
.sqFont(.semiBold)
SQText("Hello world!")
.sqFont(.bold)
SQText("Hello world!")
.sqSize(18)
.sqFont(.boldItalic)
}
}

View File

@@ -0,0 +1,30 @@
//
// SQTextConfiguration.swift
// Allovoisins
//
// Created by Claude on 22/10/2025.
// Copyright © 2025 AlloVoisins. All rights reserved.
//
import SwiftUI
/// Configuration structure for Sequoia Text components.
/// Contains all customizable options that can be set via View Modifiers.
struct SQTextConfiguration {
var size: CGFloat = 16
var font: SQTextFont = .medium
var textColor: Color = .sqNeutral(90)
}
// MARK: - Environment Key
private struct SQTextConfigurationKey: EnvironmentKey {
static let defaultValue = SQTextConfiguration()
}
extension EnvironmentValues {
var sqTextConfiguration: SQTextConfiguration {
get { self[SQTextConfigurationKey.self] }
set { self[SQTextConfigurationKey.self] = newValue }
}
}

View File

@@ -0,0 +1,78 @@
//
// SQTextModifiers.swift
// Allovoisins
//
// Created by Claude on 22/10/2025.
// Copyright © 2025 AlloVoisins. All rights reserved.
//
import SwiftUI
// MARK: - Font Modifier
private struct SQFontModifier: ViewModifier {
let font: SQTextFont
@Environment(\.sqTextConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.font = font
return content.environment(\.sqTextConfiguration, newConfig)
}
}
extension View {
/// Sets the font weight for the text.
///
/// - Parameter font: The font weight to apply (.regular, .medium, .semiBold, .bold)
/// - Returns: A view with the font configuration applied
func sqFont(_ font: SQTextFont) -> some View {
modifier(SQFontModifier(font: font))
}
}
// MARK: - Size Modifier
private struct SQSizeModifier: ViewModifier {
let size: CGFloat
@Environment(\.sqTextConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.size = size
return content.environment(\.sqTextConfiguration, newConfig)
}
}
extension View {
/// Sets the font size for the text.
///
/// - Parameter size: The font size in points
/// - Returns: A view with the size configuration applied
func sqSize(_ size: CGFloat) -> some View {
modifier(SQSizeModifier(size: size))
}
}
// MARK: - Color Modifier
private struct SQColorModifier: ViewModifier {
let textColor: Color
@Environment(\.sqTextConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.textColor = textColor
return content.environment(\.sqTextConfiguration, newConfig)
}
}
extension View {
/// Sets the text color.
///
/// - Parameter textColor: The color to apply to the text
/// - Returns: A view with the color configuration applied
func sqColor(_ textColor: Color) -> some View {
modifier(SQColorModifier(textColor: textColor))
}
}

View File

@@ -10,7 +10,6 @@ import SwiftUI
struct SQTextEditor: View { struct SQTextEditor: View {
var label: String var label: String
var placeholder: String var placeholder: String
var type: SQTextFieldType = .text
var error: Binding<SQFormFieldError> var error: Binding<SQFormFieldError>
var icon: SQIcon? var icon: SQIcon?
var isDisabled: Bool = false var isDisabled: Bool = false
@@ -28,7 +27,6 @@ struct SQTextEditor: View {
init(_ label: String, init(_ label: String,
placeholder: String, placeholder: String,
type: SQTextFieldType = .text,
error: Binding<SQFormFieldError> = .constant(.none), error: Binding<SQFormFieldError> = .constant(.none),
text: Binding<String>, text: Binding<String>,
icon: SQIcon? = nil, icon: SQIcon? = nil,
@@ -42,7 +40,6 @@ struct SQTextEditor: View {
{ {
self.label = label self.label = label
self.placeholder = placeholder self.placeholder = placeholder
self.type = type
self.error = error self.error = error
self._text = text self._text = text
self.icon = icon self.icon = icon
@@ -61,7 +58,9 @@ struct SQTextEditor: View {
SQText(label) SQText(label)
Spacer() Spacer()
if isOptional { if isOptional {
SQText("Optionnel", size: 12, textColor: .sqNeutral(50)) SQText("Optionnel")
.sqSize(12)
.sqColor(.sqNeutral(50))
} }
} }
TextEditor(text: Binding( TextEditor(text: Binding(
@@ -87,12 +86,17 @@ struct SQTextEditor: View {
if error.wrappedValue.isInError { if error.wrappedValue.isInError {
HStack(spacing: 8) { HStack(spacing: 8) {
SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed) SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed)
SQText(error.wrappedValue.message, size: 12, font: .demiBold, textColor: .sqSemanticRed) SQText(error.wrappedValue.message)
.sqSize(12)
.sqFont(.semiBold)
.sqColor(.sqSemanticRed)
} }
} }
Spacer() Spacer()
if !characterCountText.isEmpty { if !characterCountText.isEmpty {
SQText(characterCountText, size: 12, textColor: .sqNeutral(50)) SQText(characterCountText)
.sqSize(12)
.sqColor(.sqNeutral(50))
} }
} }
} }

View File

@@ -1,222 +0,0 @@
//
// SQTextField.swift
// Sequoia
//
// Created by Victor on 10/10/2024.
//
import SwiftUI
enum SQTextFieldType {
case text
case phoneNumber
}
struct SQTextField: View {
var label: String
var placeholder: String
var type: SQTextFieldType = .text
var icon: SQIcon?
var isDisabled: Bool = false
var isOptional: Bool = false
var tooltipText: String?
var helperText: String?
var error: Binding<SQFormFieldError>
var minCharacters: Int?
var maxCharacters: Int?
@Binding var text: String
@FocusState private var isFocused: Bool
@State private var showTooltip = false
let infoAction: (() -> Void)?
let iconAction: (() -> Void)?
private var accentColor: Color = .sqNeutral(80)
init(_ label: String,
placeholder: String,
type: SQTextFieldType = .text,
text: Binding<String>,
icon: SQIcon? = nil,
isDisabled: Bool = false,
isOptional: Bool = false,
tooltipText: String? = nil,
helperText: String? = nil,
error: Binding<SQFormFieldError> = .constant(.none),
minCharacters: Int? = nil,
maxCharacters: Int? = nil,
infoAction: (() -> Void)? = nil,
iconAction: (() -> Void)? = nil)
{
self.label = label
self.placeholder = placeholder
self.type = type
self._text = text
self.icon = icon
self.isDisabled = isDisabled
self.isOptional = isOptional
self.tooltipText = tooltipText
self.helperText = helperText
self.error = error
self.minCharacters = minCharacters
self.maxCharacters = maxCharacters
self.infoAction = infoAction
self.iconAction = iconAction
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
SQText(label)
if let tooltipText = tooltipText {
Button {
withAnimation {
showTooltip.toggle()
}
} label: {
SQIcon(.circle_info, color: .sqNeutral(80))
}
.overlay(
Group {
if showTooltip {
SQTooltip(text: tooltipText, isVisible: $showTooltip)
.fixedSize()
}
}
)
} else if let infoAction = infoAction {
Button {
infoAction()
} label: {
SQIcon(.circle_info, color: .sqNeutral(80))
}
}
Spacer()
if isOptional {
SQText("Optionnel", size: 12, textColor: .sqNeutral(50))
}
}
// TextField
HStack(spacing: 4) {
if type == .phoneNumber {
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
}
.keyboardType(.numberPad)
.font(.sq(.medium))
.foregroundStyle(Color.sqNeutral(100))
.tint(accentColor)
} else {
TextField(placeholder, text: Binding(
get: { self.text },
set: { self.text = String($0.prefix(self.maxCharacters ?? Int.max)) }
))
.onChange(of: self.text, perform: { _ in
self.error.wrappedValue = .none
})
.font(.sq(.medium))
.foregroundStyle(Color.sqNeutral(100))
.tint(accentColor)
if let icon = icon {
Button {
iconAction?()
} label: {
icon
}
}
}
}
.padding(16)
.foregroundStyle(Color.sqNeutral())
.background(isDisabled ? Color.sqNeutral(10) : .clear)
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.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 error.wrappedValue.isInError {
HStack(spacing: 4) {
SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed)
SQText(error.wrappedValue.message, size: 12, textColor: .sqSemanticRed)
}
} else if helperText != nil {
SQText(helperText!, size: 12, textColor: .sqNeutral(50))
}
Spacer()
if !characterCountText.isEmpty {
SQText(characterCountText, size: 12, textColor: .sqNeutral(50))
}
}
}
.popover(isPresented: $showTooltip, content: {
SQText(tooltipText ?? "")
})
// .overlay(
// Group {
// if showTooltip {
// Color.black.opacity(0.001)
// .onTapGesture {
// withAnimation {
// showTooltip = false
// }
// }
// }
// }
// )
}
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 ""
}
private func formatPhoneNumber(_ number: String) -> String {
let numbers = number.filter { $0.isNumber }
var result = ""
for (index, char) in numbers.enumerated() {
if index > 0 && index % 2 == 0 {
result += " "
}
result += String(char)
}
return result
}
private func isValidPhoneNumber(_ number: String) -> Bool {
let numbers = number.filter { $0.isNumber }
return numbers.count == 10 && numbers.first == "0"
}
}
#Preview {
VStack(spacing: 32) {
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", text: .constant(""), error: .constant(.empty))
SQTextField("Téléphone", placeholder: "01 23 45 67 89", type: .phoneNumber, text: .constant(""))
SQTextField(
"Numéro de RCS",
placeholder: "RCS VILLE B 123456789",
text: .constant(""),
isOptional: true,
tooltipText: "Vous pouvez trouver votre numéro RCS sur l'extrait Kbis de votre entreprise ou votre extrait K, si vous êtes un entrepreneur individuel.\n\nLe numéro RCS se compose tout d'abord de la mention RCS, puis du numéro d'immatriculation de la ville dans laquelle a été créée la société, d'une lettre et enfin du numéro SIREN à 9 chiffres."
)
}
.padding()
}

View File

@@ -0,0 +1,112 @@
//
// SQPasswordField.swift
// Allovoisins
//
// Created by Claude on 22/10/2025.
// Copyright © 2025 AlloVoisins. All rights reserved.
//
import SwiftUI
// MARK: - SQPasswordField
/// Specialized text field for password input with visibility toggle.
///
/// Use View Modifiers to configure the password field:
/// ```swift
/// SQPasswordField("Mot de passe", text: $password)
/// .sqPlaceholder("Entrez votre mot de passe")
/// .sqMinCharacters(8)
/// .sqHelperText("Minimum 8 caractères")
/// .sqError($passwordError)
/// ```
struct SQPasswordField: View {
private let label: String
@Binding var text: String
@FocusState private var isFocused: Bool
@State private var isPasswordVisible = false
@Environment(\.sqTextFieldConfiguration) private var config
private var accentColor: Color { .sqNeutral(80) }
/// Creates a password field with visibility toggle.
///
/// - Parameters:
/// - label: The label text displayed above the field
/// - text: Binding to the password text value
init(_ label: String, text: Binding<String>) {
self.label = label
self._text = text
}
var body: some View {
SQTextFieldBase(label, isFocused: Binding(get: { isFocused }, set: { isFocused = $0 }), textCount: text.count) {
HStack(spacing: 4) {
Group {
if isPasswordVisible {
TextField("", text: $text)
} else {
SecureField("", text: $text)
}
}
.placeholder(when: text.isEmpty) {
SQText(config.placeholder)
.sqColor(.sqNeutral(50))
}
.onChange(of: text) { newValue in
// Character limit
if let max = config.maxCharacters, newValue.count > max {
text = String(newValue.prefix(max))
}
// Reset error
config.error.wrappedValue = .none
}
.keyboardType(config.keyboardType)
.foregroundStyle(Color.sqNeutral(100))
.tint(accentColor)
.submitLabel(.done)
.focused($isFocused)
.accessibilityLabel(label)
.accessibilityValue(text.isEmpty ? "Vide" : "Rempli")
Button {
isPasswordVisible.toggle()
} label: {
SQIcon(isPasswordVisible ? .eye_slash : .eye, color: .sqNeutral(80))
}
.accessibilityLabel(isPasswordVisible ? "Masquer le mot de passe" : "Afficher le mot de passe")
}
}
}
}
// MARK: - Preview
#Preview {
VStack(spacing: 32) {
SQPasswordField("Nouveau mot de passe", text: .constant(""))
.sqPlaceholder("Mot de passe")
SQPasswordField("Mot de passe", text: .constant("MonMotDePasse123!"))
.sqPlaceholder("Mot de passe")
SQPasswordField("Mot de passe", text: .constant(""))
.sqPlaceholder("Mot de passe")
.sqOptional()
SQPasswordField("Mot de passe", text: .constant(""))
.sqPlaceholder("Mot de passe")
.disabled(true)
SQPasswordField("Nouveau mot de passe", text: .constant(""))
.sqPlaceholder("Mot de passe")
.sqMinCharacters(8)
.sqHelperText("Minimum 8 caractères")
SQPasswordField("Confirmer le mot de passe", text: .constant(""))
.sqPlaceholder("Mot de passe")
.sqTooltipText("Le mot de passe doit contenir au moins 8 caractères, une majuscule, une minuscule et un chiffre.")
}
.padding()
}

View File

@@ -0,0 +1,106 @@
//
// SQPhoneField.swift
// Allovoisins
//
// Created by Claude on 22/10/2025.
// Copyright © 2025 AlloVoisins. All rights reserved.
//
import SwiftUI
// MARK: - SQPhoneField
/// Specialized text field for phone number input with automatic formatting.
///
/// Use View Modifiers to configure the phone field:
/// ```swift
/// SQPhoneField("Téléphone", text: $phone)
/// .sqPlaceholder("01 23 45 67 89")
/// .sqHelperText("Format : 01 23 45 67 89")
/// .sqError($phoneError)
/// ```
struct SQPhoneField: View {
private let label: String
@Binding var text: String
@FocusState private var isFocused: Bool
@Environment(\.sqTextFieldConfiguration) private var config
private var accentColor: Color { .sqNeutral(80) }
/// Creates a phone number field with automatic formatting.
///
/// - Parameters:
/// - label: The label text displayed above the field
/// - text: Binding to the phone number text value
init(_ label: String, text: Binding<String>) {
self.label = label
self._text = text
}
var body: some View {
SQTextFieldBase(label, isFocused: Binding(get: { isFocused }, set: { isFocused = $0 }), textCount: text.count) {
TextField("", text: $text)
.placeholder(when: text.isEmpty) {
SQText(config.placeholder)
.sqColor(.sqNeutral(50))
}
.onChange(of: text) { newValue in
// Format and limit phone number
let formatted = formatPhoneNumber(String(newValue.prefix(15)))
if formatted != newValue {
text = formatted
}
// Reset error
config.error.wrappedValue = .none
}
.keyboardType(.numberPad)
.foregroundStyle(Color.sqNeutral(100))
.tint(accentColor)
.focused($isFocused)
.accessibilityLabel(label)
.accessibilityValue(text.isEmpty ? "Vide" : text)
}
}
// MARK: - Helper Methods
private func formatPhoneNumber(_ number: String) -> String {
let numbers = number.filter { $0.isNumber }
var result = ""
for (index, char) in numbers.enumerated() {
if index > 0 && index % 2 == 0 {
result += " "
}
result += String(char)
}
return result
}
}
// MARK: - Preview
#Preview {
VStack(spacing: 32) {
SQPhoneField("Téléphone", text: .constant(""))
.sqPlaceholder("01 23 45 67 89")
SQPhoneField("Téléphone", text: .constant("06 12 34 56 78"))
.sqPlaceholder("01 23 45 67 89")
SQPhoneField("Téléphone", text: .constant(""))
.sqPlaceholder("01 23 45 67 89")
.sqOptional()
SQPhoneField("Téléphone", text: .constant(""))
.sqPlaceholder("01 23 45 67 89")
.disabled(true)
SQPhoneField("Téléphone portable", text: .constant(""))
.sqPlaceholder("01 23 45 67 89")
.sqHelperText("Format : 01 23 45 67 89")
}
.padding()
}

View File

@@ -0,0 +1,138 @@
//
// SQTextField.swift
// Sequoia
//
// Created by Victor on 10/10/2024.
//
import Lottie
import SwiftUI
// MARK: - SQTextField
/// Standard text field component with optional icon.
///
/// Use View Modifiers to configure the text field:
/// ```swift
/// SQTextField("Name", text: $name)
/// .sqPlaceholder("Enter your name")
/// .sqMinCharacters(2)
/// .sqMaxCharacters(50)
/// .sqError($nameError)
/// ```
struct SQTextField: View {
private let label: String
@Binding var text: String
private let icon: SQIcon?
private let iconAction: (() -> Void)?
@FocusState private var isFocused: Bool
@Environment(\.sqTextFieldConfiguration) private var config
private var accentColor: Color { .sqNeutral(80) }
/// Creates a standard text field.
///
/// - Parameters:
/// - label: The label text displayed above the field
/// - text: Binding to the text value
init(_ label: String, text: Binding<String>, icon: SQIcon? = nil, iconAction: (() -> Void)? = nil) {
self.label = label
self._text = text
self.icon = icon
self.iconAction = iconAction
}
var body: some View {
SQTextFieldBase(label, isFocused: Binding(get: { isFocused }, set: { isFocused = $0 }), textCount: text.count) {
HStack(spacing: 4) {
TextField("", text: $text)
.placeholder(when: text.isEmpty) {
SQText(config.placeholder)
.sqColor(.sqNeutral(50))
}
.onChange(of: text) { newValue in
// Character limit
if let max = config.maxCharacters, newValue.count > max {
text = String(newValue.prefix(max))
}
// Reset error
config.error.wrappedValue = .none
}
.keyboardType(config.keyboardType)
.foregroundStyle(Color.sqNeutral(100))
.tint(accentColor)
.submitLabel(.done)
.focused($isFocused)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if config.keyboardType == .numberPad || config.keyboardType == .decimalPad || config.keyboardType == .phonePad {
Spacer()
Button("Done") {
isFocused = false
}
}
}
}
.accessibilityLabel(label)
.accessibilityValue(text.isEmpty ? "Vide" : text)
if config.isLoading.wrappedValue {
LottieView(animation: .named("av_loader"))
.looping()
.frame(width: 20, height: 20)
} else if let icon = icon {
Button(action: iconAction ?? {}) {
icon
}
}
}
}
}
}
// MARK: - Icon Modifier
extension SQTextField {
/// Adds an icon button on the right side of the text field.
///
/// - Parameters:
/// - icon: The icon to display
/// - action: Action to perform when the icon is tapped
/// - Returns: A text field with the icon
func sqIcon(_ icon: SQIcon, action: @escaping () -> Void = {}) -> SQTextField {
SQTextField(label, text: $text, icon: icon, iconAction: action)
}
}
// MARK: - Preview
#Preview {
VStack(spacing: 32) {
SQTextField("Label name", text: .constant("Bonjour"))
.sqPlaceholder("Placeholder")
.sqMinCharacters(20)
SQTextField("Label name", text: .constant(""))
.sqPlaceholder("Placeholder")
.sqOptional()
SQTextField("Label name", text: .constant(""))
.sqPlaceholder("Placeholder")
.disabled(true)
SQTextField("Label name", text: .constant(""))
.sqPlaceholder("Placeholder")
// Note: .sqIcon() must be called BEFORE other modifiers to work correctly
SQTextField("Label with icon", text: .constant(""))
.sqIcon(SQIcon(.magnifying_glass, color: .sqNeutral(80)))
.sqPlaceholder("Placeholder")
SQTextField("Numéro de RCS", text: .constant(""))
.sqPlaceholder("RCS VILLE B 123456789")
.sqOptional()
.sqTooltipText("Vous pouvez trouver votre numéro RCS sur l'extrait Kbis de votre entreprise ou votre extrait K, si vous êtes un entrepreneur individuel.\n\nLe numéro RCS se compose tout d'abord de la mention RCS, puis du numéro d'immatriculation de la ville dans laquelle a été créée la société, d'une lettre et enfin du numéro SIREN à 9 chiffres.")
}
.padding()
}

View File

@@ -0,0 +1,170 @@
//
// SQTextFieldBase.swift
// Allovoisins
//
// Created by Claude on 22/10/2025.
// Copyright © 2025 AlloVoisins. All rights reserved.
//
import SwiftUI
// MARK: - SQTextFieldBase
/// Base component for all Sequoia text field variants.
/// Provides common functionality: label, error display, character count, tooltip, styling, etc.
///
/// This component uses `@Environment` to read configuration set via View Modifiers,
/// following the same pattern as native SwiftUI components.
struct SQTextFieldBase<Content: View>: View {
private let label: String
@Binding var isFocused: Bool
var textCount: Int
let content: Content
@Environment(\.sqTextFieldConfiguration) private var config
@State private var showTooltip = false
private var accentColor: Color { .sqNeutral(80) }
init(
_ label: String,
isFocused: Binding<Bool>,
textCount: Int,
@ViewBuilder content: () -> Content
) {
self.label = label
self._isFocused = isFocused
self.textCount = textCount
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
// Label with tooltip/info
HStack(spacing: 8) {
SQText(label)
.sqFont(.semiBold)
.sqColor(.sqNeutral(80))
if let tooltipText = config.tooltipText {
Button {
withAnimation {
showTooltip.toggle()
}
} label: {
SQIcon(.circle_info, color: .sqNeutral(80))
}
.overlay(
Group {
if showTooltip {
SQTooltip(text: tooltipText, isVisible: $showTooltip)
.fixedSize()
}
}
)
.accessibilityLabel("Information")
.accessibilityHint("Appuyez deux fois pour afficher plus d'informations")
} else if let infoAction = config.infoAction {
Button {
infoAction()
} label: {
SQIcon(.circle_info, color: .sqNeutral(80))
}
.accessibilityLabel("Information")
.accessibilityHint("Appuyez deux fois pour afficher plus d'informations")
}
Spacer()
if config.isOptional {
SQText("Optionnel")
.sqSize(12)
.sqColor(.sqNeutral(50))
}
}
// Content (TextField/SecureField/custom)
content
.padding(16)
.foregroundStyle(Color.sqNeutral())
.background {
RoundedRectangle(cornerRadius: 8)
.fill(config.isDisabled ? Color.sqNeutral(10) : config.backgroundColor)
}
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(
config.error.wrappedValue.isInError ? .sqSemanticRed :
isFocused ? accentColor :
config.isDisabled ? Color.sqNeutral(20) : Color.sqNeutral(30),
lineWidth: 1
)
)
// Error / Helper / Character count
HStack(spacing: 16) {
if config.error.wrappedValue.isInError {
HStack(spacing: 4) {
SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed)
SQText(config.error.wrappedValue.message)
.sqSize(12)
.sqColor(.sqSemanticRed)
}
} else if let helperText = config.helperText {
SQText(helperText)
.sqSize(12)
.sqColor(.sqNeutral(50))
}
Spacer()
if !characterCountText.isEmpty && config.showCharacterCount {
SQText(characterCountText)
.sqSize(12)
.sqColor(.sqNeutral(50))
}
}
}
.disabled(config.isDisabled)
.overlay(
Group {
if showTooltip {
Color.black.opacity(0.001)
.onTapGesture {
withAnimation {
showTooltip = false
}
}
}
}
)
}
// MARK: - Helper Properties
private var characterCountText: String {
let count = textCount
if let min = config.minCharacters, count < min {
return "\(count)/\(min) min."
} else if let max = config.maxCharacters {
return count >= (max * 4 / 5) ? "\(count)/\(max) max." : "\(count)"
}
return ""
}
}
// MARK: - Placeholder Extension
extension View {
func placeholder<Content: View>(
when shouldShow: Bool,
alignment: Alignment = .leading,
@ViewBuilder placeholder: () -> Content
) -> some View {
ZStack(alignment: alignment) {
placeholder().opacity(shouldShow ? 1 : 0)
self
}
}
}

View File

@@ -0,0 +1,40 @@
//
// SQTextFieldConfiguration.swift
// Allovoisins
//
// Created by Claude on 22/10/2025.
// Copyright © 2025 AlloVoisins. All rights reserved.
//
import SwiftUI
/// Configuration structure for Sequoia TextField components.
/// Contains all customizable options that can be set via View Modifiers.
struct SQTextFieldConfiguration {
var placeholder: String = ""
var isDisabled: Bool = false
var isOptional: Bool = false
var isLoading: Binding<Bool> = .constant(false)
var tooltipText: String? = nil
var helperText: String? = nil
var error: Binding<SQFormFieldError> = .constant(.none)
var minCharacters: Int? = nil
var maxCharacters: Int? = nil
var showCharacterCount: Bool = true
var backgroundColor: Color = .clear
var keyboardType: UIKeyboardType = .default
var infoAction: (() -> Void)? = nil
}
// MARK: - Environment Key
private struct SQTextFieldConfigurationKey: EnvironmentKey {
static let defaultValue = SQTextFieldConfiguration()
}
extension EnvironmentValues {
var sqTextFieldConfiguration: SQTextFieldConfiguration {
get { self[SQTextFieldConfigurationKey.self] }
set { self[SQTextFieldConfigurationKey.self] = newValue }
}
}

View File

@@ -0,0 +1,242 @@
//
// SQTextFieldModifiers.swift
// Allovoisins
//
// Created by Claude on 22/10/2025.
// Copyright © 2025 AlloVoisins. All rights reserved.
//
import SwiftUI
// MARK: - View Modifiers
/// View modifiers for Sequoia TextField components.
/// These modifiers allow chainable configuration, similar to native SwiftUI components.
///
/// Example usage:
/// ```swift
/// SQTextField("Name", text: $name)
/// .sqPlaceholder("Enter your name")
/// .sqMinCharacters(2)
/// .sqMaxCharacters(50)
/// .sqError($nameError)
/// ```
// MARK: - Individual Modifier Structs
private struct SQPlaceholderModifier: ViewModifier {
let placeholder: String
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.placeholder = placeholder
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQMinCharactersModifier: ViewModifier {
let count: Int
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.minCharacters = count
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQMaxCharactersModifier: ViewModifier {
let count: Int
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.maxCharacters = count
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQHelperTextModifier: ViewModifier {
let text: String
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.helperText = text
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQTooltipTextModifier: ViewModifier {
let text: String
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.tooltipText = text
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQErrorModifier: ViewModifier {
let error: Binding<SQFormFieldError>
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.error = error
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQOptionalModifier: ViewModifier {
let isOptional: Bool
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.isOptional = isOptional
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQShowCharacterCountModifier: ViewModifier {
let show: Bool
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.showCharacterCount = show
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQBackgroundColorModifier: ViewModifier {
let color: Color
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.backgroundColor = color
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQKeyboardTypeModifier: ViewModifier {
let type: UIKeyboardType
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.keyboardType = type
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQInfoActionModifier: ViewModifier {
let action: () -> Void
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.infoAction = action
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQDisabledModifier: ViewModifier {
let isDisabled: Bool
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.isDisabled = isDisabled
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
private struct SQIsLoadingModifier: ViewModifier {
let isLoading: Binding<Bool>
@Environment(\.sqTextFieldConfiguration) var config
func body(content: Content) -> some View {
var newConfig = config
newConfig.isLoading = isLoading
return content.environment(\.sqTextFieldConfiguration, newConfig)
}
}
// MARK: - Public Extension
extension View {
/// Sets the placeholder text for the text field.
func sqPlaceholder(_ text: String) -> some View {
modifier(SQPlaceholderModifier(placeholder: text))
}
/// Sets the minimum number of characters required.
func sqMinCharacters(_ count: Int) -> some View {
modifier(SQMinCharactersModifier(count: count))
}
/// Sets the maximum number of characters allowed.
func sqMaxCharacters(_ count: Int) -> some View {
modifier(SQMaxCharactersModifier(count: count))
}
/// Sets helper text to display below the text field.
func sqHelperText(_ text: String) -> some View {
modifier(SQHelperTextModifier(text: text))
}
/// Sets tooltip text with an info icon.
func sqTooltipText(_ text: String) -> some View {
modifier(SQTooltipTextModifier(text: text))
}
/// Binds an error to the text field for validation display.
func sqError(_ error: Binding<SQFormFieldError>) -> some View {
modifier(SQErrorModifier(error: error))
}
/// Marks the field as optional with an "Optionnel" badge.
func sqOptional(_ isOptional: Bool = true) -> some View {
modifier(SQOptionalModifier(isOptional: isOptional))
}
/// Marks the field as optional with an "Optionnel" badge (alias for sqOptional).
func sqIsOptional(_ isOptional: Bool = true) -> some View {
modifier(SQOptionalModifier(isOptional: isOptional))
}
/// Controls the visibility of the character counter.
func sqShowCharacterCount(_ show: Bool = true) -> some View {
modifier(SQShowCharacterCountModifier(show: show))
}
/// Sets the background color of the text field.
func sqBackgroundColor(_ color: Color) -> some View {
modifier(SQBackgroundColorModifier(color: color))
}
/// Sets the keyboard type for the text field.
func sqKeyboardType(_ type: UIKeyboardType) -> some View {
modifier(SQKeyboardTypeModifier(type: type))
}
/// Sets an action to execute when the info icon is tapped.
func sqInfoAction(_ action: @escaping () -> Void) -> some View {
modifier(SQInfoActionModifier(action: action))
}
/// Disables the text field.
func sqDisabled(_ isDisabled: Bool = true) -> some View {
modifier(SQDisabledModifier(isDisabled: isDisabled))
}
/// Shows a loading indicator in the text field.
func sqIsLoading(_ isLoading: Binding<Bool>) -> some View {
modifier(SQIsLoadingModifier(isLoading: isLoading))
}
}

View File

@@ -38,9 +38,12 @@ struct SQToast: View {
HStack(spacing: 16) { HStack(spacing: 16) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if !title.isEmpty { if !title.isEmpty {
SQText(title, font: .bold) SQText(title)
.sqFont(.bold)
} }
SQText(content, size: 14, font: .demiBold) SQText(content)
.sqSize(14)
.sqFont(.semiBold)
} }
Spacer() Spacer()
if hasClose { if hasClose {

View File

@@ -21,7 +21,8 @@ struct SQTooltip: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
SQText(text, textColor: .white) SQText(text)
.sqColor(.white)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.padding(Constants.padding) .padding(Constants.padding)
.background( .background(

View File

@@ -16,7 +16,8 @@ struct SQSearchBar: View {
SQIcon(.magnifying_glass, type: .solid) SQIcon(.magnifying_glass, type: .solid)
TextField("", text: $text) TextField("", text: $text)
.placeholder(when: text.isEmpty) { .placeholder(when: text.isEmpty) {
SQText(placeholder, textColor: .sqNeutral(50)) SQText(placeholder)
.sqColor(.sqNeutral(50))
} }
.tint(Color.sqNeutral(100)) .tint(Color.sqNeutral(100))
} }
@@ -29,16 +30,3 @@ struct SQSearchBar: View {
#Preview { #Preview {
SQSearchBar(text: .constant(""), placeholder: "Rechercher") SQSearchBar(text: .constant(""), placeholder: "Rechercher")
} }
extension View {
func placeholder<Content: View>(
when shouldShow: Bool,
alignment: Alignment = .leading,
@ViewBuilder placeholder: () -> Content
) -> some View {
ZStack(alignment: alignment) {
placeholder().opacity(shouldShow ? 1 : 0)
self
}
}
}

View File

@@ -14,8 +14,8 @@ struct SQSearchBarButton: View {
var body: some View { var body: some View {
HStack(spacing: 16) { HStack(spacing: 16) {
SQIcon(.magnifying_glass, type: .solid) SQIcon(.magnifying_glass, type: .solid)
SQText(placeholder)
SQText(placeholder, textColor: .sqNeutral(50)) .sqColor(.sqNeutral(50))
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding() .padding()

View File

@@ -0,0 +1,39 @@
//
// ChangeLoginView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 23/10/2025.
//
import SwiftUI
enum ChangeLoginType {
case phone
case email
}
struct ChangeLoginView: View {
let type: ChangeLoginType = .phone
@State var phone: String = ""
@State var email: String = ""
var body: some View {
VStack(spacing: 32) {
switch type {
case .email:
SQTextField("Nouvelle adresse e-mail", text: $email)
.sqPlaceholder("Ex : pierredupont@domaine.com")
case .phone:
SQPhoneField("Nouveau numéro de téléphone", text: $phone)
.sqPlaceholder("Ex : 0600000009")
}
SQButton("Confirmer") {}
Spacer()
}
.padding()
}
}
#Preview {
ChangeLoginView()
}

View File

@@ -0,0 +1,53 @@
//
// PasswordRecommendations.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 22/10/2025.
//
import SwiftUI
struct PasswordRecommendations: View {
var body: some View {
VStack(alignment: .leading, spacing: 2) {
ForEach(PasswordRequirements.allCases, id: \.self) { requirement in
if requirement == .minChar {
requirementView(for: requirement)
SQText("Au moins 3 des 4 catégories suivantes :")
.padding(.top, 8)
.sqSize(12)
} else {
requirementView(for: requirement)
}
}
}
.padding(8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 0.95, green: 0.97, blue: 1))
.cornerRadius(8)
}
func requirementView(for requirement: PasswordRequirements, isValid: Bool = false) -> some View {
HStack(spacing: 4) {
SQIcon(.check, size: .xs, color: isValid ? .sqSemanticPositive : .sqNeutral(50))
SQText(requirement.rawValue)
.sqSize(12)
.sqColor(isValid ? .sqNeutral(80) : .sqNeutral(50))
}
}
enum PasswordRequirements: String, CaseIterable {
case minChar = "8 caractères minimum"
case oneMaj = "Une lettre majuscule"
case oneMin = "Une lettre minuscule"
case oneNum = "Un chiffre"
case oneSpe = "Un caractère spécial"
}
}
#Preview {
PasswordRecommendations()
.padding(16)
}

View File

@@ -0,0 +1,63 @@
//
// ConfirmAuthView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 22/10/2025.
//
import SwiftUI
struct ConfirmAuthView: View {
@State var password: String = ""
var body: some View {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 16) {
SQText("Par mesure de sécurité, nous vous demandons votre mot de passe avant de modifier votre numéro de téléphone.")
.sqFont(.semiBold)
SQPasswordField("Mot de passe", text: $password)
}
SQButton("Confirmer") {
}
HStack {
Rectangle()
.frame(height: 1)
.foregroundStyle(Color.sqNeutral(20))
SQText("ou")
.sqColor(.sqNeutral(70))
Rectangle()
.frame(height: 1)
.foregroundStyle(Color.sqNeutral(20))
}
SQButton("Continuer avec Apple") {
}
.sqButtonType(.line)
.sqIcon(SQIcon(.apple_brand))
.sqLarge()
SQButton("Continuer avec Google") {
}
.sqButtonType(.line)
.sqIcon(SQIcon(.google_brand))
.sqLarge()
SQButton("Continuer avec Facebook") {
}
.sqButtonType(.line)
.sqIcon(SQIcon(.facebook_f_brand))
.sqLarge()
Spacer()
}
.padding()
}
}
#Preview {
ChangeLoginView()
}

View File

@@ -0,0 +1,108 @@
//
// LoginAndPasswordView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 21/10/2025.
//
import SwiftUI
struct LoginAndPasswordView: View {
@State var email: String = ""
@State var phone: String = ""
@State var currentPassword: String = ""
@State var newPassword: String = ""
@State var newPasswordRepeat: String = ""
var body: some View {
ScrollView {
VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
SQText("Identifiants")
.sqSize(18)
.sqFont(.bold)
SQText("Vos identifiants ne sont pas communiqués aux autres membres.")
.sqFont(.semiBold)
}
VStack(spacing: 4) {
SQTextField("Adresse e-mail", text: $email)
.sqPlaceholder("pie**@d**********.com")
.sqDisabled(true)
HStack {
Spacer()
Button {
} label: {
SQText("Modifier")
.sqSize(13)
.underline()
}
}
}
VStack(spacing: 4) {
SQTextField("Numéro de téléphone", text: $phone)
.sqPlaceholder("06******09")
.sqDisabled(true)
HStack {
Spacer()
Button {
} label: {
SQText("Modifier")
.sqSize(13)
.underline()
}
}
}
}
.padding(.horizontal)
.padding(.bottom, 16)
Rectangle()
.frame(height: 16)
.foregroundColor(Color.sqNeutral(10))
VStack(alignment: .center, spacing: 16) {
VStack(alignment: .leading, spacing: 16) {
SQText("Modifier mon mot de passe")
.sqSize(18)
.sqFont(.bold)
SQPasswordField("Mot de passe actuel", text: $currentPassword)
.sqPlaceholder("*********")
SQPasswordField("Nouveau mot de passe", text: $newPassword)
.sqPlaceholder("*********")
PasswordRecommendations()
SQPasswordField("Confirmez votre mot de passe", text: $newPasswordRepeat)
.sqPlaceholder("*********")
}
VStack(alignment: .center, spacing: 16) {
SQButton("Modifier mon mot de passe") {
}
.sqButtonType(.line)
Button {
} label: {
SQText("Mot de passe oublié")
.sqSize(13)
.sqFont(.semiBold)
.underline()
}
}
.padding(.top, 16)
.padding(.horizontal)
}
.padding()
}
}
.sqNavigationBar(title: "Identifiants et mot de passe")
}
}
#Preview {
LoginAndPasswordView()
}

View File

@@ -18,9 +18,13 @@ struct BoosterConfirmationScreen: View {
Image("booster_logo") Image("booster_logo")
.resizable() .resizable()
.frame(width: 210, height: 180) .frame(width: 210, height: 180)
SQText("Cest confirmé !", size: 18, font: .bold) SQText("Cest confirmé !")
.sqSize(18)
.sqFont(.bold)
.foregroundColor(.white) .foregroundColor(.white)
SQText("Votre profil sera boosté dès demain.", size: 32, font: .bold) SQText("Votre profil sera boosté dès demain.")
.sqSize(32)
.sqFont(.bold)
.foregroundColor(.white) .foregroundColor(.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
}.padding() }.padding()
@@ -29,7 +33,8 @@ struct BoosterConfirmationScreen: View {
SQButton("Accéder à mon option") { SQButton("Accéder à mon option") {
} }
SQText("XX,XX € / mois, sans engagement.", size: 13) SQText("XX,XX € / mois, sans engagement.")
.sqSize(13)
.foregroundColor(.white) .foregroundColor(.white)
} }
} }

View File

@@ -13,30 +13,40 @@ struct BoosterKnowAboutScreen: View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
VStack(spacing: 16) { VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
SQText("Comment ça marche ?", size: 18, font: .bold) SQText("Comment ça marche ?")
.sqSize(18)
.sqFont(.bold)
SQText("Loption Booster sactive sur les catégories de votre abonnement Premier. Par exemple, vous êtes abonné Premier sur les catégories « Plomberie - Installation Sanitaire » et « Carrelage », votre profil remontera en tête de liste des résultats en lien avec la plomberie et le carrelage.") SQText("Loption Booster sactive sur les catégories de votre abonnement Premier. Par exemple, vous êtes abonné Premier sur les catégories « Plomberie - Installation Sanitaire » et « Carrelage », votre profil remontera en tête de liste des résultats en lien avec la plomberie et le carrelage.")
} }
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
SQText("Vous pouvez choisir de remonter votre profil :", size: 18, font: .bold) SQText("Vous pouvez choisir de remonter votre profil :")
.sqSize(18)
.sqFont(.bold)
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQText("• Tous les jours\n• 3 jours par semaine\n• 1 jour par semaine") SQText("• Tous les jours\n• 3 jours par semaine\n• 1 jour par semaine")
SQText("Tous les jours", font: .demiBold) SQText("Tous les jours")
.sqFont(.semiBold)
VStack(spacing: 8) { VStack(spacing: 8) {
SQText("Cette option permet de remonter votre profil chaque jour dans les listes de résultats, à compter du lendemain de la souscription à loption Booster, jusquà la résiliation de votre option.") SQText("Cette option permet de remonter votre profil chaque jour dans les listes de résultats, à compter du lendemain de la souscription à loption Booster, jusquà la résiliation de votre option.")
SQText("Exemple : vous souscrivez un dimanche à 15h00, votre option sera activée dès lundi à 00h00.", font: .mediumItalic) SQText("Exemple : vous souscrivez un dimanche à 15h00, votre option sera activée dès lundi à 00h00.")
.sqFont(.mediumItalic)
} }
.padding(.leading) .padding(.leading)
SQText("3 jours par semaine", font: .demiBold) SQText("3 jours par semaine")
.sqFont(.semiBold)
VStack(spacing: 8) { VStack(spacing: 8) {
SQText("Cette option permet de remonter votre profil 3 jours par semaine dans les listes de résultats, à compter du lendemain de la souscription à loption Booster, jusquà la résiliation de votre option.") SQText("Cette option permet de remonter votre profil 3 jours par semaine dans les listes de résultats, à compter du lendemain de la souscription à loption Booster, jusquà la résiliation de votre option.")
SQText("Exemple : vous souscrivez un dimanche à 15h00, votre profil sera boosté le lundi, puis tous les deux jours.", font: .mediumItalic) SQText("Exemple : vous souscrivez un dimanche à 15h00, votre profil sera boosté le lundi, puis tous les deux jours.")
.sqFont(.mediumItalic)
} }
.padding(.leading) .padding(.leading)
SQText("1 jour par semaine", font: .demiBold) SQText("1 jour par semaine")
.sqFont(.semiBold)
VStack(spacing: 8) { VStack(spacing: 8) {
SQText("Cette option permet de remonter votre profil 1 jour par semaine dans les listes de résultats, à compter du lendemain de la souscription à loption Booster, jusquà la résiliation de votre option.") SQText("Cette option permet de remonter votre profil 1 jour par semaine dans les listes de résultats, à compter du lendemain de la souscription à loption Booster, jusquà la résiliation de votre option.")
SQText("Exemple : vous souscrivez un dimanche à 15h00, votre profil sera boosté le lundi, puis tous les six jours.", font: .mediumItalic) SQText("Exemple : vous souscrivez un dimanche à 15h00, votre profil sera boosté le lundi, puis tous les six jours.")
.sqFont(.mediumItalic)
} }
.padding(.leading) .padding(.leading)
} }
@@ -52,7 +62,9 @@ struct BoosterKnowAboutScreen: View {
}) })
} }
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
SQText("En savoir plus", size: 18, font: .bold) SQText("En savoir plus")
.sqSize(18)
.sqFont(.bold)
} }
} }
.padding(.horizontal) .padding(.horizontal)

View File

@@ -17,8 +17,10 @@ struct BoosterSubscriptionManagementScreen: View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
BoosterActiveHeaderView() BoosterActiveHeaderView()
SQText("Mes boosters", size: 20, font: .bold) SQText("Mes boosters")
.sqSize(20)
.sqFont(.bold)
Picker("", selection: $selectedValue) { Picker("", selection: $selectedValue) {
SQText("À venir").tag(0) SQText("À venir").tag(0)
SQText("Passées").tag(1) SQText("Passées").tag(1)

View File

@@ -82,7 +82,8 @@ struct BoosterSubscriptionSelectionScreen: View {
SQButton("C'est parti !") {} SQButton("C'est parti !") {}
if cancellation { if cancellation {
Button(action: /*@START_MENU_TOKEN@*/ {}/*@END_MENU_TOKEN@*/, label: { Button(action: /*@START_MENU_TOKEN@*/ {}/*@END_MENU_TOKEN@*/, label: {
SQText("Non merci, je souhaite résilier", size: 13) SQText("Non merci, je souhaite résilier")
.sqSize(12)
.foregroundColor(.black) .foregroundColor(.black)
}) })
.padding() .padding()

View File

@@ -15,9 +15,12 @@ struct BoosterActiveHeaderView: View {
.resizable() .resizable()
.frame(width: 93, height: 80) .frame(width: 93, height: 80)
VStack { VStack {
SQText("Booster en cours", size: 18, font: .bold) SQText("Booster en cours")
.sqSize(18)
.sqFont(.bold)
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
SQText("Aujourdhui", size: 14) SQText("Aujourdhui")
.sqSize(14)
} }
} }
.padding() .padding()

View File

@@ -10,25 +10,31 @@ import SwiftUI
struct BoosterFeaturesView: View { struct BoosterFeaturesView: View {
var body: some View { var body: some View {
VStack(alignment: .center, spacing: 24) { VStack(alignment: .center, spacing: 24) {
SQText("Boostez votre activité !", size: 32, font: .bold) SQText("Boostez votre activité !")
.sqSize(32)
.sqFont(.bold)
.foregroundColor(.white) .foregroundColor(.white)
VStack(alignment: .center) { VStack(alignment: .center) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("Votre profil est mis en avant:", font: .bold) SQText("Votre profil est mis en avant:")
.sqFont(.bold)
.foregroundColor(.white) .foregroundColor(.white)
HStack { HStack {
SQIcon(.check, type: .solid, color: .white) SQIcon(.check, type: .solid, color: .white)
SQText("Affichage en tête de liste des résultats", font: .bold) SQText("Affichage en tête de liste des résultats")
.sqFont(.bold)
.foregroundColor(.white) .foregroundColor(.white)
} }
HStack { HStack {
SQIcon(.check, type: .solid, color: .white) SQIcon(.check, type: .solid, color: .white)
SQText("Badge “Profil boosté” à chaque affichage de votre profil", font: .bold) SQText("Badge “Profil boosté” à chaque affichage de votre profil")
.sqFont(.bold)
.foregroundColor(.white) .foregroundColor(.white)
} }
HStack { HStack {
SQIcon(.check, type: .solid, color: .white) SQIcon(.check, type: .solid, color: .white)
SQText("Référencement boosté sur les moteurs de recherche", font: .bold) SQText("Référencement boosté sur les moteurs de recherche")
.sqFont(.bold)
.foregroundColor(.white) .foregroundColor(.white)
} }
} }

View File

@@ -14,7 +14,8 @@ struct BoosterHistoryCellView: View {
ZStack { ZStack {
HStack(alignment: .center) { HStack(alignment: .center) {
SQIcon(.calendar, color: .sqRoyal()) SQIcon(.calendar, color: .sqRoyal())
SQText("Samedi 20 avril", font: .bold) SQText("Samedi 20 avril")
.sqFont(.bold)
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)

View File

@@ -12,7 +12,8 @@ struct BoosterLockedToPremierView: View {
var body: some View { var body: some View {
HStack { HStack {
SQIcon(.lock_keyhole, size: .xs, type: .solid, color: .white) SQIcon(.lock_keyhole, size: .xs, type: .solid, color: .white)
SQText("Abonnés Premier", size: 13) SQText("Abonnés Premier")
.sqSize(13)
.foregroundColor(.white) .foregroundColor(.white)
} }
} }

View File

@@ -12,15 +12,17 @@ struct BoosterPromotionView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
VStack(spacing: 16) { VStack(spacing: 16) {
VStack { VStack {
SQText("Envie de booster votre activité ?", font: .bold) SQText("Envie de booster votre activité ?")
SQText("Activez loption Booster !", font: .demiBold) .sqFont(.bold)
SQText("Activez loption Booster !")
.sqFont(.semiBold)
} }
BoosterStatsView() BoosterStatsView()
SQButton("Activer loption Booster") { SQButton("Activer loption Booster") {
} }
.colorScheme(.royal) .sqColorScheme(.royal)
} }
.padding() .padding()
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())

View File

@@ -26,7 +26,8 @@ struct BoosterSelectionView: View {
} }
} }
} }
SQText("* 1 mois gratuit valable uniquement sur la formule “3 jours par semaine.”", size: 14) SQText("* 1 mois gratuit valable uniquement sur la formule “3 jours par semaine.”")
.sqSize(14)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
} }

View File

@@ -14,10 +14,16 @@ struct BoosterStatsView: View {
HStack(alignment: .top, spacing: 8) { HStack(alignment: .top, spacing: 8) {
VStack { VStack {
HStack(spacing: 0) { HStack(spacing: 0) {
SQText("x", size: 24, font: .bold) SQText("x")
SQText("3", size: 32, font: .bold) .sqSize(24)
.sqFont(.bold)
SQText("3")
.sqSize(32)
.sqFont(.bold)
} }
SQText("Demandes privées*", size: 14, font: .demiBold) SQText("Demandes privées*")
.sqSize(14)
.sqFont(.semiBold)
.lineLimit(2) .lineLimit(2)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
@@ -29,10 +35,16 @@ struct BoosterStatsView: View {
.frame(height: 72) .frame(height: 72)
VStack { VStack {
HStack(spacing: 0) { HStack(spacing: 0) {
SQText("x", size: 24, font: .bold) SQText("x")
SQText("3", size: 32, font: .bold) .sqSize(24)
.sqFont(.bold)
SQText("3")
.sqSize(32)
.sqFont(.bold)
} }
SQText("Évaluations*", size: 14, font: .demiBold) SQText("Évaluations*")
.sqSize(14)
.sqFont(.semiBold)
.lineLimit(2) .lineLimit(2)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
@@ -44,17 +56,24 @@ struct BoosterStatsView: View {
.frame(height: 72) .frame(height: 72)
VStack { VStack {
HStack(spacing: 0) { HStack(spacing: 0) {
SQText("x", size: 24, font: .bold) SQText("x")
SQText("2.9", size: 32, font: .bold) .sqSize(24)
.sqFont(.bold)
SQText("2.9")
.sqSize(32)
.sqFont(.bold)
} }
SQText("Mise en favori*", size: 14, font: .demiBold) SQText("Mise en favori*")
.sqSize(14)
.sqFont(.semiBold)
.lineLimit(2) .lineLimit(2)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
.frame(minWidth: 100, maxWidth: .infinity) .frame(minWidth: 100, maxWidth: .infinity)
} }
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
SQText("*Performances moyennes obtenues par les profils boostés, par rapport aux Abonnés Premier.", size: 11, font: .medium) SQText("*Performances moyennes obtenues par les profils boostés, par rapport aux Abonnés Premier.")
.sqSize(11)
.lineLimit(2) .lineLimit(2)
} }
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())

View File

@@ -27,16 +27,20 @@ struct BoosterSubscriptionCardView: View {
var body: some View { var body: some View {
VStack { VStack {
if currentOption { if currentOption {
SQText("Option actuelle", font: .bold) SQText("Option actuelle")
.sqFont(.bold)
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
} }
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
VStack { VStack {
SQText("3 jours", size: 32, font: .bold) SQText("3 jours")
.sqSize(32)
.sqFont(.bold)
.minimumScaleFactor(0.5) .minimumScaleFactor(0.5)
.lineLimit(1) .lineLimit(1)
.foregroundColor(.white) .foregroundColor(.white)
SQText("par semaine", size: 14) SQText("par semaine")
.sqSize(14)
.minimumScaleFactor(0.5) .minimumScaleFactor(0.5)
.lineLimit(1) .lineLimit(1)
.foregroundColor(.white) .foregroundColor(.white)
@@ -47,7 +51,8 @@ struct BoosterSubscriptionCardView: View {
// .minimumScaleFactor(0.5) // .minimumScaleFactor(0.5)
// .lineLimit(1) // .lineLimit(1)
// .foregroundColor(.white) // .foregroundColor(.white)
SQText("Sans engagement", size: 12) SQText("Sans engagement")
.sqSize(12)
.minimumScaleFactor(0.5) .minimumScaleFactor(0.5)
.lineLimit(1) .lineLimit(1)
.foregroundColor(.white) .foregroundColor(.white)
@@ -73,7 +78,8 @@ struct BoosterSubscriptionCardView: View {
startPoint: .top, endPoint: .bottom)) startPoint: .top, endPoint: .bottom))
) )
if isFree { if isFree {
SQText("1 mois gratuit *", font: .bold) SQText("1 mois gratuit *")
.sqFont(.bold)
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
} }
} }

View File

@@ -12,7 +12,9 @@ struct BoosterSubscriptionOptionsView: View {
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {
SQText("Booster", size: 18, font: .bold) SQText("Booster")
.sqSize(18)
.sqFont(.bold)
Spacer() Spacer()
SQIcon(.xmark, color: .white) SQIcon(.xmark, color: .white)
} }

View File

@@ -22,7 +22,9 @@ struct ProfileComplimentListView: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("Compliments reçus", size: 18, font: .bold) SQText("Compliments reçus")
.sqSize(18)
.sqFont(.bold)
.padding(.horizontal) .padding(.horizontal)
VStack(alignment: .center, spacing: 16) { VStack(alignment: .center, spacing: 16) {
VStack(alignment: .leading) { VStack(alignment: .leading) {

View File

@@ -17,7 +17,9 @@ struct ComplimentPillView: View {
.frame(width: 32, height: 32, alignment: .center) .frame(width: 32, height: 32, alignment: .center)
.background(Color.sqGreen(10)) .background(Color.sqGreen(10))
.cornerRadius(32) .cornerRadius(32)
SQText("Excellent rapport qualité/prix", size: 12, font: .demiBold) SQText("Excellent rapport qualité/prix")
.sqSize(12)
.sqFont(.semiBold)
} }
.padding(.leading, 1) .padding(.leading, 1)
.padding(.trailing, 16) .padding(.trailing, 16)

View File

@@ -17,7 +17,9 @@ struct ComplimentView: View {
.frame(width: 56, height: 56, alignment: .center) .frame(width: 56, height: 56, alignment: .center)
.background(Color.sqGreen(10)) .background(Color.sqGreen(10))
.cornerRadius(100) .cornerRadius(100)
SQText("Excellent rapport qualité/prix", size: 11, font: .demiBold) SQText("Excellent rapport qualité/prix")
.sqSize(11)
.sqFont(.semiBold)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundColor(Color.sqNeutral(90)) .foregroundColor(Color.sqNeutral(90))
} }
@@ -25,7 +27,9 @@ struct ComplimentView: View {
.frame(width: 87) .frame(width: 87)
HStack { HStack {
SQText("1", size: 11, font: .bold) SQText("1")
.sqSize(11)
.sqFont(.bold)
.foregroundColor(Color.sqNeutral()) .foregroundColor(Color.sqNeutral())
.padding(.horizontal, 6) .padding(.horizontal, 6)
.frame(height: 16) .frame(height: 16)

View File

@@ -14,7 +14,9 @@ struct CurrentDebugUser: View {
VStack { VStack {
SQText("Harbin Leduc") SQText("Harbin Leduc")
SQText("Torphy LLC") SQText("Torphy LLC")
SQText("Auto-entrepreneur", size: 12, font: .demiBold) SQText("Auto-entrepreneur")
.sqSize(12)
.sqFont(.semiBold)
.padding(.vertical, 4) .padding(.vertical, 4)
.padding(.horizontal, 8) .padding(.horizontal, 8)
.background { .background {
@@ -24,7 +26,8 @@ struct CurrentDebugUser: View {
} }
HStack { HStack {
SQText("UserID:", font: .demiBold) SQText("UserID:")
.sqFont(.semiBold)
Spacer() Spacer()
SQText("103135") SQText("103135")
Button {} label: { Button {} label: {
@@ -37,7 +40,8 @@ struct CurrentDebugUser: View {
} }
} }
HStack { HStack {
SQText("Email:", font: .demiBold) SQText("Email:")
.sqFont(.semiBold)
Spacer() Spacer()
SQText("test@test.com") SQText("test@test.com")
Button {} label: { Button {} label: {
@@ -50,7 +54,8 @@ struct CurrentDebugUser: View {
} }
} }
HStack { HStack {
SQText("Téléphone:", font: .demiBold) SQText("Téléphone:")
.sqFont(.semiBold)
Spacer() Spacer()
SQText("0612345678") SQText("0612345678")
Button {} label: { Button {} label: {

View File

@@ -0,0 +1,31 @@
//
// DebugActionView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 14/08/2025.
//
import SwiftUI
struct DebugActionView: View {
let title: String
@State var isOn = false
var body: some View {
VStack(spacing: 16) {
SQText(title)
.sqFont(.semiBold)
Toggle("", isOn: $isOn)
}
.labelsHidden()
.frame(maxWidth: .infinity, maxHeight: 128)
.background {
RoundedRectangle(cornerRadius: 10)
.fill(Color.sqNeutral(10))
}
}
}
#Preview {
DebugActionView(title: "Anglais")
}

View File

@@ -23,16 +23,21 @@ struct ConfigPrestationSearchView: View {
VStack { VStack {
Spacer() Spacer()
VStack(spacing: 16) { VStack(spacing: 16) {
SQText("Configuration de la recherche des prestations", size: 18, font: .bold) SQText("Configuration de la recherche des prestations")
.sqSize(18)
.sqFont(.bold)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Divider() Divider()
SQText("Types à rechercher :", font: .demiBold) SQText("Types à rechercher :")
.sqFont(.semiBold)
SQPicker(selection: $selectedSearchType, options: searchTypes) SQPicker(selection: $selectedSearchType, options: searchTypes)
Toggle(isOn: $showSuggested) { Toggle(isOn: $showSuggested) {
SQText("Afficher une suggestion", font: .demiBold) SQText("Afficher une suggestion")
.sqFont(.semiBold)
} }
Toggle(isOn: $showAllCategories) { Toggle(isOn: $showAllCategories) {
SQText("Afficher \"Toutes les catégories\"", font: .demiBold) SQText("Afficher \"Toutes les catégories\"")
.sqFont(.semiBold)
} }
} }
.padding() .padding()

View File

@@ -43,9 +43,21 @@ struct DebugLandView: View {
CurrentDebugUser() CurrentDebugUser()
VStack { VStack {
SQText("Environnement :", font: .demiBold) SQText("Environnement :")
SQPicker(selection: $currentEnv, options: pickerOptions) .sqFont(.semiBold)
SQText("Prod")
.sqFont(.bold)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background {
RoundedRectangle(cornerRadius: 8)
.fill(Color.sqRed())
}
SQButton("Changer d'environnement") {
}
} }
.frame(maxWidth: .infinity)
.padding() .padding()
.background { .background {
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
@@ -54,16 +66,19 @@ struct DebugLandView: View {
.padding(.horizontal) .padding(.horizontal)
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
SQText("User ID :", font: .demiBold) SQText("User ID :")
.sqFont(.semiBold)
HStack(spacing: 8) { HStack(spacing: 8) {
VStack { VStack {
SQTextField("Visiter le profil :", placeholder: "UserID", text: $userId) SQTextField("Visiter le profil :", text: $userId)
.sqPlaceholder("UserID")
SQButton("Visiter le profil") { SQButton("Visiter le profil") {
} }
} }
VStack { VStack {
SQTextField("Se connecter sur :", placeholder: "UserID", text: $userId) SQTextField("Se connecter sur :", text: $userId)
.sqPlaceholder("UserID")
.keyboardType(.numberPad) .keyboardType(.numberPad)
SQButton("Se connecter") { SQButton("Se connecter") {
@@ -77,11 +92,24 @@ struct DebugLandView: View {
.fill(Color.sqNeutral(10)) .fill(Color.sqNeutral(10))
} }
.padding(.horizontal) .padding(.horizontal)
VStack {
LazyHGrid(rows: [
GridItem.init(),
GridItem.init()
]) {
DebugActionView(title: "")
DebugActionView(title: "")
DebugActionView(title: "")
DebugActionView(title: "")
}
}
} }
} }
.toolbar { .toolbar {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
SQText("🦄 Debug Land 🦄", font: .bold) SQText("🦄 Debug Land 🦄")
.sqFont(.bold)
} }
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
@@ -89,6 +117,11 @@ struct DebugLandView: View {
SQIcon(.user_group) SQIcon(.user_group)
} }
} }
ToolbarItem(placement: .topBarTrailing) {
Button {} label: {
SQIcon(.ballot_check)
}
}
} }
} }
} }

View File

@@ -0,0 +1,20 @@
//
// IdentityValidationNavigationManager.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 30/06/2025.
//
import Foundation
enum IdentityValidationRoute: NavigationRoute {
case informationsValidation
case livenessTutorial
case liveness
case livenessSucess
case livenessError
case identityDocumentsList
case identityDocumentsValidation
}
class IdentityValidationNavigationManager: BaseNavigationManager<IdentityValidationRoute> {}

View File

@@ -0,0 +1,13 @@
//
// IdentityValidationViewModel.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 30/06/2025.
//
import SwiftUI
@MainActor
class IdentityValidationViewModel: ObservableObject {
@Published var isLoading: Bool = false
}

View File

@@ -0,0 +1,36 @@
//
// IdentityDocumentsValidationLoaderView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 01/07/2025.
//
import Lottie
import SwiftUI
struct IdentityDocumentsValidationLoaderView: View {
var body: some View {
VStack {
HStack {
SQProgressIndicator(.constant(true))
SQProgressIndicator(.constant(false))
SQProgressIndicator(.constant(false))
}
Spacer()
VStack(spacing: 16) {
LottieView(animation: .named("av_loader"))
.looping()
.frame(width: 60, height: 60)
SQText("Vérification en cours...")
.sqSize(20)
.sqFont(.semiBold)
}
Spacer()
}
.padding(.horizontal)
}
}
#Preview {
IdentityDocumentsValidationLoaderView()
}

View File

@@ -0,0 +1,74 @@
//
// IdentityDocumentsValidationResultView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 01/07/2025.
//
import SwiftUI
struct IdentityDocumentsValidationResultView: View {
var body: some View {
VStack(spacing: 32) {
HStack {
SQProgressIndicator(.constant(true))
SQProgressIndicator(.constant(false))
SQProgressIndicator(.constant(false))
}
if true {
VStack(spacing: 32) {
VStack(spacing: 16) {
SQIcon(.xmark, customSize: 60, color: .sqSemanticRed)
SQText("Votre identité na pas été validée.")
.sqSize(20)
.sqFont(.semiBold)
}
VStack(alignment: .leading, spacing: 8) {
SQText("Assurez-vous que :")
.sqSize(18)
.sqFont(.bold)
HStack(alignment: .top) {
SQText("1.")
SQText("Le document est valable et en cours de validité.")
}
HStack(alignment: .top) {
SQText("2.")
SQText("Les informations renseignées sur le document transmis sont strictement identiques aux informations indiquées sur votre compte AlloVoisins.")
}
HStack(alignment: .top) {
SQText("3.")
SQText("Vous avez plus de 18 ans.")
}
HStack(alignment: .top) {
SQText("4.")
SQText("Le document vous appartient.")
}
}
}
} else {
Spacer()
VStack(spacing: 16) {
SQIcon(.check, customSize: 60, color: .sqSemanticGreen)
SQText("Votre identité a été vérifiée.")
.sqSize(20)
.sqFont(.semiBold)
}
}
Spacer()
if true {
SQButton("Recommencer") {
}
} else {
SQButton("Terminer") {
}
}
}
.padding(.horizontal)
}
}
#Preview {
IdentityDocumentsValidationResultView()
}

View File

@@ -0,0 +1,52 @@
//
// IdentityInformationsValidationView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 30/06/2025.
//
import SwiftUI
struct IdentityInformationsValidationView: View {
var body: some View {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 24) {
SQText("Les étapes de vérification didentité :")
.sqSize(20)
.sqFont(.bold)
VStack(alignment: .leading, spacing: 8) {
SQText("1. Vous allez prendre une vidéo de votre visage")
SQText("2. Vous allez prendre une photo de votre pièce didentité.")
SQText("3. Nous allons comparer la vidéo de votre visage et les informations de votre compte AlloVoisins avec les éléments figurant sur votre pièce didentité.")
}
VStack(alignment: .leading, spacing: 8) {
HStack {
SQIcon(.circle_exclamation)
SQText("Vérifiez que :")
.sqFont(.semiBold)
}
SQText("Les informations renseignées sur le document didentité transmis doivent être strictement identiques aux informations ci-dessous, indiquées sur votre compte AlloVoisins :")
VStack(alignment: .leading) {
SQText(" ‧ Prénom : Maëlys-Gaëlle")
SQText(" ‧ Nom : Martin")
SQText(" ‧ Date de naissance : 13/07/1990")
}
}
}
SQButton("Mettre à jour mes informations") {
}
.sqButtonType(.line)
Spacer()
SQButton("Commencer") {
}
}
.padding([.trailing, .leading, .top])
.sqNavigationBar(title: "Vérification de mon identité")
}
}
#Preview {
IdentityInformationsValidationView()
}

View File

@@ -0,0 +1,48 @@
//
// IdentityValidationNavigationView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 30/06/2025.
//
import SwiftUI
struct IdentityValidationNavigationView: View {
@ObservedObject var navigationManager: IdentityValidationNavigationManager
@ObservedObject var viewModel: IdentityValidationViewModel
init(navigationManager: IdentityValidationNavigationManager) {
self.navigationManager = navigationManager
self.viewModel = .init()
}
var body: some View {
NavigationStack(path: $navigationManager.navigationPath) {
IdentityInformationsValidationView()
.navigationDestination(for: IdentityValidationRoute.self) { route in
switch route {
case .livenessTutorial:
LivenessTutorialView()
case .liveness:
LivenessView()
case .livenessSucess:
LivenessView()
case .livenessError:
LivenessView()
case .informationsValidation:
IdentityInformationsValidationView()
case .identityDocumentsList:
IdentityInformationsValidationView()
case .identityDocumentsValidation:
IdentityInformationsValidationView()
}
}
}
.environmentObject(navigationManager)
.environmentObject(viewModel)
}
}
#Preview {
IdentityValidationNavigationView(navigationManager: IdentityValidationNavigationManager())
}

View File

@@ -0,0 +1,34 @@
//
// LivenessErrorView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 30/06/2025.
//
import SwiftUI
struct LivenessErrorView: View {
var body: some View {
VStack(spacing: 24) {
HStack {
SQProgressIndicator(.constant(true))
SQProgressIndicator(.constant(false))
SQProgressIndicator(.constant(false))
}
SQIcon(.xmark, customSize: 60, color: .sqSemanticRed)
SQText("Nous navons pas réussi à détecter votre visage.")
.sqSize(20)
.sqFont(.semiBold)
SQText("Assurez-vous que les conditions déclairage sont bonnes et que votre téléphone est bien en face de vous.\nLimage ne doit pas être floue, coupée, ou présenter des reflets.")
Spacer()
SQButton("Recommencer") {
}
}
.padding()
}
}
#Preview {
LivenessErrorView()
}

View File

@@ -0,0 +1,53 @@
//
// LivenessSuccessView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 30/06/2025.
//
import SwiftUI
struct LivenessSuccessView: View {
var body: some View {
VStack(spacing: 24) {
HStack {
SQProgressIndicator(.constant(true))
SQProgressIndicator(.constant(false))
SQProgressIndicator(.constant(false))
}
SQImage("lemonsieur", height: 365)
.clipShape(Ellipse())
.overlay(
Ellipse()
.stroke(Color.gray, lineWidth: 0)
)
VStack(alignment: .leading) {
HStack {
SQIcon(.check, color: .sqSemanticPositive)
SQText("Votre visage est bien dans le cadre.")
Spacer()
}
HStack {
SQIcon(.check, color: .sqSemanticPositive)
SQText("Limage nest pas floue et na pas de reflet.")
Spacer()
}
HStack {
SQIcon(.check, color: .sqSemanticPositive)
SQText("Vous ne portez pas daccessoires couvrant.")
Spacer()
}
}
Spacer()
SQButton("Continuer") {
}
}
.padding()
}
}
#Preview {
LivenessSuccessView()
}

View File

@@ -0,0 +1,54 @@
//
// LivenessTutorialView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 30/06/2025.
//
import SwiftUI
struct LivenessTutorialView: View {
var body: some View {
VStack(spacing: 0) {
HStack {
SQProgressIndicator(.constant(true))
SQProgressIndicator(.constant(false))
SQProgressIndicator(.constant(false))
}
VStack(spacing: 32) {
VStack(alignment: .leading, spacing: 8) {
SQText("Pour vérifier votre identité, vous allez devoir réaliser une courte vidéo de votre visage, nous permettant de valider que vous êtes une personne réelle.")
.sqFont(.semiBold)
SQText("Cette vidéo ne sera pas stockée par AlloVoisins.\nElle est éphémère et nest utilisée que dans le cadre du processus de vérification didentité.")
}
.padding(.vertical, 16)
VStack(alignment: .leading, spacing: 8) {
SQText("Pour garantir le bon déroulement de la vérification didentité :")
.sqSize(18)
.sqFont(.bold)
HStack {
SQImage("portrait_reflection", height: 80)
SQText("Trouvez un endroit bien éclairé.\nUne bonne lumière est essentielle, mais évitez la lumière directe du soleil sur votre visage.")
}
HStack {
SQImage("portrait_accessories", height: 80)
SQText("Veuillez retirer vos lunettes de soleil, chapeau, masque ou tout autre accessoire. Ceux-ci rendent votre identification plus difficile.")
}
HStack {
SQImage("portrait_angle", height: 80)
SQText("Positionnez votre visage dans le cadre de l'écran et suivez les instructions.")
}
}
Spacer()
SQButton("Continuer") {
}
}
}
.padding([.horizontal, .top])
}
}
#Preview {
LivenessTutorialView()
}

View File

@@ -0,0 +1,31 @@
//
// LivenessView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 30/06/2025.
//
import SwiftUI
struct LivenessView: View {
@Environment(\.presentationMode) var presentationMode
@State private var isPresentingLiveness = true
var body: some View {
// FaceLivenessDetectorView(
// sessionID: "",
// region: "",
// isPresented: $isPresentingLiveness,
// onCompletion: { result in
// switch result {
// case .success: break
// case .failure(_): break
// }
// }
// )
}
}
#Preview {
LivenessView()
}

View File

@@ -5,6 +5,7 @@
// Created by Victor on 16/12/2024. // Created by Victor on 16/12/2024.
// //
import Lottie
import SwiftUI import SwiftUI
struct DocumentPreviewView: View { struct DocumentPreviewView: View {
@@ -19,37 +20,35 @@ struct DocumentPreviewView: View {
SQImage("visit_card_new", height: 160) SQImage("visit_card_new", height: 160)
} else { } else {
SQIcon(.file_pdf, size: .l, color: .sqNeutral(80)) SQIcon(.file_pdf, size: .l, color: .sqNeutral(80))
SQText("PDF", size: 16, font: .demiBold) SQText("PDF")
.sqFont(.semiBold)
} }
} }
.frame(width: 160, height: 160, alignment: .center) .frame(width: 160, height: 160, alignment: .center)
.background(Color.sqNeutral(10))
.overlay( .overlay(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5) .inset(by: 0.5)
.stroke(Color.sqNeutral(20), lineWidth: 1) .stroke(Color.sqNeutral(20), lineWidth: 1)
) )
Button { Button {} label: {
SQIcon(.xmark, size: .s, color: .white)
} label: {
SQIcon(.trash_can, size: .s, color: .white)
.padding() .padding()
.background { .background {
Circle() Circle()
.foregroundColor(Color.sqSemanticRed) .foregroundColor(Color.sqSemanticRed)
.frame(height: 24) .frame(height: 32)
} }
.padding(-16)
} }
} }
SQText(fileName, size: 13) SQText(fileName)
.sqSize(13)
} }
} }
} }
#Preview { #Preview {
DocumentPreviewView(fileName: "le-nom-de-mon-fichier.pdf") { DocumentPreviewView(fileName: "le-nom-de-mon-fichier.pdf") {}
}
} }

View File

@@ -67,7 +67,9 @@ struct IdentityDocumentButton: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText(type.title) SQText(type.title)
if !type.subtitle.isEmpty { if !type.subtitle.isEmpty {
SQText(type.subtitle, size: 13, textColor: .sqNeutral(70)) SQText(type.subtitle)
.sqSize(13)
.sqColor(.sqNeutral(70))
} }
} }
Spacer() Spacer()

View File

@@ -69,15 +69,20 @@ struct KYCDocumentButton: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {
SQText(name, font: .bold) SQText(name)
.sqFont(.bold)
Spacer() Spacer()
HStack(spacing: 4) { HStack(spacing: 4) {
SQText(status.text, size: 13, font: .demiBold, textColor: status.color) SQText(status.text)
.sqSize(13)
.sqFont(.semiBold)
.sqColor(status.color)
status.icon status.icon
} }
} }
HStack { HStack {
SQText(desc, size: 14) SQText(desc)
.sqSize(14)
Spacer() Spacer()
if [.missing, .expired, .refused].contains(status) { if [.missing, .expired, .refused].contains(status) {
SQIcon(.chevron_right, size: .l) SQIcon(.chevron_right, size: .l)

View File

@@ -12,7 +12,10 @@ struct KYCInformationAlertView: View {
HStack { HStack {
SQIcon(.triangle_exclamation, size: .xl, color: .sqRed(80)) SQIcon(.triangle_exclamation, size: .xl, color: .sqRed(80))
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("Mangopay facturera le montant de la vérification dès lors que vous aurez soumis un document à vérification.", size: 14, font: .demiBold, textColor: .sqRed(80)) SQText("Mangopay facturera le montant de la vérification dès lors que vous aurez soumis un document à vérification.")
.sqSize(14)
.sqFont(.semiBold)
.sqColor(.sqRed(80))
} }
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)

View File

@@ -18,7 +18,9 @@ struct CardColorSelectionView: View {
VStack(alignment: .leading, spacing: 32) { VStack(alignment: .leading, spacing: 32) {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQColorPicker("Couleur principale", selectedColor: $selectedColor, subtitle: "Choisissez la couleur qui sera appliquée sur vos cartes de visite.") SQColorPicker("Couleur principale", selectedColor: $selectedColor, subtitle: "Choisissez la couleur qui sera appliquée sur vos cartes de visite.")
SQText("Aperçu", size: 18, font: .bold) SQText("Aperçu")
.sqSize(18)
.sqFont(.bold)
selectedTemplate.imageColorTemplate(isLight: selectedColor.isLight) selectedTemplate.imageColorTemplate(isLight: selectedColor.isLight)
.background(selectedColor) .background(selectedColor)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -28,7 +30,8 @@ struct CardColorSelectionView: View {
SQText("Appliquer cette couleur sur mes prospectus, mes devis et mes factures") SQText("Appliquer cette couleur sur mes prospectus, mes devis et mes factures")
} }
.tint(Color.sqNeutral(100)) .tint(Color.sqNeutral(100))
SQText("Nous vous recommandons dappliquer la même couleur sur tous vos documents.", size: 12) SQText("Nous vous recommandons dappliquer la même couleur sur tous vos documents.")
.sqSize(12)
} }
Spacer() Spacer()
} }

View File

@@ -28,11 +28,14 @@ struct CardFormView: View {
VStack { VStack {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("Informations", size: 24, font: .bold) SQText("Informations")
.sqSize(24)
.sqFont(.bold)
SQText("Les modifications apportées sur votre carte de visite ne seront pas reportées sur votre profil.") SQText("Les modifications apportées sur votre carte de visite ne seront pas reportées sur votre profil.")
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("Image", font: .demiBold) SQText("Image")
.sqFont(.semiBold)
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
Button(action: { showActionSheet = true }) { Button(action: { showActionSheet = true }) {
@@ -60,17 +63,25 @@ struct CardFormView: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
SQTextField("Titre", placeholder: "Ex : Pro Solutions", text: $title) SQTextField("Titre", text: $title)
SQTextField("Sous-titre", placeholder: "Ex : Martin Dupont", text: $subtitle, isOptional: true) .sqPlaceholder("Ex : Pro Solutions")
SQTextField("Métier", placeholder: "Ex : Dépannage électroménager", text: $job, isOptional: true) SQTextField("Sous-titre", text: $subtitle)
.sqPlaceholder("Ex : Martin Dupont")
.sqOptional()
SQTextField("Métier", text: $job)
.sqPlaceholder("Ex : Dépannage électroménager")
.sqOptional()
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Toggle(isOn: $showRating) { Toggle(isOn: $showRating) {
SQText("Afficher ma note AlloVoisins", font: .demiBold) SQText("Afficher ma note AlloVoisins")
.sqFont(.semiBold)
} }
.tint(Color.sqNeutral(100)) .tint(Color.sqNeutral(100))
HStack(spacing: 4) { HStack(spacing: 4) {
SQIcon(.star, type: .solid, color: .sqGold(50)) SQIcon(.star, type: .solid, color: .sqGold(50))
SQText("4,7/5 sur", size: 12, font: .demiBold) SQText("4,7/5 sur")
.sqSize(12)
.sqFont(.semiBold)
SQImage("logo", height: 14) SQImage("logo", height: 14)
} }
} }
@@ -83,18 +94,20 @@ struct CardFormView: View {
.foregroundColor(Color.sqNeutral(10)) .foregroundColor(Color.sqNeutral(10))
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQTextField("Numéro de téléphone", placeholder: "Ex : 06 12 34 56 78", text: $phoneNumber) SQTextField("Numéro de téléphone", text: $phoneNumber)
SQTextField("Adresse complète", placeholder: "Ex : 1 rue de la gare, 67000 Strasbourg", text: $address) .sqPlaceholder("Ex : 06 12 34 56 78")
SQTextField("Adresse complète", text: $address)
.sqPlaceholder("Ex : 1 rue de la gare, 67000 Strasbourg")
} }
.padding() .padding()
} }
} }
SQFooter { SQFooter {
SQButton("Aperçu") {} SQButton("Aperçu") {}
.icon(SQIcon(.eye, color: .sqNeutral(100))) .sqIcon(SQIcon(.eye, color: .sqNeutral(100)))
.buttonType(.line) .sqButtonType(.line)
SQButton("Imprimer") {} SQButton("Imprimer") {}
.icon(SQIcon(.print, color: .white)) .sqIcon(SQIcon(.print, color: .white))
} }
} }
.confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: { .confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: {

View File

@@ -10,7 +10,8 @@ import SwiftUI
struct CardPrintView: View { struct CardPrintView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQText("Génération dune planche au format A4 comprenant 8 cartes de visite.", font: .demiBold) SQText("Génération dune planche au format A4 comprenant 8 cartes de visite.")
.sqFont(.semiBold)
Image("visit_card_print_template") Image("visit_card_print_template")
.resizable() .resizable()
.scaledToFit() .scaledToFit()

View File

@@ -18,7 +18,9 @@ struct FlyerColorSelectionView: View {
VStack(alignment: .leading, spacing: 32) { VStack(alignment: .leading, spacing: 32) {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQColorPicker("Couleur principale", selectedColor: $selectedColor, subtitle: "Choisissez la couleur qui sera appliquée sur vos prospectus.") SQColorPicker("Couleur principale", selectedColor: $selectedColor, subtitle: "Choisissez la couleur qui sera appliquée sur vos prospectus.")
SQText("Aperçu", size: 18, font: .bold) SQText("Aperçu")
.sqSize(18)
.sqFont(.bold)
selectedTemplate.imageColorTemplate(isLight: selectedColor.isLight) selectedTemplate.imageColorTemplate(isLight: selectedColor.isLight)
.background(selectedColor) .background(selectedColor)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -28,7 +30,8 @@ struct FlyerColorSelectionView: View {
SQText("Appliquer cette couleur sur mes cartes de visite, mes devis et mes factures") SQText("Appliquer cette couleur sur mes cartes de visite, mes devis et mes factures")
} }
.tint(Color.sqNeutral(100)) .tint(Color.sqNeutral(100))
SQText("Nous vous recommandons dappliquer la même couleur sur tous vos documents.", size: 12) SQText("Nous vous recommandons dappliquer la même couleur sur tous vos documents.")
.sqSize(12)
} }
Spacer() Spacer()
} }

View File

@@ -30,11 +30,14 @@ struct FlyerFormView: View {
VStack { VStack {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("Informations", size: 24, font: .bold) SQText("Informations")
.sqSize(24)
.sqFont(.bold)
SQText("Les modifications apportées sur votre prospectus ne seront pas reportées sur votre profil.") SQText("Les modifications apportées sur votre prospectus ne seront pas reportées sur votre profil.")
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("Image", font: .demiBold) SQText("Image")
.sqFont(.semiBold)
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
Button(action: { showActionSheet = true }) { Button(action: { showActionSheet = true }) {
@@ -62,17 +65,25 @@ struct FlyerFormView: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
SQTextField("Titre", placeholder: "Ex : Pro Solutions", text: $title) SQTextField("Titre", text: $title)
SQTextField("Sous-titre", placeholder: "Ex : Martin Dupont", text: $subtitle, isOptional: true) .sqPlaceholder("Ex : Pro Solutions")
SQTextField("Métier", placeholder: "Ex : Dépannage électroménager", text: $job, isOptional: true) SQTextField("Sous-titre", text: $subtitle)
.sqPlaceholder("Ex : Martin Dupont")
.sqOptional()
SQTextField("Métier", text: $job)
.sqPlaceholder("Ex : Dépannage électroménager")
.sqOptional()
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Toggle(isOn: $showRating) { Toggle(isOn: $showRating) {
SQText("Afficher ma note AlloVoisins", font: .demiBold) SQText("Afficher ma note AlloVoisins")
.sqFont(.semiBold)
} }
.tint(Color.sqNeutral(100)) .tint(Color.sqNeutral(100))
HStack(spacing: 4) { HStack(spacing: 4) {
SQIcon(.star, type: .solid, color: .sqGold(50)) SQIcon(.star, type: .solid, color: .sqGold(50))
SQText("4,7/5 sur", size: 12, font: .demiBold) SQText("4,7/5 sur")
.sqSize(12)
.sqFont(.semiBold)
SQImage("logo", height: 14) SQImage("logo", height: 14)
} }
} }
@@ -87,11 +98,21 @@ struct FlyerFormView: View {
VStack(alignment: .leading, spacing: 16) { 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("")) 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", text: $job, isOptional: true) SQTextField("Prestation 1", text: $job)
SQTextField("Prestation 2", placeholder: "Ex : Réparation machine à laver", text: $job, isOptional: true) .sqPlaceholder("Ex : Réparation lave-vaisselle")
SQTextField("Prestation 3", placeholder: "Ex : Réparation four", text: $job, isOptional: true) .sqOptional()
SQTextField("Prestation 4", placeholder: "Ex : Réparation outillage", text: $job, isOptional: true) SQTextField("Prestation 2", text: $job)
SQTextField("Prestation 5", placeholder: "Ex : Dépannage électroménager", text: $job, isOptional: true) .sqPlaceholder("Ex : Réparation machine à laver")
.sqOptional()
SQTextField("Prestation 3", text: $job)
.sqPlaceholder("Ex : Réparation four")
.sqOptional()
SQTextField("Prestation 4", text: $job)
.sqPlaceholder("Ex : Réparation outillage")
.sqOptional()
SQTextField("Prestation 5", text: $job)
.sqPlaceholder("Ex : Dépannage électroménager")
.sqOptional()
} }
.padding() .padding()
@@ -100,8 +121,10 @@ struct FlyerFormView: View {
.foregroundColor(Color.sqNeutral(10)) .foregroundColor(Color.sqNeutral(10))
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQTextField("Numéro de téléphone", placeholder: "Ex : 06 12 34 56 78", text: $phoneNumber) SQTextField("Numéro de téléphone", text: $phoneNumber)
SQTextField("Adresse complète", placeholder: "Ex : 1 rue de la gare, 67000 Strasbourg", text: $address) .sqPlaceholder("Ex : 06 12 34 56 78")
SQTextField("Adresse complète", text: $address)
.sqPlaceholder("Ex : 1 rue de la gare, 67000 Strasbourg")
} }
.padding() .padding()
@@ -110,18 +133,36 @@ struct FlyerFormView: View {
.foregroundColor(Color.sqNeutral(10)) .foregroundColor(Color.sqNeutral(10))
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQText("Mentions légales obligatoires", size: 18, font: .bold) SQText("Mentions légales obligatoires")
.sqSize(18)
.sqFont(.bold)
SQTextField("Dénomination sociale", placeholder: "Ex : Pro solutions", text: $job, isOptional: true) SQTextField("Dénomination sociale", text: $job)
SQTextField("Adresse du siège social", placeholder: "Ex : 16 rue de la Redoute, 67500 Haguenau", text: $job, isOptional: true) .sqPlaceholder("Ex : Pro solutions")
.sqOptional()
SQTextField("Adresse du siège social", text: $job)
.sqPlaceholder("Ex : 16 rue de la Redoute, 67500 Haguenau")
.sqOptional()
VStack(spacing: 0) { VStack(spacing: 0) {
SQTextField("Numéro SIRET", placeholder: "Ex : 12345678901234", text: $job, isOptional: true) SQTextField("Numéro SIRET", text: $job)
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)) .sqPlaceholder("Ex : 12345678901234")
.sqOptional()
SQText("Le numéro SIRET se compose de 14 chiffres : les 9 chiffres du SIREN + 5 chiffres propres à chaque établissement (NIC).")
.sqSize(12)
.sqColor(.sqNeutral(50))
} }
SQTextField("Numéro de RCS", placeholder: "Ex : RCS STRASBOURG B 123456789", text: $job, isOptional: true) SQTextField("Numéro de RCS", text: $job)
SQTextField("Statut juridique", placeholder: "Ex : SARL", text: $job, isOptional: true) .sqPlaceholder("Ex : RCS STRASBOURG B 123456789")
SQTextField("Montant du capital social (€)", placeholder: "Ex : 1 000,00", text: $job, isOptional: true) .sqOptional()
SQTextField("Autre champ relatif à votre activité", placeholder: "Ex : Pour votre santé, mangez 5 fruits et légumes par jour", text: $job, isOptional: true) SQTextField("Statut juridique", text: $job)
.sqPlaceholder("Ex : SARL")
.sqOptional()
SQTextField("Montant du capital social (€)", text: $job)
.sqPlaceholder("Ex : 1 000,00")
.sqOptional()
SQTextField("Autre champ relatif à votre activité", text: $job)
.sqPlaceholder("Ex : Pour votre santé, mangez 5 fruits et légumes par jour")
.sqOptional()
SQCheckbox("Je comprends que jengage ma responsabilité sur lexhaustivité et lauthenticité des informations renseignées ci-dessus.", isChecked: $authentConfirm, error: .constant(.none)) SQCheckbox("Je comprends que jengage ma responsabilité sur lexhaustivité et lauthenticité des informations renseignées ci-dessus.", isChecked: $authentConfirm, error: .constant(.none))
} }
.padding() .padding()
@@ -129,14 +170,14 @@ struct FlyerFormView: View {
} }
SQFooter { SQFooter {
SQButton("Aperçu") {} SQButton("Aperçu") {}
.icon(SQIcon(.eye, color: .sqNeutral(100))) .sqIcon(SQIcon(.eye, color: .sqNeutral(100)))
.buttonType(.line) .sqButtonType(.line)
SQButton("Imprimer") { SQButton("Imprimer") {
if authentConfirm == false { if authentConfirm == false {
self.confirmIsInError = true self.confirmIsInError = true
} }
} }
.icon(SQIcon(.print, color: .white)) .sqIcon(SQIcon(.print, color: .white))
} }
} }
.confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: { .confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: {

View File

@@ -20,7 +20,9 @@ struct MarketingSupportSelectionView: View {
SQImage("visit_card_new", height: 200) SQImage("visit_card_new", height: 200)
.cornerRadius(8, corners: [.topLeft, .topRight]) .cornerRadius(8, corners: [.topLeft, .topRight])
HStack { HStack {
SQText("Mes cartes de visite", size: 18, font: .bold) SQText("Mes cartes de visite")
.sqSize(18)
.sqFont(.bold)
Spacer() Spacer()
SQImage("for_pro_icon", height: 13) SQImage("for_pro_icon", height: 13)
} }
@@ -41,7 +43,9 @@ struct MarketingSupportSelectionView: View {
SQImage("flyers", height: 200) SQImage("flyers", height: 200)
.cornerRadius(8, corners: [.topLeft, .topRight]) .cornerRadius(8, corners: [.topLeft, .topRight])
HStack { HStack {
SQText("Mes prospetus", size: 18, font: .bold) SQText("Mes prospetus")
.sqSize(18)
.sqFont(.bold)
Spacer() Spacer()
SQImage("for_pro_icon", height: 13) SQImage("for_pro_icon", height: 13)
} }
@@ -59,7 +63,9 @@ struct MarketingSupportSelectionView: View {
SQImage("quotes", height: 200) SQImage("quotes", height: 200)
.cornerRadius(8, corners: [.topLeft, .topRight]) .cornerRadius(8, corners: [.topLeft, .topRight])
HStack { HStack {
SQText("Mes prospetus", size: 18, font: .bold) SQText("Mes prospetus")
.sqSize(18)
.sqFont(.bold)
Spacer() Spacer()
SQImage("for_pro_icon", height: 13) SQImage("for_pro_icon", height: 13)
} }

View File

@@ -0,0 +1,78 @@
//
// CreateMeetingView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 25/09/2025.
//
import SwiftUI
struct CreateMeetingView: View {
@State var date: Date = .init()
@State var selectedOption: Int? = nil
var body: some View {
ScrollView {
VStack {
VStack(alignment: .leading) {
SQText("Les avantages de la prise de “rendez-vous” :")
.sqSize(14)
.sqFont(.bold)
VStack(alignment: .leading) {
HStack {
SQText(" . ")
SQText("Engagement mutuel")
}
HStack {
SQText(" . ")
SQText("Ajout à lagenda personnel")
}
HStack {
SQText(" . ")
SQText("Rappels SMS avant le rendez-vous")
}
HStack {
SQText(" . ")
SQText("Lancement de la navigation GPS")
}
HStack {
SQText(" . ")
SQText("Rappel “Laisser un avis” après le rendez-vous")
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.sqNeutral(10))
.cornerRadius(8)
VStack(alignment: .leading) {
SQDatePickerView("Date", placeholder: "")
HStack(spacing: 16) {
SQDatePickerView("Date", placeholder: "")
SQDatePickerView("Date", placeholder: "")
}
SQRadio(title: "Modalités du rendez-vous", options: [
SQRadioOption(title: "Appel vocal ou vidéo"),
SQRadioOption(title: "Adresse de la demande"),
SQRadioOption(title: "Chez {display_name_offreur}", desc: "2 rue de Rivoli, 75004 Paris"),
SQRadioOption(title: "À une autre adresse"),
], selectedIndex: $selectedOption)
SQTextEditor("Informations pratiques", placeholder: "Ex : Itinéraire, étage, digicode, interphone ...", text: .constant(""), isOptional: true, maxCharacters: 500)
SQCheckbox("Je comprends que l'adresse et les informations pratiques seront communiquées lorsque le rendez-vous sera accepté par {display_name_offreur}.", isChecked: .constant(true))
}
SQButton("Proposer le rendez-vous") {
}
}
.padding(.horizontal)
}
.sqNavigationBar(title: "Proposer un rendez-vous", style: .modal)
}
}
#Preview {
NavigationView {
CreateMeetingView()
}
}

View File

@@ -0,0 +1,40 @@
//
// MessagingBlurView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 23/07/2025.
//
import SwiftUI
struct MessagingBlurView: View {
var body: some View {
ZStack {
Image("messaging")
VStack(spacing: 4) {
Spacer()
SQIcon(.eye_slash, size: .xl, color: .white)
SQText("Par mesure de sécurité, nous avons suspendu le compte de cet utilisateur.")
.sqSize(18)
.sqFont(.semiBold)
.sqColor(.white)
.multilineTextAlignment(.center)
.padding(20)
SQButton("Afficher") {}
.sqButtonType(.line)
.sqColorScheme(.white)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
Color.sqNeutral(100)
.opacity(0.5)
)
.background(.ultraThinMaterial)
}
}
}
#Preview {
MessagingBlurView()
}

View File

@@ -12,7 +12,9 @@ struct MoreNeighborsSelectedBadge: View {
var body: some View { var body: some View {
HStack(alignment: .center, spacing: 10) { HStack(alignment: .center, spacing: 10) {
SQText("+\(selectedNeighbors)", font: .bold, textColor: .sqNeutral(50)) SQText("+\(selectedNeighbors)")
.sqFont(.bold)
.sqColor(.sqNeutral(50))
} }
.frame(width: 32, height: 32, alignment: .center) .frame(width: 32, height: 32, alignment: .center)
.background(Color.sqNeutral(20)) .background(Color.sqNeutral(20))

View File

@@ -26,7 +26,8 @@ struct NeighborsSelectionFooter: View {
size: .l, size: .l,
isFull: false isFull: false
) {} ) {}
SQText("Sélectionner", font: .demiBold) SQText("Sélectionner")
.sqFont(.semiBold)
} }
Spacer() Spacer()
SQCircleButton( SQCircleButton(
@@ -51,7 +52,9 @@ struct NeighborsSelectionFooter: View {
} }
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
SQText("Nous vous recommandons de sélectionner 3 offreurs :", size: 13, font: .bold) SQText("Nous vous recommandons de sélectionner 3 offreurs :")
.sqSize(13)
.sqFont(.bold)
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) { HStack(spacing: 8) {
SQImage("neighbor_avatar", height: 32) SQImage("neighbor_avatar", height: 32)

View File

@@ -13,10 +13,17 @@ struct NeihborsCard: View {
HStack(spacing: 8) { 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)) 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) { VStack(alignment: .leading) {
SQText("Julien L.", font: .demiBold) SQText("Julien L.")
SQText("Julien, Petits Jobs À Nantes", size: 12, font: .demiBold) .sqFont(.semiBold)
SQText("Nantes (Gloriette-Feydeaux) - 3,1 km", size: 12, textColor: .sqNeutral(80)) SQText("Julien, Petits Jobs À Nantes")
SQText("En ligne", size: 12, textColor: .sqNeutral(80)) .sqSize(12)
.sqFont(.semiBold)
SQText("Nantes (Gloriette-Feydeaux) - 3,1 km")
.sqSize(12)
.sqColor(.sqNeutral(80))
SQText("En ligne")
.sqSize(12)
.sqColor(.sqNeutral(80))
} }
Spacer() Spacer()
} }
@@ -28,7 +35,8 @@ struct NeihborsCard: View {
HStack { HStack {
SQIcon(.star, type: .solid, color: .sqGold(50)) SQIcon(.star, type: .solid, color: .sqGold(50))
SQText("4,5/5", font: .bold) SQText("4,5/5")
.sqFont(.bold)
SQText("(35 avis)") SQText("(35 avis)")
} }
} }

View File

@@ -14,7 +14,8 @@ struct CategorySelectorView: View {
SQText("Zone de texte sur la partie haute de lécran, pouvant être sur plusieurs lignes.") SQText("Zone de texte sur la partie haute de lécran, pouvant être sur plusieurs lignes.")
HStack(alignment: .top, spacing: 0) { HStack(alignment: .top, spacing: 0) {
VStack { VStack {
SQText("Services", font: .demiBold) SQText("Services")
.sqFont(.semiBold)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
Rectangle() Rectangle()
.fill(Color.sqNeutral(100)) .fill(Color.sqNeutral(100))
@@ -22,7 +23,8 @@ struct CategorySelectorView: View {
.padding(.top, 1) .padding(.top, 1)
} }
VStack { VStack {
SQText("Objets", font: .demiBold) SQText("Objets")
.sqFont(.semiBold)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
Rectangle() Rectangle()
.fill(Color.sqNeutral(20)) .fill(Color.sqNeutral(20))

View File

@@ -10,7 +10,8 @@ import SwiftUI
struct AllCategoriesCell: View { struct AllCategoriesCell: View {
var body: some View { var body: some View {
HStack { HStack {
SQText("Voir toutes les catégories", font: .demiBold) SQText("Voir toutes les catégories")
.sqFont(.semiBold)
Spacer() Spacer()
SQIcon(.chevron_right) SQIcon(.chevron_right)
} }

View File

@@ -13,7 +13,9 @@ struct PrestationCell: View {
SQIcon(.magnifying_glass) SQIcon(.magnifying_glass)
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("remplacement de vitre") SQText("remplacement de vitre")
SQText("dans Menuiserie - Huisserie - Agencement", size: 12, textColor: .sqNeutral(60)) SQText("dans Menuiserie - Huisserie - Agencement")
.sqSize(12)
.sqColor(.sqNeutral(60))
} }
} }
} }

View File

@@ -13,7 +13,9 @@ struct PrestationHistorySearchCell: View {
SQIcon(.clock) SQIcon(.clock)
VStack(alignment: .leading) { VStack(alignment: .leading) {
SQText("remplacement de vitre") SQText("remplacement de vitre")
SQText("dans Menuiserie - Huisserie - Agencement", size: 12, textColor: .sqNeutral(60)) SQText("dans Menuiserie - Huisserie - Agencement")
.sqSize(12)
.sqColor(.sqNeutral(60))
} }
Spacer() Spacer()
SQIcon(.xmark) SQIcon(.xmark)

View File

@@ -35,14 +35,18 @@ struct PrestationSearchView: View {
Divider() Divider()
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQText("Mes dernières recherches", size: 18, font: .bold) SQText("Mes dernières recherches")
.sqSize(18)
.sqFont(.bold)
ForEach(0..<3) { _ in ForEach(0..<3) { _ in
PrestationHistorySearchCell() PrestationHistorySearchCell()
} }
} }
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQText("Recherches les plus populaires", size: 18, font: .bold) SQText("Recherches les plus populaires")
.sqSize(18)
.sqFont(.bold)
ForEach(0..<3) { _ in ForEach(0..<3) { _ in
PrestationCell() PrestationCell()
} }

View File

@@ -0,0 +1,59 @@
//
// AssurancesView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 20/11/2025.
//
import SwiftUI
struct AssurancesView: View {
@State private var selectedOption = PickerOption(text: "Responsabilité Civile Professionnelle")
private let options: [PickerOption] = [
PickerOption(text: "Responsabilité Civile Professionnelle"),
PickerOption(text: "Garantie décennale"),
PickerOption(text: "Autre"),
]
var body: some View {
VStack(spacing: 16) {
SQText("Ajouter une assurance")
.sqFont(.bold)
.sqSize(18)
VStack(alignment: .leading, spacing: 4) {
SQText("Type dassurance")
.sqFont(.semiBold)
.sqColor(.sqNeutral(80))
SQPicker(selection: $selectedOption, options: options)
}
SQDatePickerView("Date d'expiration", placeholder: "JJ/MM/AAAA")
if true {
DocumentPreviewView(fileName: "garantie-decennale.pdf") {}
} else {
SQButton("Importer une attestation") {}
.sqIcon(.init(.inbox_in))
.sqButtonType(.line)
}
SQText("Formats de fichier acceptés : jpg, pdf. Votre fichier ne doit pas dépasser un poids maximum de 7Mo.")
.sqSize(14)
.sqColor(.sqNeutral(80))
SQCheckbox("Je certifie lexactitude des éléments renseignés et je mengage à fournir mes attestations dassurance à première demande.", isChecked: .constant(false), error: .constant(SQFormFieldError.none))
.padding(.top, 16)
SQButton("Enregistrer") {}
Spacer()
}
.padding()
}
}
#Preview {
NavigationView {
AssurancesView()
}
}

View File

@@ -0,0 +1,66 @@
//
// ProfileAnswerToCell.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 18/08/2025.
//
import SwiftUI
struct ProfileAnswerToCell: View {
var body: some View {
VStack(spacing: 8) {
VStack(alignment: .leading, spacing: 8) {
HStack {
SQText("Répond aux demandes de :")
.sqFont(.semiBold)
Spacer()
SQText("Abonné Premier")
.sqSize(13)
.sqFont(.semiBold)
.sqColor(.sqOrange(70))
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(Color.sqOrange(20))
.cornerRadius(4)
}
VStack(alignment: .leading, spacing: 4) {
HStack {
SQText("")
SQText("Peinture - Tapisserie")
.sqColor(.sqNeutral(80))
SQText("(43 réponses)")
.sqSize(13)
.sqColor(.sqNeutral(80))
}
HStack {
SQText("")
SQText("Aide à domicile")
.sqColor(.sqNeutral(80))
SQText("(4 réponses)")
.sqSize(13)
.sqColor(.sqNeutral(80))
}
HStack {
SQText("")
SQText("Livraison de courses")
.sqColor(.sqNeutral(80))
SQText("(2 réponses)")
.sqSize(13)
.sqColor(.sqNeutral(80))
}
}
}
SQButton("Voir tout") {
}
.sqTextSize(13)
.sqButtonType(.line)
}
}
}
#Preview {
ProfileAnswerToCell()
.padding()
}

View File

@@ -0,0 +1,53 @@
//
// ProfileDisponibilityCell.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 18/08/2025.
//
import SwiftUI
struct ProfileDisponibilityCell: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
SQText("Informations pratiques")
.sqSize(18)
.sqFont(.semiBold)
VStack(alignment: .leading, spacing: 8) {
SQText("Informations tarifaires et promotionnelles")
.sqFont(.semiBold)
SQText("Mes devis sont toujours gratuits, et joffre 5% à ma clientèle refaisant appel à moi pour leur futur projet.")
}
SQDivider()
VStack(alignment: .leading, spacing: 8) {
SQText("Disponiblités")
.sqFont(.semiBold)
HStack(spacing: 24) {
VStack(alignment: .leading, spacing: 8) {
SQText("Lundi :")
SQText("Mardi :")
SQText("Mercredi :")
SQText("Jeudi :")
SQText("Vendredi :")
SQText("Samedi :")
SQText("Dimanche :")
}
VStack(alignment: .leading, spacing: 8) {
SQText("Non renseigné")
SQText("Non renseigné")
SQText("Non renseigné")
SQText("Non renseigné")
SQText("Non renseigné")
SQText("Non renseigné")
SQText("Non renseigné")
}
}
}
}
}
}
#Preview {
ProfileDisponibilityCell()
.padding()
}

View File

@@ -0,0 +1,112 @@
//
// ProfileInformationsCell.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 18/08/2025.
//
import SwiftUI
struct PopoverModel: Identifiable {
var id: String { message }
let message: String
}
struct ProfileInformationsCell: View {
@State private var popover: PopoverModel?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
SQText("Informations administratives et vérifications")
.sqSize(18)
.sqFont(.semiBold)
VStack(alignment: .leading) {
SQText("Vérifications")
.sqFont(.semiBold)
HStack(spacing: 8) {
SQIcon(.circle_check, color: .sqGreen(50))
SQText("Adresse e-mail")
}
HStack(spacing: 8) {
SQIcon(.circle_check, color: .sqGreen(50))
SQText("Téléphone")
}
HStack(spacing: 8) {
SQIcon(.circle_check, color: .sqGreen(50))
SQText("Identité")
}
HStack {
Spacer()
SQButton("Appeler") {}
.sqIcon(SQIcon(.phone))
.sqButtonType(.line)
.sqTextSize(13)
Spacer()
}
}
SQDivider()
VStack(alignment: .leading, spacing: 8) {
SQText("Informations légales")
.sqFont(.semiBold)
HStack(alignment: .top) {
SQText("")
SQText("Numéro SIRET : 869 322 037 - 0043")
}
HStack(alignment: .top) {
SQText("")
SQText("Forme juridique : Société à responsabilité limitée (SARL)")
}
HStack(alignment: .top) {
SQText("")
SQText("Entreprise créée en septembre 2022")
}
}
SQDivider()
VStack(alignment: .leading, spacing: 8) {
HStack {
SQText("Assurances")
.sqFont(.semiBold)
Button() {
popover = PopoverModel(message: "Cet utilisateur a renseigné ses attestations dassurance et sengage à vous les fournir à tout moment.")
} label: {
SQIcon(.circle_info)
}
.popover(item: $popover, arrowEdge: .bottom) { detail in
Text("\(detail.message)")
.padding()
}
}
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .top) {
SQText("")
SQText("Responsabilité Civile Professionnelle")
}
HStack(alignment: .top) {
SQText("")
SQText("Garantie décennale")
}
}
}
SQDivider()
VStack(alignment: .leading, spacing: 8) {
SQText("Certifications / Diplômes")
.sqFont(.semiBold)
VStack(alignment: .leading) {
SQText("BEP de plombier chauffagiste")
SQText("Date de délivrance : mars 2024")
.sqSize(12)
}
VStack(alignment: .leading) {
SQText("Attestation fluidique cat:1")
SQText("Date de délivrance : février 2020")
.sqSize(12)
}
}
}
}
}
#Preview {
ProfileInformationsCell()
.padding()
}

View File

@@ -0,0 +1,37 @@
//
// ProfilePresentationCell.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 18/08/2025.
//
import SwiftUI
struct ProfilePresentationCell: View {
var presentationText: String
var body: some View {
VStack(alignment: .leading) {
HStack {
SQText("« \(presentationText) »")
.sqSize(18)
.multilineTextAlignment(.leading)
Spacer()
}
.frame(maxWidth: .infinity)
.padding(16)
.background {
Rectangle()
.fill(Color.sqNeutral(10))
}
.cornerRadius(8, corners: [.topLeft, .topRight, .bottomLeft])
}
.padding(16)
.background(Color.white)
}
}
#Preview {
ProfilePresentationCell(presentationText: "Hello zfopze greozj giorezjgogiijrzg iezjg ooijfzgio dfiozjg oirezj jeoizkg ok,ezkog, oeizg,o o,zojrezov oez,vov eziov ezio, ezoi,vio ez,vio,e ozv,oi,zirziii")
.padding()
}

View File

@@ -0,0 +1,44 @@
//
// ProfileStatisticsCell.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 18/08/2025.
//
import SwiftUI
struct ProfileStatisticsCell: View {
var body: some View {
HStack {
Spacer()
VStack(spacing: 4) {
HStack(spacing: 8) {
SQIcon(.user)
SQText("18 mars 2017")
.sqFont(.bold)
}
SQText("date dinscription")
.sqSize(13)
}
Spacer()
SQDivider()
.frame(height: 55)
Spacer()
VStack(spacing: 4) {
HStack(spacing: 8) {
SQIcon(.users)
SQText("-")
.sqFont(.bold)
}
SQText("mises en relation")
.sqSize(13)
}
Spacer()
}
}
}
#Preview {
ProfileStatisticsCell()
.padding()
}

View File

@@ -0,0 +1,58 @@
//
// WelcomeProModal.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 13/10/2025.
//
import SwiftUI
struct WelcomeProModal: View {
var body: some View {
VStack(spacing: 48) {
VStack(spacing: 12) {
SQText("Bienvenue sur")
.sqSize(32)
.sqFont(.bold)
SQImage("logo-pro-color", height: 47)
}
HStack {
VStack(alignment: .leading, spacing: 16) {
SQText("Vous pouvez désormais :")
.sqSize(18)
.sqFont(.semiBold)
HStack(alignment: .top, spacing: 8) {
SQIcon(.check, color: .sqGrape())
SQText("Répondre à toutes les demandes, dont celles réservées aux pros")
}
HStack(alignment: .top, spacing: 8) {
SQIcon(.check, color: .sqGrape())
SQText("Créer vos supports de communication personnalisés (cartes de visite et prospectus)")
}
HStack(alignment: .top, spacing: 8) {
SQIcon(.check, color: .sqGrape())
SQText("Utiliser notre logiciel de facturation")
}
}
Spacer()
}
.padding(16)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color.sqNeutral(20), lineWidth: 1)
)
Spacer()
SQButton("Cest parti !") {}
.sqColorScheme(.grape)
}
.sqNavigationBar(title: "")
.padding(16)
}
}
#Preview {
WelcomeProModal()
}

View File

@@ -12,13 +12,17 @@ struct ReportConfirmationView: View {
VStack(spacing: 16) { VStack(spacing: 16) {
VStack { VStack {
SQIcon(.check, customSize: 64, type: .solid, color: .sqSemanticGreen) SQIcon(.check, customSize: 64, type: .solid, color: .sqSemanticGreen)
SQText("Votre signalement a bien été pris en compte.", size: 18, font: .bold) SQText("Votre signalement a bien été pris en compte.")
.sqSize(18)
.sqFont(.bold)
} }
VStack(alignment: .leading, spacing: 16) { 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) 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.")
.sqFont(.semiBold)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
SQText("Souhaitez-vous bloquer cet utilisateur ?", font: .demiBold) SQText("Souhaitez-vous bloquer cet utilisateur ?")
.sqFont(.semiBold)
HStack { HStack {
Toggle(isOn: .constant(true)) { Toggle(isOn: .constant(true)) {
SQText("En bloquant cet utilisateur, il ne pourra plus vous contacter.") SQText("En bloquant cet utilisateur, il ne pourra plus vous contacter.")

Some files were not shown because too many files have changed in this diff Show More