✨New views
This commit is contained in:
@@ -199,7 +199,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
@@ -256,7 +256,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
|
||||
@@ -12,7 +12,7 @@ struct AlloVoisinsSwiftUIApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
NavigationStack {
|
||||
EmptyView()
|
||||
DebugLandView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
AlloVoisinsSwiftUI/Core/Managers/BaseNavigationManager.swift
Normal file
49
AlloVoisinsSwiftUI/Core/Managers/BaseNavigationManager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,11 @@ struct MessageNotificationView: View {
|
||||
AVAvatar(model: AVAvatarModel(avatarString: notification.avatarUrl, onlineStatus: .online(), sizing: .M))
|
||||
.frame(width: 48, height: 48)
|
||||
VStack(alignment: .leading) {
|
||||
SQText(notification.displayName, font: .demiBold)
|
||||
SQText(notification.message, size: 14, textColor: .sqNeutral(70))
|
||||
SQText(notification.displayName)
|
||||
.sqFont(.semiBold)
|
||||
SQText(notification.message)
|
||||
.sqSize(14)
|
||||
.sqColor(.sqNeutral(70))
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
@@ -12,13 +12,15 @@ struct OnlyForPremierView: View {
|
||||
var body: some View {
|
||||
VStack(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é.")
|
||||
}
|
||||
SQButton( "Découvrir l’abonnement Premier") {
|
||||
|
||||
}
|
||||
.colorScheme(.orange)
|
||||
.sqColorScheme(.orange)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
@@ -27,26 +27,33 @@ struct OnlyForPremierModal: View {
|
||||
Color.white
|
||||
VStack(spacing: 48) {
|
||||
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)
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQText("Augmentez votre chiffre d’affaires en bénéficiant de tous les avantages inclus dans l’abonnement Premier.", font: .bold)
|
||||
SQText("Augmentez votre chiffre d’affaires en bénéficiant de tous les avantages inclus dans l’abonnement Premier.")
|
||||
.sqFont(.bold)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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()
|
||||
VStack(spacing: 16) {
|
||||
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")
|
||||
}
|
||||
SQButton("Je m'abonne") {
|
||||
|
||||
}
|
||||
.colorScheme(.orange)
|
||||
.sqColorScheme(.orange)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 24)
|
||||
|
||||
@@ -12,34 +12,38 @@ struct RegulatedProfessionEditProfilModal: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
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) {
|
||||
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. L’activité de déménageur avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent l’exercer.", 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.", 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. L’activité de déménageur avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent l’exercer.")
|
||||
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) {
|
||||
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 {
|
||||
SQIcon(.xmark, size: .m, type: .solid, color: .red)
|
||||
SQText("Titre du profil", font: .medium)
|
||||
SQText("Titre du profil")
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.xmark, size: .m, type: .solid, color: .red)
|
||||
SQText("Présentation profil", font: .medium)
|
||||
SQText("Présentation profil")
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.xmark, size: .m, type: .solid, color: .red)
|
||||
SQText("Photo de couverture", font: .medium)
|
||||
SQText("Photo de couverture")
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.xmark, size: .m, type: .solid, color: .red)
|
||||
SQText("Photo de profil", font: .medium)
|
||||
SQText("Photo de profil")
|
||||
}
|
||||
HStack {
|
||||
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éé.")
|
||||
// SQText("[Retrouvez ici](https://www.codingwithrashid.com) les informations utiles pour devenir déménageur agréé.", font: .medium)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,15 @@ struct RegulatedProfessionModal: View {
|
||||
var body: some View {
|
||||
VStack() {
|
||||
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) {
|
||||
SQText("L’activité de déménagement avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent l’exercer.")
|
||||
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("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)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ struct NeighborBanner: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
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))
|
||||
.padding(16)
|
||||
|
||||
@@ -12,12 +12,15 @@ struct SQAddressModifyLine: View {
|
||||
let attributedText = NSAttributedString("Modifier")
|
||||
|
||||
HStack {
|
||||
SQText("à 8 allée Baco, 44000 Nantes", size: 14)
|
||||
SQText("à 8 allée Baco, 44000 Nantes")
|
||||
.sqSize(14)
|
||||
Spacer()
|
||||
Button {
|
||||
print("Modifier")
|
||||
} label: {
|
||||
SQText("Modifier", size: 14, font: .bold)
|
||||
SQText("Modifier")
|
||||
.sqSize(14)
|
||||
.sqFont(.bold)
|
||||
.underline(true, pattern: .solid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,14 @@ struct SQButton: View {
|
||||
@State private var type: SQButtonType = .solid
|
||||
@State private var colorScheme: SQButtonColorScheme = .neutral
|
||||
@State private var textSize: CGFloat = 16
|
||||
@State private var font: SQTextFont = .demiBold
|
||||
@State private var font: SQTextFont = .semiBold
|
||||
@State private var icon: SQIcon? = nil
|
||||
@State private var isLoading: Bool = false
|
||||
@State private var isDisabled: Bool = false
|
||||
@State private var isPressed: Bool = false
|
||||
@State private var isLarge: Bool = false
|
||||
|
||||
@Binding var isLoading: Bool
|
||||
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
private var textWidth: CGFloat {
|
||||
let font = UIFont.systemFont(ofSize: textSize)
|
||||
@@ -27,8 +31,12 @@ struct SQButton: View {
|
||||
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._isLoading = isLoading
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@@ -37,13 +45,14 @@ struct SQButton: View {
|
||||
type: type,
|
||||
colorScheme: colorScheme,
|
||||
isLoading: isLoading,
|
||||
isDisabled: isDisabled
|
||||
isDisabled: !isEnabled,
|
||||
isPressed: isPressed
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
if !isLoading && !isDisabled {
|
||||
if !isLoading, !isEnabled {
|
||||
action()
|
||||
}
|
||||
}, label: {
|
||||
@@ -58,10 +67,14 @@ struct SQButton: View {
|
||||
.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(maxWidth: isLarge ? .infinity : nil, alignment: .center)
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.vertical, 12)
|
||||
.frame(height: 40, alignment: .center)
|
||||
@@ -73,51 +86,57 @@ struct SQButton: View {
|
||||
.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
|
||||
|
||||
extension SQButton {
|
||||
func buttonType(_ type: SQButtonType) -> SQButton {
|
||||
func sqButtonType(_ type: SQButtonType) -> SQButton {
|
||||
var copy = self
|
||||
copy._type = State(initialValue: type)
|
||||
return copy
|
||||
}
|
||||
|
||||
func colorScheme(_ scheme: SQButtonColorScheme) -> SQButton {
|
||||
func sqColorScheme(_ scheme: SQButtonColorScheme) -> SQButton {
|
||||
var copy = self
|
||||
copy._colorScheme = State(initialValue: scheme)
|
||||
return copy
|
||||
}
|
||||
|
||||
func textSize(_ size: CGFloat) -> SQButton {
|
||||
func sqTextSize(_ size: CGFloat) -> SQButton {
|
||||
var copy = self
|
||||
copy._textSize = State(initialValue: size)
|
||||
return copy
|
||||
}
|
||||
|
||||
func font(_ font: SQTextFont) -> SQButton {
|
||||
func sqFont(_ font: SQTextFont) -> SQButton {
|
||||
var copy = self
|
||||
copy._font = State(initialValue: font)
|
||||
return copy
|
||||
}
|
||||
|
||||
func icon(_ icon: SQIcon?) -> SQButton {
|
||||
func sqIcon(_ icon: SQIcon?) -> SQButton {
|
||||
var copy = self
|
||||
copy._icon = State(initialValue: icon)
|
||||
return copy
|
||||
}
|
||||
|
||||
func loading(_ isLoading: Bool) -> SQButton {
|
||||
func sqLarge(_ isLarge: Bool = true) -> SQButton {
|
||||
var copy = self
|
||||
copy._isLoading = State(initialValue: isLoading)
|
||||
return copy
|
||||
}
|
||||
|
||||
func disabled(_ isDisabled: Bool) -> SQButton {
|
||||
var copy = self
|
||||
copy._isDisabled = State(initialValue: isDisabled)
|
||||
copy._isLarge = State(initialValue: isLarge)
|
||||
return copy
|
||||
}
|
||||
}
|
||||
@@ -143,6 +162,7 @@ enum SQButtonColorScheme: String, CaseIterable {
|
||||
case grape
|
||||
case forest
|
||||
case royal
|
||||
case white
|
||||
|
||||
var baseColor: Color {
|
||||
switch self {
|
||||
@@ -168,6 +188,8 @@ enum SQButtonColorScheme: String, CaseIterable {
|
||||
return .sqForest(100)
|
||||
case .royal:
|
||||
return .sqRoyal(60)
|
||||
case .white:
|
||||
return .white
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +217,8 @@ enum SQButtonColorScheme: String, CaseIterable {
|
||||
return .sqForest(50)
|
||||
case .royal:
|
||||
return .sqRoyal(30)
|
||||
case .white:
|
||||
return .white
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +246,8 @@ enum SQButtonColorScheme: String, CaseIterable {
|
||||
return .sqForest(10)
|
||||
case .royal:
|
||||
return .sqRoyal(10)
|
||||
case .white:
|
||||
return .white
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,8 +257,22 @@ struct SQButtonStyle {
|
||||
let colorScheme: SQButtonColorScheme
|
||||
let isLoading: Bool
|
||||
let isDisabled: Bool
|
||||
let isPressed: Bool
|
||||
|
||||
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 {
|
||||
switch type {
|
||||
case .solid:
|
||||
@@ -273,8 +313,11 @@ struct SQButtonStyle {
|
||||
|
||||
var borderColor: Color {
|
||||
switch type {
|
||||
|
||||
case .solid:
|
||||
if isPressed && !isDisabled && !isLoading {
|
||||
return colorScheme.baseColor.opacity(0.8)
|
||||
}
|
||||
|
||||
if isDisabled {
|
||||
return colorScheme.lightColor.opacity(0.5)
|
||||
}
|
||||
@@ -282,7 +325,12 @@ struct SQButtonStyle {
|
||||
if isLoading {
|
||||
return colorScheme.lightColor.opacity(0.8)
|
||||
}
|
||||
|
||||
case .line:
|
||||
if isPressed && !isDisabled && !isLoading {
|
||||
return colorScheme.baseColor.opacity(0.8)
|
||||
}
|
||||
|
||||
if isDisabled {
|
||||
return colorScheme.mediumColor
|
||||
}
|
||||
@@ -294,6 +342,10 @@ struct SQButtonStyle {
|
||||
return colorScheme.baseColor
|
||||
|
||||
case .light:
|
||||
if isPressed && !isDisabled && !isLoading {
|
||||
return colorScheme.lightColor.opacity(0.8)
|
||||
}
|
||||
|
||||
if isDisabled {
|
||||
return colorScheme.lightColor.opacity(0.5)
|
||||
}
|
||||
@@ -312,6 +364,15 @@ struct SQButtonStyle {
|
||||
}
|
||||
|
||||
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 {
|
||||
switch type {
|
||||
case .solid:
|
||||
@@ -335,67 +396,37 @@ struct SQButtonStyle {
|
||||
HStack {
|
||||
VStack(spacing: 16) {
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.sqColorScheme(.royal)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.line)
|
||||
.sqColorScheme(.royal)
|
||||
.sqButtonType(.line)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.light)
|
||||
.sqColorScheme(.royal)
|
||||
.sqButtonType(.light)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.glass)
|
||||
.sqColorScheme(.royal)
|
||||
.sqButtonType(.glass)
|
||||
}
|
||||
VStack(spacing: 16) {
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.loading(true)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.line)
|
||||
.loading(true)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.light)
|
||||
.loading(true)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.glass)
|
||||
.loading(true)
|
||||
SQButton("C'est parti !", isLoading: .constant(true)) {}
|
||||
.sqColorScheme(.royal)
|
||||
SQButton("C'est parti !", isLoading: .constant(true)) {}
|
||||
.sqColorScheme(.royal)
|
||||
.sqButtonType(.line)
|
||||
SQButton("C'est parti !", isLoading: .constant(true)) {}
|
||||
.sqColorScheme(.royal)
|
||||
.sqButtonType(.light)
|
||||
SQButton("C'est parti !", isLoading: .constant(true)) {}
|
||||
.sqColorScheme(.royal)
|
||||
.sqButtonType(.glass)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
VStack(spacing: 16) {
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.line)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.light)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.glass)
|
||||
}
|
||||
VStack(spacing: 16) {
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.disabled(true)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.line)
|
||||
.disabled(true)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.light)
|
||||
.disabled(true)
|
||||
SQButton("C'est parti !") {}
|
||||
.colorScheme(.royal)
|
||||
.buttonType(.glass)
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
SQButton("Continuer avec Apple") {}
|
||||
.sqButtonType(.line)
|
||||
.sqIcon(SQIcon(.apple_brand))
|
||||
.sqLarge()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,10 @@ struct SQCheckbox: View {
|
||||
|
||||
HStack {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ fileprivate struct SQChip: 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(.vertical, 8)
|
||||
.background(isSelected ? option.bgColor : Color.clear)
|
||||
|
||||
@@ -72,7 +72,9 @@ struct SQCircleButton: View {
|
||||
.shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 4)
|
||||
|
||||
if !text.isEmpty {
|
||||
SQText(text, font: .demiBold, textColor: .sqNeutral(80))
|
||||
SQText(text)
|
||||
.sqFont(.semiBold)
|
||||
.sqColor(.sqNeutral(80))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ struct SQColorPicker: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading) {
|
||||
SQText(title, size: 18, font: .bold)
|
||||
SQText(title)
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
SQText(subtitle ?? "")
|
||||
.lineLimit(2)
|
||||
.lineSpacing(1)
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -21,7 +21,8 @@ struct SQImagePicker: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
SQText("Images", font: .demiBold)
|
||||
SQText("Images")
|
||||
.sqFont(.semiBold)
|
||||
|
||||
LazyVGrid(columns: columns, alignment: .leading) {
|
||||
ForEach(0 ..< nbOfImages) { _ in
|
||||
@@ -98,7 +99,9 @@ struct SQImagePickerSelectorSheet: View {
|
||||
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)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ struct SQNavigationBar: ViewModifier {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
SQText(title, size: 18, font: .bold)
|
||||
SQText(title)
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(style.foregroundColor)
|
||||
}
|
||||
if backAction != nil {
|
||||
|
||||
@@ -59,14 +59,14 @@ struct MenuPicker: UIViewRepresentable {
|
||||
let options: [PickerOption]
|
||||
@Binding var isPresenting: Bool
|
||||
|
||||
func makeUIView(context: Context) -> UIButton {
|
||||
func makeUIView(context _: Context) -> UIButton {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitle("", for: .normal)
|
||||
updateMenu(button)
|
||||
return button
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIButton, context: Context) {
|
||||
func updateUIView(_ uiView: UIButton, context _: Context) {
|
||||
updateMenu(uiView)
|
||||
if isPresenting {
|
||||
uiView.sendActions(for: .menuActionTriggered)
|
||||
@@ -106,7 +106,7 @@ struct SQPickerPreview: View {
|
||||
let options = [
|
||||
PickerOption(text: "1 mois"),
|
||||
PickerOption(text: "2 mois"),
|
||||
PickerOption(text: "3 mois")
|
||||
PickerOption(text: "3 mois"),
|
||||
]
|
||||
_selectedDuration = State(initialValue: options[0])
|
||||
durationOptions = options
|
||||
@@ -119,8 +119,5 @@ struct SQPickerPreview: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ZStack {
|
||||
Color.black
|
||||
SQPickerPreview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import SwiftUI
|
||||
struct SQProgressIndicator: View {
|
||||
@Binding var isActivated: Bool
|
||||
|
||||
@State private var colorScheme: SQProgressIndicatorColorScheme = .neutral
|
||||
|
||||
init(_ isActivated: Binding<Bool>) {
|
||||
self._isActivated = isActivated
|
||||
}
|
||||
@@ -18,11 +20,52 @@ struct SQProgressIndicator: View {
|
||||
Rectangle()
|
||||
.foregroundColor(.clear)
|
||||
.frame(maxWidth: .infinity, minHeight: 4, maxHeight: 4)
|
||||
.background(isActivated ? Color.sqSemanticGreen : Color.sqNeutral(20))
|
||||
.background(isActivated ? colorScheme.baseColor : Color.sqNeutral(20))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SQProgressIndicator(.constant(true))
|
||||
// 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 {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ struct SQRadio: View {
|
||||
var body: some View {
|
||||
VStack(alignment: orientation == .vertical ? .leading : .center, spacing: 16) {
|
||||
if let title = title {
|
||||
SQText(title, size: titleSize, font: .bold)
|
||||
SQText(title)
|
||||
.sqSize(titleSize)
|
||||
.sqFont(.bold)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
Group {
|
||||
@@ -61,7 +63,10 @@ struct SQRadio: View {
|
||||
}
|
||||
|
||||
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 {
|
||||
SQText(desc, size: 12)
|
||||
SQText(desc)
|
||||
.sqSize(12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
struct SQTextEditor: View {
|
||||
var label: String
|
||||
var placeholder: String
|
||||
var type: SQTextFieldType = .text
|
||||
var error: Binding<SQFormFieldError>
|
||||
var icon: SQIcon?
|
||||
var isDisabled: Bool = false
|
||||
@@ -28,7 +27,6 @@ struct SQTextEditor: View {
|
||||
|
||||
init(_ label: String,
|
||||
placeholder: String,
|
||||
type: SQTextFieldType = .text,
|
||||
error: Binding<SQFormFieldError> = .constant(.none),
|
||||
text: Binding<String>,
|
||||
icon: SQIcon? = nil,
|
||||
@@ -42,7 +40,6 @@ struct SQTextEditor: View {
|
||||
{
|
||||
self.label = label
|
||||
self.placeholder = placeholder
|
||||
self.type = type
|
||||
self.error = error
|
||||
self._text = text
|
||||
self.icon = icon
|
||||
@@ -61,7 +58,9 @@ struct SQTextEditor: View {
|
||||
SQText(label)
|
||||
Spacer()
|
||||
if isOptional {
|
||||
SQText("Optionnel", size: 12, textColor: .sqNeutral(50))
|
||||
SQText("Optionnel")
|
||||
.sqSize(12)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
}
|
||||
TextEditor(text: Binding(
|
||||
@@ -87,12 +86,17 @@ struct SQTextEditor: View {
|
||||
if error.wrappedValue.isInError {
|
||||
HStack(spacing: 8) {
|
||||
SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed)
|
||||
SQText(error.wrappedValue.message, size: 12, font: .demiBold, textColor: .sqSemanticRed)
|
||||
SQText(error.wrappedValue.message)
|
||||
.sqSize(12)
|
||||
.sqFont(.semiBold)
|
||||
.sqColor(.sqSemanticRed)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if !characterCountText.isEmpty {
|
||||
SQText(characterCountText, size: 12, textColor: .sqNeutral(50))
|
||||
SQText(characterCountText)
|
||||
.sqSize(12)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,12 @@ struct SQToast: View {
|
||||
HStack(spacing: 16) {
|
||||
VStack(alignment: .leading) {
|
||||
if !title.isEmpty {
|
||||
SQText(title, font: .bold)
|
||||
SQText(title)
|
||||
.sqFont(.bold)
|
||||
}
|
||||
SQText(content, size: 14, font: .demiBold)
|
||||
SQText(content)
|
||||
.sqSize(14)
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
Spacer()
|
||||
if hasClose {
|
||||
|
||||
@@ -21,7 +21,8 @@ struct SQTooltip: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
SQText(text, textColor: .white)
|
||||
SQText(text)
|
||||
.sqColor(.white)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(Constants.padding)
|
||||
.background(
|
||||
|
||||
@@ -16,7 +16,8 @@ struct SQSearchBar: View {
|
||||
SQIcon(.magnifying_glass, type: .solid)
|
||||
TextField("", text: $text)
|
||||
.placeholder(when: text.isEmpty) {
|
||||
SQText(placeholder, textColor: .sqNeutral(50))
|
||||
SQText(placeholder)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
.tint(Color.sqNeutral(100))
|
||||
}
|
||||
@@ -29,16 +30,3 @@ struct SQSearchBar: View {
|
||||
#Preview {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ struct SQSearchBarButton: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
SQIcon(.magnifying_glass, type: .solid)
|
||||
|
||||
SQText(placeholder, textColor: .sqNeutral(50))
|
||||
SQText(placeholder)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -18,9 +18,13 @@ struct BoosterConfirmationScreen: View {
|
||||
Image("booster_logo")
|
||||
.resizable()
|
||||
.frame(width: 210, height: 180)
|
||||
SQText("C’est confirmé !", size: 18, font: .bold)
|
||||
SQText("C’est confirmé !")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
.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)
|
||||
.multilineTextAlignment(.center)
|
||||
}.padding()
|
||||
@@ -29,7 +33,8 @@ struct BoosterConfirmationScreen: View {
|
||||
SQButton("Accéder à mon option") {
|
||||
|
||||
}
|
||||
SQText("XX,XX € / mois, sans engagement.", size: 13)
|
||||
SQText("XX,XX € / mois, sans engagement.")
|
||||
.sqSize(13)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,30 +13,40 @@ struct BoosterKnowAboutScreen: View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Comment ça marche ?", size: 18, font: .bold)
|
||||
SQText("Comment ça marche ?")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
SQText("L’option Booster s’active 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) {
|
||||
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) {
|
||||
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) {
|
||||
SQText("Cette option permet de remonter votre profil chaque jour dans les listes de résultats, à compter du lendemain de la souscription à l’option 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)
|
||||
SQText("3 jours par semaine", font: .demiBold)
|
||||
SQText("3 jours par semaine")
|
||||
.sqFont(.semiBold)
|
||||
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 à l’option 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)
|
||||
SQText("1 jour par semaine", font: .demiBold)
|
||||
SQText("1 jour par semaine")
|
||||
.sqFont(.semiBold)
|
||||
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 à l’option 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)
|
||||
}
|
||||
@@ -52,7 +62,9 @@ struct BoosterKnowAboutScreen: View {
|
||||
})
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
SQText("En savoir plus", size: 18, font: .bold)
|
||||
SQText("En savoir plus")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
@@ -17,7 +17,9 @@ struct BoosterSubscriptionManagementScreen: View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
BoosterActiveHeaderView()
|
||||
|
||||
SQText("Mes boosters", size: 20, font: .bold)
|
||||
SQText("Mes boosters")
|
||||
.sqSize(20)
|
||||
.sqFont(.bold)
|
||||
|
||||
Picker("", selection: $selectedValue) {
|
||||
SQText("À venir").tag(0)
|
||||
|
||||
@@ -82,7 +82,8 @@ struct BoosterSubscriptionSelectionScreen: View {
|
||||
SQButton("C'est parti !") {}
|
||||
if cancellation {
|
||||
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)
|
||||
})
|
||||
.padding()
|
||||
|
||||
@@ -15,9 +15,12 @@ struct BoosterActiveHeaderView: View {
|
||||
.resizable()
|
||||
.frame(width: 93, height: 80)
|
||||
VStack {
|
||||
SQText("Booster en cours", size: 18, font: .bold)
|
||||
SQText("Booster en cours")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.sqRoyal())
|
||||
SQText("Aujourd’hui", size: 14)
|
||||
SQText("Aujourd’hui")
|
||||
.sqSize(14)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
@@ -10,25 +10,31 @@ import SwiftUI
|
||||
struct BoosterFeaturesView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 24) {
|
||||
SQText("Boostez votre activité !", size: 32, font: .bold)
|
||||
SQText("Boostez votre activité !")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.white)
|
||||
VStack(alignment: .center) {
|
||||
VStack(alignment: .leading) {
|
||||
SQText("Votre profil est mis en avant:", font: .bold)
|
||||
SQText("Votre profil est mis en avant:")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.white)
|
||||
HStack {
|
||||
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)
|
||||
}
|
||||
HStack {
|
||||
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)
|
||||
}
|
||||
HStack {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ struct BoosterHistoryCellView: View {
|
||||
ZStack {
|
||||
HStack(alignment: .center) {
|
||||
SQIcon(.calendar, color: .sqRoyal())
|
||||
SQText("Samedi 20 avril", font: .bold)
|
||||
SQText("Samedi 20 avril")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.sqRoyal())
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
@@ -12,7 +12,8 @@ struct BoosterLockedToPremierView: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
SQIcon(.lock_keyhole, size: .xs, type: .solid, color: .white)
|
||||
SQText("Abonnés Premier", size: 13)
|
||||
SQText("Abonnés Premier")
|
||||
.sqSize(13)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,17 @@ struct BoosterPromotionView: View {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 16) {
|
||||
VStack {
|
||||
SQText("Envie de booster votre activité ?", font: .bold)
|
||||
SQText("Activez l’option Booster !", font: .demiBold)
|
||||
SQText("Envie de booster votre activité ?")
|
||||
.sqFont(.bold)
|
||||
SQText("Activez l’option Booster !")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
BoosterStatsView()
|
||||
|
||||
SQButton("Activer l’option Booster") {
|
||||
|
||||
}
|
||||
.colorScheme(.royal)
|
||||
.sqColorScheme(.royal)
|
||||
}
|
||||
.padding()
|
||||
.foregroundColor(.sqRoyal())
|
||||
|
||||
@@ -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)
|
||||
.foregroundColor(.sqRoyal())
|
||||
}
|
||||
|
||||
@@ -14,10 +14,16 @@ struct BoosterStatsView: View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
VStack {
|
||||
HStack(spacing: 0) {
|
||||
SQText("x", size: 24, font: .bold)
|
||||
SQText("3", size: 32, font: .bold)
|
||||
SQText("x")
|
||||
.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)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
@@ -29,10 +35,16 @@ struct BoosterStatsView: View {
|
||||
.frame(height: 72)
|
||||
VStack {
|
||||
HStack(spacing: 0) {
|
||||
SQText("x", size: 24, font: .bold)
|
||||
SQText("3", size: 32, font: .bold)
|
||||
SQText("x")
|
||||
.sqSize(24)
|
||||
.sqFont(.bold)
|
||||
SQText("3")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
}
|
||||
SQText("Évaluations*", size: 14, font: .demiBold)
|
||||
SQText("Évaluations*")
|
||||
.sqSize(14)
|
||||
.sqFont(.semiBold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
@@ -44,17 +56,24 @@ struct BoosterStatsView: View {
|
||||
.frame(height: 72)
|
||||
VStack {
|
||||
HStack(spacing: 0) {
|
||||
SQText("x", size: 24, font: .bold)
|
||||
SQText("2.9", size: 32, font: .bold)
|
||||
SQText("x")
|
||||
.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)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(minWidth: 100, maxWidth: .infinity)
|
||||
}
|
||||
.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)
|
||||
}
|
||||
.foregroundColor(.sqRoyal())
|
||||
|
||||
@@ -27,16 +27,20 @@ struct BoosterSubscriptionCardView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
if currentOption {
|
||||
SQText("Option actuelle", font: .bold)
|
||||
SQText("Option actuelle")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.sqRoyal())
|
||||
}
|
||||
ZStack(alignment: .topLeading) {
|
||||
VStack {
|
||||
SQText("3 jours", size: 32, font: .bold)
|
||||
SQText("3 jours")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.white)
|
||||
SQText("par semaine", size: 14)
|
||||
SQText("par semaine")
|
||||
.sqSize(14)
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.white)
|
||||
@@ -47,7 +51,8 @@ struct BoosterSubscriptionCardView: View {
|
||||
// .minimumScaleFactor(0.5)
|
||||
// .lineLimit(1)
|
||||
// .foregroundColor(.white)
|
||||
SQText("Sans engagement", size: 12)
|
||||
SQText("Sans engagement")
|
||||
.sqSize(12)
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.white)
|
||||
@@ -73,7 +78,8 @@ struct BoosterSubscriptionCardView: View {
|
||||
startPoint: .top, endPoint: .bottom))
|
||||
)
|
||||
if isFree {
|
||||
SQText("1 mois gratuit *", font: .bold)
|
||||
SQText("1 mois gratuit *")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.sqRoyal())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ struct BoosterSubscriptionOptionsView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
SQText("Booster", size: 18, font: .bold)
|
||||
SQText("Booster")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
Spacer()
|
||||
SQIcon(.xmark, color: .white)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ struct ProfileComplimentListView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
SQText("Compliments reçus", size: 18, font: .bold)
|
||||
SQText("Compliments reçus")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
.padding(.horizontal)
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
@@ -17,7 +17,9 @@ struct ComplimentPillView: View {
|
||||
.frame(width: 32, height: 32, alignment: .center)
|
||||
.background(Color.sqGreen(10))
|
||||
.cornerRadius(32)
|
||||
SQText("Excellent rapport qualité/prix", size: 12, font: .demiBold)
|
||||
SQText("Excellent rapport qualité/prix")
|
||||
.sqSize(12)
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
.padding(.leading, 1)
|
||||
.padding(.trailing, 16)
|
||||
|
||||
@@ -17,7 +17,9 @@ struct ComplimentView: View {
|
||||
.frame(width: 56, height: 56, alignment: .center)
|
||||
.background(Color.sqGreen(10))
|
||||
.cornerRadius(100)
|
||||
SQText("Excellent rapport qualité/prix", size: 11, font: .demiBold)
|
||||
SQText("Excellent rapport qualité/prix")
|
||||
.sqSize(11)
|
||||
.sqFont(.semiBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(Color.sqNeutral(90))
|
||||
}
|
||||
@@ -25,7 +27,9 @@ struct ComplimentView: View {
|
||||
.frame(width: 87)
|
||||
|
||||
HStack {
|
||||
SQText("1", size: 11, font: .bold)
|
||||
SQText("1")
|
||||
.sqSize(11)
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(Color.sqNeutral())
|
||||
.padding(.horizontal, 6)
|
||||
.frame(height: 16)
|
||||
|
||||
@@ -14,7 +14,9 @@ struct CurrentDebugUser: View {
|
||||
VStack {
|
||||
SQText("Harbin Leduc")
|
||||
SQText("Torphy LLC")
|
||||
SQText("Auto-entrepreneur", size: 12, font: .demiBold)
|
||||
SQText("Auto-entrepreneur")
|
||||
.sqSize(12)
|
||||
.sqFont(.semiBold)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 8)
|
||||
.background {
|
||||
@@ -24,7 +26,8 @@ struct CurrentDebugUser: View {
|
||||
}
|
||||
|
||||
HStack {
|
||||
SQText("UserID:", font: .demiBold)
|
||||
SQText("UserID:")
|
||||
.sqFont(.semiBold)
|
||||
Spacer()
|
||||
SQText("103135")
|
||||
Button {} label: {
|
||||
@@ -37,7 +40,8 @@ struct CurrentDebugUser: View {
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
SQText("Email:", font: .demiBold)
|
||||
SQText("Email:")
|
||||
.sqFont(.semiBold)
|
||||
Spacer()
|
||||
SQText("test@test.com")
|
||||
Button {} label: {
|
||||
@@ -50,7 +54,8 @@ struct CurrentDebugUser: View {
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
SQText("Téléphone:", font: .demiBold)
|
||||
SQText("Téléphone:")
|
||||
.sqFont(.semiBold)
|
||||
Spacer()
|
||||
SQText("0612345678")
|
||||
Button {} label: {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -23,16 +23,21 @@ struct ConfigPrestationSearchView: View {
|
||||
VStack {
|
||||
Spacer()
|
||||
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)
|
||||
Divider()
|
||||
SQText("Types à rechercher :", font: .demiBold)
|
||||
SQText("Types à rechercher :")
|
||||
.sqFont(.semiBold)
|
||||
SQPicker(selection: $selectedSearchType, options: searchTypes)
|
||||
Toggle(isOn: $showSuggested) {
|
||||
SQText("Afficher une suggestion", font: .demiBold)
|
||||
SQText("Afficher une suggestion")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
Toggle(isOn: $showAllCategories) {
|
||||
SQText("Afficher \"Toutes les catégories\"", font: .demiBold)
|
||||
SQText("Afficher \"Toutes les catégories\"")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
@@ -43,9 +43,21 @@ struct DebugLandView: View {
|
||||
CurrentDebugUser()
|
||||
|
||||
VStack {
|
||||
SQText("Environnement :", font: .demiBold)
|
||||
SQPicker(selection: $currentEnv, options: pickerOptions)
|
||||
SQText("Environnement :")
|
||||
.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()
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
@@ -54,16 +66,19 @@ struct DebugLandView: View {
|
||||
.padding(.horizontal)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("User ID :", font: .demiBold)
|
||||
SQText("User ID :")
|
||||
.sqFont(.semiBold)
|
||||
HStack(spacing: 8) {
|
||||
VStack {
|
||||
SQTextField("Visiter le profil :", placeholder: "UserID", text: $userId)
|
||||
SQTextField("Visiter le profil :", text: $userId)
|
||||
.sqPlaceholder("UserID")
|
||||
SQButton("Visiter le profil") {
|
||||
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
SQTextField("Se connecter sur :", placeholder: "UserID", text: $userId)
|
||||
SQTextField("Se connecter sur :", text: $userId)
|
||||
.sqPlaceholder("UserID")
|
||||
.keyboardType(.numberPad)
|
||||
SQButton("Se connecter") {
|
||||
|
||||
@@ -77,11 +92,24 @@ struct DebugLandView: View {
|
||||
.fill(Color.sqNeutral(10))
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
VStack {
|
||||
LazyHGrid(rows: [
|
||||
GridItem.init(),
|
||||
GridItem.init()
|
||||
]) {
|
||||
DebugActionView(title: "")
|
||||
DebugActionView(title: "")
|
||||
DebugActionView(title: "")
|
||||
DebugActionView(title: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
SQText("🦄 Debug Land 🦄", font: .bold)
|
||||
SQText("🦄 Debug Land 🦄")
|
||||
.sqFont(.bold)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
@@ -89,6 +117,11 @@ struct DebugLandView: View {
|
||||
SQIcon(.user_group)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {} label: {
|
||||
SQIcon(.ballot_check)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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é n’a 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()
|
||||
}
|
||||
@@ -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 d’identité :")
|
||||
.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 d’identité.")
|
||||
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 d’identité.")
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
SQIcon(.circle_exclamation)
|
||||
SQText("Vérifiez que :")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
SQText("Les informations renseignées sur le document d’identité 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()
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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 n’avons 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.\nL’image ne doit pas être floue, coupée, ou présenter des reflets.")
|
||||
Spacer()
|
||||
SQButton("Recommencer") {
|
||||
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LivenessErrorView()
|
||||
}
|
||||
@@ -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("L’image n’est pas floue et n’a pas de reflet.")
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.check, color: .sqSemanticPositive)
|
||||
SQText("Vous ne portez pas d’accessoires couvrant.")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
SQButton("Continuer") {
|
||||
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LivenessSuccessView()
|
||||
}
|
||||
@@ -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 n’est utilisée que dans le cadre du processus de vérification d’identité.")
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Pour garantir le bon déroulement de la vérification d’identité :")
|
||||
.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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by Victor on 16/12/2024.
|
||||
//
|
||||
|
||||
import Lottie
|
||||
import SwiftUI
|
||||
|
||||
struct DocumentPreviewView: View {
|
||||
@@ -19,37 +20,35 @@ struct DocumentPreviewView: View {
|
||||
SQImage("visit_card_new", height: 160)
|
||||
} else {
|
||||
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)
|
||||
.background(Color.sqNeutral(10))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.inset(by: 0.5)
|
||||
.stroke(Color.sqNeutral(20), lineWidth: 1)
|
||||
)
|
||||
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
SQIcon(.trash_can, size: .s, color: .white)
|
||||
Button {} label: {
|
||||
SQIcon(.xmark, size: .s, color: .white)
|
||||
.padding()
|
||||
.background {
|
||||
Circle()
|
||||
.foregroundColor(Color.sqSemanticRed)
|
||||
.frame(height: 24)
|
||||
.frame(height: 32)
|
||||
}
|
||||
.padding(-16)
|
||||
}
|
||||
}
|
||||
|
||||
SQText(fileName, size: 13)
|
||||
SQText(fileName)
|
||||
.sqSize(13)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DocumentPreviewView(fileName: "le-nom-de-mon-fichier.pdf") {
|
||||
|
||||
}
|
||||
DocumentPreviewView(fileName: "le-nom-de-mon-fichier.pdf") {}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ struct IdentityDocumentButton: View {
|
||||
VStack(alignment: .leading) {
|
||||
SQText(type.title)
|
||||
if !type.subtitle.isEmpty {
|
||||
SQText(type.subtitle, size: 13, textColor: .sqNeutral(70))
|
||||
SQText(type.subtitle)
|
||||
.sqSize(13)
|
||||
.sqColor(.sqNeutral(70))
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
@@ -69,15 +69,20 @@ struct KYCDocumentButton: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
SQText(name, font: .bold)
|
||||
SQText(name)
|
||||
.sqFont(.bold)
|
||||
Spacer()
|
||||
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
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
SQText(desc, size: 14)
|
||||
SQText(desc)
|
||||
.sqSize(14)
|
||||
Spacer()
|
||||
if [.missing, .expired, .refused].contains(status) {
|
||||
SQIcon(.chevron_right, size: .l)
|
||||
|
||||
@@ -12,7 +12,10 @@ struct KYCInformationAlertView: View {
|
||||
HStack {
|
||||
SQIcon(.triangle_exclamation, size: .xl, color: .sqRed(80))
|
||||
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)
|
||||
|
||||
@@ -18,7 +18,9 @@ struct CardColorSelectionView: View {
|
||||
VStack(alignment: .leading, spacing: 32) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
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)
|
||||
.background(selectedColor)
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -28,7 +30,8 @@ struct CardColorSelectionView: View {
|
||||
SQText("Appliquer cette couleur sur mes prospectus, mes devis et mes factures")
|
||||
}
|
||||
.tint(Color.sqNeutral(100))
|
||||
SQText("Nous vous recommandons d’appliquer la même couleur sur tous vos documents.", size: 12)
|
||||
SQText("Nous vous recommandons d’appliquer la même couleur sur tous vos documents.")
|
||||
.sqSize(12)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
@@ -28,11 +28,14 @@ struct CardFormView: View {
|
||||
VStack {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
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.")
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
SQText("Image", font: .demiBold)
|
||||
SQText("Image")
|
||||
.sqFont(.semiBold)
|
||||
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Button(action: { showActionSheet = true }) {
|
||||
@@ -60,17 +63,25 @@ struct CardFormView: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
SQTextField("Titre", placeholder: "Ex : Pro Solutions", text: $title)
|
||||
SQTextField("Sous-titre", placeholder: "Ex : Martin Dupont", text: $subtitle, isOptional: true)
|
||||
SQTextField("Métier", placeholder: "Ex : Dépannage électroménager", text: $job, isOptional: true)
|
||||
SQTextField("Titre", text: $title)
|
||||
.sqPlaceholder("Ex : Pro Solutions")
|
||||
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) {
|
||||
Toggle(isOn: $showRating) {
|
||||
SQText("Afficher ma note AlloVoisins", font: .demiBold)
|
||||
SQText("Afficher ma note AlloVoisins")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
.tint(Color.sqNeutral(100))
|
||||
HStack(spacing: 4) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -83,18 +94,20 @@ struct CardFormView: View {
|
||||
.foregroundColor(Color.sqNeutral(10))
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQTextField("Numéro de téléphone", placeholder: "Ex : 06 12 34 56 78", text: $phoneNumber)
|
||||
SQTextField("Adresse complète", placeholder: "Ex : 1 rue de la gare, 67000 Strasbourg", text: $address)
|
||||
SQTextField("Numéro de téléphone", text: $phoneNumber)
|
||||
.sqPlaceholder("Ex : 06 12 34 56 78")
|
||||
SQTextField("Adresse complète", text: $address)
|
||||
.sqPlaceholder("Ex : 1 rue de la gare, 67000 Strasbourg")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
SQFooter {
|
||||
SQButton("Aperçu") {}
|
||||
.icon(SQIcon(.eye, color: .sqNeutral(100)))
|
||||
.buttonType(.line)
|
||||
.sqIcon(SQIcon(.eye, color: .sqNeutral(100)))
|
||||
.sqButtonType(.line)
|
||||
SQButton("Imprimer") {}
|
||||
.icon(SQIcon(.print, color: .white))
|
||||
.sqIcon(SQIcon(.print, color: .white))
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: {
|
||||
|
||||
@@ -10,7 +10,8 @@ import SwiftUI
|
||||
struct CardPrintView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQText("Génération d’une planche au format A4 comprenant 8 cartes de visite.", font: .demiBold)
|
||||
SQText("Génération d’une planche au format A4 comprenant 8 cartes de visite.")
|
||||
.sqFont(.semiBold)
|
||||
Image("visit_card_print_template")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
|
||||
@@ -18,7 +18,9 @@ struct FlyerColorSelectionView: View {
|
||||
VStack(alignment: .leading, spacing: 32) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
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)
|
||||
.background(selectedColor)
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -28,7 +30,8 @@ struct FlyerColorSelectionView: View {
|
||||
SQText("Appliquer cette couleur sur mes cartes de visite, mes devis et mes factures")
|
||||
}
|
||||
.tint(Color.sqNeutral(100))
|
||||
SQText("Nous vous recommandons d’appliquer la même couleur sur tous vos documents.", size: 12)
|
||||
SQText("Nous vous recommandons d’appliquer la même couleur sur tous vos documents.")
|
||||
.sqSize(12)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
@@ -30,11 +30,14 @@ struct FlyerFormView: View {
|
||||
VStack {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
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.")
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
SQText("Image", font: .demiBold)
|
||||
SQText("Image")
|
||||
.sqFont(.semiBold)
|
||||
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Button(action: { showActionSheet = true }) {
|
||||
@@ -62,17 +65,25 @@ struct FlyerFormView: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
SQTextField("Titre", placeholder: "Ex : Pro Solutions", text: $title)
|
||||
SQTextField("Sous-titre", placeholder: "Ex : Martin Dupont", text: $subtitle, isOptional: true)
|
||||
SQTextField("Métier", placeholder: "Ex : Dépannage électroménager", text: $job, isOptional: true)
|
||||
SQTextField("Titre", text: $title)
|
||||
.sqPlaceholder("Ex : Pro Solutions")
|
||||
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) {
|
||||
Toggle(isOn: $showRating) {
|
||||
SQText("Afficher ma note AlloVoisins", font: .demiBold)
|
||||
SQText("Afficher ma note AlloVoisins")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
.tint(Color.sqNeutral(100))
|
||||
HStack(spacing: 4) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -87,11 +98,21 @@ struct FlyerFormView: View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQTextEditor("Zone de text", placeholder: "Ex : Pro Solutions propose une gamme de services complète pour tout type de dépannage électroménager. Bénéficiez de nos 10 ans d’expérience ! Devis gratuit", text: .constant(""))
|
||||
|
||||
SQTextField("Prestation 1", placeholder: "Ex : Réparation lave-vaisselle", text: $job, isOptional: true)
|
||||
SQTextField("Prestation 2", placeholder: "Ex : Réparation machine à laver", text: $job, isOptional: true)
|
||||
SQTextField("Prestation 3", placeholder: "Ex : Réparation four", text: $job, isOptional: true)
|
||||
SQTextField("Prestation 4", placeholder: "Ex : Réparation outillage", text: $job, isOptional: true)
|
||||
SQTextField("Prestation 5", placeholder: "Ex : Dépannage électroménager", text: $job, isOptional: true)
|
||||
SQTextField("Prestation 1", text: $job)
|
||||
.sqPlaceholder("Ex : Réparation lave-vaisselle")
|
||||
.sqOptional()
|
||||
SQTextField("Prestation 2", text: $job)
|
||||
.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()
|
||||
|
||||
@@ -100,8 +121,10 @@ struct FlyerFormView: View {
|
||||
.foregroundColor(Color.sqNeutral(10))
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQTextField("Numéro de téléphone", placeholder: "Ex : 06 12 34 56 78", text: $phoneNumber)
|
||||
SQTextField("Adresse complète", placeholder: "Ex : 1 rue de la gare, 67000 Strasbourg", text: $address)
|
||||
SQTextField("Numéro de téléphone", text: $phoneNumber)
|
||||
.sqPlaceholder("Ex : 06 12 34 56 78")
|
||||
SQTextField("Adresse complète", text: $address)
|
||||
.sqPlaceholder("Ex : 1 rue de la gare, 67000 Strasbourg")
|
||||
}
|
||||
.padding()
|
||||
|
||||
@@ -110,18 +133,36 @@ struct FlyerFormView: View {
|
||||
.foregroundColor(Color.sqNeutral(10))
|
||||
|
||||
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("Adresse du siège social", placeholder: "Ex : 16 rue de la Redoute, 67500 Haguenau", text: $job, isOptional: true)
|
||||
SQTextField("Dénomination sociale", text: $job)
|
||||
.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) {
|
||||
SQTextField("Numéro SIRET", placeholder: "Ex : 12345678901234", text: $job, isOptional: true)
|
||||
SQText("Le numéro SIRET se compose de 14 chiffres : les 9 chiffres du SIREN + 5 chiffres propres à chaque établissement (NIC).", size: 12, textColor: .sqNeutral(50))
|
||||
SQTextField("Numéro SIRET", text: $job)
|
||||
.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("Statut juridique", placeholder: "Ex : SARL", text: $job, isOptional: true)
|
||||
SQTextField("Montant du capital social (€)", placeholder: "Ex : 1 000,00", text: $job, isOptional: true)
|
||||
SQTextField("Autre champ relatif à votre activité", placeholder: "Ex : Pour votre santé, mangez 5 fruits et légumes par jour", text: $job, isOptional: true)
|
||||
SQTextField("Numéro de RCS", text: $job)
|
||||
.sqPlaceholder("Ex : RCS STRASBOURG B 123456789")
|
||||
.sqOptional()
|
||||
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 j’engage ma responsabilité sur l’exhaustivité et l’authenticité des informations renseignées ci-dessus.", isChecked: $authentConfirm, error: .constant(.none))
|
||||
}
|
||||
.padding()
|
||||
@@ -129,14 +170,14 @@ struct FlyerFormView: View {
|
||||
}
|
||||
SQFooter {
|
||||
SQButton("Aperçu") {}
|
||||
.icon(SQIcon(.eye, color: .sqNeutral(100)))
|
||||
.buttonType(.line)
|
||||
.sqIcon(SQIcon(.eye, color: .sqNeutral(100)))
|
||||
.sqButtonType(.line)
|
||||
SQButton("Imprimer") {
|
||||
if authentConfirm == false {
|
||||
self.confirmIsInError = true
|
||||
}
|
||||
}
|
||||
.icon(SQIcon(.print, color: .white))
|
||||
.sqIcon(SQIcon(.print, color: .white))
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: {
|
||||
|
||||
@@ -20,7 +20,9 @@ struct MarketingSupportSelectionView: View {
|
||||
SQImage("visit_card_new", height: 200)
|
||||
.cornerRadius(8, corners: [.topLeft, .topRight])
|
||||
HStack {
|
||||
SQText("Mes cartes de visite", size: 18, font: .bold)
|
||||
SQText("Mes cartes de visite")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
Spacer()
|
||||
SQImage("for_pro_icon", height: 13)
|
||||
}
|
||||
@@ -41,7 +43,9 @@ struct MarketingSupportSelectionView: View {
|
||||
SQImage("flyers", height: 200)
|
||||
.cornerRadius(8, corners: [.topLeft, .topRight])
|
||||
HStack {
|
||||
SQText("Mes prospetus", size: 18, font: .bold)
|
||||
SQText("Mes prospetus")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
Spacer()
|
||||
SQImage("for_pro_icon", height: 13)
|
||||
}
|
||||
@@ -59,7 +63,9 @@ struct MarketingSupportSelectionView: View {
|
||||
SQImage("quotes", height: 200)
|
||||
.cornerRadius(8, corners: [.topLeft, .topRight])
|
||||
HStack {
|
||||
SQText("Mes prospetus", size: 18, font: .bold)
|
||||
SQText("Mes prospetus")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
Spacer()
|
||||
SQImage("for_pro_icon", height: 13)
|
||||
}
|
||||
|
||||
@@ -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 à l’agenda 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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -12,7 +12,9 @@ struct MoreNeighborsSelectedBadge: View {
|
||||
|
||||
var body: some View {
|
||||
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)
|
||||
.background(Color.sqNeutral(20))
|
||||
|
||||
@@ -26,7 +26,8 @@ struct NeighborsSelectionFooter: View {
|
||||
size: .l,
|
||||
isFull: false
|
||||
) {}
|
||||
SQText("Sélectionner", font: .demiBold)
|
||||
SQText("Sélectionner")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
Spacer()
|
||||
SQCircleButton(
|
||||
@@ -51,7 +52,9 @@ struct NeighborsSelectionFooter: View {
|
||||
}
|
||||
|
||||
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) {
|
||||
HStack(spacing: 8) {
|
||||
SQImage("neighbor_avatar", height: 32)
|
||||
|
||||
@@ -13,10 +13,17 @@ struct NeihborsCard: View {
|
||||
HStack(spacing: 8) {
|
||||
AVAvatar(model: AVAvatarModel(avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", onlineStatus: .online(), sizing: .L))
|
||||
VStack(alignment: .leading) {
|
||||
SQText("Julien L.", font: .demiBold)
|
||||
SQText("Julien, Petits Jobs À Nantes", size: 12, font: .demiBold)
|
||||
SQText("Nantes (Gloriette-Feydeaux) - 3,1 km", size: 12, textColor: .sqNeutral(80))
|
||||
SQText("En ligne", size: 12, textColor: .sqNeutral(80))
|
||||
SQText("Julien L.")
|
||||
.sqFont(.semiBold)
|
||||
SQText("Julien, Petits Jobs À Nantes")
|
||||
.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()
|
||||
}
|
||||
@@ -28,7 +35,8 @@ struct NeihborsCard: View {
|
||||
|
||||
HStack {
|
||||
SQIcon(.star, type: .solid, color: .sqGold(50))
|
||||
SQText("4,5/5", font: .bold)
|
||||
SQText("4,5/5")
|
||||
.sqFont(.bold)
|
||||
SQText("(35 avis)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ struct CategorySelectorView: View {
|
||||
SQText("Zone de texte sur la partie haute de l’écran, pouvant être sur plusieurs lignes.")
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
VStack {
|
||||
SQText("Services", font: .demiBold)
|
||||
SQText("Services")
|
||||
.sqFont(.semiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
Rectangle()
|
||||
.fill(Color.sqNeutral(100))
|
||||
@@ -22,7 +23,8 @@ struct CategorySelectorView: View {
|
||||
.padding(.top, 1)
|
||||
}
|
||||
VStack {
|
||||
SQText("Objets", font: .demiBold)
|
||||
SQText("Objets")
|
||||
.sqFont(.semiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
Rectangle()
|
||||
.fill(Color.sqNeutral(20))
|
||||
|
||||
@@ -10,7 +10,8 @@ import SwiftUI
|
||||
struct AllCategoriesCell: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
SQText("Voir toutes les catégories", font: .demiBold)
|
||||
SQText("Voir toutes les catégories")
|
||||
.sqFont(.semiBold)
|
||||
Spacer()
|
||||
SQIcon(.chevron_right)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ struct PrestationCell: View {
|
||||
SQIcon(.magnifying_glass)
|
||||
VStack(alignment: .leading) {
|
||||
SQText("remplacement de vitre")
|
||||
SQText("dans Menuiserie - Huisserie - Agencement", size: 12, textColor: .sqNeutral(60))
|
||||
SQText("dans Menuiserie - Huisserie - Agencement")
|
||||
.sqSize(12)
|
||||
.sqColor(.sqNeutral(60))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ struct PrestationHistorySearchCell: View {
|
||||
SQIcon(.clock)
|
||||
VStack(alignment: .leading) {
|
||||
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()
|
||||
SQIcon(.xmark)
|
||||
|
||||
@@ -35,14 +35,18 @@ struct PrestationSearchView: View {
|
||||
Divider()
|
||||
|
||||
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
|
||||
PrestationHistorySearchCell()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
PrestationCell()
|
||||
}
|
||||
|
||||
@@ -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 d’assurance")
|
||||
.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 l’exactitude des éléments renseignés et je m’engage à fournir mes attestations d’assurance à première demande.", isChecked: .constant(false), error: .constant(SQFormFieldError.none))
|
||||
.padding(.top, 16)
|
||||
SQButton("Enregistrer") {}
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
AssurancesView()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 j’offre 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()
|
||||
}
|
||||
@@ -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 d’assurance et s’engage à 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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 d’inscription")
|
||||
.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()
|
||||
}
|
||||
@@ -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("C’est parti !") {}
|
||||
.sqColorScheme(.grape)
|
||||
}
|
||||
.sqNavigationBar(title: "")
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
WelcomeProModal()
|
||||
}
|
||||
@@ -12,13 +12,17 @@ struct ReportConfirmationView: View {
|
||||
VStack(spacing: 16) {
|
||||
VStack {
|
||||
SQIcon(.check, customSize: 64, type: .solid, color: .sqSemanticGreen)
|
||||
SQText("Votre signalement a bien été pris en compte.", size: 18, font: .bold)
|
||||
SQText("Votre signalement a bien été pris en compte.")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQText("Merci de participer à améliorer la communauté. Nous nous efforçons de traiter votre signalement dans les plus brefs délais et nous vous informerons de la suite qui sera donnée.", font: .demiBold)
|
||||
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) {
|
||||
SQText("Souhaitez-vous bloquer cet utilisateur ?", font: .demiBold)
|
||||
SQText("Souhaitez-vous bloquer cet utilisateur ?")
|
||||
.sqFont(.semiBold)
|
||||
HStack {
|
||||
Toggle(isOn: .constant(true)) {
|
||||
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
Reference in New Issue
Block a user