Compare commits

10 Commits

Author SHA1 Message Date
Victor Bodinaud
e305b1697a New views 2025-12-10 16:31:08 +01:00
Victor Bodinaud
08666a6818 Add Views 2025-06-30 11:25:36 +02:00
Victor Bodinaud
eb99d76108 New adds 2025-03-26 11:20:12 +01:00
Victor Bodinaud
b09cf50619 💄Lot of adds 2025-01-08 08:51:06 +01:00
Victor Bodinaud
295a0515c3 💄 Add a lot of components 2024-11-29 10:02:38 +01:00
Victor Bodinaud
2031687f53 💄Add business cards 2024-11-25 10:25:11 +01:00
Victor Bodinaud
6e711be25d 🏗️ Reorganize files 2024-11-14 10:07:56 +01:00
Victor Bodinaud
d962262874 💄 More UI 2024-10-17 15:19:18 +02:00
Victor Bodinaud
c211091406 🗑️ Remove unused Code 2024-10-14 17:07:54 +02:00
Victor Bodinaud
e9719414e9 🚀 Start Project 2024-10-14 17:02:14 +02:00
322 changed files with 11712 additions and 1358 deletions

View File

@@ -6,13 +6,30 @@
objectVersion = 77; objectVersion = 77;
objects = { objects = {
/* Begin PBXBuildFile section */
573DAF242CF0CC54006DF58A /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 573DAF232CF0CC54006DF58A /* Lottie */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
57282ACB2CBD2810000C443E /* AlloVoisinsSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AlloVoisinsSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 57282ACB2CBD2810000C443E /* AlloVoisinsSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AlloVoisinsSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
57282BA72CBD579A000C443E /* Exceptions for "AlloVoisinsSwiftUI" folder in "AlloVoisinsSwiftUI" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 57282ACA2CBD2810000C443E /* AlloVoisinsSwiftUI */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
57282ACD2CBD2810000C443E /* AlloVoisinsSwiftUI */ = { 57282ACD2CBD2810000C443E /* AlloVoisinsSwiftUI */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
57282BA72CBD579A000C443E /* Exceptions for "AlloVoisinsSwiftUI" folder in "AlloVoisinsSwiftUI" target */,
);
path = AlloVoisinsSwiftUI; path = AlloVoisinsSwiftUI;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -23,6 +40,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
573DAF242CF0CC54006DF58A /* Lottie in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -65,6 +83,7 @@
); );
name = AlloVoisinsSwiftUI; name = AlloVoisinsSwiftUI;
packageProductDependencies = ( packageProductDependencies = (
573DAF232CF0CC54006DF58A /* Lottie */,
); );
productName = AlloVoisinsSwiftUI; productName = AlloVoisinsSwiftUI;
productReference = 57282ACB2CBD2810000C443E /* AlloVoisinsSwiftUI.app */; productReference = 57282ACB2CBD2810000C443E /* AlloVoisinsSwiftUI.app */;
@@ -94,6 +113,9 @@
); );
mainGroup = 57282AC22CBD2810000C443E; mainGroup = 57282AC22CBD2810000C443E;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = (
573DAF222CF0CC54006DF58A /* XCRemoteSwiftPackageReference "lottie-spm" */,
);
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = 57282ACC2CBD2810000C443E /* Products */; productRefGroup = 57282ACC2CBD2810000C443E /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -177,7 +199,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
@@ -234,7 +256,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
@@ -255,11 +277,14 @@
DEVELOPMENT_TEAM = WVH3Y23X7X; DEVELOPMENT_TEAM = WVH3Y23X7X;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AlloVoisinsSwiftUI/Info.plist;
INFOPLIST_KEY_NSCameraUsageDescription = "Nous utilisons la caméra afin de compléter la photo de profil";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UIUserInterfaceStyle = Light;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -284,11 +309,14 @@
DEVELOPMENT_TEAM = WVH3Y23X7X; DEVELOPMENT_TEAM = WVH3Y23X7X;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AlloVoisinsSwiftUI/Info.plist;
INFOPLIST_KEY_NSCameraUsageDescription = "Nous utilisons la caméra afin de compléter la photo de profil";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UIUserInterfaceStyle = Light;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -324,6 +352,25 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
573DAF222CF0CC54006DF58A /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.5.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
573DAF232CF0CC54006DF58A /* Lottie */ = {
isa = XCSwiftPackageProductDependency;
package = 573DAF222CF0CC54006DF58A /* XCRemoteSwiftPackageReference "lottie-spm" */;
productName = Lottie;
};
/* End XCSwiftPackageProductDependency section */
}; };
rootObject = 57282AC32CBD2810000C443E /* Project object */; rootObject = 57282AC32CBD2810000C443E /* Project object */;
} }

View File

@@ -0,0 +1,15 @@
{
"originHash" : "6b3386dc9ff1f3a74f1534de9c41d47137eae0901cfe819ed442f1b241549359",
"pins" : [
{
"identity" : "lottie-spm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/airbnb/lottie-spm.git",
"state" : {
"revision" : "b842598f1295f3ffa1475b1580672d1fe5b83580",
"version" : "4.5.0"
}
}
],
"version" : 3
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "9F0CD4E1-EA25-475C-9981-B5E4AE8BD8B9"
type = "1"
version = "2.0">
</Bucket>

View File

@@ -11,7 +11,9 @@ import SwiftUI
struct AlloVoisinsSwiftUIApp: App { struct AlloVoisinsSwiftUIApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() NavigationStack {
DebugLandView()
}
} }
} }
} }

View File

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

View File

@@ -0,0 +1,34 @@
//
// SQFormFieldError.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 22/05/2025.
//
enum SQFormFieldError: Equatable {
case none
case empty
case siret
case invalidPhoneNumber
case confirmation
case minCharacters(Int)
case api(String)
case custom(String)
var isInError: Bool {
self != .none
}
var message: String {
switch self {
case .none: return ""
case .empty: return "Ce champ est obligatoire."
case .siret: return "Le numéro SIRET doit être composé de 14 chiffres."
case .invalidPhoneNumber: return "Le numéro de téléphone n'est pas valide."
case .confirmation: return "Cette condition est obligatoire."
case let .minCharacters(count): return "Ce champ doit comporter au minimum \(count) caractères."
case let .api(message): return message
case let .custom(message): return message
}
}
}

View File

@@ -0,0 +1,159 @@
//
// ResponsiveSheetWrapper.swift
//
// Created by Bacem Ben Afia on 04/08/2022.
// ba.bessem@gmail.com
import SwiftUI
import UIKit
//MARK: - ViewModifier
fileprivate struct ResponsiveSheetWrapper: ViewModifier {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
func body(content: Content) -> some View {
/// check device type (ipad sheet are centred / iPhone sheet pinned to bottom )
if UIDevice.current.userInterfaceIdiom == .phone {
ZStack (alignment: .bottom){
Color(UIColor.systemBackground.withAlphaComponent(0.01))
.onTapGesture {
// tap outside the view to dismiss
presentationMode.wrappedValue.dismiss()
}
.edgesIgnoringSafeArea(.all)
VStack {
/// the small thumb for bottom sheet native like
content
}
/// redesign the content
.asResponsiveSheetContent()
}
/// remove system background
.clearSheetSystemBackground()
.edgesIgnoringSafeArea(.bottom)
} else {
ZStack {
Color(UIColor.systemBackground.withAlphaComponent(0.01))
.onTapGesture {
// tap outside the view to dismiss
presentationMode.wrappedValue.dismiss()
}
content
/// redesign the content
.asResponsiveSheetContent()
}
/// remove system background
.clearSheetSystemBackground()
.edgesIgnoringSafeArea(.all)
}
}
}
fileprivate struct ResponsiveSheetContent: ViewModifier {
/// ResponsiveSheetContent will create the form of a bottom sheet (apply corners radius for both iPad an iPhone sheet)
@Environment(\.safeAreaInsets) private var safeAreaInsets
func body(content: Content) -> some View {
if UIDevice.current.userInterfaceIdiom == .phone {
content
.padding(.bottom, safeAreaInsets.bottom)
.background(Color(UIColor.systemBackground))
.cornerRadius(10, corners: [.topLeft, .topRight])
} else {
content
.padding()
.background(Color(UIColor.systemBackground))
.cornerRadius(10, corners: [.allCorners])
}
}
}
fileprivate struct ClearBackgroundViewModifier: ViewModifier {
func body(content: Content) -> some View {
content
.background(ClearBackgroundView())
}
}
fileprivate struct CornerRadiusStyle: ViewModifier {
var radius: CGFloat
var corners: UIRectCorner
struct CornerRadiusShape: Shape {
var radius = CGFloat.infinity
var corners = UIRectCorner.allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
func body(content: Content) -> some View {
content
.clipShape(CornerRadiusShape(radius: radius, corners: corners))
}
}
//MARK: - UIViewRepresentable
fileprivate struct ClearBackgroundView: UIViewRepresentable {
/// The Key
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async {
/// GOD BLESS UI KIT
/// Target sheet system background view
/// Apply clear Color
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
//MARK: - View Extension
extension View {
/// Return a formatted View Based on Device Type
/// if iPad => Centred Alert-Like View
/// if iPhone => Bottom Sheet View
/// That's all folks
func asResponsiveSheet() -> some View {
self.modifier(ResponsiveSheetWrapper())
}
fileprivate func asResponsiveSheetContent() -> some View {
self.modifier(ResponsiveSheetContent())
}
fileprivate func clearSheetSystemBackground() -> some View {
self.modifier(ClearBackgroundViewModifier())
}
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
self.modifier(CornerRadiusStyle(radius: radius, corners: corners))
}
}
//MARK: - EnvironmentKey & Values
fileprivate struct SafeAreaInsetsKey: EnvironmentKey {
static var defaultValue: EdgeInsets {
(UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.safeAreaInsets ?? .zero).insets
}
}
fileprivate extension EnvironmentValues {
var safeAreaInsets: EdgeInsets {
self[SafeAreaInsetsKey.self]
}
}
//MARK: - UIEdgeInsets Extension
fileprivate extension UIEdgeInsets {
var insets: EdgeInsets {
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
}
}

View File

@@ -0,0 +1,36 @@
//
// FontRegistration.swift
//
//
// Created by Victor on 12/06/2024.
//
import CoreGraphics
import CoreText
import UIKit
struct FontRegistration {
public enum FontError: Swift.Error {
case failedToRegisterFont
}
func registerFonts() throws {
let fontNames = [
"TTChocolates-Bold-Italic",
"TTChocolates-Bold",
"TTChocolates-DemiBold",
"TTChocolates-Medium",
"TTChocolates-MediumIt"
]
for name in fontNames {
guard let asset = NSDataAsset(name: "Fonts/\(name)"),
let provider = CGDataProvider(data: asset.data as NSData),
let font = CGFont(provider),
CTFontManagerRegisterGraphicsFont(font, nil)
else {
throw FontError.failedToRegisterFont
}
}
}
}

View File

@@ -0,0 +1,65 @@
//
// MessageNotificationView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 29/01/2025.
//
import SwiftUI
struct AVMessageNotification: Codable {
let displayName: String
let message: String
let avatarUrl: String
let actionUrl: String
let isNewUser: Bool
let countUnreadMessage: Int
enum CodingKeys: String, CodingKey {
case displayName = "display_name"
case message
case avatarUrl = "avatar_url"
case actionUrl = "action_url"
case isNewUser = "is_new_user"
case countUnreadMessage = "count_unread_message"
}
}
struct MessageNotificationView: View {
let notification: AVMessageNotification
var body: some View {
HStack(alignment: .center, spacing: 8) {
AVAvatar(model: AVAvatarModel(avatarString: notification.avatarUrl, onlineStatus: .online(), sizing: .M))
.frame(width: 48, height: 48)
VStack(alignment: .leading) {
SQText(notification.displayName)
.sqFont(.semiBold)
SQText(notification.message)
.sqSize(14)
.sqColor(.sqNeutral(70))
.lineLimit(1)
}
Spacer()
VStack {
Circle()
.frame(width: 12, height: 12)
.foregroundStyle(Color.sqSemanticBlue)
.padding(.top, 8)
Spacer()
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(height: 80)
.background {
RoundedRectangle(cornerRadius: 8)
.fill(Color.white)
.shadow(color: .black.opacity(0.15), radius: 8, x: 0, y: 4)
}
}
}
#Preview {
MessageNotificationView(notification: AVMessageNotification(displayName: "Lucas E.", message: "Bonjour je voudrais une super voiture pour déplacer des ", avatarUrl: "https://public-allo10.allovoisins.com//assets/default_avatars/100/Avatar0.png", actionUrl: "", isNewUser: true, countUnreadMessage: 1))
}

View File

@@ -8,22 +8,19 @@
import SwiftUI import SwiftUI
struct OnlyForPremierView: View { struct OnlyForPremierView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View { var body: some View {
VStack(spacing: 8) { VStack(spacing: 8) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
SQText("Réservé aux abonnés Premier", size: 18, font: .bold) SQText("Réservé aux abonnés Premier")
.sqSize(18)
.sqFont(.bold)
SQText("Seuls les abonnés Premier peuvent profiter de cette fonctionnalité.") SQText("Seuls les abonnés Premier peuvent profiter de cette fonctionnalité.")
} }
SQButton( "Découvrir labonnement Premier", color: Color.sqOrange(50), textColor: .white) { SQButton( "Découvrir labonnement Premier") {
} }
.sqColorScheme(.orange)
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }

View File

@@ -8,11 +8,6 @@
import SwiftUI import SwiftUI
struct OnlyForPremierModal: View { struct OnlyForPremierModal: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
var body: some View { var body: some View {
NavigationView { NavigationView {
@@ -23,7 +18,7 @@ struct OnlyForPremierModal: View {
.fill(Color.sqOrange(50)) .fill(Color.sqOrange(50))
.frame(height: 200) .frame(height: 200)
VStack(alignment: .leading, spacing: 32) { VStack(alignment: .leading, spacing: 32) {
Image("only_for_premier", bundle: .module) Image("only_for_premier")
.resizable() .resizable()
.frame(width: 307, height: 108) .frame(width: 307, height: 108)
} }
@@ -32,26 +27,33 @@ struct OnlyForPremierModal: View {
Color.white Color.white
VStack(spacing: 48) { VStack(spacing: 48) {
VStack(spacing: 24) { VStack(spacing: 24) {
SQText("Vous avez déjà répondu à 4 demandes de services ce mois-ci", size: 24, font: .bold) SQText("Vous avez déjà répondu à 4 demandes de services ce mois-ci")
.sqSize(24)
.sqFont(.bold)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
SQText("Augmentez votre chiffre daffaires en bénéficiant de tous les avantages inclus dans labonnement Premier.", font: .bold) SQText("Augmentez votre chiffre daffaires en bénéficiant de tous les avantages inclus dans labonnement Premier.")
.sqFont(.bold)
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50)) SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
SQText("Répondez aux demandes en illimité", font: .demiBold) SQText("Répondez aux demandes en illimité")
.sqFont(.semiBold)
} }
HStack { HStack {
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50)) SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
SQText("Augmentez votre visibilité sur Google", font: .demiBold) SQText("Augmentez votre visibilité sur Google")
.sqFont(.semiBold)
} }
HStack { HStack {
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50)) SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
SQText("Affichez votre numéro de téléphone sur votre profil", font: .demiBold) SQText("Affichez votre numéro de téléphone sur votre profil")
.sqFont(.semiBold)
} }
HStack { HStack {
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50)) SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
SQText("Gérez vos devis, factures, encaissements...", font: .demiBold) SQText("Gérez vos devis, factures, encaissements...")
.sqFont(.semiBold)
} }
} }
} }
@@ -60,12 +62,15 @@ struct OnlyForPremierModal: View {
Spacer() Spacer()
VStack(spacing: 16) { VStack(spacing: 16) {
VStack { VStack {
SQText("Essai gratuit de 14 jours", size: 18, font: .bold) SQText("Essai gratuit de 14 jours")
.sqSize(18)
.sqFont(.bold)
SQText("à partir de 29,99 € / mois") SQText("à partir de 29,99 € / mois")
} }
SQButton("Je m'abonne", color: .sqOrange(50), textColor: .white) { SQButton("Je m'abonne") {
} }
.sqColorScheme(.orange)
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 24) .padding(.vertical, 24)

View File

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

View File

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

View File

@@ -0,0 +1,367 @@
//
// AVAvatar.swift
// Allovoisins
//
// Created by Florian Baudin on 15/03/2023.
// Copyright © 2023 AlloVoisins. All rights reserved.
//
import SwiftUI
enum AVOnlineState {
case online
case recently
case offline
case hourlyDispo
}
class AVOnlineStatus: ObservableObject {
@Published var state: AVOnlineState!
@Published var lastConnection: Int?
init(state: AVOnlineState, lastConnection: Int? = nil) {
self.state = state
self.lastConnection = lastConnection
}
init(from lastOnlineMinutes: Int?, hourlyDispo: Bool = false) {
guard let lastOnlineMinutes else {
self.state = .offline
self.lastConnection = nil
return
}
switch lastOnlineMinutes {
case 0 ... 5:
self.state = .online
self.lastConnection = lastOnlineMinutes
case 5 ..< 60:
self.state = .recently
self.lastConnection = lastOnlineMinutes
default:
self.state = .offline
self.lastConnection = nil
}
guard hourlyDispo else { return }
self.state = .hourlyDispo
}
static func online() -> AVOnlineStatus {
return AVOnlineStatus(state: .online, lastConnection: .zero)
}
}
public class AVAvatarModel: ObservableObject {
enum Source {
case profile
case other
}
@Published var onlineStatus: AVOnlineStatus
@Published var avatarString: String?
var isNewUser: Bool
var isPro: Bool
var sizing: AVAvatarSizing
var bordered: Bool
var source: Source
init(avatarString: String?, isNew: Bool = false, isPro: Bool = false, onlineStatus: AVOnlineStatus, sizing: AVAvatarSizing, bordered: Bool = false, source: Source = .other) {
self.avatarString = avatarString
self.isNewUser = isNew
self.isPro = isPro
self.onlineStatus = onlineStatus
self.sizing = sizing
self.bordered = bordered
self.source = source
}
static func empty() -> AVAvatarModel {
return AVAvatarModel(avatarString: nil,
isNew: false,
isPro: false,
onlineStatus: AVOnlineStatus(state: .offline),
sizing: .M)
}
}
struct AVAvatar: View {
@ObservedObject var model: AVAvatarModel
var body: (some View)? {
ZStack {
Color.clear
self.avatarImage
.overlay(avatarBorder)
.overlay(newUserOverlay, alignment: .topLeading)
.overlay(onlineCircle, alignment: .bottomTrailing)
.frame(width: self.model.sizing.size,
height: self.model.sizing.size)
}
}
private var avatarBorder: (some View)? {
Group {
if self.model.onlineStatus.state == .hourlyDispo && self.model.source != .profile {
Circle()
.stroke(Color.yellow, lineWidth: 3)
} else if self.model.bordered {
if self.model.source == .profile {
Circle()
.stroke(.white, lineWidth: 6)
} else {
Circle()
.stroke(.white, lineWidth: 4)
}
} else {
EmptyView()
}
}
}
private var avatarImage: (some View)? {
if #available(iOS 15.0, *) {
return AsyncImage(
url: URL(string: self.model.avatarString ?? "")
) { image in
image
.resizable()
.clipShape(Circle())
} placeholder: {
Image("AVAsset.Icon.DEFAULT_AVATAR")
.resizable()
.clipShape(Circle())
}
} else {
return Image("")
.resizable()
.clipShape(Circle())
}
}
private var newUserOverlay: (some View)? {
Group {
if self.model.isNewUser && [.online, .offline].contains(self.model.onlineStatus.state) {
Image("")
.resizable()
.clipShape(Circle())
.rotationEffect(Angle(degrees: 30))
} else {
EmptyView()
}
}
}
private var onlineCircle: (some View)? {
Group {
switch self.model.onlineStatus.state {
case .online:
let sizing = self.model.sizing.onlineSize
Circle()
.strokeBorder(Color.white, lineWidth: sizing.borderWidth)
.background(Circle().foregroundColor(Color.white))
.frame(width: sizing.height, height: sizing.height)
.offset(x: sizing.padding.trailing, y: sizing.padding.bottom)
case .recently:
let recentlyText = "\(self.model.onlineStatus.lastConnection ?? 1) min"
let sizing = self.model.sizing.recentlySize
SQText(recentlyText)
.padding(EdgeInsets(top: 0, leading: 3, bottom: 0, trailing: 2))
.foregroundColor(Color.white)
.background(Color.white)
.cornerRadius(sizing.height / 2)
.overlay(
RoundedRectangle(cornerRadius: sizing.height / 2)
.stroke(.white, lineWidth: sizing.borderWidth)
)
.frame(height: sizing.height)
case .hourlyDispo:
let sizing = self.model.sizing.onlineSize
Image("")
.resizable()
.overlay(RoundedRectangle(cornerRadius: sizing.height / 2)
.stroke(.white, lineWidth: sizing.borderWidth))
.background(Circle().foregroundColor(Color(.yellow)))
.frame(width: sizing.height, height: sizing.height)
.offset(x: sizing.padding.trailing, y: sizing.padding.bottom)
default:
EmptyView()
}
}
}
}
struct AVAvatar_Previews: PreviewProvider {
static var previews: some View {
VStack {
AVAvatar(model: AVAvatarModel(
avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg",
isNew: true,
onlineStatus: AVOnlineStatus(state: .offline), sizing: AVAvatarSizing.XS
))
AVAvatar(model: AVAvatarModel(
avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg",
isNew: false,
onlineStatus: AVOnlineStatus(state: .offline, lastConnection: 59), sizing: AVAvatarSizing.XS
))
AVAvatar(model: AVAvatarModel(
avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg",
isNew: true,
onlineStatus: AVOnlineStatus(state: .online), sizing: AVAvatarSizing.S
))
AVAvatar(model: AVAvatarModel(
avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg",
isNew: false,
onlineStatus: AVOnlineStatus(state: .recently, lastConnection: 59), sizing: AVAvatarSizing.S
))
AVAvatar(model: AVAvatarModel(
avatarString: nil,
isNew: true,
onlineStatus: AVOnlineStatus(state: .online), sizing: AVAvatarSizing.M
))
AVAvatar(model: AVAvatarModel(
avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg",
isNew: false,
isPro: true,
onlineStatus: AVOnlineStatus(state: .recently,
lastConnection: 59), sizing: AVAvatarSizing.M, bordered: true
))
AVAvatar(model: AVAvatarModel(
avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg",
isNew: true,
onlineStatus: AVOnlineStatus(state: .hourlyDispo), sizing: AVAvatarSizing.L, bordered: true
))
AVAvatar(model: AVAvatarModel(
avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg",
isNew: false,
onlineStatus: AVOnlineStatus(state: .recently, lastConnection: 59), sizing: AVAvatarSizing.L
))
AVAvatar(model: AVAvatarModel(
avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg",
isNew: true,
onlineStatus: AVOnlineStatus(state: .online), sizing: AVAvatarSizing.XL
))
AVAvatar(model: AVAvatarModel(
avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg",
isNew: false,
isPro: true,
onlineStatus: AVOnlineStatus(state: .recently,
lastConnection: 59), sizing: AVAvatarSizing.XL
))
}
}
}
enum AVAvatarSizing {
case XXS
case XS
case S
case M
case L
case XL
case XXL
var size: CGFloat {
switch self {
case .XXS:
return 26
case .XS:
return 32
case .S:
return 40
case .M:
return 48
case .L:
return 64
case .XL:
return 90
case .XXL:
return 160
}
}
struct OnlineStateSize {
var height: CGFloat
var fontSize: CGFloat? = 0
var borderWidth: CGFloat
var padding: EdgeSpace
struct EdgeSpace {
var bottom: CGFloat
var trailing: CGFloat
}
}
var onlineSize: OnlineStateSize {
switch self {
case .XXS, .XS:
return OnlineStateSize(height: .zero,
borderWidth: 0,
padding: OnlineStateSize.EdgeSpace(bottom: 0,
trailing: 0))
case .S, .M:
return OnlineStateSize(height: 14,
borderWidth: 2,
padding: OnlineStateSize.EdgeSpace(bottom: 0,
trailing: 0))
case .L:
return OnlineStateSize(height: 18,
borderWidth: 3,
padding: OnlineStateSize.EdgeSpace(bottom: 0,
trailing: 0))
case .XL:
return OnlineStateSize(height: 18,
borderWidth: 3,
padding: OnlineStateSize.EdgeSpace(bottom: -3,
trailing: -3))
case .XXL:
return OnlineStateSize(height: 30,
borderWidth: 4,
padding: OnlineStateSize.EdgeSpace(bottom: -8,
trailing: -8))
}
}
var recentlySize: OnlineStateSize {
switch self {
case .XXS, .XS:
return OnlineStateSize(height: .zero,
borderWidth: 0,
padding: OnlineStateSize.EdgeSpace(bottom: 0,
trailing: 0))
case .S:
return OnlineStateSize(height: 13,
fontSize: 9,
borderWidth: 2,
padding: OnlineStateSize.EdgeSpace(bottom: 2,
trailing: 2))
case .M:
return OnlineStateSize(height: 17,
fontSize: 11,
borderWidth: 2,
padding: OnlineStateSize.EdgeSpace(bottom: 2,
trailing: 2))
case .L, .XL:
return OnlineStateSize(height: 17,
fontSize: 11,
borderWidth: 3,
padding: OnlineStateSize.EdgeSpace(bottom: -1,
trailing: 3))
case .XXL:
return OnlineStateSize(height: 30,
fontSize: 18,
borderWidth: 4,
padding: OnlineStateSize.EdgeSpace(bottom: -3,
trailing: 0))
}
}
}

View File

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

View File

@@ -0,0 +1,33 @@
//
// SQAddressModifyLine.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 06/01/2025.
//
import SwiftUI
struct SQAddressModifyLine: View {
var body: some View {
let attributedText = NSAttributedString("Modifier")
HStack {
SQText("à 8 allée Baco, 44000 Nantes")
.sqSize(14)
Spacer()
Button {
print("Modifier")
} label: {
SQText("Modifier")
.sqSize(14)
.sqFont(.bold)
.underline(true, pattern: .solid)
}
}
}
}
#Preview {
SQAddressModifyLine()
.padding()
}

View File

@@ -0,0 +1,432 @@
//
// SQButton.swift
//
//
// Created by Victor on 12/06/2024.
//
import Lottie
import SwiftUI
struct SQButton: View {
let title: String
let action: () -> Void
@State private var type: SQButtonType = .solid
@State private var colorScheme: SQButtonColorScheme = .neutral
@State private var textSize: CGFloat = 16
@State private var font: SQTextFont = .semiBold
@State private var icon: SQIcon? = nil
@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)
let attributes = [NSAttributedString.Key.font: font]
let size = (title as NSString).size(withAttributes: attributes)
return size.width + (icon != nil ? 24 : 0)
}
init(_ title: String,
isLoading: Binding<Bool> = .constant(false),
action: @escaping () -> Void)
{
self.title = title
self._isLoading = isLoading
self.action = action
}
private var buttonStyle: SQButtonStyle {
SQButtonStyle(
type: type,
colorScheme: colorScheme,
isLoading: isLoading,
isDisabled: !isEnabled,
isPressed: isPressed
)
}
var body: some View {
Button(action: {
if !isLoading, !isEnabled {
action()
}
}, label: {
HStack {
if isLoading {
LottieView(animation: .named("av_loader"))
.looping()
.frame(width: 20)
} else {
if icon != nil {
icon
.foregroundColor(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)
.background(buttonStyle.backgroundColor)
.cornerRadius(100)
.overlay(
RoundedRectangle(cornerRadius: 100)
.inset(by: 0.5)
.stroke(buttonStyle.borderColor, lineWidth: 1)
)
})
.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 sqButtonType(_ type: SQButtonType) -> SQButton {
var copy = self
copy._type = State(initialValue: type)
return copy
}
func sqColorScheme(_ scheme: SQButtonColorScheme) -> SQButton {
var copy = self
copy._colorScheme = State(initialValue: scheme)
return copy
}
func sqTextSize(_ size: CGFloat) -> SQButton {
var copy = self
copy._textSize = State(initialValue: size)
return copy
}
func sqFont(_ font: SQTextFont) -> SQButton {
var copy = self
copy._font = State(initialValue: font)
return copy
}
func sqIcon(_ icon: SQIcon?) -> SQButton {
var copy = self
copy._icon = State(initialValue: icon)
return copy
}
func sqLarge(_ isLarge: Bool = true) -> SQButton {
var copy = self
copy._isLarge = State(initialValue: isLarge)
return copy
}
}
// MARK: - Properties
enum SQButtonType: String, CaseIterable {
case solid
case line
case light
case glass
}
enum SQButtonColorScheme: String, CaseIterable {
case neutral
case green
case pink
case blue
case yellow
case gold
case orange
case red
case grape
case forest
case royal
case white
var baseColor: Color {
switch self {
case .neutral:
return .sqNeutral(100)
case .green:
return .sqGreen(60)
case .pink:
return .sqPink(60)
case .blue:
return .sqBlue(50)
case .yellow:
return .sqYellow(50)
case .gold:
return .sqGold(50)
case .orange:
return .sqOrange(50)
case .red:
return .sqRed(60)
case .grape:
return .sqGrape(80)
case .forest:
return .sqForest(100)
case .royal:
return .sqRoyal(60)
case .white:
return .white
}
}
var mediumColor: Color {
switch self {
case .neutral:
return .sqNeutral(30)
case .green:
return .sqGreen(30)
case .pink:
return .sqPink(30)
case .blue:
return .sqBlue(30)
case .yellow:
return .sqYellow(30)
case .gold:
return .sqGold(30)
case .orange:
return .sqOrange(30)
case .red:
return .sqRed(10)
case .grape:
return .sqGrape(30)
case .forest:
return .sqForest(50)
case .royal:
return .sqRoyal(30)
case .white:
return .white
}
}
var lightColor: Color {
switch self {
case .neutral:
return .sqNeutral(15)
case .green:
return .sqGreen(10)
case .pink:
return .sqPink(10)
case .blue:
return .sqBlue(10)
case .yellow:
return .sqYellow(10)
case .gold:
return .sqGold(10)
case .orange:
return .sqOrange(10)
case .red:
return .sqRed(10)
case .grape:
return .sqGrape(10)
case .forest:
return .sqForest(10)
case .royal:
return .sqRoyal(10)
case .white:
return .white
}
}
}
struct SQButtonStyle {
let type: SQButtonType
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:
return colorScheme.mediumColor
case .line:
return .clear
case .light:
return colorScheme.lightColor
case .glass:
return .clear
}
}
if isLoading {
switch type {
case .solid:
return colorScheme.mediumColor
case .line:
return .clear
case .light:
return colorScheme.lightColor
case .glass:
return .clear
}
}
switch type {
case .solid:
return colorScheme.baseColor
case .line:
return .clear
case .light:
return colorScheme.lightColor
case .glass:
return .clear
}
}
var borderColor: Color {
switch type {
case .solid:
if isPressed && !isDisabled && !isLoading {
return colorScheme.baseColor.opacity(0.8)
}
if isDisabled {
return colorScheme.lightColor.opacity(0.5)
}
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
}
if isLoading {
return colorScheme.mediumColor
}
return colorScheme.baseColor
case .light:
if isPressed && !isDisabled && !isLoading {
return colorScheme.lightColor.opacity(0.8)
}
if isDisabled {
return colorScheme.lightColor.opacity(0.5)
}
if isLoading {
return colorScheme.lightColor.opacity(0.8)
}
return colorScheme.lightColor
case .glass:
return .clear
}
return colorScheme.baseColor
}
var textColor: Color {
if 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:
return .white
case .line, .light, .glass:
return colorScheme.mediumColor
}
}
switch type {
case .solid:
return .white
case .line, .light, .glass:
return colorScheme.baseColor
}
}
}
#Preview {
VStack(spacing: 32) {
HStack {
VStack(spacing: 16) {
SQButton("C'est parti !") {}
.sqColorScheme(.royal)
SQButton("C'est parti !") {}
.sqColorScheme(.royal)
.sqButtonType(.line)
SQButton("C'est parti !") {}
.sqColorScheme(.royal)
.sqButtonType(.light)
SQButton("C'est parti !") {}
.sqColorScheme(.royal)
.sqButtonType(.glass)
}
VStack(spacing: 16) {
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)
}
}
SQButton("Continuer avec Apple") {}
.sqButtonType(.line)
.sqIcon(SQIcon(.apple_brand))
.sqLarge()
}
.padding()
}

View File

@@ -0,0 +1,58 @@
//
// SQCheckbox.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 25/11/2024.
//
import SwiftUI
struct SQCheckbox: View {
var text: String
@Binding var isChecked: Bool
var error: Binding<SQFormFieldError>
var alignment: VerticalAlignment = .top
init(_ text: String, isChecked: Binding<Bool>, error: Binding<SQFormFieldError> = .constant(.none), alignment: VerticalAlignment = .top) {
self.text = text
self._isChecked = isChecked
self.error = error
self.alignment = alignment
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: alignment) {
if isChecked {
SQImage("checked_neutral", height: 20)
} else if error.wrappedValue.isInError {
SQImage("checkbox_unchecked_error", height: 20)
} else {
SQImage("checkbox_unchecked", height: 20)
}
SQText(text)
}
.onTapGesture {
isChecked.toggle()
if error.wrappedValue.isInError && isChecked {
error.wrappedValue = .none
}
}
HStack {
SQIcon(.circle_exclamation, customSize: 13, type: .solid, color: .sqSemanticRed)
SQText(error.wrappedValue.message)
.sqSize(12)
.sqFont(.semiBold)
.sqColor(.sqSemanticRed)
}
.isHidden(hidden: !error.wrappedValue.isInError)
}
}
}
#Preview {
SQCheckbox("Je comprends que jengage ma responsabilité sur lexhaustivité et lauthenticité des informations renseignées ci-dessus.", isChecked: .constant(false))
SQCheckbox("Je comprends que jengage ma responsabilité sur lexhaustivité et lauthenticité des informations renseignées ci-dessus.", isChecked: .constant(false), error: .constant(.empty))
}

View File

@@ -0,0 +1,63 @@
//
// SQChips.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 24/10/2024.
//
import SwiftUI
struct SQChipOption: Identifiable, Equatable {
var id: Int
let label: String
let bgColor: Color
}
struct SQChips: View {
var options: [SQChipOption]
@Binding var selectedOption: SQChipOption
var body: some View {
HStack {
ForEach(options) { option in
SQChip(option: option, selectedOption: $selectedOption)
.onTapGesture {
self.selectedOption = option
}
}
}
}
}
fileprivate struct SQChip: View {
var option: SQChipOption
@Binding var selectedOption: SQChipOption
private var isSelected: Bool {
selectedOption.id == option.id
}
var body: some View {
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)
.cornerRadius(8, corners: .allCorners)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(
isSelected && option.bgColor != .white ? option.bgColor : Color.sqNeutral(100), lineWidth: 1
)
)
}
}
#Preview {
SQChips(options: [
SQChipOption(id: 0, label: "Toutes", bgColor: .sqNeutral(100)),
SQChipOption(id: 1, label: "Non lues", bgColor: .sqSemanticBlue),
SQChipOption(id: 2, label: "Archivées", bgColor: .sqSemanticCritical)
], selectedOption: .constant(SQChipOption(id: 1, label: "Non lues", bgColor: .sqSemanticBlue)))
}

View File

@@ -0,0 +1,98 @@
//
// SQCircleButton.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 05/06/2025.
//
import SwiftUI
enum SQCircleButtonSize {
case s
case m
case l
var vertical: CGFloat {
switch self {
case .s:
return 12
case .m:
return 12
case .l:
return 16
}
}
var horizontal: CGFloat {
switch self {
case .s:
return 30
case .m:
return 30
case .l:
return 25
}
}
}
struct SQCircleButton: View {
let icon: SQIcon
let text: String
var color: Color
var size: SQCircleButtonSize
var isFull: Bool
let action: () -> Void
init(_ icon: SQIcon, text: String = "", color: Color = .white, size: SQCircleButtonSize = .m, isFull: Bool = true, action: @escaping () -> Void) {
self.icon = icon
self.text = text
self.color = color
self.size = size
self.isFull = isFull
self.action = action
}
var body: some View {
Button {
self.action()
} label: {
VStack(spacing: 2) {
icon
.padding(.horizontal, size.horizontal)
.padding(.vertical, size.vertical)
.cornerRadius(100)
.background {
Circle()
.fill(isFull ? color : .white)
.overlay(
Circle()
.stroke(isFull ? Color.white : color, lineWidth: 1)
)
}
.shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 4)
if !text.isEmpty {
SQText(text)
.sqFont(.semiBold)
.sqColor(.sqNeutral(80))
}
}
}
}
}
#Preview {
SQCircleButton(
SQIcon(.arrow_right, size: .l)
) {}
SQCircleButton(
SQIcon(.arrow_left, size: .l)
) {}
SQCircleButton(
SQIcon(.plus, size: .l, color: .sqNeutral(80)),
text: "Sélectionner",
color: .sqNeutral(80),
size: .l,
isFull: false
) {}
}

View File

@@ -0,0 +1,154 @@
//
// SQColorPicker.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 13/11/2024.
//
import SwiftUI
struct SQColorPicker: View {
@Binding var selectedColor: Color
@State private var showColorPicker = false
@State private var isCustomColor = false
let colors: [Color] = [
.sqNeutral(),
.sqGreen(50),
.sqPink(),
.sqBlue(50),
.sqOrange(50),
.sqGrape(80),
.sqForest(80)
]
var title: String
var subtitle: String?
init(_ title: String, selectedColor: Binding<Color>, subtitle: String? = nil) {
self.title = title
self.subtitle = subtitle
self._selectedColor = selectedColor
let initialColor = selectedColor.wrappedValue
_isCustomColor = State(initialValue: !colors.contains(initialColor))
}
var body: some View {
VStack(alignment: .leading) {
VStack(alignment: .leading) {
SQText(title)
.sqSize(18)
.sqFont(.bold)
SQText(subtitle ?? "")
.lineLimit(2)
.lineSpacing(1)
.fixedSize(horizontal: false, vertical: true)
}
HStack {
ForEach(colors, id: \.self) { color in
colorDot(color)
Spacer()
}
SQImage("multicolor", height: 32)
.overlay {
if isCustomColor {
ZStack {
Circle()
.foregroundColor(selectedColor)
.frame(width: 20, height: 20)
Circle()
.inset(by: -2)
.strokeBorder(Color.white, lineWidth: 3)
.frame(width: 20, height: 20)
}
}
}
.onTapGesture {
isCustomColor = true
showColorPicker.toggle()
}
}
}
.sheet(isPresented: $showColorPicker) {
UIColorPickerViewController_SwiftUI(selectedColor: $selectedColor)
.background(Color.white)
.sqNavigationBar(title: "Choisir une couleur personnalisée")
SQButton("Valider") {
showColorPicker.toggle()
}
}
}
private func colorDot(_ color: Color) -> some View {
HStack(alignment: .center, spacing: 8) {}
.frame(width: 32, height: 32, alignment: .center)
.background(color)
.cornerRadius(32)
.overlay(
ZStack {
if selectedColor == color && !isCustomColor {
Circle()
.foregroundColor(color)
.frame(width: 20, height: 20)
Circle()
.inset(by: -2)
.strokeBorder(Color.white, lineWidth: 3)
.frame(width: 20, height: 20)
}
}
)
.onTapGesture {
selectedColor = color
isCustomColor = false
}
}
}
struct UIColorPickerViewController_SwiftUI: UIViewControllerRepresentable {
@Binding var selectedColor: Color
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let picker = UIColorPickerViewController()
picker.delegate = context.coordinator
picker.selectedColor = UIColor(selectedColor)
return picker
}
func updateUIViewController(_ uiViewController: UIColorPickerViewController, context: Context) {
uiViewController.selectedColor = UIColor(selectedColor)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIColorPickerViewControllerDelegate {
var parent: UIColorPickerViewController_SwiftUI
init(_ parent: UIColorPickerViewController_SwiftUI) {
self.parent = parent
}
func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) {
parent.selectedColor = Color(viewController.selectedColor)
}
func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {
parent.selectedColor = Color(viewController.selectedColor)
}
}
}
// Preview
struct ColorPickerPreview: View {
@State private var selectedColor: Color = .sqNeutral()
var body: some View {
SQColorPicker("Couleur principale", selectedColor: $selectedColor, subtitle: "Choisissez la couleur qui sera appliquée sur vos cartes de visite.")
.padding()
}
}
#Preview {
ColorPickerPreview()
}

View File

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

View File

@@ -0,0 +1,38 @@
//
// SQFooter.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 13/11/2024.
//
import SwiftUI
struct SQFooter<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
HStack {
content
}
.padding()
.frame(maxWidth: .infinity, alignment: .center)
.background(Color.white)
.cornerRadius(8)
.shadow(color: Color(red: 0.09, green: 0.14, blue: 0.2).opacity(0.1), radius: 8, x: 0, y: -4)
}
}
#Preview {
SQFooter {
SQButton("test") {
}
SQButton("test") {
}
}
}

View File

@@ -57,6 +57,7 @@ enum SQIconName: String {
case camera_plus case camera_plus
case camera_rotate case camera_rotate
case camera case camera
case car_side
case chart_line_up case chart_line_up
case chart_user case chart_user
case check case check
@@ -141,10 +142,12 @@ enum SQIconName: String {
case paper_plane_top case paper_plane_top
case paper_plane case paper_plane
case paperclip case paperclip
case passport
case payer case payer
case pen_to_square case pen_to_square
case phone_flip case phone_flip
case phone case phone
case phone_hangup
case play_store_brand case play_store_brand
case plus case plus
case print case print
@@ -180,6 +183,7 @@ enum SQIconName: String {
case user case user
case users case users
case video case video
case video_slash
case whatsapp_brand case whatsapp_brand
case xmark case xmark
case x_twitter_brand case x_twitter_brand
@@ -201,7 +205,7 @@ struct SQIcon: View {
} }
var body: some View { var body: some View {
Image(type == .solid ? "\(name.rawValue)_solid" : name.rawValue, bundle: .module) Image(type == .solid ? "\(name.rawValue)_solid" : name.rawValue)
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(height: customSize ?? size.rawValue) .frame(height: customSize ?? size.rawValue)

View File

@@ -0,0 +1,29 @@
//
// SQImage.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 13/11/2024.
//
import SwiftUI
struct SQImage: View {
var imageName: String
var height: CGFloat
init(_ imageName: String, height: CGFloat) {
self.imageName = imageName
self.height = height
}
var body: some View {
Image(imageName)
.resizable()
.scaledToFit()
.frame(height: height)
}
}
#Preview {
SQImage("flyers", height: 100)
}

View File

@@ -0,0 +1,144 @@
//
// SQImagePicker.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 27/11/2024.
//
import SwiftUI
struct SQImagePicker: View {
let nbOfImages: Int = 4
var images: [Image] = []
private let itemSpacing: CGFloat = 8
private var columns: [GridItem] {
[
GridItem(.fixed(160), spacing: itemSpacing),
GridItem(.fixed(160), spacing: itemSpacing)
]
}
var body: some View {
VStack(alignment: .leading) {
SQText("Images")
.sqFont(.semiBold)
LazyVGrid(columns: columns, alignment: .leading) {
ForEach(0 ..< nbOfImages) { _ in
SQImagePickerView()
}
}
}
}
}
struct SQImagePickerView: View {
var image: Image?
@State var showSelection: Bool = false
var body: some View {
VStack {
if let image = image {
image
.frame(width: 160, height: 160)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.sqNeutral(10)))
} else {
HStack {
SQIcon(.camera, size: .xl)
}
.frame(width: 160, height: 160)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.sqNeutral(10)))
}
}
.onTapGesture {
showSelection = true
}
.sheet(isPresented: $showSelection) {
SQImagePickerSelectorSheet()
}
}
}
struct SQImagePickerSelectorSheet: View {
@Environment(\.dismiss) var dismiss
var categoriesImages: [CategoriesImages] = [
CategoriesImages(name: "Entretien - Réparation autres véhicules", pictures: [
"square.and.arrow.up.fill",
"paperplane.circle.fill",
"tray.fill"
]),
CategoriesImages(name: "Plomberie - Installation sanitaire", pictures: [
"xmark.bin.circle.fill"
]),
CategoriesImages(name: "Carrelage", pictures: [
"document.on.document",
"richtext.page.fill.he"
])
]
private let itemSpacing: CGFloat = 8
private var columns: [GridItem] {
[
GridItem(.fixed(160), spacing: itemSpacing),
GridItem(.fixed(160), spacing: itemSpacing)
]
}
var body: some View {
VStack(spacing: 0) {
HStack {
Button {
dismiss()
} label: {
SQIcon(.xmark, size: .l)
}
SQText("Sélectionner une photo")
.sqSize(19)
.sqFont(.bold)
.frame(maxWidth: .infinity)
Spacer()
}
.padding()
ScrollView {
ForEach(categoriesImages, id: \.name) { category in
VStack {
VStack(alignment: .leading) {
SQText(category.name)
LazyVGrid(columns: columns, alignment: .leading) {
ForEach(category.pictures, id: \.self) { picture in
Image(systemName: picture)
.frame(width: 160, height: 120)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.sqNeutral(10)))
}
}
}
.padding()
Rectangle()
.frame(height: 16)
.foregroundColor(Color.sqNeutral(10))
}
}
}
}
}
}
#Preview {
SQImagePicker()
.padding()
}
struct CategoriesImages {
var name: String
var pictures: [String]
}

View File

@@ -0,0 +1,94 @@
//
// SQNavigationBar.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 14/10/2024.
//
import SwiftUI
struct SQNavigationBar: ViewModifier {
@Environment(\.dismiss) private var dismiss
let title: String
let style: SQNavigationBarStyle
let showBackButton: Bool
let backAction: (() -> Void)?
func body(content: Content) -> some View {
content
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
SQText(title)
.sqSize(18)
.sqFont(.bold)
.foregroundColor(style.foregroundColor)
}
if backAction != nil {
ToolbarItem(placement: .navigationBarLeading) {
if showBackButton {
Button(action: {
backAction?()
}) {
style.leadingIcon
}
}
}
}
if style == .modal || style == .booster {
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
} label: {
style.trailingIcon
}
}
}
}
.toolbarBackground(style.backgroundColor, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarBackButtonHidden()
}
}
extension View {
func sqNavigationBar(title: String, style: SQNavigationBarStyle = .white, showBackButton: Bool = true, backAction: (() -> Void)? = nil) -> some View {
modifier(SQNavigationBar(title: title, style: style, showBackButton: showBackButton, backAction: backAction))
}
}
enum SQNavigationBarStyle {
case white
case modal
case booster
case boosterFreeTrialResiliation
var backgroundColor: Color {
switch self {
case .white, .modal: return .white
case .booster, .boosterFreeTrialResiliation: return .sqRoyal(60)
}
}
var foregroundColor: Color {
switch self {
case .white, .modal: return .sqNeutral(100)
case .booster, .boosterFreeTrialResiliation: return .white
}
}
var leadingIcon: SQIcon {
switch self {
default: return SQIcon(.chevron_left, size: .l, color: foregroundColor)
}
}
var trailingIcon: SQIcon {
switch self {
case .modal, .booster: return .init(.xmark, size: .l, color: foregroundColor)
default:
return .init(.xmark, size: .l, color: .clear)
}
}
}

View File

@@ -0,0 +1,123 @@
//
// SQPicker.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 18/10/2024.
//
import SwiftUI
struct PickerOption: Identifiable, Hashable {
let id = UUID()
let text: String
}
struct SQPicker: View {
@Binding var selection: PickerOption
let options: [PickerOption]
@State private var isPresenting = false
var body: some View {
GeometryReader { geometry in
ZStack {
// Contenu visuel
HStack {
SQText(selection.text)
Spacer()
SQIcon(.chevron_down)
}
.padding()
.frame(width: geometry.size.width)
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color.sqNeutral(30), lineWidth: 1)
)
// Menu invisible mais cliquable
MenuPicker(selection: $selection, options: options, isPresenting: $isPresenting)
.frame(width: geometry.size.width)
}
}
.frame(maxWidth: .infinity)
.frame(height: 51)
.fixedSize(horizontal: false, vertical: true)
.tint(.sqNeutral(100))
.background {
RoundedRectangle(cornerRadius: 8)
.fill(Color.white)
}
}
func triggerPickerMenu() {
isPresenting = true
}
}
struct MenuPicker: UIViewRepresentable {
@Binding var selection: PickerOption
let options: [PickerOption]
@Binding var isPresenting: Bool
func makeUIView(context _: Context) -> UIButton {
let button = UIButton(type: .custom)
button.setTitle("", for: .normal)
updateMenu(button)
return button
}
func updateUIView(_ uiView: UIButton, context _: Context) {
updateMenu(uiView)
if isPresenting {
uiView.sendActions(for: .menuActionTriggered)
isPresenting = false
}
}
private func updateMenu(_ button: UIButton) {
let menu = UIMenu(title: "", children: options.map { option in
UIAction(title: option.text, state: option == selection ? .on : .off) { _ in
self.selection = option
}
}.reversed())
button.showsMenuAsPrimaryAction = true
button.menu = menu
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var parent: MenuPicker
init(_ parent: MenuPicker) {
self.parent = parent
}
}
}
struct SQPickerPreview: View {
@State private var selectedDuration: PickerOption
let durationOptions: [PickerOption]
init() {
let options = [
PickerOption(text: "1 mois"),
PickerOption(text: "2 mois"),
PickerOption(text: "3 mois"),
]
_selectedDuration = State(initialValue: options[0])
durationOptions = options
}
var body: some View {
SQPicker(selection: $selectedDuration, options: durationOptions)
.padding()
}
}
#Preview {
SQPickerPreview()
}

View File

@@ -0,0 +1,37 @@
//
// SQProgressBar.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 17/10/2024.
//
import SwiftUI
struct SQProgressBar: View {
var progress: Double // Valeur entre 0 et 1
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(Color.sqNeutral(20))
.frame(width: geometry.size.width, height: 12)
.cornerRadius(50)
Rectangle()
.foregroundColor(Color.sqNeutral(60)) // Vous pouvez changer la couleur ici
.frame(width: min(CGFloat(self.progress) * geometry.size.width, geometry.size.width), height: 12)
.cornerRadius(50)
}
}
.frame(height: 12)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(Color.sqNeutral(20))
.cornerRadius(12)
}
}
#Preview {
SQProgressBar(progress: 0.2)
.padding()
}

View File

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

View File

@@ -0,0 +1,195 @@
//
// SQRadio.swift
// Sequoia
//
// Created by Victor on 10/10/2024.
//
import SwiftUI
enum SQRadioOrientation {
case horizontal
case vertical
}
struct SQRadioOption: Identifiable {
let id: Int = UUID().hashValue
let title: String
var desc: String? = nil
init(title: String, desc: String? = nil) {
self.title = title
self.desc = desc
}
}
struct SQRadio: View {
let title: String?
let titleSize: CGFloat
var radioTextSize: CGFloat = 16
var orientation: SQRadioOrientation = .vertical
let options: [SQRadioOption]
var error: Binding<SQFormFieldError>
@Binding var selectedIndex: Int?
init(title: String? = nil, titleSize: CGFloat = 24, radioTextSize: CGFloat = 16, orientation: SQRadioOrientation = .vertical, options: [SQRadioOption], selectedIndex: Binding<Int?>, error: Binding<SQFormFieldError> = .constant(.none)) {
self.title = title
self.titleSize = titleSize
self.radioTextSize = radioTextSize
self.orientation = orientation
self.options = options
self.error = error
self._selectedIndex = selectedIndex
}
var body: some View {
VStack(alignment: orientation == .vertical ? .leading : .center, spacing: 16) {
if let title = title {
SQText(title)
.sqSize(titleSize)
.sqFont(.bold)
.multilineTextAlignment(.center)
}
Group {
if orientation == .horizontal {
HorizontalRadioButtons(options: options, titleSize: radioTextSize, selectedIndex: $selectedIndex, isInError: error.wrappedValue.isInError)
.frame(maxWidth: .infinity)
} else {
VStack(alignment: .leading, spacing: 16) {
radioButtons
}
}
}
if error.wrappedValue != .none {
SQText(error.wrappedValue.message)
.sqSize(12)
.sqFont(.semiBold)
.sqColor(.sqSemanticRed)
}
}
}
private var radioButtons: some View {
ForEach(Array(options.enumerated()), id: \.offset) { index, option in
RadioButton(
title: option.title,
desc: option.desc,
titleSize: radioTextSize,
isSelected: Binding(
get: { selectedIndex == index },
set: { _ in selectedIndex = index }
),
isInError: error.wrappedValue.isInError,
orientation: orientation
)
}
}
private struct HorizontalRadioButtons: View {
let options: [SQRadioOption]
let titleSize: CGFloat
@Binding var selectedIndex: Int?
var isInError: Bool
var body: some View {
GeometryReader { geometry in
HStack(alignment: .top, spacing: 0) {
Spacer()
ForEach(Array(options.enumerated()), id: \.offset) { index, option in
RadioButton(
title: option.title,
desc: option.desc,
titleSize: titleSize,
isSelected: Binding(
get: { selectedIndex == index },
set: { _ in selectedIndex = index }
),
isInError: isInError,
orientation: .horizontal
)
.frame(width: geometry.size.width / CGFloat(options.count))
}
Spacer()
}
}
.frame(height: 80)
}
}
private struct RadioButton: View {
let title: String
let desc: String?
let titleSize: CGFloat
@Binding var isSelected: Bool
var isInError: Bool
let orientation: SQRadioOrientation
var body: some View {
Group {
if orientation == .horizontal {
VStack(spacing: 8) {
radioCircle
SQText(title)
.multilineTextAlignment(.center)
}
} else {
VStack(alignment: .leading) {
HStack(spacing: 8) {
radioCircle
SQText(title)
}
if let desc = desc {
SQText(desc)
.sqSize(12)
}
}
}
}
.contentShape(Rectangle()) // Make the entire area tappable
.onTapGesture {
isSelected.toggle()
}
}
private var radioCircle: some View {
ZStack {
Circle()
.stroke(isInError ? Color.sqSemanticRed :
(isSelected ?
Color.sqNeutral(100) :
Color.sqNeutral(30)), lineWidth: 1)
.frame(width: 20, height: 20)
if isSelected {
Circle()
.inset(by: 3)
.stroke(isInError ? Color.sqSemanticRed : Color.sqNeutral(100), lineWidth: 6)
.frame(width: 20, height: 20)
}
}
}
}
}
#Preview {
VStack(alignment: .leading, spacing: 32) {
SQRadio(title: "Vertical", options: [
SQRadioOption(title: "Option 1", desc: "Lutilisateur semble proposer un service relevant dune activité réglementée sans disposer des autorisations requises."),
SQRadioOption(title: "Option 2", desc: "Lutilisateur semble évoquer ou proposer des activités interdites par la loi (trafic danimaux, substances illicites, armes, etc.)."),
SQRadioOption(title: "Option 3", desc: "Lutilisateur semble fournir de fausses informations dans son profil."),
SQRadioOption(title: "Option 4", desc: "Lutilisateur semble avoir usurpé mon identité ou lidentité dun autre utilisateur (SIRET, numéro de téléphone...).")
], selectedIndex: .constant(0), error: .constant(.custom("Merci de détailler votre signalement")))
SQRadio(title: "Horizontal", orientation: .horizontal, options: [
SQRadioOption(title: "1\n Non Jamais"),
SQRadioOption(title: "2"),
SQRadioOption(title: "3"),
SQRadioOption(title: "4"),
SQRadioOption(title: "5\nOui")
], selectedIndex: .constant(2))
}
.padding()
}

View File

@@ -0,0 +1,33 @@
//
// SQRowDivider.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 05/06/2025.
//
import SwiftUI
struct SQRowDivider: View {
var body: some View {
Rectangle()
.frame(height: 16)
.foregroundColor(Color.sqNeutral(10))
}
}
struct SQDivider: View {
var backgroundColor: Color
init(_ backgroundColor: Color = Color.sqNeutral(20)) {
self.backgroundColor = backgroundColor
}
var body: some View {
Divider()
.overlay(backgroundColor)
}
}
#Preview {
SQRowDivider()
}

View File

@@ -0,0 +1,36 @@
//
// SQSocialBar.swift
//
//
// Created by Victor on 18/07/2024.
//
import SwiftUI
struct SQSocialBar: View {
var style: SQSocialBarStyle = .full
init(style: SQSocialBarStyle) {
self.style = style
}
var body: some View {
VStack {
HStack {
}
HStack {
}
}
}
}
enum SQSocialBarStyle {
case full
case compact
}
#Preview {
SQSocialBar(style: .full)
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,121 @@
//
// SQTextEditor.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 27/11/2024.
//
import SwiftUI
struct SQTextEditor: View {
var label: String
var placeholder: String
var error: Binding<SQFormFieldError>
var icon: SQIcon?
var isDisabled: Bool = false
var isOptional: Bool = false
var tooltipText: String?
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,
error: Binding<SQFormFieldError> = .constant(.none),
text: Binding<String>,
icon: SQIcon? = nil,
isDisabled: Bool = false,
isOptional: Bool = false,
tooltipText: String? = nil,
minCharacters: Int? = nil,
maxCharacters: Int? = nil,
infoAction: (() -> Void)? = nil,
iconAction: (() -> Void)? = nil)
{
self.label = label
self.placeholder = placeholder
self.error = error
self._text = text
self.icon = icon
self.isDisabled = isDisabled
self.isOptional = isOptional
self.tooltipText = tooltipText
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)
Spacer()
if isOptional {
SQText("Optionnel")
.sqSize(12)
.sqColor(.sqNeutral(50))
}
}
TextEditor(text: Binding(
get: { self.text },
set: { self.text = String($0.prefix(self.maxCharacters ?? Int.max)) }
))
.onChange(of: self.text, perform: { _ in
error.wrappedValue = .none
})
.font(.sq(.medium))
.foregroundStyle(Color.sqNeutral(100))
.tint(accentColor)
.frame(height: 108)
.foregroundStyle(Color.sqNeutral())
.background(isDisabled ? Color.sqNeutral(10) : .clear)
.padding(16)
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(error.wrappedValue.isInError ? .sqSemanticRed : isFocused ? accentColor : isDisabled ? Color.sqNeutral(20) : Color.sqNeutral(30), lineWidth: 1)
)
HStack(spacing: 16) {
if error.wrappedValue.isInError {
HStack(spacing: 8) {
SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed)
SQText(error.wrappedValue.message)
.sqSize(12)
.sqFont(.semiBold)
.sqColor(.sqSemanticRed)
}
}
Spacer()
if !characterCountText.isEmpty {
SQText(characterCountText)
.sqSize(12)
.sqColor(.sqNeutral(50))
}
}
}
}
private var characterCountText: String {
let count = text.count
if let min = minCharacters, count < min {
return "\(count)/\(min)"
} else if let max = maxCharacters {
return count >= (max * 4 / 5) ? "\(count)/\(max)" : "\(count)"
}
return ""
}
}
#Preview {
SQTextEditor("Zone de texte", placeholder: "Ex : Pro Solutions propose une gamme de services complète pour tout type de dépannage électroménager. Bénéficiez de nos 10 ans dexpérience ! Devis gratuit", text: .constant("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
.padding()
SQTextEditor("Zone de texte", placeholder: "Ex : Pro Solutions propose une gamme de services complète pour tout type de dépannage électroménager. Bénéficiez de nos 10 ans dexpérience ! Devis gratuit", error: .constant(.empty), text: .constant("Lorem ipsum dolor sit amet, consectetur"), minCharacters: 50)
.padding()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,9 +27,6 @@ struct SQToast: View {
} }
init(_ title: String = "", content: String, style: SQToastStyle = .success, hasClose: Bool = false) { init(_ title: String = "", content: String, style: SQToastStyle = .success, hasClose: Bool = false) {
do {
try FontRegistration().registerFonts()
} catch {}
self.title = title self.title = title
self.content = content self.content = content
self.style = style self.style = style
@@ -41,9 +38,12 @@ struct SQToast: View {
HStack(spacing: 16) { HStack(spacing: 16) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if !title.isEmpty { if !title.isEmpty {
SQText(title, font: .bold) SQText(title)
.sqFont(.bold)
} }
SQText(content, size: 14, font: .demiBold) SQText(content)
.sqSize(14)
.sqFont(.semiBold)
} }
Spacer() Spacer()
if hasClose { if hasClose {

View File

@@ -0,0 +1,44 @@
//
// SQTooltip.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 25/11/2024.
//
import SwiftUI
struct SQTooltip: View {
let text: String
@Binding var isVisible: Bool
@State private var tooltipHeight: CGFloat = 0
private struct Constants {
static let cornerRadius: CGFloat = 8
static let padding: CGFloat = 16
static let maxWidth: CGFloat = 300
static let spacing: CGFloat = 4
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
SQText(text)
.sqColor(.white)
.fixedSize(horizontal: false, vertical: true)
.padding(Constants.padding)
.background(
GeometryReader { geo in
Color.sqNeutral(100)
.clipShape(RoundedRectangle(cornerRadius: Constants.cornerRadius))
.onAppear {
tooltipHeight = geo.size.height
}
}
)
.frame(maxWidth: Constants.maxWidth)
}
.position(x: 210, y: 0)
.offset(y: -(tooltipHeight/2 + Constants.spacing))
.opacity(isVisible ? 1 : 0)
.animation(.easeInOut(duration: 0.2), value: isVisible)
}
}

View File

@@ -0,0 +1,32 @@
//
// SQSearchBar.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 06/01/2025.
//
import SwiftUI
struct SQSearchBar: View {
@Binding var text: String
var placeholder: String
var body: some View {
HStack(spacing: 16) {
SQIcon(.magnifying_glass, type: .solid)
TextField("", text: $text)
.placeholder(when: text.isEmpty) {
SQText(placeholder)
.sqColor(.sqNeutral(50))
}
.tint(Color.sqNeutral(100))
}
.padding()
.background(Color.sqNeutral(10))
.cornerRadius(8)
}
}
#Preview {
SQSearchBar(text: .constant(""), placeholder: "Rechercher")
}

View File

@@ -0,0 +1,34 @@
//
// SQSearchBarButton.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 08/01/2025.
//
import SwiftUI
struct SQSearchBarButton: View {
var placeholder: String
var action: () -> Void = {}
var body: some View {
HStack(spacing: 16) {
SQIcon(.magnifying_glass, type: .solid)
SQText(placeholder)
.sqColor(.sqNeutral(50))
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.sqNeutral(10))
.cornerRadius(8)
.onTapGesture {
action()
}
}
}
#Preview {
SQSearchBarButton(placeholder: "Rechercher") {
print("search")
}
}

View File

@@ -0,0 +1,96 @@
//
// Extension+Color.swift
//
//
// Created by Victor on 12/06/2024.
//
import SwiftUI
extension Color {
static func sqRoyal(_ variant: Int = 60) -> Color {
return Color("ROYAL_\(variant)")
}
static func sqPurple(_ variant: Int = 60) -> Color {
return Color("PURPLE_\(variant)")
}
static func sqNeutral(_ variant: Int = 60) -> Color {
return Color("NEUTRAL_\(variant)")
}
static func sqOrange(_ variant: Int = 60) -> Color {
return Color("ORANGE_\(variant)")
}
static func sqGreen(_ variant: Int = 60) -> Color {
return Color("GREEN_\(variant)")
}
static func sqYellow(_ variant: Int = 60) -> Color {
return Color("YELLOW_\(variant)")
}
static func sqGold(_ variant: Int = 60) -> Color {
return Color("GOLD_\(variant)")
}
static func sqBlue(_ variant: Int = 60) -> Color {
return Color("BLUE_\(variant)")
}
static func sqGrape(_ variant: Int = 60) -> Color {
return Color("GRAPE_\(variant)")
}
static func sqPink(_ variant: Int = 60) -> Color {
return Color("PINK_\(variant)")
}
static func sqForest(_ variant: Int = 60) -> Color {
return Color("FOREST_\(variant)")
}
static func sqRed(_ variant: Int = 60) -> Color {
return Color("RED_\(variant)")
}
static let sqSemanticRed = Color("SEMANTIC_RED")
static let sqSemanticOrange = Color("SEMANTIC_CRITICAL")
static let sqSemanticGreen = Color("SEMANTIC_GREEN")
static let sqSemanticBlue = Color("SEMANTIC_BLUE")
static let sqSemanticCritical = Color("SEMANTIC_CRITICAL")
static let sqSemanticWarning = Color("SEMANTIC_WARNING")
static let sqSemanticPositive = Color("SEMANTIC_GREEN")
static let sqSemanticNegative = Color("SEMANTIC_RED")
var isLight: Bool {
let components = UIColor(self).cgColor.components ?? [0, 0, 0, 0]
if components.count == 2 {
let gray = components[0]
let luminance = 0.299 * gray + 0.587 * gray + 0.114 * gray
return luminance > 0.5
}
if components.count >= 3 {
let red = components[0]
let green = components[1]
let blue = components[2]
let r = red * 255
let g = green * 255
let b = blue * 255
let luminance = 0.299 * r + 0.587 * g + 0.114 * b
return luminance > 128
}
return false
}
var appropriateTextColor: Color {
isLight ? .black : .white
}
}

View File

@@ -0,0 +1,66 @@
//
// Extension+View.swift
//
//
// Created by Victor on 19/06/2024.
//
import SwiftUI
//extension View {
// func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
// clipShape( RoundedCorner(radius: radius, corners: corners) )
// }
//}
//
//struct RoundedCorner: Shape {
// let radius: CGFloat
// let corners: UIRectCorner
//
// init(radius: CGFloat = .infinity, corners: UIRectCorner = .allCorners) {
// self.radius = radius
// self.corners = corners
// }
//
// func path(in rect: CGRect) -> Path {
// let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
// return Path(path.cgPath)
// }
//}
extension View {
func sqFont(_ font: SQTextFont, size: CGFloat = 16) -> some View {
self.font(.custom(font.rawValue, size: size))
}
}
extension Font {
static func sq(_ font: SQTextFont, size: CGFloat = 16) -> Font {
.custom(font.rawValue, size: size)
}
}
extension View {
func isHidden(hidden: Bool = false, remove: Bool = false) -> some View {
modifier(
IsHidden(
hidden: hidden,
remove: remove
))
}
}
struct IsHidden: ViewModifier {
var hidden = false
var remove = false
func body(content: Content) -> some View {
if hidden {
if remove {
} else {
content.hidden()
}
} else {
content
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,13 +8,6 @@
import SwiftUI import SwiftUI
struct BoosterConfirmationScreen: View { struct BoosterConfirmationScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View { var body: some View {
ZStack { ZStack {
@@ -22,12 +15,16 @@ struct BoosterConfirmationScreen: View {
.ignoresSafeArea() .ignoresSafeArea()
VStack { VStack {
VStack(spacing: 32) { VStack(spacing: 32) {
Image("booster_logo", bundle: Bundle.module) Image("booster_logo")
.resizable() .resizable()
.frame(width: 210, height: 180) .frame(width: 210, height: 180)
SQText("Cest confirmé !", size: 18, font: .bold) SQText("Cest confirmé !")
.sqSize(18)
.sqFont(.bold)
.foregroundColor(.white) .foregroundColor(.white)
SQText("Votre profil sera boosté dès demain.", size: 32, font: .bold) SQText("Votre profil sera boosté dès demain.")
.sqSize(32)
.sqFont(.bold)
.foregroundColor(.white) .foregroundColor(.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
}.padding() }.padding()
@@ -36,7 +33,8 @@ struct BoosterConfirmationScreen: View {
SQButton("Accéder à mon option") { SQButton("Accéder à mon option") {
} }
SQText("XX,XX € / mois, sans engagement.", size: 13) SQText("XX,XX € / mois, sans engagement.")
.sqSize(13)
.foregroundColor(.white) .foregroundColor(.white)
} }
} }

View File

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

View File

@@ -8,11 +8,6 @@
import SwiftUI import SwiftUI
struct BoosterSubscriptionManagementScreen: View { struct BoosterSubscriptionManagementScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
@State var selectedValue = 0 @State var selectedValue = 0
@State var presentControls = false @State var presentControls = false
@@ -22,7 +17,9 @@ struct BoosterSubscriptionManagementScreen: View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
BoosterActiveHeaderView() BoosterActiveHeaderView()
SQText("Mes boosters", size: 20, font: .bold) SQText("Mes boosters")
.sqSize(20)
.sqFont(.bold)
Picker("", selection: $selectedValue) { Picker("", selection: $selectedValue) {
SQText("À venir").tag(0) SQText("À venir").tag(0)
@@ -43,9 +40,9 @@ struct BoosterSubscriptionManagementScreen: View {
} }
} }
} }
.bottomSheet(isShowing: $presentControls, content: { // .bottomSheet(isShowing: $presentControls, content: {
BoosterSubscriptionOptionsView() // BoosterSubscriptionOptionsView()
}) // })
} }
} }

View File

@@ -40,12 +40,6 @@ struct BoosterSubscriptionSelectionScreen: View {
@State var mode: BoosterSubscriptionMode = .edit @State var mode: BoosterSubscriptionMode = .edit
@State var isNotPremier = false @State var isNotPremier = false
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
var body: some View { var body: some View {
NavigationView { NavigationView {
ZStack(alignment: .top) { ZStack(alignment: .top) {
@@ -58,12 +52,14 @@ struct BoosterSubscriptionSelectionScreen: View {
VStack(spacing: 48) { VStack(spacing: 48) {
ZStack { ZStack {
ElipseShape() ElipseShape()
.fill(Color.sqPurple(60))
Rectangle()
.fill(backgroundGradient) .fill(backgroundGradient)
VStack(alignment: .trailing) { VStack(alignment: .trailing) {
BoosterLockedToPremierView() BoosterLockedToPremierView()
VStack(spacing: 16) { VStack(spacing: 16) {
Image("booster_logo", bundle: Bundle.module) Image("booster_logo")
.resizable() .resizable()
.frame(width: 210, height: 180) .frame(width: 210, height: 180)
@@ -86,7 +82,8 @@ struct BoosterSubscriptionSelectionScreen: View {
SQButton("C'est parti !") {} SQButton("C'est parti !") {}
if cancellation { if cancellation {
Button(action: /*@START_MENU_TOKEN@*/ {}/*@END_MENU_TOKEN@*/, label: { Button(action: /*@START_MENU_TOKEN@*/ {}/*@END_MENU_TOKEN@*/, label: {
SQText("Non merci, je souhaite résilier", size: 13) SQText("Non merci, je souhaite résilier")
.sqSize(12)
.foregroundColor(.black) .foregroundColor(.black)
}) })
.padding() .padding()

View File

@@ -8,29 +8,26 @@
import SwiftUI import SwiftUI
struct BoosterActiveHeaderView: View { struct BoosterActiveHeaderView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View { var body: some View {
VStack { VStack {
Image("booster_logo", bundle: Bundle.module) Image("booster_logo")
.resizable() .resizable()
.frame(width: 93, height: 80) .frame(width: 93, height: 80)
VStack { VStack {
SQText("Booster en cours", size: 18, font: .bold) SQText("Booster en cours")
.sqSize(18)
.sqFont(.bold)
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
SQText("Aujourdhui", size: 14) SQText("Aujourdhui")
.sqSize(14)
} }
} }
.padding() .padding()
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.overlay( .overlay(
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
Image("booster_corner", bundle: Bundle.module) Image("booster_corner")
.resizable() .resizable()
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.cornerRadius(8, corners: [.topLeft]) .cornerRadius(8, corners: [.topLeft])

View File

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

View File

@@ -10,25 +10,19 @@ import SwiftUI
struct BoosterHistoryCellView: View { struct BoosterHistoryCellView: View {
var isEnded: Bool = false var isEnded: Bool = false
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View { var body: some View {
ZStack { ZStack {
HStack(alignment: .center) { HStack(alignment: .center) {
SQIcon(.calendar, color: .sqRoyal()) SQIcon(.calendar, color: .sqRoyal())
SQText("Samedi 20 avril", font: .bold) SQText("Samedi 20 avril")
.sqFont(.bold)
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.overlay( .overlay(
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
Image("booster_corner", bundle: Bundle.module) Image("booster_corner")
.resizable() .resizable()
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.cornerRadius(8, corners: [.topLeft, .bottomLeft]) .cornerRadius(8, corners: [.topLeft, .bottomLeft])

View File

@@ -8,17 +8,12 @@
import SwiftUI import SwiftUI
struct BoosterLockedToPremierView: View { struct BoosterLockedToPremierView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View { var body: some View {
HStack { HStack {
SQIcon(.lock_keyhole, size: .xs, type: .solid, color: .white) SQIcon(.lock_keyhole, size: .xs, type: .solid, color: .white)
SQText("Abonnés Premier", size: 13) SQText("Abonnés Premier")
.sqSize(13)
.foregroundColor(.white) .foregroundColor(.white)
} }
} }

View File

@@ -12,20 +12,23 @@ struct BoosterPromotionView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
VStack(spacing: 16) { VStack(spacing: 16) {
VStack { VStack {
SQText("Envie de booster votre activité ?", font: .bold) SQText("Envie de booster votre activité ?")
SQText("Activez loption Booster !", font: .demiBold) .sqFont(.bold)
SQText("Activez loption Booster !")
.sqFont(.semiBold)
} }
BoosterStatsView() BoosterStatsView()
SQButton("Activer loption Booster", color: .sqRoyal(), textColor: .white) { SQButton("Activer loption Booster") {
} }
.sqColorScheme(.royal)
} }
.padding() .padding()
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
.overlay( .overlay(
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
Image("booster_corner", bundle: Bundle.module) Image("booster_corner")
.resizable() .resizable()
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.cornerRadius(8, corners: .topLeft) .cornerRadius(8, corners: .topLeft)

View File

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

View File

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

View File

@@ -18,10 +18,6 @@ struct BoosterSubscriptionCardView: View {
} }
init(id: Int, selectedId: Binding<Int>, currentOption: Bool = false, isFree: Bool = false) { init(id: Int, selectedId: Binding<Int>, currentOption: Bool = false, isFree: Bool = false) {
do {
try FontRegistration().registerFonts()
} catch {}
self.id = id self.id = id
self._selectedId = selectedId self._selectedId = selectedId
self.currentOption = currentOption self.currentOption = currentOption
@@ -31,16 +27,20 @@ struct BoosterSubscriptionCardView: View {
var body: some View { var body: some View {
VStack { VStack {
if currentOption { if currentOption {
SQText("Option actuelle", font: .bold) SQText("Option actuelle")
.sqFont(.bold)
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
} }
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
VStack { VStack {
SQText("3 jours", size: 32, font: .bold) SQText("3 jours")
.sqSize(32)
.sqFont(.bold)
.minimumScaleFactor(0.5) .minimumScaleFactor(0.5)
.lineLimit(1) .lineLimit(1)
.foregroundColor(.white) .foregroundColor(.white)
SQText("par semaine", size: 14) SQText("par semaine")
.sqSize(14)
.minimumScaleFactor(0.5) .minimumScaleFactor(0.5)
.lineLimit(1) .lineLimit(1)
.foregroundColor(.white) .foregroundColor(.white)
@@ -51,7 +51,8 @@ struct BoosterSubscriptionCardView: View {
// .minimumScaleFactor(0.5) // .minimumScaleFactor(0.5)
// .lineLimit(1) // .lineLimit(1)
// .foregroundColor(.white) // .foregroundColor(.white)
SQText("Sans engagement", size: 12) SQText("Sans engagement")
.sqSize(12)
.minimumScaleFactor(0.5) .minimumScaleFactor(0.5)
.lineLimit(1) .lineLimit(1)
.foregroundColor(.white) .foregroundColor(.white)
@@ -61,7 +62,7 @@ struct BoosterSubscriptionCardView: View {
.padding(.top, 30) .padding(.top, 30)
.padding([.leading, .trailing]) .padding([.leading, .trailing])
Image("booster_corner_light", bundle: Bundle.module) Image("booster_corner_light")
.resizable() .resizable()
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.position(x: 25, y: 25) .position(x: 25, y: 25)
@@ -77,7 +78,8 @@ struct BoosterSubscriptionCardView: View {
startPoint: .top, endPoint: .bottom)) startPoint: .top, endPoint: .bottom))
) )
if isFree { if isFree {
SQText("1 mois gratuit *", font: .bold) SQText("1 mois gratuit *")
.sqFont(.bold)
.foregroundColor(.sqRoyal()) .foregroundColor(.sqRoyal())
} }
} }

View File

@@ -8,17 +8,13 @@
import SwiftUI import SwiftUI
struct BoosterSubscriptionOptionsView: View { struct BoosterSubscriptionOptionsView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {
SQText("Booster", size: 18, font: .bold) SQText("Booster")
.sqSize(18)
.sqFont(.bold)
Spacer() Spacer()
SQIcon(.xmark, color: .white) SQIcon(.xmark, color: .white)
} }

View File

@@ -0,0 +1,50 @@
//
// ProfileComplimentListView.swift
//
//
// Created by Victor on 31/07/2024.
//
import SwiftUI
private struct Compliment: Identifiable {
let id: Int
}
struct ProfileComplimentListView: View {
private let compliments: [Compliment] = [
Compliment(id: 1),
Compliment(id: 2),
Compliment(id: 3),
Compliment(id: 4),
Compliment(id: 5)
]
var body: some View {
VStack(alignment: .leading) {
SQText("Compliments reçus")
.sqSize(18)
.sqFont(.bold)
.padding(.horizontal)
VStack(alignment: .center, spacing: 16) {
VStack(alignment: .leading) {
ForEach(Array(stride(from: 0, to: compliments.count, by: 3)), id: \.self) { index in
HStack(spacing: 24) {
ForEach(index ..< min(index + 3, compliments.count), id: \.self) { _ in
ComplimentView()
}
}
}
}
Rectangle()
.frame(height: 16)
.foregroundColor(Color.sqNeutral(10))
}
}
}
}
#Preview {
ProfileComplimentListView()
}

View File

@@ -0,0 +1,38 @@
//
// ComplimentPillView.swift
//
//
// Created by Victor on 31/07/2024.
//
import SwiftUI
struct ComplimentPillView: View {
var body: some View {
HStack(spacing: 4) {
VStack {
SQIcon(.gen_euro_sign, size: .s, color: .sqGreen(100))
}
.padding(3)
.frame(width: 32, height: 32, alignment: .center)
.background(Color.sqGreen(10))
.cornerRadius(32)
SQText("Excellent rapport qualité/prix")
.sqSize(12)
.sqFont(.semiBold)
}
.padding(.leading, 1)
.padding(.trailing, 16)
.padding(.vertical, 1)
.cornerRadius(40)
.overlay(
RoundedRectangle(cornerRadius: 40)
.inset(by: 0.5)
.stroke(Color.sqNeutral(20), lineWidth: 1)
)
}
}
#Preview {
ComplimentPillView()
}

View File

@@ -0,0 +1,50 @@
//
// ComplimentView.swift
//
//
// Created by Victor on 31/07/2024.
//
import SwiftUI
struct ComplimentView: View {
var body: some View {
ZStack(alignment: .topTrailing) {
VStack(spacing: 4) {
VStack {
SQIcon(.gen_euro_sign, size: .xl, color: .sqGreen(100))
}
.frame(width: 56, height: 56, alignment: .center)
.background(Color.sqGreen(10))
.cornerRadius(100)
SQText("Excellent rapport qualité/prix")
.sqSize(11)
.sqFont(.semiBold)
.multilineTextAlignment(.center)
.foregroundColor(Color.sqNeutral(90))
}
.padding(.top, 4)
.frame(width: 87)
HStack {
SQText("1")
.sqSize(11)
.sqFont(.bold)
.foregroundColor(Color.sqNeutral())
.padding(.horizontal, 6)
.frame(height: 16)
.background(.white)
.cornerRadius(30)
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.sqNeutral(60), lineWidth: 1)
)
}
.padding(.trailing, 10)
}
}
}
#Preview {
ComplimentView()
}

View File

@@ -0,0 +1,97 @@
//
// CurrentDebugUser.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 15/01/2025.
//
import SwiftUI
struct CurrentDebugUser: View {
var body: some View {
VStack(spacing: 8) {
SQImage("avatar-1", height: 64)
VStack {
SQText("Harbin Leduc")
SQText("Torphy LLC")
SQText("Auto-entrepreneur")
.sqSize(12)
.sqFont(.semiBold)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background {
RoundedRectangle(cornerRadius: 8)
.fill(Color.sqPurple(20))
}
}
HStack {
SQText("UserID:")
.sqFont(.semiBold)
Spacer()
SQText("103135")
Button {} label: {
SQIcon(.files)
.padding(8)
.background {
Circle()
.fill(Color.sqNeutral(20))
}
}
}
HStack {
SQText("Email:")
.sqFont(.semiBold)
Spacer()
SQText("test@test.com")
Button {} label: {
SQIcon(.files)
.padding(8)
.background {
Circle()
.fill(Color.sqNeutral(20))
}
}
}
HStack {
SQText("Téléphone:")
.sqFont(.semiBold)
Spacer()
SQText("0612345678")
Button {} label: {
SQIcon(.files)
.padding(8)
.background {
Circle()
.fill(Color.sqNeutral(20))
}
}
}
// HStack {
// SQText("Auth Token:", font: .demiBold)
// Spacer()
// SQText("fpoFZ4G4ZGhrhreF4Z6ZZG53")
// }
// HStack {
// SQText("Airship ID:", font: .demiBold)
// Spacer()
// SQText("34HZRH657653H636")
// }
// HStack {
// SQText("Firebase Token:", font: .demiBold)
// Spacer()
// SQText("rzogjiéG245G24gégéregezgrz")
// }
}
.padding()
.background {
RoundedRectangle(cornerRadius: 10)
.fill(Color.sqNeutral(10))
}
.padding(.horizontal)
}
}
#Preview {
CurrentDebugUser()
}

View File

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

View File

@@ -0,0 +1,63 @@
//
// ConfigPrestationSearchView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 20/01/2025.
//
import SwiftUI
struct ConfigPrestationSearchView: View {
@State var showSearchView: Bool = false
@State var showSuggested: Bool = false
@State var showAllCategories: Bool = false
@State var selectedSearchType: PickerOption = .init(text: "Prestations")
@State var searchTypes: [PickerOption] = [
PickerOption(text: "Prestations"),
PickerOption(text: "Prestation Catégories"),
PickerOption(text: "Prestations + Prestation Catégories")
]
var body: some View {
VStack {
Spacer()
VStack(spacing: 16) {
SQText("Configuration de la recherche des prestations")
.sqSize(18)
.sqFont(.bold)
.multilineTextAlignment(.center)
Divider()
SQText("Types à rechercher :")
.sqFont(.semiBold)
SQPicker(selection: $selectedSearchType, options: searchTypes)
Toggle(isOn: $showSuggested) {
SQText("Afficher une suggestion")
.sqFont(.semiBold)
}
Toggle(isOn: $showAllCategories) {
SQText("Afficher \"Toutes les catégories\"")
.sqFont(.semiBold)
}
}
.padding()
.background {
RoundedRectangle(cornerRadius: 10)
.fill(Color.sqNeutral(10))
}
.padding(.horizontal)
Spacer()
SQFooter {
SQButton("Afficher la vue") {
showSearchView.toggle()
}
}
.sheet(isPresented: $showSearchView) {}
}
.ignoresSafeArea(.container, edges: .bottom)
}
}
#Preview {
ConfigPrestationSearchView()
}

View File

@@ -0,0 +1,133 @@
//
// DebugLandView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 15/01/2025.
//
import SwiftUI
enum DebugEnvironment {
case allo1
case allo2
case preprod
case hotfix
case prod
}
struct DebugLandView: View {
@State var currentEnv: PickerOption = .init(text: "Preprod")
@State var userId: String = ""
var pickerOptions: [PickerOption] {
[
PickerOption(text: "Allo1"),
PickerOption(text: "Allo2"),
PickerOption(text: "Allo3"),
PickerOption(text: "Allo4"),
PickerOption(text: "Allo5"),
PickerOption(text: "Allo6"),
PickerOption(text: "Allo7"),
PickerOption(text: "Allo8"),
PickerOption(text: "Allo9"),
PickerOption(text: "Allo10"),
PickerOption(text: "Preprod"),
PickerOption(text: "Hotfix"),
PickerOption(text: "Prod"),
]
}
var body: some View {
ScrollView {
VStack(spacing: 16) {
CurrentDebugUser()
VStack {
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)
.fill(Color.sqNeutral(10))
}
.padding(.horizontal)
VStack(alignment: .leading, spacing: 8) {
SQText("User ID :")
.sqFont(.semiBold)
HStack(spacing: 8) {
VStack {
SQTextField("Visiter le profil :", text: $userId)
.sqPlaceholder("UserID")
SQButton("Visiter le profil") {
}
}
VStack {
SQTextField("Se connecter sur :", text: $userId)
.sqPlaceholder("UserID")
.keyboardType(.numberPad)
SQButton("Se connecter") {
}
}
}
}
.padding()
.background {
RoundedRectangle(cornerRadius: 10)
.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 🦄")
.sqFont(.bold)
}
ToolbarItem(placement: .primaryAction) {
Button {} label: {
SQIcon(.user_group)
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {} label: {
SQIcon(.ballot_check)
}
}
}
}
}
#Preview {
NavigationStack {
DebugLandView()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
//
// DocumentPreviewView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 16/12/2024.
//
import Lottie
import SwiftUI
struct DocumentPreviewView: View {
let fileName: String
let onDelete: () -> Void
var body: some View {
VStack {
ZStack(alignment: .topTrailing) {
VStack {
if true {
SQImage("visit_card_new", height: 160)
} else {
SQIcon(.file_pdf, size: .l, color: .sqNeutral(80))
SQText("PDF")
.sqFont(.semiBold)
}
}
.frame(width: 160, height: 160, alignment: .center)
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color.sqNeutral(20), lineWidth: 1)
)
Button {} label: {
SQIcon(.xmark, size: .s, color: .white)
.padding()
.background {
Circle()
.foregroundColor(Color.sqSemanticRed)
.frame(height: 32)
}
.padding(-16)
}
}
SQText(fileName)
.sqSize(13)
}
}
}
#Preview {
DocumentPreviewView(fileName: "le-nom-de-mon-fichier.pdf") {}
}

View File

@@ -0,0 +1,104 @@
//
// IdentityDocumentButton.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 11/12/2024.
//
import SwiftUI
enum IdentityDocumentButtonType {
case nationalID
case passport
case driverLicense
case residencePermit
var title: String {
switch self {
case .nationalID:
"Carte nationale didentité"
case .passport:
"Passeport"
case .driverLicense:
"Permis de conduire"
case .residencePermit:
"Titre de séjour"
}
}
var subtitle: String {
switch self {
case .driverLicense:
"Nouveau format (après septembre 2013)"
default:
""
}
}
var icon: SQIcon {
switch self {
case .nationalID:
SQIcon(.address_card)
case .passport:
SQIcon(.passport)
case .driverLicense:
SQIcon(.car_side)
case .residencePermit:
SQIcon(.address_card)
}
}
}
struct IdentityDocumentButton: View {
var type: IdentityDocumentButtonType
var action: () -> Void
var body: some View {
Button {
action()
} label: {
HStack {
type.icon
.padding(8)
.background {
RoundedRectangle(cornerRadius: 8)
.fill(Color.sqNeutral(10))
}
VStack(alignment: .leading) {
SQText(type.title)
if !type.subtitle.isEmpty {
SQText(type.subtitle)
.sqSize(13)
.sqColor(.sqNeutral(70))
}
}
Spacer()
SQIcon(.chevron_right, size: .xl)
}
.padding()
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color.sqNeutral(30), lineWidth: 1)
)
}
}
}
#Preview {
VStack {
IdentityDocumentButton(type: .nationalID) {
}
IdentityDocumentButton(type: .passport) {
}
IdentityDocumentButton(type: .driverLicense) {
}
IdentityDocumentButton(type: .residencePermit) {
}
}
.padding()
}

View File

@@ -0,0 +1,108 @@
//
// KYCDocumentButton.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 11/12/2024.
//
import SwiftUI
enum KYCDocumentStatus: String {
case accepted
case missing
case imported
case pending
case refused
case expired
var text: String {
switch self {
case .accepted:
"Accepté"
case .missing:
"Manquant"
case .imported:
"Importé"
case .pending:
"En cours de vérification"
case .refused:
"Refusé"
case .expired:
"Expiré"
}
}
var color: Color {
switch self {
case .accepted:
.sqSemanticPositive
case .missing:
.sqNeutral(80)
case .imported, .pending:
.sqSemanticCritical
case .refused, .expired:
.sqSemanticNegative
}
}
var icon: SQIcon {
switch self {
case .accepted:
SQIcon(.check, size: .xs, color: color)
case .missing:
SQIcon(.circle_exclamation, size: .xs, color: color)
case .imported, .pending:
SQIcon(.hourglass_half, size: .xs, color: color)
case .refused:
SQIcon(.xmark, size: .xs, color: color)
case .expired:
SQIcon(.triangle_exclamation, size: .xs, color: color)
}
}
}
struct KYCDocumentButton: View {
var name: String
var desc: String
var status: KYCDocumentStatus
var body: some View {
VStack(alignment: .leading) {
HStack {
SQText(name)
.sqFont(.bold)
Spacer()
HStack(spacing: 4) {
SQText(status.text)
.sqSize(13)
.sqFont(.semiBold)
.sqColor(status.color)
status.icon
}
}
HStack {
SQText(desc)
.sqSize(14)
Spacer()
if [.missing, .expired, .refused].contains(status) {
SQIcon(.chevron_right, size: .l)
}
}
}
.padding()
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color.sqNeutral(30), lineWidth: 1)
)
}
}
#Preview {
VStack {
KYCDocumentButton(name: "Document didentité", desc: "Carte didentité, passeport, permis de conduire ou titre de séjour du représentant légal.", status: .pending)
KYCDocumentButton(name: "Preuve dimmatriculation", desc: "Extrait Kbis de moins de 3 mois.", status: .accepted)
KYCDocumentButton(name: "Statuts de la société", desc: "Statuts complets, à jour et signés.", status: .refused)
}
.padding()
}

View File

@@ -0,0 +1,32 @@
//
// KYCInformationAlertView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 11/12/2024.
//
import SwiftUI
struct KYCInformationAlertView: View {
var body: some 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.")
.sqSize(14)
.sqFont(.semiBold)
.sqColor(.sqRed(80))
}
}
.frame(maxWidth: .infinity)
.foregroundStyle(.white)
.padding(16)
.background(Color.sqRed(20))
.cornerRadius(8)
}
}
#Preview {
KYCInformationAlertView()
.padding()
}

View File

@@ -0,0 +1,54 @@
//
// CardColorSelectionView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 12/11/2024.
//
import SwiftUI
struct CardColorSelectionView: View {
@State var goToNext: Bool = false
@State private var applyToAll: Bool = true
@State private var selectedColor: Color = .sqNeutral()
var selectedTemplate: CardTemplate
var body: some View {
VStack {
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")
.sqSize(18)
.sqFont(.bold)
selectedTemplate.imageColorTemplate(isLight: selectedColor.isLight)
.background(selectedColor)
.frame(maxWidth: .infinity)
}
VStack(alignment: .leading) {
Toggle(isOn: $applyToAll) {
SQText("Appliquer cette couleur sur mes prospectus, mes devis et mes factures")
}
.tint(Color.sqNeutral(100))
SQText("Nous vous recommandons dappliquer la même couleur sur tous vos documents.")
.sqSize(12)
}
Spacer()
}
.padding()
SQFooter {
SQButton("Continuer") {
self.goToNext.toggle()
}
}
}
.sqNavigationBar(title: "Choix de la couleur ")
.navigationDestination(isPresented: $goToNext) {
CardFormView()
}
}
}
#Preview {
CardColorSelectionView(selectedTemplate: CardTemplate.template4)
}

View File

@@ -0,0 +1,178 @@
//
// CardFormView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 12/11/2024.
//
import PhotosUI
import SwiftUI
struct CardFormView: View {
@State var title: String = ""
@State var subtitle: String = ""
@State var job: String = ""
@State var showRating: Bool = true
@State var phoneNumber: String = ""
@State var address: String = ""
@State var selectedPicture: PhotosPickerItem?
@State private var image: Image?
@State private var showImagePicker = false
@State private var showPhotoPicker = false
@State private var showActionSheet = false
@State private var useCamera = false
var body: some View {
VStack {
ScrollView {
VStack {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading) {
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")
.sqFont(.semiBold)
ZStack(alignment: .bottomTrailing) {
Button(action: { showActionSheet = true }) {
if let image = image {
image
.resizable()
.scaledToFill()
.frame(width: 160, height: 160)
.clipShape(Circle())
} else {
SQIcon(.camera, customSize: 40)
.frame(width: 160, height: 160)
.background(Color.sqNeutral(10))
.clipShape(Circle())
}
}
if image != nil {
SQIcon(.pen_to_square, size: .m)
.padding(8)
.background(Circle().fill(Color.sqNeutral(15)))
.shadow(color: .sqNeutral(100).opacity(0.1), radius: 4, x: 0, y: 2)
.offset(x: -8, y: -8)
}
}
.frame(maxWidth: .infinity)
}
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")
.sqFont(.semiBold)
}
.tint(Color.sqNeutral(100))
HStack(spacing: 4) {
SQIcon(.star, type: .solid, color: .sqGold(50))
SQText("4,7/5 sur")
.sqSize(12)
.sqFont(.semiBold)
SQImage("logo", height: 14)
}
}
.padding(.horizontal, 2)
}
.padding()
Rectangle()
.frame(height: 16)
.foregroundColor(Color.sqNeutral(10))
VStack(alignment: .leading, spacing: 16) {
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") {}
.sqIcon(SQIcon(.eye, color: .sqNeutral(100)))
.sqButtonType(.line)
SQButton("Imprimer") {}
.sqIcon(SQIcon(.print, color: .white))
}
}
.confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: {
Button("Appareil photo") {
useCamera = true
showImagePicker = true
}
Button("Galerie") {
useCamera = false
showPhotoPicker = true
}
Button("Annuler", role: .cancel) {}
})
.photosPicker(isPresented: $showPhotoPicker, selection: $selectedPicture)
.sheet(isPresented: $showImagePicker) {
if useCamera {
ImagePicker(image: $image, sourceType: .camera)
}
}
.task(id: selectedPicture) {
if let data = try? await selectedPicture?.loadTransferable(type: Image.self) {
image = data
}
}
.sqNavigationBar(title: "Ma carte de visite")
}
}
struct ImagePicker: UIViewControllerRepresentable {
@Binding var image: Image?
let sourceType: UIImagePickerController.SourceType
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any])
{
if let uiImage = info[.originalImage] as? UIImage {
parent.image = Image(uiImage: uiImage)
}
picker.dismiss(animated: true)
}
}
}
#Preview {
CardFormView()
}

View File

@@ -0,0 +1,31 @@
//
// CardPrintView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 13/11/2024.
//
import SwiftUI
struct CardPrintView: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
SQText("Génération dune planche au format A4 comprenant 8 cartes de visite.")
.sqFont(.semiBold)
Image("visit_card_print_template")
.resizable()
.scaledToFit()
.frame(height: 100)
.frame(maxWidth: .infinity)
SQText("Nous vous recommandons : \n • Une impression haute qualité, en couleur \n • Lutilisation dun papier dau moins200 g/m2 ")
SQButton("Imprimer") {}
.frame(maxWidth: .infinity)
Spacer()
}
.padding()
}
}
#Preview {
CardPrintView()
}

View File

@@ -0,0 +1,87 @@
//
// CardTemplateSelectionView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 12/11/2024.
//
import SwiftUI
enum CardTemplate: String, CaseIterable {
case template1
case template2
case template3
case template4
var image: SQImage {
switch self {
case .template1:
SQImage("visit_card_template_1", height: 222)
case .template2:
SQImage("visit_card_template_2", height: 222)
case .template3:
SQImage("visit_card_template_3", height: 222)
case .template4:
SQImage("visit_card_template_4", height: 222)
}
}
func imageColorTemplate(isLight: Bool) -> SQImage {
switch self {
case .template1:
return SQImage("visit_card_color_template_1", height: 222)
case .template2:
return SQImage("visit_card_color_template_2", height: 222)
case .template3:
return SQImage("visit_card_color_template_3", height: 222)
case .template4:
if isLight {
return SQImage("visit_card_color_template_4_black", height: 222)
} else {
return SQImage("visit_card_color_template_4_white", height: 222)
}
}
}
}
struct CardTemplateSelectionView: View {
@State var goToNext: Bool = false
@State var selectedTemplate: CardTemplate?
var body: some View {
VStack {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(CardTemplate.allCases, id: \.self) { template in
template.image
.overlay(
Rectangle()
.inset(by: selectedTemplate == template ? 1 : 0.5)
.stroke(selectedTemplate == template ? Color.sqNeutral(100) : Color.sqNeutral(20), lineWidth: selectedTemplate == template ? 2 : 1)
)
.opacity(selectedTemplate != nil ? selectedTemplate == template ? 1 : 0.5 : 1)
.onTapGesture {
selectedTemplate = template
}
}
}
}
SQFooter {
SQButton("Continuer") {
if selectedTemplate != nil {
goToNext.toggle()
}
}
.disabled(selectedTemplate == nil)
}
}
.sqNavigationBar(title: "Choix du modèle")
.navigationDestination(isPresented: $goToNext) {
CardColorSelectionView(selectedTemplate: selectedTemplate ?? CardTemplate.template1)
}
}
}
#Preview {
CardTemplateSelectionView()
}

View File

@@ -0,0 +1,54 @@
//
// FlyerColorSelectionView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 25/11/2024.
//
import SwiftUI
struct FlyerColorSelectionView: View {
@State var goToNext: Bool = false
@State private var applyToAll: Bool = true
@State private var selectedColor: Color = .sqNeutral()
var selectedTemplate: FlyerTemplate
var body: some View {
VStack {
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")
.sqSize(18)
.sqFont(.bold)
selectedTemplate.imageColorTemplate(isLight: selectedColor.isLight)
.background(selectedColor)
.frame(maxWidth: .infinity)
}
VStack(alignment: .leading) {
Toggle(isOn: $applyToAll) {
SQText("Appliquer cette couleur sur mes cartes de visite, mes devis et mes factures")
}
.tint(Color.sqNeutral(100))
SQText("Nous vous recommandons dappliquer la même couleur sur tous vos documents.")
.sqSize(12)
}
Spacer()
}
.padding()
SQFooter {
SQButton("Continuer") {
self.goToNext.toggle()
}
}
}
.sqNavigationBar(title: "Choix de la couleur ")
.navigationDestination(isPresented: $goToNext) {
CardFormView()
}
}
}
#Preview {
FlyerColorSelectionView(selectedTemplate: .template4)
}

View File

@@ -0,0 +1,213 @@
//
// FlyerFormView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 25/11/2024.
//
import SwiftUI
import _PhotosUI_SwiftUI
struct FlyerFormView: View {
@State var title: String = ""
@State var subtitle: String = ""
@State var job: String = ""
@State var showRating: Bool = true
@State var phoneNumber: String = ""
@State var address: String = ""
@State var selectedPicture: PhotosPickerItem?
@State private var image: Image?
@State private var showImagePicker = false
@State private var showPhotoPicker = false
@State private var showActionSheet = false
@State private var useCamera = false
@State private var authentConfirm: Bool = false
@State private var confirmIsInError: Bool = false
var body: some View {
VStack {
ScrollView {
VStack {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading) {
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")
.sqFont(.semiBold)
ZStack(alignment: .bottomTrailing) {
Button(action: { showActionSheet = true }) {
if let image = image {
image
.resizable()
.scaledToFill()
.frame(width: 160, height: 160)
.clipShape(Circle())
} else {
SQIcon(.camera, customSize: 40)
.frame(width: 160, height: 160)
.background(Color.sqNeutral(10))
.clipShape(Circle())
}
}
if image != nil {
SQIcon(.pen_to_square, size: .m)
.padding(8)
.background(Circle().fill(Color.sqNeutral(15)))
.shadow(color: .sqNeutral(100).opacity(0.1), radius: 4, x: 0, y: 2)
.offset(x: -8, y: -8)
}
}
.frame(maxWidth: .infinity)
}
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")
.sqFont(.semiBold)
}
.tint(Color.sqNeutral(100))
HStack(spacing: 4) {
SQIcon(.star, type: .solid, color: .sqGold(50))
SQText("4,7/5 sur")
.sqSize(12)
.sqFont(.semiBold)
SQImage("logo", height: 14)
}
}
.padding(.horizontal, 2)
}
.padding()
Rectangle()
.frame(height: 16)
.foregroundColor(Color.sqNeutral(10))
VStack(alignment: .leading, spacing: 16) {
SQTextEditor("Zone de text", placeholder: "Ex : Pro Solutions propose une gamme de services complète pour tout type de dépannage électroménager. Bénéficiez de nos 10 ans dexpérience ! Devis gratuit", text: .constant(""))
SQTextField("Prestation 1", 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()
Rectangle()
.frame(height: 16)
.foregroundColor(Color.sqNeutral(10))
VStack(alignment: .leading, spacing: 16) {
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()
Rectangle()
.frame(height: 16)
.foregroundColor(Color.sqNeutral(10))
VStack(alignment: .leading, spacing: 16) {
SQText("Mentions légales obligatoires")
.sqSize(18)
.sqFont(.bold)
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", 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", 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 jengage ma responsabilité sur lexhaustivité et lauthenticité des informations renseignées ci-dessus.", isChecked: $authentConfirm, error: .constant(.none))
}
.padding()
}
}
SQFooter {
SQButton("Aperçu") {}
.sqIcon(SQIcon(.eye, color: .sqNeutral(100)))
.sqButtonType(.line)
SQButton("Imprimer") {
if authentConfirm == false {
self.confirmIsInError = true
}
}
.sqIcon(SQIcon(.print, color: .white))
}
}
.confirmationDialog("Choisir une image", isPresented: $showActionSheet, actions: {
Button("Appareil photo") {
useCamera = true
showImagePicker = true
}
Button("Galerie") {
useCamera = false
showPhotoPicker = true
}
Button("Annuler", role: .cancel) {}
})
.photosPicker(isPresented: $showPhotoPicker, selection: $selectedPicture)
.sheet(isPresented: $showImagePicker) {
if useCamera {
ImagePicker(image: $image, sourceType: .camera)
}
}
.task(id: selectedPicture) {
if let data = try? await selectedPicture?.loadTransferable(type: Image.self) {
image = data
}
}
.sqNavigationBar(title: "Mon prospectus")
}
}
#Preview {
FlyerFormView()
}

View File

@@ -1,18 +1,18 @@
// //
// TrialWarningCellView.swift // FlyerPrintView.swift
// AlloVoisinsSwiftUI // AlloVoisinsSwiftUI
// //
// Created by Victor on 14/10/2024. // Created by Victor on 25/11/2024.
// //
import SwiftUI import SwiftUI
struct TrialWarningCellView: View { struct FlyerPrintView: View {
var body: some View { var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
} }
} }
#Preview { #Preview {
TrialWarningCellView() FlyerPrintView()
} }

View File

@@ -0,0 +1,108 @@
//
// FlyerTemplateSelectionView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 25/11/2024.
//
import SwiftUI
enum FlyerTemplate: String, CaseIterable {
case template1
case template2
case template3
case template4
var image: SQImage {
switch self {
case .template1:
SQImage("flyer_template_1", height: 284)
case .template2:
SQImage("flyer_template_2", height: 284)
case .template3:
SQImage("flyer_template_3", height: 284)
case .template4:
SQImage("flyer_template_4", height: 284)
}
}
var nbOfPictures: Int {
switch self {
case .template1:
1
case .template2:
3
case .template3:
4
case .template4:
2
}
}
func imageColorTemplate(isLight: Bool) -> SQImage {
switch self {
case .template1:
if isLight {
return SQImage("flyer_color_template_1_black", height: 284)
} else {
return SQImage("flyer_color_template_1_white", height: 284)
}
case .template2:
return SQImage("flyer_color_template_2", height: 284)
case .template3:
if isLight {
return SQImage("flyer_color_template_3_black", height: 284)
} else {
return SQImage("flyer_color_template_3_white", height: 284)
}
case .template4:
if isLight {
return SQImage("flyer_color_template_4_black", height: 284)
} else {
return SQImage("flyer_color_template_4_white", height: 284)
}
}
}
}
struct FlyerTemplateSelectionView: View {
@State var goToNext: Bool = false
@State var selectedTemplate: FlyerTemplate?
var body: some View {
VStack {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(FlyerTemplate.allCases, id: \.self) { template in
template.image
.overlay(
Rectangle()
.inset(by: selectedTemplate == template ? 1 : 0.5)
.stroke(selectedTemplate == template ? Color.sqNeutral(100) : Color.sqNeutral(20), lineWidth: selectedTemplate == template ? 2 : 1)
)
.opacity(selectedTemplate != nil ? selectedTemplate == template ? 1 : 0.5 : 1)
.onTapGesture {
selectedTemplate = template
}
}
}
}
SQFooter {
SQButton("Continuer") {
if selectedTemplate != nil {
goToNext.toggle()
}
}
.disabled(selectedTemplate == nil)
}
}
.sqNavigationBar(title: "Choix du modèle")
.navigationDestination(isPresented: $goToNext) {
// FlyerColorSelectionView(selectedTemplate: selectedTemplate ?? FlyerTemplate.template1)
}
}
}
#Preview {
FlyerTemplateSelectionView()
}

View File

@@ -0,0 +1,92 @@
//
// MarketingSupportSelectionView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 12/11/2024.
//
import SwiftUI
struct MarketingSupportSelectionView: View {
@State var goToNext: Bool = false
var body: some View {
ScrollView {
VStack(spacing: 16) {
Spacer()
.frame(height: 16)
// Visit cards
VStack(alignment: .leading, spacing: 0) {
SQImage("visit_card_new", height: 200)
.cornerRadius(8, corners: [.topLeft, .topRight])
HStack {
SQText("Mes cartes de visite")
.sqSize(18)
.sqFont(.bold)
Spacer()
SQImage("for_pro_icon", height: 13)
}
.padding(16)
}
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color.sqNeutral(20), lineWidth: 1)
)
.onTapGesture {
self.goToNext.toggle()
}
.fixedSize(horizontal: true, vertical: false)
// Flyers
VStack(alignment: .leading, spacing: 0) {
SQImage("flyers", height: 200)
.cornerRadius(8, corners: [.topLeft, .topRight])
HStack {
SQText("Mes prospetus")
.sqSize(18)
.sqFont(.bold)
Spacer()
SQImage("for_pro_icon", height: 13)
}
.padding(16)
}
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color.sqNeutral(20), lineWidth: 1)
)
.fixedSize(horizontal: true, vertical: false)
// Quotes
VStack(alignment: .center, spacing: 0) {
SQImage("quotes", height: 200)
.cornerRadius(8, corners: [.topLeft, .topRight])
HStack {
SQText("Mes prospetus")
.sqSize(18)
.sqFont(.bold)
Spacer()
SQImage("for_pro_icon", height: 13)
}
.padding(16)
}
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color.sqNeutral(20), lineWidth: 1)
)
.fixedSize(horizontal: true, vertical: false)
}
}
.scrollIndicators(.hidden)
.sqNavigationBar(title: "Mes supports de communication")
.navigationDestination(isPresented: $goToNext) {
CardTemplateSelectionView()
}
}
}
#Preview {
MarketingSupportSelectionView()
}

View File

@@ -0,0 +1,58 @@
//
// MessageCenterChipFiltersCellView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 24/10/2024.
//
import SwiftUI
enum MessageCenterFilter: CaseIterable, Equatable {
case all
case unread
case archived
var chipOption: SQChipOption {
switch self {
case .all:
return SQChipOption(id: 0, label: "Toutes", bgColor: Color.sqNeutral(100))
case .unread:
return SQChipOption(id: 1, label: "Non lues", bgColor: Color.sqSemanticBlue)
case .archived:
return SQChipOption(id: 2, label: "Archivées", bgColor: Color.sqSemanticCritical)
}
}
}
struct MessageCenterChipFiltersCellView: View {
@State var selectedOption: SQChipOption
let options: [SQChipOption]
let onFilterSelected: (MessageCenterFilter) -> Void
init(onFilterSelected: @escaping (MessageCenterFilter) -> Void) {
self.options = [
MessageCenterFilter.all.chipOption,
MessageCenterFilter.unread.chipOption,
MessageCenterFilter.archived.chipOption
]
self._selectedOption = State(initialValue: MessageCenterFilter.all.chipOption)
self.onFilterSelected = onFilterSelected
}
var body: some View {
VStack {
SQChips(options: options, selectedOption: $selectedOption)
.onChange(of: selectedOption) { newValue in
if let filter = MessageCenterFilter.allCases.first(where: { $0.chipOption.id == newValue.id }) {
onFilterSelected(filter)
}
}
.padding()
}
}
}
#Preview {
MessageCenterChipFiltersCellView { _ in
}
}

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