Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e305b1697a | ||
|
|
08666a6818 | ||
|
|
eb99d76108 | ||
|
|
b09cf50619 | ||
|
|
295a0515c3 | ||
|
|
2031687f53 | ||
|
|
6e711be25d | ||
|
|
d962262874 | ||
|
|
c211091406 | ||
|
|
e9719414e9 |
@@ -6,13 +6,30 @@
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
573DAF242CF0CC54006DF58A /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 573DAF232CF0CC54006DF58A /* Lottie */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
57282ACB2CBD2810000C443E /* AlloVoisinsSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AlloVoisinsSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* 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 */
|
||||
57282ACD2CBD2810000C443E /* AlloVoisinsSwiftUI */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
57282BA72CBD579A000C443E /* Exceptions for "AlloVoisinsSwiftUI" folder in "AlloVoisinsSwiftUI" target */,
|
||||
);
|
||||
path = AlloVoisinsSwiftUI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -23,6 +40,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
573DAF242CF0CC54006DF58A /* Lottie in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -65,6 +83,7 @@
|
||||
);
|
||||
name = AlloVoisinsSwiftUI;
|
||||
packageProductDependencies = (
|
||||
573DAF232CF0CC54006DF58A /* Lottie */,
|
||||
);
|
||||
productName = AlloVoisinsSwiftUI;
|
||||
productReference = 57282ACB2CBD2810000C443E /* AlloVoisinsSwiftUI.app */;
|
||||
@@ -94,6 +113,9 @@
|
||||
);
|
||||
mainGroup = 57282AC22CBD2810000C443E;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
573DAF222CF0CC54006DF58A /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 57282ACC2CBD2810000C443E /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -177,7 +199,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
@@ -234,7 +256,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
@@ -255,11 +277,14 @@
|
||||
DEVELOPMENT_TEAM = WVH3Y23X7X;
|
||||
ENABLE_PREVIEWS = 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_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
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 = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -284,11 +309,14 @@
|
||||
DEVELOPMENT_TEAM = WVH3Y23X7X;
|
||||
ENABLE_PREVIEWS = 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_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
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 = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -324,6 +352,25 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "9F0CD4E1-EA25-475C-9981-B5E4AE8BD8B9"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
||||
@@ -11,7 +11,9 @@ import SwiftUI
|
||||
struct AlloVoisinsSwiftUIApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
NavigationStack {
|
||||
DebugLandView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
AlloVoisinsSwiftUI/Core/Managers/BaseNavigationManager.swift
Normal file
49
AlloVoisinsSwiftUI/Core/Managers/BaseNavigationManager.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// BaseNavigationManager.swift
|
||||
// Allovoisins
|
||||
//
|
||||
// Created by Victor on 06/12/2024.
|
||||
// Copyright © 2024 AlloVoisins. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
protocol NavigationRoute: Hashable {}
|
||||
|
||||
class BaseNavigationManager<Route: NavigationRoute>: ObservableObject {
|
||||
@Published var navigationPath = NavigationPath()
|
||||
@Published private var routeStack: [Route] = []
|
||||
@Published var shouldDismissToRoot: Bool = false
|
||||
@Published var shouldDismiss: Bool = false
|
||||
|
||||
var currentRoute: Route? {
|
||||
routeStack.last
|
||||
}
|
||||
|
||||
var canGoBack: Bool {
|
||||
!routeStack.isEmpty && !navigationPath.isEmpty
|
||||
}
|
||||
|
||||
func navigateTo(_ route: Route) {
|
||||
navigationPath.append(route)
|
||||
routeStack.append(route)
|
||||
}
|
||||
|
||||
func goBack() {
|
||||
if canGoBack {
|
||||
navigationPath.removeLast()
|
||||
routeStack.removeLast()
|
||||
} else {
|
||||
shouldDismiss = true
|
||||
}
|
||||
}
|
||||
|
||||
func resetNavigation() {
|
||||
navigationPath = NavigationPath()
|
||||
routeStack = []
|
||||
}
|
||||
|
||||
func dismissToRoot() {
|
||||
shouldDismissToRoot = true
|
||||
}
|
||||
}
|
||||
34
AlloVoisinsSwiftUI/Core/Models/SQFormFieldError.swift
Normal file
34
AlloVoisinsSwiftUI/Core/Models/SQFormFieldError.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
159
AlloVoisinsSwiftUI/Core/Utils/File.swift
Normal file
159
AlloVoisinsSwiftUI/Core/Utils/File.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
36
AlloVoisinsSwiftUI/Core/Utils/FontRegistration.swift
Normal file
36
AlloVoisinsSwiftUI/Core/Utils/FontRegistration.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
AlloVoisinsSwiftUI/Core/Views/MessageNotificationView.swift
Normal file
65
AlloVoisinsSwiftUI/Core/Views/MessageNotificationView.swift
Normal 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))
|
||||
}
|
||||
@@ -8,22 +8,19 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OnlyForPremierView: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Réservé aux abonnés Premier", size: 18, font: .bold)
|
||||
SQText("Réservé aux abonnés Premier")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
SQText("Seuls les abonnés Premier peuvent profiter de cette fonctionnalité.")
|
||||
}
|
||||
SQButton( "Découvrir l’abonnement Premier", color: Color.sqOrange(50), textColor: .white) {
|
||||
SQButton( "Découvrir l’abonnement Premier") {
|
||||
|
||||
}
|
||||
.sqColorScheme(.orange)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
@@ -8,11 +8,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OnlyForPremierModal: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
@@ -23,7 +18,7 @@ struct OnlyForPremierModal: View {
|
||||
.fill(Color.sqOrange(50))
|
||||
.frame(height: 200)
|
||||
VStack(alignment: .leading, spacing: 32) {
|
||||
Image("only_for_premier", bundle: .module)
|
||||
Image("only_for_premier")
|
||||
.resizable()
|
||||
.frame(width: 307, height: 108)
|
||||
}
|
||||
@@ -32,26 +27,33 @@ struct OnlyForPremierModal: View {
|
||||
Color.white
|
||||
VStack(spacing: 48) {
|
||||
VStack(spacing: 24) {
|
||||
SQText("Vous avez déjà répondu à 4 demandes de services ce mois-ci", size: 24, font: .bold)
|
||||
SQText("Vous avez déjà répondu à 4 demandes de services ce mois-ci")
|
||||
.sqSize(24)
|
||||
.sqFont(.bold)
|
||||
.multilineTextAlignment(.center)
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQText("Augmentez votre chiffre d’affaires en bénéficiant de tous les avantages inclus dans l’abonnement Premier.", font: .bold)
|
||||
SQText("Augmentez votre chiffre d’affaires en bénéficiant de tous les avantages inclus dans l’abonnement Premier.")
|
||||
.sqFont(.bold)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
|
||||
SQText("Répondez aux demandes en illimité", font: .demiBold)
|
||||
SQText("Répondez aux demandes en illimité")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
|
||||
SQText("Augmentez votre visibilité sur Google", font: .demiBold)
|
||||
SQText("Augmentez votre visibilité sur Google")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
|
||||
SQText("Affichez votre numéro de téléphone sur votre profil", font: .demiBold)
|
||||
SQText("Affichez votre numéro de téléphone sur votre profil")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.check, size: .s, type: .solid, color: .sqOrange(50))
|
||||
SQText("Gérez vos devis, factures, encaissements...", font: .demiBold)
|
||||
SQText("Gérez vos devis, factures, encaissements...")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,12 +62,15 @@ struct OnlyForPremierModal: View {
|
||||
Spacer()
|
||||
VStack(spacing: 16) {
|
||||
VStack {
|
||||
SQText("Essai gratuit de 14 jours", size: 18, font: .bold)
|
||||
SQText("Essai gratuit de 14 jours")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
SQText("à partir de 29,99 € / mois")
|
||||
}
|
||||
SQButton("Je m'abonne", color: .sqOrange(50), textColor: .white) {
|
||||
SQButton("Je m'abonne") {
|
||||
|
||||
}
|
||||
.sqColorScheme(.orange)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 24)
|
||||
@@ -8,43 +8,42 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RegulatedProfessionEditProfilModal: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(spacing: 24) {
|
||||
SQText("Profession réglementée", size: 32, font: .bold)
|
||||
SQText("Profession réglementée")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQText("Vous ne pouvez pas enregistrer votre profil car des éléments indiquent que vous proposez vos services en déménagement avec véhicule. L’activité de déménageur avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent l’exercer.", font: .medium)
|
||||
SQText("Aussi, vous n’êtes pas autorisé à proposer vos services de déménageur avec véhicule, ni à en faire mention sur votre profil.", font: .medium)
|
||||
SQText("Vous ne pouvez pas enregistrer votre profil car des éléments indiquent que vous proposez vos services en déménagement avec véhicule. L’activité de déménageur avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent l’exercer.")
|
||||
SQText("Aussi, vous n’êtes pas autorisé à proposer vos services de déménageur avec véhicule, ni à en faire mention sur votre profil.")
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Nous vous invitons à corriger les éléments suivants :", size: 16, font: .demiBold)
|
||||
SQText("Nous vous invitons à corriger les éléments suivants :")
|
||||
.sqFont(.semiBold)
|
||||
HStack {
|
||||
SQIcon(.xmark, size: .m, type: .solid, color: .red)
|
||||
SQText("Titre du profil", font: .medium)
|
||||
SQText("Titre du profil")
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.xmark, size: .m, type: .solid, color: .red)
|
||||
SQText("Présentation profil", font: .medium)
|
||||
SQText("Présentation profil")
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.xmark, size: .m, type: .solid, color: .red)
|
||||
SQText("Photo de couverture", font: .medium)
|
||||
SQText("Photo de couverture")
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.xmark, size: .m, type: .solid, color: .red)
|
||||
SQText("Photo de profil", font: .medium)
|
||||
SQText("Photo de profil")
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.xmark, size: .m, type: .solid, color: .red)
|
||||
SQText("Photos de réalisation", font: .medium)
|
||||
SQText("Photos de réalisation")
|
||||
}
|
||||
}
|
||||
SQText("À défaut, ces éléments seront supprimés de votre profil en septembre.", font: .demiBold)
|
||||
SQText("À défaut, ces éléments seront supprimés de votre profil en septembre.")
|
||||
.sqFont(.semiBold)
|
||||
Text("[Retrouvez ici](https://www.codingwithrashid.com) les informations utiles pour devenir déménageur agréé.")
|
||||
// SQText("[Retrouvez ici](https://www.codingwithrashid.com) les informations utiles pour devenir déménageur agréé.", font: .medium)
|
||||
}
|
||||
@@ -53,7 +52,8 @@ struct RegulatedProfessionEditProfilModal: View {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
SQButton("fermer", color: .sqNeutral(100), textColor: .white) {}
|
||||
SQButton("fermer") {}
|
||||
.disabled(true)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 24)
|
||||
@@ -8,28 +8,26 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RegulatedProfessionModal: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack() {
|
||||
VStack(spacing: 16) {
|
||||
SQText("Attention au cadre juridique", size: 24, font: .bold)
|
||||
SQText("Attention au cadre juridique")
|
||||
.sqSize(24)
|
||||
.sqFont(.bold)
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQText("L’activité de déménagement avec véhicule est juridiquement réglementée et seuls les professionnels agréés peuvent l’exercer.")
|
||||
SQText("Vous pouvez proposer vos services pour participer à un déménagement, mais vous n’êtes pas autorisé à mettre à disposition un véhicule.")
|
||||
SQText("Vous trouverez plus d'informations sur les activités réglementées dans la FAQ.")
|
||||
SQText("Le non-respect de ce cadre légal pourra entraîner la suspension de votre compte.", font: .demiBold)
|
||||
SQText("Le non-respect de ce cadre légal pourra entraîner la suspension de votre compte.")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
SQButton("J'ai compris", color: .sqNeutral(100), textColor: .white) {}
|
||||
SQButton("J'ai compris") {}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 24)
|
||||
367
AlloVoisinsSwiftUI/Core/Views/Sequoia/AVAvatar.swift
Normal file
367
AlloVoisinsSwiftUI/Core/Views/Sequoia/AVAvatar.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,12 @@
|
||||
import SwiftUI
|
||||
|
||||
struct NeighborBanner: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
SQIcon(.lightbulb_on, size: .xl, color: .sqGreen(80))
|
||||
SQText("Nous vous recommandons de sélectionner 3 offreurs.", size: 16, font: .demiBold)
|
||||
SQText("Nous vous recommandons de sélectionner 3 offreurs.")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
.foregroundColor(.sqGreen(80))
|
||||
.padding(16)
|
||||
@@ -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()
|
||||
}
|
||||
432
AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQButton.swift
Normal file
432
AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQButton.swift
Normal 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()
|
||||
}
|
||||
|
||||
@@ -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 j’engage ma responsabilité sur l’exhaustivité et l’authenticité des informations renseignées ci-dessus.", isChecked: .constant(false))
|
||||
SQCheckbox("Je comprends que j’engage ma responsabilité sur l’exhaustivité et l’authenticité des informations renseignées ci-dessus.", isChecked: .constant(false), error: .constant(.empty))
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ enum SQIconName: String {
|
||||
case camera_plus
|
||||
case camera_rotate
|
||||
case camera
|
||||
case car_side
|
||||
case chart_line_up
|
||||
case chart_user
|
||||
case check
|
||||
@@ -141,10 +142,12 @@ enum SQIconName: String {
|
||||
case paper_plane_top
|
||||
case paper_plane
|
||||
case paperclip
|
||||
case passport
|
||||
case payer
|
||||
case pen_to_square
|
||||
case phone_flip
|
||||
case phone
|
||||
case phone_hangup
|
||||
case play_store_brand
|
||||
case plus
|
||||
case print
|
||||
@@ -180,6 +183,7 @@ enum SQIconName: String {
|
||||
case user
|
||||
case users
|
||||
case video
|
||||
case video_slash
|
||||
case whatsapp_brand
|
||||
case xmark
|
||||
case x_twitter_brand
|
||||
@@ -201,7 +205,7 @@ struct SQIcon: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Image(type == .solid ? "\(name.rawValue)_solid" : name.rawValue, bundle: .module)
|
||||
Image(type == .solid ? "\(name.rawValue)_solid" : name.rawValue)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(height: customSize ?? size.rawValue)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQPicker.swift
Normal file
123
AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQPicker.swift
Normal 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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
195
AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQRadio.swift
Normal file
195
AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQRadio.swift
Normal 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: "L’utilisateur semble proposer un service relevant d’une activité réglementée sans disposer des autorisations requises."),
|
||||
SQRadioOption(title: "Option 2", desc: "L’utilisateur semble évoquer ou proposer des activités interdites par la loi (trafic d’animaux, substances illicites, armes, etc.)."),
|
||||
SQRadioOption(title: "Option 3", desc: "L’utilisateur semble fournir de fausses informations dans son profil."),
|
||||
SQRadioOption(title: "Option 4", desc: "L’utilisateur semble avoir usurpé mon identité ou l’identité d’un autre utilisateur (SIRET, numéro de téléphone...).")
|
||||
], selectedIndex: .constant(0), error: .constant(.custom("Merci de détailler votre signalement")))
|
||||
|
||||
SQRadio(title: "Horizontal", orientation: .horizontal, options: [
|
||||
SQRadioOption(title: "1\n Non Jamais"),
|
||||
SQRadioOption(title: "2"),
|
||||
SQRadioOption(title: "3"),
|
||||
SQRadioOption(title: "4"),
|
||||
SQRadioOption(title: "5\nOui")
|
||||
], selectedIndex: .constant(2))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// SQText.swift
|
||||
// Allovoisins
|
||||
//
|
||||
// Created by Victor on 13/06/2024.
|
||||
// Copyright © 2024 AlloVoisins. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - SQTextFont
|
||||
|
||||
enum SQTextFont: String {
|
||||
case medium = "TTChocolates-Medium"
|
||||
case mediumItalic = "TTChocolates-MediumIt"
|
||||
case semiBold = "TTChocolates-DemiBold"
|
||||
case bold = "TTChocolates-Bold"
|
||||
case boldItalic = "TTChocolates-Bold-Italic"
|
||||
}
|
||||
|
||||
// MARK: - SQText
|
||||
|
||||
/// Text component with custom Sequoia typography.
|
||||
///
|
||||
/// Use View Modifiers to configure the text:
|
||||
/// ```swift
|
||||
/// SQText("Hello world!")
|
||||
/// .sqSize(18)
|
||||
/// .sqFont(.bold)
|
||||
/// .sqColor(.sqNeutral(100))
|
||||
/// ```
|
||||
struct SQText: View {
|
||||
private let text: String
|
||||
private let attributedText: AttributedString?
|
||||
|
||||
@Environment(\.sqTextConfiguration) private var config
|
||||
|
||||
/// Creates a text view with plain string.
|
||||
///
|
||||
/// - Parameter text: The string to display
|
||||
init(_ text: String) {
|
||||
self.text = text
|
||||
self.attributedText = nil
|
||||
}
|
||||
|
||||
/// Creates a text view with attributed string.
|
||||
///
|
||||
/// - Parameter attributedText: The attributed string to display
|
||||
init(_ attributedText: AttributedString) {
|
||||
self.text = ""
|
||||
self.attributedText = attributedText
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let attributedText = attributedText {
|
||||
Text(attributedText)
|
||||
.font(.custom(config.font.rawValue, fixedSize: config.size))
|
||||
.foregroundStyle(config.textColor)
|
||||
} else {
|
||||
Text(text)
|
||||
.font(.custom(config.font.rawValue, fixedSize: config.size))
|
||||
.foregroundStyle(config.textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 10) {
|
||||
SQText("Hello world!")
|
||||
.sqFont(.medium)
|
||||
|
||||
SQText("Hello world!")
|
||||
.sqFont(.mediumItalic)
|
||||
|
||||
SQText("Hello world!")
|
||||
.sqSize(18)
|
||||
.sqFont(.semiBold)
|
||||
|
||||
SQText("Hello world!")
|
||||
.sqFont(.bold)
|
||||
|
||||
SQText("Hello world!")
|
||||
.sqSize(18)
|
||||
.sqFont(.boldItalic)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// SQTextConfiguration.swift
|
||||
// Allovoisins
|
||||
//
|
||||
// Created by Claude on 22/10/2025.
|
||||
// Copyright © 2025 AlloVoisins. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Configuration structure for Sequoia Text components.
|
||||
/// Contains all customizable options that can be set via View Modifiers.
|
||||
struct SQTextConfiguration {
|
||||
var size: CGFloat = 16
|
||||
var font: SQTextFont = .medium
|
||||
var textColor: Color = .sqNeutral(90)
|
||||
}
|
||||
|
||||
// MARK: - Environment Key
|
||||
|
||||
private struct SQTextConfigurationKey: EnvironmentKey {
|
||||
static let defaultValue = SQTextConfiguration()
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var sqTextConfiguration: SQTextConfiguration {
|
||||
get { self[SQTextConfigurationKey.self] }
|
||||
set { self[SQTextConfigurationKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// SQTextModifiers.swift
|
||||
// Allovoisins
|
||||
//
|
||||
// Created by Claude on 22/10/2025.
|
||||
// Copyright © 2025 AlloVoisins. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Font Modifier
|
||||
|
||||
private struct SQFontModifier: ViewModifier {
|
||||
let font: SQTextFont
|
||||
@Environment(\.sqTextConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.font = font
|
||||
return content.environment(\.sqTextConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Sets the font weight for the text.
|
||||
///
|
||||
/// - Parameter font: The font weight to apply (.regular, .medium, .semiBold, .bold)
|
||||
/// - Returns: A view with the font configuration applied
|
||||
func sqFont(_ font: SQTextFont) -> some View {
|
||||
modifier(SQFontModifier(font: font))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Size Modifier
|
||||
|
||||
private struct SQSizeModifier: ViewModifier {
|
||||
let size: CGFloat
|
||||
@Environment(\.sqTextConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.size = size
|
||||
return content.environment(\.sqTextConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Sets the font size for the text.
|
||||
///
|
||||
/// - Parameter size: The font size in points
|
||||
/// - Returns: A view with the size configuration applied
|
||||
func sqSize(_ size: CGFloat) -> some View {
|
||||
modifier(SQSizeModifier(size: size))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Color Modifier
|
||||
|
||||
private struct SQColorModifier: ViewModifier {
|
||||
let textColor: Color
|
||||
@Environment(\.sqTextConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.textColor = textColor
|
||||
return content.environment(\.sqTextConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Sets the text color.
|
||||
///
|
||||
/// - Parameter textColor: The color to apply to the text
|
||||
/// - Returns: A view with the color configuration applied
|
||||
func sqColor(_ textColor: Color) -> some View {
|
||||
modifier(SQColorModifier(textColor: textColor))
|
||||
}
|
||||
}
|
||||
@@ -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 d’expérience ! Devis gratuit", text: .constant("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
|
||||
.padding()
|
||||
SQTextEditor("Zone de texte", placeholder: "Ex : Pro Solutions propose une gamme de services complète pour tout type de dépannage électroménager. Bénéficiez de nos 10 ans d’expérience ! Devis gratuit", error: .constant(.empty), text: .constant("Lorem ipsum dolor sit amet, consectetur"), minCharacters: 50)
|
||||
.padding()
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// SQPasswordField.swift
|
||||
// Allovoisins
|
||||
//
|
||||
// Created by Claude on 22/10/2025.
|
||||
// Copyright © 2025 AlloVoisins. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - SQPasswordField
|
||||
|
||||
/// Specialized text field for password input with visibility toggle.
|
||||
///
|
||||
/// Use View Modifiers to configure the password field:
|
||||
/// ```swift
|
||||
/// SQPasswordField("Mot de passe", text: $password)
|
||||
/// .sqPlaceholder("Entrez votre mot de passe")
|
||||
/// .sqMinCharacters(8)
|
||||
/// .sqHelperText("Minimum 8 caractères")
|
||||
/// .sqError($passwordError)
|
||||
/// ```
|
||||
struct SQPasswordField: View {
|
||||
private let label: String
|
||||
@Binding var text: String
|
||||
|
||||
@FocusState private var isFocused: Bool
|
||||
@State private var isPasswordVisible = false
|
||||
@Environment(\.sqTextFieldConfiguration) private var config
|
||||
|
||||
private var accentColor: Color { .sqNeutral(80) }
|
||||
|
||||
/// Creates a password field with visibility toggle.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - label: The label text displayed above the field
|
||||
/// - text: Binding to the password text value
|
||||
init(_ label: String, text: Binding<String>) {
|
||||
self.label = label
|
||||
self._text = text
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SQTextFieldBase(label, isFocused: Binding(get: { isFocused }, set: { isFocused = $0 }), textCount: text.count) {
|
||||
HStack(spacing: 4) {
|
||||
Group {
|
||||
if isPasswordVisible {
|
||||
TextField("", text: $text)
|
||||
} else {
|
||||
SecureField("", text: $text)
|
||||
}
|
||||
}
|
||||
.placeholder(when: text.isEmpty) {
|
||||
SQText(config.placeholder)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
.onChange(of: text) { newValue in
|
||||
// Character limit
|
||||
if let max = config.maxCharacters, newValue.count > max {
|
||||
text = String(newValue.prefix(max))
|
||||
}
|
||||
// Reset error
|
||||
config.error.wrappedValue = .none
|
||||
}
|
||||
.keyboardType(config.keyboardType)
|
||||
.foregroundStyle(Color.sqNeutral(100))
|
||||
.tint(accentColor)
|
||||
.submitLabel(.done)
|
||||
.focused($isFocused)
|
||||
.accessibilityLabel(label)
|
||||
.accessibilityValue(text.isEmpty ? "Vide" : "Rempli")
|
||||
|
||||
Button {
|
||||
isPasswordVisible.toggle()
|
||||
} label: {
|
||||
SQIcon(isPasswordVisible ? .eye_slash : .eye, color: .sqNeutral(80))
|
||||
}
|
||||
.accessibilityLabel(isPasswordVisible ? "Masquer le mot de passe" : "Afficher le mot de passe")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 32) {
|
||||
SQPasswordField("Nouveau mot de passe", text: .constant(""))
|
||||
.sqPlaceholder("Mot de passe")
|
||||
|
||||
SQPasswordField("Mot de passe", text: .constant("MonMotDePasse123!"))
|
||||
.sqPlaceholder("Mot de passe")
|
||||
|
||||
SQPasswordField("Mot de passe", text: .constant(""))
|
||||
.sqPlaceholder("Mot de passe")
|
||||
.sqOptional()
|
||||
|
||||
SQPasswordField("Mot de passe", text: .constant(""))
|
||||
.sqPlaceholder("Mot de passe")
|
||||
.disabled(true)
|
||||
|
||||
SQPasswordField("Nouveau mot de passe", text: .constant(""))
|
||||
.sqPlaceholder("Mot de passe")
|
||||
.sqMinCharacters(8)
|
||||
.sqHelperText("Minimum 8 caractères")
|
||||
|
||||
SQPasswordField("Confirmer le mot de passe", text: .constant(""))
|
||||
.sqPlaceholder("Mot de passe")
|
||||
.sqTooltipText("Le mot de passe doit contenir au moins 8 caractères, une majuscule, une minuscule et un chiffre.")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// SQPhoneField.swift
|
||||
// Allovoisins
|
||||
//
|
||||
// Created by Claude on 22/10/2025.
|
||||
// Copyright © 2025 AlloVoisins. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - SQPhoneField
|
||||
|
||||
/// Specialized text field for phone number input with automatic formatting.
|
||||
///
|
||||
/// Use View Modifiers to configure the phone field:
|
||||
/// ```swift
|
||||
/// SQPhoneField("Téléphone", text: $phone)
|
||||
/// .sqPlaceholder("01 23 45 67 89")
|
||||
/// .sqHelperText("Format : 01 23 45 67 89")
|
||||
/// .sqError($phoneError)
|
||||
/// ```
|
||||
struct SQPhoneField: View {
|
||||
private let label: String
|
||||
@Binding var text: String
|
||||
|
||||
@FocusState private var isFocused: Bool
|
||||
@Environment(\.sqTextFieldConfiguration) private var config
|
||||
|
||||
private var accentColor: Color { .sqNeutral(80) }
|
||||
|
||||
/// Creates a phone number field with automatic formatting.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - label: The label text displayed above the field
|
||||
/// - text: Binding to the phone number text value
|
||||
init(_ label: String, text: Binding<String>) {
|
||||
self.label = label
|
||||
self._text = text
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SQTextFieldBase(label, isFocused: Binding(get: { isFocused }, set: { isFocused = $0 }), textCount: text.count) {
|
||||
TextField("", text: $text)
|
||||
.placeholder(when: text.isEmpty) {
|
||||
SQText(config.placeholder)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
.onChange(of: text) { newValue in
|
||||
// Format and limit phone number
|
||||
let formatted = formatPhoneNumber(String(newValue.prefix(15)))
|
||||
if formatted != newValue {
|
||||
text = formatted
|
||||
}
|
||||
// Reset error
|
||||
config.error.wrappedValue = .none
|
||||
}
|
||||
.keyboardType(.numberPad)
|
||||
.foregroundStyle(Color.sqNeutral(100))
|
||||
.tint(accentColor)
|
||||
.focused($isFocused)
|
||||
.accessibilityLabel(label)
|
||||
.accessibilityValue(text.isEmpty ? "Vide" : text)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func formatPhoneNumber(_ number: String) -> String {
|
||||
let numbers = number.filter { $0.isNumber }
|
||||
var result = ""
|
||||
|
||||
for (index, char) in numbers.enumerated() {
|
||||
if index > 0 && index % 2 == 0 {
|
||||
result += " "
|
||||
}
|
||||
result += String(char)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 32) {
|
||||
SQPhoneField("Téléphone", text: .constant(""))
|
||||
.sqPlaceholder("01 23 45 67 89")
|
||||
|
||||
SQPhoneField("Téléphone", text: .constant("06 12 34 56 78"))
|
||||
.sqPlaceholder("01 23 45 67 89")
|
||||
|
||||
SQPhoneField("Téléphone", text: .constant(""))
|
||||
.sqPlaceholder("01 23 45 67 89")
|
||||
.sqOptional()
|
||||
|
||||
SQPhoneField("Téléphone", text: .constant(""))
|
||||
.sqPlaceholder("01 23 45 67 89")
|
||||
.disabled(true)
|
||||
|
||||
SQPhoneField("Téléphone portable", text: .constant(""))
|
||||
.sqPlaceholder("01 23 45 67 89")
|
||||
.sqHelperText("Format : 01 23 45 67 89")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//
|
||||
// SQTextField.swift
|
||||
// Sequoia
|
||||
//
|
||||
// Created by Victor on 10/10/2024.
|
||||
//
|
||||
|
||||
import Lottie
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - SQTextField
|
||||
|
||||
/// Standard text field component with optional icon.
|
||||
///
|
||||
/// Use View Modifiers to configure the text field:
|
||||
/// ```swift
|
||||
/// SQTextField("Name", text: $name)
|
||||
/// .sqPlaceholder("Enter your name")
|
||||
/// .sqMinCharacters(2)
|
||||
/// .sqMaxCharacters(50)
|
||||
/// .sqError($nameError)
|
||||
/// ```
|
||||
struct SQTextField: View {
|
||||
private let label: String
|
||||
@Binding var text: String
|
||||
private let icon: SQIcon?
|
||||
private let iconAction: (() -> Void)?
|
||||
|
||||
@FocusState private var isFocused: Bool
|
||||
@Environment(\.sqTextFieldConfiguration) private var config
|
||||
|
||||
private var accentColor: Color { .sqNeutral(80) }
|
||||
|
||||
/// Creates a standard text field.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - label: The label text displayed above the field
|
||||
/// - text: Binding to the text value
|
||||
init(_ label: String, text: Binding<String>, icon: SQIcon? = nil, iconAction: (() -> Void)? = nil) {
|
||||
self.label = label
|
||||
self._text = text
|
||||
self.icon = icon
|
||||
self.iconAction = iconAction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SQTextFieldBase(label, isFocused: Binding(get: { isFocused }, set: { isFocused = $0 }), textCount: text.count) {
|
||||
HStack(spacing: 4) {
|
||||
TextField("", text: $text)
|
||||
.placeholder(when: text.isEmpty) {
|
||||
SQText(config.placeholder)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
.onChange(of: text) { newValue in
|
||||
// Character limit
|
||||
if let max = config.maxCharacters, newValue.count > max {
|
||||
text = String(newValue.prefix(max))
|
||||
}
|
||||
// Reset error
|
||||
config.error.wrappedValue = .none
|
||||
}
|
||||
.keyboardType(config.keyboardType)
|
||||
.foregroundStyle(Color.sqNeutral(100))
|
||||
.tint(accentColor)
|
||||
.submitLabel(.done)
|
||||
.focused($isFocused)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
if config.keyboardType == .numberPad || config.keyboardType == .decimalPad || config.keyboardType == .phonePad {
|
||||
Spacer()
|
||||
Button("Done") {
|
||||
isFocused = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(label)
|
||||
.accessibilityValue(text.isEmpty ? "Vide" : text)
|
||||
|
||||
if config.isLoading.wrappedValue {
|
||||
LottieView(animation: .named("av_loader"))
|
||||
.looping()
|
||||
.frame(width: 20, height: 20)
|
||||
} else if let icon = icon {
|
||||
Button(action: iconAction ?? {}) {
|
||||
icon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Icon Modifier
|
||||
|
||||
extension SQTextField {
|
||||
/// Adds an icon button on the right side of the text field.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - icon: The icon to display
|
||||
/// - action: Action to perform when the icon is tapped
|
||||
/// - Returns: A text field with the icon
|
||||
func sqIcon(_ icon: SQIcon, action: @escaping () -> Void = {}) -> SQTextField {
|
||||
SQTextField(label, text: $text, icon: icon, iconAction: action)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 32) {
|
||||
SQTextField("Label name", text: .constant("Bonjour"))
|
||||
.sqPlaceholder("Placeholder")
|
||||
.sqMinCharacters(20)
|
||||
|
||||
SQTextField("Label name", text: .constant(""))
|
||||
.sqPlaceholder("Placeholder")
|
||||
.sqOptional()
|
||||
|
||||
SQTextField("Label name", text: .constant(""))
|
||||
.sqPlaceholder("Placeholder")
|
||||
.disabled(true)
|
||||
|
||||
SQTextField("Label name", text: .constant(""))
|
||||
.sqPlaceholder("Placeholder")
|
||||
|
||||
// Note: .sqIcon() must be called BEFORE other modifiers to work correctly
|
||||
SQTextField("Label with icon", text: .constant(""))
|
||||
.sqIcon(SQIcon(.magnifying_glass, color: .sqNeutral(80)))
|
||||
.sqPlaceholder("Placeholder")
|
||||
|
||||
SQTextField("Numéro de RCS", text: .constant(""))
|
||||
.sqPlaceholder("RCS VILLE B 123456789")
|
||||
.sqOptional()
|
||||
.sqTooltipText("Vous pouvez trouver votre numéro RCS sur l'extrait Kbis de votre entreprise ou votre extrait K, si vous êtes un entrepreneur individuel.\n\nLe numéro RCS se compose tout d'abord de la mention RCS, puis du numéro d'immatriculation de la ville dans laquelle a été créée la société, d'une lettre et enfin du numéro SIREN à 9 chiffres.")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// SQTextFieldBase.swift
|
||||
// Allovoisins
|
||||
//
|
||||
// Created by Claude on 22/10/2025.
|
||||
// Copyright © 2025 AlloVoisins. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - SQTextFieldBase
|
||||
|
||||
/// Base component for all Sequoia text field variants.
|
||||
/// Provides common functionality: label, error display, character count, tooltip, styling, etc.
|
||||
///
|
||||
/// This component uses `@Environment` to read configuration set via View Modifiers,
|
||||
/// following the same pattern as native SwiftUI components.
|
||||
struct SQTextFieldBase<Content: View>: View {
|
||||
private let label: String
|
||||
@Binding var isFocused: Bool
|
||||
var textCount: Int
|
||||
let content: Content
|
||||
|
||||
@Environment(\.sqTextFieldConfiguration) private var config
|
||||
@State private var showTooltip = false
|
||||
|
||||
private var accentColor: Color { .sqNeutral(80) }
|
||||
|
||||
init(
|
||||
_ label: String,
|
||||
isFocused: Binding<Bool>,
|
||||
textCount: Int,
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
self.label = label
|
||||
self._isFocused = isFocused
|
||||
self.textCount = textCount
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Label with tooltip/info
|
||||
HStack(spacing: 8) {
|
||||
SQText(label)
|
||||
.sqFont(.semiBold)
|
||||
.sqColor(.sqNeutral(80))
|
||||
|
||||
if let tooltipText = config.tooltipText {
|
||||
Button {
|
||||
withAnimation {
|
||||
showTooltip.toggle()
|
||||
}
|
||||
} label: {
|
||||
SQIcon(.circle_info, color: .sqNeutral(80))
|
||||
}
|
||||
.overlay(
|
||||
Group {
|
||||
if showTooltip {
|
||||
SQTooltip(text: tooltipText, isVisible: $showTooltip)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
)
|
||||
.accessibilityLabel("Information")
|
||||
.accessibilityHint("Appuyez deux fois pour afficher plus d'informations")
|
||||
} else if let infoAction = config.infoAction {
|
||||
Button {
|
||||
infoAction()
|
||||
} label: {
|
||||
SQIcon(.circle_info, color: .sqNeutral(80))
|
||||
}
|
||||
.accessibilityLabel("Information")
|
||||
.accessibilityHint("Appuyez deux fois pour afficher plus d'informations")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if config.isOptional {
|
||||
SQText("Optionnel")
|
||||
.sqSize(12)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
}
|
||||
|
||||
// Content (TextField/SecureField/custom)
|
||||
content
|
||||
.padding(16)
|
||||
.foregroundStyle(Color.sqNeutral())
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(config.isDisabled ? Color.sqNeutral(10) : config.backgroundColor)
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.inset(by: 0.5)
|
||||
.stroke(
|
||||
config.error.wrappedValue.isInError ? .sqSemanticRed :
|
||||
isFocused ? accentColor :
|
||||
config.isDisabled ? Color.sqNeutral(20) : Color.sqNeutral(30),
|
||||
lineWidth: 1
|
||||
)
|
||||
)
|
||||
|
||||
// Error / Helper / Character count
|
||||
HStack(spacing: 16) {
|
||||
if config.error.wrappedValue.isInError {
|
||||
HStack(spacing: 4) {
|
||||
SQIcon(.circle_exclamation, customSize: 12, type: .solid, color: .sqSemanticRed)
|
||||
SQText(config.error.wrappedValue.message)
|
||||
.sqSize(12)
|
||||
.sqColor(.sqSemanticRed)
|
||||
}
|
||||
} else if let helperText = config.helperText {
|
||||
SQText(helperText)
|
||||
.sqSize(12)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if !characterCountText.isEmpty && config.showCharacterCount {
|
||||
SQText(characterCountText)
|
||||
.sqSize(12)
|
||||
.sqColor(.sqNeutral(50))
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(config.isDisabled)
|
||||
.overlay(
|
||||
Group {
|
||||
if showTooltip {
|
||||
Color.black.opacity(0.001)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
showTooltip = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Helper Properties
|
||||
|
||||
private var characterCountText: String {
|
||||
let count = textCount
|
||||
if let min = config.minCharacters, count < min {
|
||||
return "\(count)/\(min) min."
|
||||
} else if let max = config.maxCharacters {
|
||||
return count >= (max * 4 / 5) ? "\(count)/\(max) max." : "\(count)"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Placeholder Extension
|
||||
|
||||
extension View {
|
||||
func placeholder<Content: View>(
|
||||
when shouldShow: Bool,
|
||||
alignment: Alignment = .leading,
|
||||
@ViewBuilder placeholder: () -> Content
|
||||
) -> some View {
|
||||
ZStack(alignment: alignment) {
|
||||
placeholder().opacity(shouldShow ? 1 : 0)
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// SQTextFieldConfiguration.swift
|
||||
// Allovoisins
|
||||
//
|
||||
// Created by Claude on 22/10/2025.
|
||||
// Copyright © 2025 AlloVoisins. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Configuration structure for Sequoia TextField components.
|
||||
/// Contains all customizable options that can be set via View Modifiers.
|
||||
struct SQTextFieldConfiguration {
|
||||
var placeholder: String = ""
|
||||
var isDisabled: Bool = false
|
||||
var isOptional: Bool = false
|
||||
var isLoading: Binding<Bool> = .constant(false)
|
||||
var tooltipText: String? = nil
|
||||
var helperText: String? = nil
|
||||
var error: Binding<SQFormFieldError> = .constant(.none)
|
||||
var minCharacters: Int? = nil
|
||||
var maxCharacters: Int? = nil
|
||||
var showCharacterCount: Bool = true
|
||||
var backgroundColor: Color = .clear
|
||||
var keyboardType: UIKeyboardType = .default
|
||||
var infoAction: (() -> Void)? = nil
|
||||
}
|
||||
|
||||
// MARK: - Environment Key
|
||||
|
||||
private struct SQTextFieldConfigurationKey: EnvironmentKey {
|
||||
static let defaultValue = SQTextFieldConfiguration()
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var sqTextFieldConfiguration: SQTextFieldConfiguration {
|
||||
get { self[SQTextFieldConfigurationKey.self] }
|
||||
set { self[SQTextFieldConfigurationKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
//
|
||||
// SQTextFieldModifiers.swift
|
||||
// Allovoisins
|
||||
//
|
||||
// Created by Claude on 22/10/2025.
|
||||
// Copyright © 2025 AlloVoisins. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - View Modifiers
|
||||
|
||||
/// View modifiers for Sequoia TextField components.
|
||||
/// These modifiers allow chainable configuration, similar to native SwiftUI components.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```swift
|
||||
/// SQTextField("Name", text: $name)
|
||||
/// .sqPlaceholder("Enter your name")
|
||||
/// .sqMinCharacters(2)
|
||||
/// .sqMaxCharacters(50)
|
||||
/// .sqError($nameError)
|
||||
/// ```
|
||||
|
||||
// MARK: - Individual Modifier Structs
|
||||
|
||||
private struct SQPlaceholderModifier: ViewModifier {
|
||||
let placeholder: String
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.placeholder = placeholder
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQMinCharactersModifier: ViewModifier {
|
||||
let count: Int
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.minCharacters = count
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQMaxCharactersModifier: ViewModifier {
|
||||
let count: Int
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.maxCharacters = count
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQHelperTextModifier: ViewModifier {
|
||||
let text: String
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.helperText = text
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQTooltipTextModifier: ViewModifier {
|
||||
let text: String
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.tooltipText = text
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQErrorModifier: ViewModifier {
|
||||
let error: Binding<SQFormFieldError>
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.error = error
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQOptionalModifier: ViewModifier {
|
||||
let isOptional: Bool
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.isOptional = isOptional
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQShowCharacterCountModifier: ViewModifier {
|
||||
let show: Bool
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.showCharacterCount = show
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQBackgroundColorModifier: ViewModifier {
|
||||
let color: Color
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.backgroundColor = color
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQKeyboardTypeModifier: ViewModifier {
|
||||
let type: UIKeyboardType
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.keyboardType = type
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQInfoActionModifier: ViewModifier {
|
||||
let action: () -> Void
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.infoAction = action
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQDisabledModifier: ViewModifier {
|
||||
let isDisabled: Bool
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.isDisabled = isDisabled
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SQIsLoadingModifier: ViewModifier {
|
||||
let isLoading: Binding<Bool>
|
||||
@Environment(\.sqTextFieldConfiguration) var config
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var newConfig = config
|
||||
newConfig.isLoading = isLoading
|
||||
return content.environment(\.sqTextFieldConfiguration, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public Extension
|
||||
|
||||
extension View {
|
||||
/// Sets the placeholder text for the text field.
|
||||
func sqPlaceholder(_ text: String) -> some View {
|
||||
modifier(SQPlaceholderModifier(placeholder: text))
|
||||
}
|
||||
|
||||
/// Sets the minimum number of characters required.
|
||||
func sqMinCharacters(_ count: Int) -> some View {
|
||||
modifier(SQMinCharactersModifier(count: count))
|
||||
}
|
||||
|
||||
/// Sets the maximum number of characters allowed.
|
||||
func sqMaxCharacters(_ count: Int) -> some View {
|
||||
modifier(SQMaxCharactersModifier(count: count))
|
||||
}
|
||||
|
||||
/// Sets helper text to display below the text field.
|
||||
func sqHelperText(_ text: String) -> some View {
|
||||
modifier(SQHelperTextModifier(text: text))
|
||||
}
|
||||
|
||||
/// Sets tooltip text with an info icon.
|
||||
func sqTooltipText(_ text: String) -> some View {
|
||||
modifier(SQTooltipTextModifier(text: text))
|
||||
}
|
||||
|
||||
/// Binds an error to the text field for validation display.
|
||||
func sqError(_ error: Binding<SQFormFieldError>) -> some View {
|
||||
modifier(SQErrorModifier(error: error))
|
||||
}
|
||||
|
||||
/// Marks the field as optional with an "Optionnel" badge.
|
||||
func sqOptional(_ isOptional: Bool = true) -> some View {
|
||||
modifier(SQOptionalModifier(isOptional: isOptional))
|
||||
}
|
||||
|
||||
/// Marks the field as optional with an "Optionnel" badge (alias for sqOptional).
|
||||
func sqIsOptional(_ isOptional: Bool = true) -> some View {
|
||||
modifier(SQOptionalModifier(isOptional: isOptional))
|
||||
}
|
||||
|
||||
/// Controls the visibility of the character counter.
|
||||
func sqShowCharacterCount(_ show: Bool = true) -> some View {
|
||||
modifier(SQShowCharacterCountModifier(show: show))
|
||||
}
|
||||
|
||||
/// Sets the background color of the text field.
|
||||
func sqBackgroundColor(_ color: Color) -> some View {
|
||||
modifier(SQBackgroundColorModifier(color: color))
|
||||
}
|
||||
|
||||
/// Sets the keyboard type for the text field.
|
||||
func sqKeyboardType(_ type: UIKeyboardType) -> some View {
|
||||
modifier(SQKeyboardTypeModifier(type: type))
|
||||
}
|
||||
|
||||
/// Sets an action to execute when the info icon is tapped.
|
||||
func sqInfoAction(_ action: @escaping () -> Void) -> some View {
|
||||
modifier(SQInfoActionModifier(action: action))
|
||||
}
|
||||
|
||||
/// Disables the text field.
|
||||
func sqDisabled(_ isDisabled: Bool = true) -> some View {
|
||||
modifier(SQDisabledModifier(isDisabled: isDisabled))
|
||||
}
|
||||
|
||||
/// Shows a loading indicator in the text field.
|
||||
func sqIsLoading(_ isLoading: Binding<Bool>) -> some View {
|
||||
modifier(SQIsLoadingModifier(isLoading: isLoading))
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,6 @@ struct SQToast: View {
|
||||
}
|
||||
|
||||
init(_ title: String = "", content: String, style: SQToastStyle = .success, hasClose: Bool = false) {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {}
|
||||
self.title = title
|
||||
self.content = content
|
||||
self.style = style
|
||||
@@ -41,9 +38,12 @@ struct SQToast: View {
|
||||
HStack(spacing: 16) {
|
||||
VStack(alignment: .leading) {
|
||||
if !title.isEmpty {
|
||||
SQText(title, font: .bold)
|
||||
SQText(title)
|
||||
.sqFont(.bold)
|
||||
}
|
||||
SQText(content, size: 14, font: .demiBold)
|
||||
SQText(content)
|
||||
.sqSize(14)
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
Spacer()
|
||||
if hasClose {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
96
AlloVoisinsSwiftUI/Extensions/Extension+Color.swift
Normal file
96
AlloVoisinsSwiftUI/Extensions/Extension+Color.swift
Normal 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
|
||||
}
|
||||
}
|
||||
66
AlloVoisinsSwiftUI/Extensions/Extension+View.swift
Normal file
66
AlloVoisinsSwiftUI/Extensions/Extension+View.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// ChangeLoginView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 23/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum ChangeLoginType {
|
||||
case phone
|
||||
case email
|
||||
}
|
||||
|
||||
struct ChangeLoginView: View {
|
||||
let type: ChangeLoginType = .phone
|
||||
@State var phone: String = ""
|
||||
@State var email: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 32) {
|
||||
switch type {
|
||||
case .email:
|
||||
SQTextField("Nouvelle adresse e-mail", text: $email)
|
||||
.sqPlaceholder("Ex : pierredupont@domaine.com")
|
||||
case .phone:
|
||||
SQPhoneField("Nouveau numéro de téléphone", text: $phone)
|
||||
.sqPlaceholder("Ex : 0600000009")
|
||||
}
|
||||
SQButton("Confirmer") {}
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ChangeLoginView()
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// PasswordRecommendations.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 22/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PasswordRecommendations: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
ForEach(PasswordRequirements.allCases, id: \.self) { requirement in
|
||||
if requirement == .minChar {
|
||||
requirementView(for: requirement)
|
||||
|
||||
SQText("Au moins 3 des 4 catégories suivantes :")
|
||||
.padding(.top, 8)
|
||||
.sqSize(12)
|
||||
} else {
|
||||
requirementView(for: requirement)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(red: 0.95, green: 0.97, blue: 1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
func requirementView(for requirement: PasswordRequirements, isValid: Bool = false) -> some View {
|
||||
HStack(spacing: 4) {
|
||||
SQIcon(.check, size: .xs, color: isValid ? .sqSemanticPositive : .sqNeutral(50))
|
||||
SQText(requirement.rawValue)
|
||||
.sqSize(12)
|
||||
.sqColor(isValid ? .sqNeutral(80) : .sqNeutral(50))
|
||||
}
|
||||
}
|
||||
|
||||
enum PasswordRequirements: String, CaseIterable {
|
||||
case minChar = "8 caractères minimum"
|
||||
case oneMaj = "Une lettre majuscule"
|
||||
case oneMin = "Une lettre minuscule"
|
||||
case oneNum = "Un chiffre"
|
||||
case oneSpe = "Un caractère spécial"
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PasswordRecommendations()
|
||||
.padding(16)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// ConfirmAuthView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 22/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ConfirmAuthView: View {
|
||||
@State var password: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQText("Par mesure de sécurité, nous vous demandons votre mot de passe avant de modifier votre numéro de téléphone.")
|
||||
.sqFont(.semiBold)
|
||||
SQPasswordField("Mot de passe", text: $password)
|
||||
}
|
||||
SQButton("Confirmer") {
|
||||
|
||||
}
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundStyle(Color.sqNeutral(20))
|
||||
SQText("ou")
|
||||
.sqColor(.sqNeutral(70))
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundStyle(Color.sqNeutral(20))
|
||||
}
|
||||
|
||||
SQButton("Continuer avec Apple") {
|
||||
|
||||
}
|
||||
.sqButtonType(.line)
|
||||
.sqIcon(SQIcon(.apple_brand))
|
||||
.sqLarge()
|
||||
|
||||
SQButton("Continuer avec Google") {
|
||||
|
||||
}
|
||||
.sqButtonType(.line)
|
||||
.sqIcon(SQIcon(.google_brand))
|
||||
.sqLarge()
|
||||
|
||||
SQButton("Continuer avec Facebook") {
|
||||
|
||||
}
|
||||
.sqButtonType(.line)
|
||||
.sqIcon(SQIcon(.facebook_f_brand))
|
||||
.sqLarge()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ChangeLoginView()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// LoginAndPasswordView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 21/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoginAndPasswordView: View {
|
||||
@State var email: String = ""
|
||||
@State var phone: String = ""
|
||||
@State var currentPassword: String = ""
|
||||
@State var newPassword: String = ""
|
||||
@State var newPasswordRepeat: String = ""
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Identifiants")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
SQText("Vos identifiants ne sont pas communiqués aux autres membres.")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
VStack(spacing: 4) {
|
||||
SQTextField("Adresse e-mail", text: $email)
|
||||
.sqPlaceholder("pie**@d**********.com")
|
||||
.sqDisabled(true)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
SQText("Modifier")
|
||||
.sqSize(13)
|
||||
.underline()
|
||||
}
|
||||
}
|
||||
}
|
||||
VStack(spacing: 4) {
|
||||
SQTextField("Numéro de téléphone", text: $phone)
|
||||
.sqPlaceholder("06******09")
|
||||
.sqDisabled(true)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
SQText("Modifier")
|
||||
.sqSize(13)
|
||||
.underline()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 16)
|
||||
.foregroundColor(Color.sqNeutral(10))
|
||||
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQText("Modifier mon mot de passe")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
|
||||
SQPasswordField("Mot de passe actuel", text: $currentPassword)
|
||||
.sqPlaceholder("*********")
|
||||
SQPasswordField("Nouveau mot de passe", text: $newPassword)
|
||||
.sqPlaceholder("*********")
|
||||
PasswordRecommendations()
|
||||
SQPasswordField("Confirmez votre mot de passe", text: $newPasswordRepeat)
|
||||
.sqPlaceholder("*********")
|
||||
}
|
||||
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
SQButton("Modifier mon mot de passe") {
|
||||
|
||||
}
|
||||
.sqButtonType(.line)
|
||||
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
SQText("Mot de passe oublié")
|
||||
.sqSize(13)
|
||||
.sqFont(.semiBold)
|
||||
.underline()
|
||||
}
|
||||
}
|
||||
.padding(.top, 16)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.sqNavigationBar(title: "Identifiants et mot de passe")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LoginAndPasswordView()
|
||||
}
|
||||
@@ -8,13 +8,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BoosterConfirmationScreen: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -22,12 +15,16 @@ struct BoosterConfirmationScreen: View {
|
||||
.ignoresSafeArea()
|
||||
VStack {
|
||||
VStack(spacing: 32) {
|
||||
Image("booster_logo", bundle: Bundle.module)
|
||||
Image("booster_logo")
|
||||
.resizable()
|
||||
.frame(width: 210, height: 180)
|
||||
SQText("C’est confirmé !", size: 18, font: .bold)
|
||||
SQText("C’est confirmé !")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.white)
|
||||
SQText("Votre profil sera boosté dès demain.", size: 32, font: .bold)
|
||||
SQText("Votre profil sera boosté dès demain.")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.white)
|
||||
.multilineTextAlignment(.center)
|
||||
}.padding()
|
||||
@@ -36,7 +33,8 @@ struct BoosterConfirmationScreen: View {
|
||||
SQButton("Accéder à mon option") {
|
||||
|
||||
}
|
||||
SQText("XX,XX € / mois, sans engagement.", size: 13)
|
||||
SQText("XX,XX € / mois, sans engagement.")
|
||||
.sqSize(13)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
@@ -8,42 +8,45 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BoosterKnowAboutScreen: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Comment ça marche ?", size: 18, font: .bold)
|
||||
SQText("Comment ça marche ?")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
SQText("L’option Booster s’active sur les catégories de votre abonnement Premier. Par exemple, vous êtes abonné Premier sur les catégories « Plomberie - Installation Sanitaire » et « Carrelage », votre profil remontera en tête de liste des résultats en lien avec la plomberie et le carrelage.")
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Vous pouvez choisir de remonter votre profil :", size: 18, font: .bold)
|
||||
SQText("Vous pouvez choisir de remonter votre profil :")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SQText("• Tous les jours\n• 3 jours par semaine\n• 1 jour par semaine")
|
||||
|
||||
SQText("Tous les jours", font: .demiBold)
|
||||
SQText("Tous les jours")
|
||||
.sqFont(.semiBold)
|
||||
VStack(spacing: 8) {
|
||||
SQText("Cette option permet de remonter votre profil chaque jour dans les listes de résultats, à compter du lendemain de la souscription à l’option Booster, jusqu’à la résiliation de votre option.")
|
||||
SQText("Exemple : vous souscrivez un dimanche à 15h00, votre option sera activée dès lundi à 00h00.", font: .mediumItalic)
|
||||
SQText("Exemple : vous souscrivez un dimanche à 15h00, votre option sera activée dès lundi à 00h00.")
|
||||
.sqFont(.mediumItalic)
|
||||
}
|
||||
.padding(.leading)
|
||||
SQText("3 jours par semaine", font: .demiBold)
|
||||
SQText("3 jours par semaine")
|
||||
.sqFont(.semiBold)
|
||||
VStack(spacing: 8) {
|
||||
SQText("Cette option permet de remonter votre profil 3 jours par semaine dans les listes de résultats, à compter du lendemain de la souscription à l’option Booster, jusqu’à la résiliation de votre option.")
|
||||
SQText("Exemple : vous souscrivez un dimanche à 15h00, votre profil sera boosté le lundi, puis tous les deux jours.", font: .mediumItalic)
|
||||
SQText("Exemple : vous souscrivez un dimanche à 15h00, votre profil sera boosté le lundi, puis tous les deux jours.")
|
||||
.sqFont(.mediumItalic)
|
||||
}
|
||||
.padding(.leading)
|
||||
SQText("1 jour par semaine", font: .demiBold)
|
||||
SQText("1 jour par semaine")
|
||||
.sqFont(.semiBold)
|
||||
VStack(spacing: 8) {
|
||||
SQText("Cette option permet de remonter votre profil 1 jour par semaine dans les listes de résultats, à compter du lendemain de la souscription à l’option Booster, jusqu’à la résiliation de votre option.")
|
||||
SQText("Exemple : vous souscrivez un dimanche à 15h00, votre profil sera boosté le lundi, puis tous les six jours.", font: .mediumItalic)
|
||||
SQText("Exemple : vous souscrivez un dimanche à 15h00, votre profil sera boosté le lundi, puis tous les six jours.")
|
||||
.sqFont(.mediumItalic)
|
||||
}
|
||||
.padding(.leading)
|
||||
}
|
||||
@@ -59,7 +62,9 @@ struct BoosterKnowAboutScreen: View {
|
||||
})
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
SQText("En savoir plus", size: 18, font: .bold)
|
||||
SQText("En savoir plus")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
@@ -8,11 +8,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BoosterSubscriptionManagementScreen: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@State var selectedValue = 0
|
||||
@State var presentControls = false
|
||||
@@ -22,7 +17,9 @@ struct BoosterSubscriptionManagementScreen: View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
BoosterActiveHeaderView()
|
||||
|
||||
SQText("Mes boosters", size: 20, font: .bold)
|
||||
SQText("Mes boosters")
|
||||
.sqSize(20)
|
||||
.sqFont(.bold)
|
||||
|
||||
Picker("", selection: $selectedValue) {
|
||||
SQText("À venir").tag(0)
|
||||
@@ -43,9 +40,9 @@ struct BoosterSubscriptionManagementScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.bottomSheet(isShowing: $presentControls, content: {
|
||||
BoosterSubscriptionOptionsView()
|
||||
})
|
||||
// .bottomSheet(isShowing: $presentControls, content: {
|
||||
// BoosterSubscriptionOptionsView()
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,6 @@ struct BoosterSubscriptionSelectionScreen: View {
|
||||
@State var mode: BoosterSubscriptionMode = .edit
|
||||
@State var isNotPremier = false
|
||||
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack(alignment: .top) {
|
||||
@@ -58,12 +52,14 @@ struct BoosterSubscriptionSelectionScreen: View {
|
||||
VStack(spacing: 48) {
|
||||
ZStack {
|
||||
ElipseShape()
|
||||
.fill(Color.sqPurple(60))
|
||||
Rectangle()
|
||||
.fill(backgroundGradient)
|
||||
|
||||
VStack(alignment: .trailing) {
|
||||
BoosterLockedToPremierView()
|
||||
VStack(spacing: 16) {
|
||||
Image("booster_logo", bundle: Bundle.module)
|
||||
Image("booster_logo")
|
||||
.resizable()
|
||||
.frame(width: 210, height: 180)
|
||||
|
||||
@@ -86,7 +82,8 @@ struct BoosterSubscriptionSelectionScreen: View {
|
||||
SQButton("C'est parti !") {}
|
||||
if cancellation {
|
||||
Button(action: /*@START_MENU_TOKEN@*/ {}/*@END_MENU_TOKEN@*/, label: {
|
||||
SQText("Non merci, je souhaite résilier", size: 13)
|
||||
SQText("Non merci, je souhaite résilier")
|
||||
.sqSize(12)
|
||||
.foregroundColor(.black)
|
||||
})
|
||||
.padding()
|
||||
@@ -8,29 +8,26 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BoosterActiveHeaderView: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image("booster_logo", bundle: Bundle.module)
|
||||
Image("booster_logo")
|
||||
.resizable()
|
||||
.frame(width: 93, height: 80)
|
||||
VStack {
|
||||
SQText("Booster en cours", size: 18, font: .bold)
|
||||
SQText("Booster en cours")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.sqRoyal())
|
||||
SQText("Aujourd’hui", size: 14)
|
||||
SQText("Aujourd’hui")
|
||||
.sqSize(14)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.overlay(
|
||||
ZStack(alignment: .topLeading) {
|
||||
Image("booster_corner", bundle: Bundle.module)
|
||||
Image("booster_corner")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(8, corners: [.topLeft])
|
||||
@@ -10,25 +10,31 @@ import SwiftUI
|
||||
struct BoosterFeaturesView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 24) {
|
||||
SQText("Boostez votre activité !", size: 32, font: .bold)
|
||||
SQText("Boostez votre activité !")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.white)
|
||||
VStack(alignment: .center) {
|
||||
VStack(alignment: .leading) {
|
||||
SQText("Votre profil est mis en avant:", font: .bold)
|
||||
SQText("Votre profil est mis en avant:")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.white)
|
||||
HStack {
|
||||
SQIcon(.check, type: .solid, color: .white)
|
||||
SQText("Affichage en tête de liste des résultats", font: .bold)
|
||||
SQText("Affichage en tête de liste des résultats")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.check, type: .solid, color: .white)
|
||||
SQText("Badge “Profil boosté” à chaque affichage de votre profil", font: .bold)
|
||||
SQText("Badge “Profil boosté” à chaque affichage de votre profil")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.check, type: .solid, color: .white)
|
||||
SQText("Référencement boosté sur les moteurs de recherche", font: .bold)
|
||||
SQText("Référencement boosté sur les moteurs de recherche")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
@@ -10,25 +10,19 @@ import SwiftUI
|
||||
struct BoosterHistoryCellView: View {
|
||||
var isEnded: Bool = false
|
||||
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
var body: some View {
|
||||
ZStack {
|
||||
HStack(alignment: .center) {
|
||||
SQIcon(.calendar, color: .sqRoyal())
|
||||
SQText("Samedi 20 avril", font: .bold)
|
||||
SQText("Samedi 20 avril")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.sqRoyal())
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.overlay(
|
||||
ZStack(alignment: .topLeading) {
|
||||
Image("booster_corner", bundle: Bundle.module)
|
||||
Image("booster_corner")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(8, corners: [.topLeft, .bottomLeft])
|
||||
@@ -8,17 +8,12 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BoosterLockedToPremierView: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
var body: some View {
|
||||
HStack {
|
||||
SQIcon(.lock_keyhole, size: .xs, type: .solid, color: .white)
|
||||
SQText("Abonnés Premier", size: 13)
|
||||
SQText("Abonnés Premier")
|
||||
.sqSize(13)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
@@ -12,20 +12,23 @@ struct BoosterPromotionView: View {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 16) {
|
||||
VStack {
|
||||
SQText("Envie de booster votre activité ?", font: .bold)
|
||||
SQText("Activez l’option Booster !", font: .demiBold)
|
||||
SQText("Envie de booster votre activité ?")
|
||||
.sqFont(.bold)
|
||||
SQText("Activez l’option Booster !")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
BoosterStatsView()
|
||||
|
||||
SQButton("Activer l’option Booster", color: .sqRoyal(), textColor: .white) {
|
||||
SQButton("Activer l’option Booster") {
|
||||
|
||||
}
|
||||
.sqColorScheme(.royal)
|
||||
}
|
||||
.padding()
|
||||
.foregroundColor(.sqRoyal())
|
||||
.overlay(
|
||||
ZStack(alignment: .topLeading) {
|
||||
Image("booster_corner", bundle: Bundle.module)
|
||||
Image("booster_corner")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(8, corners: .topLeft)
|
||||
@@ -26,7 +26,8 @@ struct BoosterSelectionView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
SQText("* 1 mois gratuit valable uniquement sur la formule “3 jours par semaine.”", size: 14)
|
||||
SQText("* 1 mois gratuit valable uniquement sur la formule “3 jours par semaine.”")
|
||||
.sqSize(14)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.sqRoyal())
|
||||
}
|
||||
@@ -8,23 +8,22 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BoosterStatsView: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
VStack {
|
||||
HStack(spacing: 0) {
|
||||
SQText("x", size: 24, font: .bold)
|
||||
SQText("3", size: 32, font: .bold)
|
||||
SQText("x")
|
||||
.sqSize(24)
|
||||
.sqFont(.bold)
|
||||
SQText("3")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
}
|
||||
SQText("Demandes privées*", size: 14, font: .demiBold)
|
||||
SQText("Demandes privées*")
|
||||
.sqSize(14)
|
||||
.sqFont(.semiBold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
@@ -36,10 +35,16 @@ struct BoosterStatsView: View {
|
||||
.frame(height: 72)
|
||||
VStack {
|
||||
HStack(spacing: 0) {
|
||||
SQText("x", size: 24, font: .bold)
|
||||
SQText("3", size: 32, font: .bold)
|
||||
SQText("x")
|
||||
.sqSize(24)
|
||||
.sqFont(.bold)
|
||||
SQText("3")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
}
|
||||
SQText("Évaluations*", size: 14, font: .demiBold)
|
||||
SQText("Évaluations*")
|
||||
.sqSize(14)
|
||||
.sqFont(.semiBold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
@@ -51,17 +56,24 @@ struct BoosterStatsView: View {
|
||||
.frame(height: 72)
|
||||
VStack {
|
||||
HStack(spacing: 0) {
|
||||
SQText("x", size: 24, font: .bold)
|
||||
SQText("2.9", size: 32, font: .bold)
|
||||
SQText("x")
|
||||
.sqSize(24)
|
||||
.sqFont(.bold)
|
||||
SQText("2.9")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
}
|
||||
SQText("Mise en favori*", size: 14, font: .demiBold)
|
||||
SQText("Mise en favori*")
|
||||
.sqSize(14)
|
||||
.sqFont(.semiBold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(minWidth: 100, maxWidth: .infinity)
|
||||
}
|
||||
.foregroundColor(.sqRoyal())
|
||||
SQText("*Performances moyennes obtenues par les profils boostés, par rapport aux Abonnés Premier.", size: 11, font: .medium)
|
||||
SQText("*Performances moyennes obtenues par les profils boostés, par rapport aux Abonnés Premier.")
|
||||
.sqSize(11)
|
||||
.lineLimit(2)
|
||||
}
|
||||
.foregroundColor(.sqRoyal())
|
||||
@@ -18,10 +18,6 @@ struct BoosterSubscriptionCardView: View {
|
||||
}
|
||||
|
||||
init(id: Int, selectedId: Binding<Int>, currentOption: Bool = false, isFree: Bool = false) {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {}
|
||||
|
||||
self.id = id
|
||||
self._selectedId = selectedId
|
||||
self.currentOption = currentOption
|
||||
@@ -31,16 +27,20 @@ struct BoosterSubscriptionCardView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
if currentOption {
|
||||
SQText("Option actuelle", font: .bold)
|
||||
SQText("Option actuelle")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.sqRoyal())
|
||||
}
|
||||
ZStack(alignment: .topLeading) {
|
||||
VStack {
|
||||
SQText("3 jours", size: 32, font: .bold)
|
||||
SQText("3 jours")
|
||||
.sqSize(32)
|
||||
.sqFont(.bold)
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.white)
|
||||
SQText("par semaine", size: 14)
|
||||
SQText("par semaine")
|
||||
.sqSize(14)
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.white)
|
||||
@@ -51,7 +51,8 @@ struct BoosterSubscriptionCardView: View {
|
||||
// .minimumScaleFactor(0.5)
|
||||
// .lineLimit(1)
|
||||
// .foregroundColor(.white)
|
||||
SQText("Sans engagement", size: 12)
|
||||
SQText("Sans engagement")
|
||||
.sqSize(12)
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.white)
|
||||
@@ -61,7 +62,7 @@ struct BoosterSubscriptionCardView: View {
|
||||
.padding(.top, 30)
|
||||
.padding([.leading, .trailing])
|
||||
|
||||
Image("booster_corner_light", bundle: Bundle.module)
|
||||
Image("booster_corner_light")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.position(x: 25, y: 25)
|
||||
@@ -77,7 +78,8 @@ struct BoosterSubscriptionCardView: View {
|
||||
startPoint: .top, endPoint: .bottom))
|
||||
)
|
||||
if isFree {
|
||||
SQText("1 mois gratuit *", font: .bold)
|
||||
SQText("1 mois gratuit *")
|
||||
.sqFont(.bold)
|
||||
.foregroundColor(.sqRoyal())
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,13 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BoosterSubscriptionOptionsView: View {
|
||||
init() {
|
||||
do {
|
||||
try FontRegistration().registerFonts()
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
SQText("Booster", size: 18, font: .bold)
|
||||
SQText("Booster")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
Spacer()
|
||||
SQIcon(.xmark, color: .white)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
133
AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift
Normal file
133
AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// IdentityValidationNavigationManager.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 30/06/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum IdentityValidationRoute: NavigationRoute {
|
||||
case informationsValidation
|
||||
case livenessTutorial
|
||||
case liveness
|
||||
case livenessSucess
|
||||
case livenessError
|
||||
case identityDocumentsList
|
||||
case identityDocumentsValidation
|
||||
}
|
||||
|
||||
class IdentityValidationNavigationManager: BaseNavigationManager<IdentityValidationRoute> {}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// IdentityValidationViewModel.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 30/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
class IdentityValidationViewModel: ObservableObject {
|
||||
@Published var isLoading: Bool = false
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// IdentityDocumentsValidationLoaderView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 01/07/2025.
|
||||
//
|
||||
|
||||
import Lottie
|
||||
import SwiftUI
|
||||
|
||||
struct IdentityDocumentsValidationLoaderView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
SQProgressIndicator(.constant(true))
|
||||
SQProgressIndicator(.constant(false))
|
||||
SQProgressIndicator(.constant(false))
|
||||
}
|
||||
Spacer()
|
||||
VStack(spacing: 16) {
|
||||
LottieView(animation: .named("av_loader"))
|
||||
.looping()
|
||||
.frame(width: 60, height: 60)
|
||||
SQText("Vérification en cours...")
|
||||
.sqSize(20)
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IdentityDocumentsValidationLoaderView()
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// IdentityDocumentsValidationResultView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 01/07/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IdentityDocumentsValidationResultView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 32) {
|
||||
HStack {
|
||||
SQProgressIndicator(.constant(true))
|
||||
SQProgressIndicator(.constant(false))
|
||||
SQProgressIndicator(.constant(false))
|
||||
}
|
||||
if true {
|
||||
VStack(spacing: 32) {
|
||||
VStack(spacing: 16) {
|
||||
SQIcon(.xmark, customSize: 60, color: .sqSemanticRed)
|
||||
SQText("Votre identité n’a pas été validée.")
|
||||
.sqSize(20)
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Assurez-vous que :")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
HStack(alignment: .top) {
|
||||
SQText("1.")
|
||||
SQText("Le document est valable et en cours de validité.")
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
SQText("2.")
|
||||
SQText("Les informations renseignées sur le document transmis sont strictement identiques aux informations indiquées sur votre compte AlloVoisins.")
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
SQText("3.")
|
||||
SQText("Vous avez plus de 18 ans.")
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
SQText("4.")
|
||||
SQText("Le document vous appartient.")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Spacer()
|
||||
VStack(spacing: 16) {
|
||||
SQIcon(.check, customSize: 60, color: .sqSemanticGreen)
|
||||
SQText("Votre identité a été vérifiée.")
|
||||
.sqSize(20)
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if true {
|
||||
SQButton("Recommencer") {
|
||||
|
||||
}
|
||||
} else {
|
||||
SQButton("Terminer") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IdentityDocumentsValidationResultView()
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// IdentityInformationsValidationView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 30/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IdentityInformationsValidationView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
SQText("Les étapes de vérification d’identité :")
|
||||
.sqSize(20)
|
||||
.sqFont(.bold)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("1. Vous allez prendre une vidéo de votre visage")
|
||||
SQText("2. Vous allez prendre une photo de votre pièce d’identité.")
|
||||
SQText("3. Nous allons comparer la vidéo de votre visage et les informations de votre compte AlloVoisins avec les éléments figurant sur votre pièce d’identité.")
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
SQIcon(.circle_exclamation)
|
||||
SQText("Vérifiez que :")
|
||||
.sqFont(.semiBold)
|
||||
}
|
||||
SQText("Les informations renseignées sur le document d’identité transmis doivent être strictement identiques aux informations ci-dessous, indiquées sur votre compte AlloVoisins :")
|
||||
VStack(alignment: .leading) {
|
||||
SQText(" ‧ Prénom : Maëlys-Gaëlle")
|
||||
SQText(" ‧ Nom : Martin")
|
||||
SQText(" ‧ Date de naissance : 13/07/1990")
|
||||
}
|
||||
}
|
||||
}
|
||||
SQButton("Mettre à jour mes informations") {
|
||||
|
||||
}
|
||||
.sqButtonType(.line)
|
||||
Spacer()
|
||||
SQButton("Commencer") {
|
||||
|
||||
}
|
||||
}
|
||||
.padding([.trailing, .leading, .top])
|
||||
.sqNavigationBar(title: "Vérification de mon identité")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IdentityInformationsValidationView()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// IdentityValidationNavigationView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 30/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IdentityValidationNavigationView: View {
|
||||
@ObservedObject var navigationManager: IdentityValidationNavigationManager
|
||||
@ObservedObject var viewModel: IdentityValidationViewModel
|
||||
|
||||
init(navigationManager: IdentityValidationNavigationManager) {
|
||||
self.navigationManager = navigationManager
|
||||
self.viewModel = .init()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigationManager.navigationPath) {
|
||||
IdentityInformationsValidationView()
|
||||
.navigationDestination(for: IdentityValidationRoute.self) { route in
|
||||
switch route {
|
||||
case .livenessTutorial:
|
||||
LivenessTutorialView()
|
||||
case .liveness:
|
||||
LivenessView()
|
||||
case .livenessSucess:
|
||||
LivenessView()
|
||||
case .livenessError:
|
||||
LivenessView()
|
||||
case .informationsValidation:
|
||||
IdentityInformationsValidationView()
|
||||
case .identityDocumentsList:
|
||||
IdentityInformationsValidationView()
|
||||
case .identityDocumentsValidation:
|
||||
IdentityInformationsValidationView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.environmentObject(navigationManager)
|
||||
.environmentObject(viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IdentityValidationNavigationView(navigationManager: IdentityValidationNavigationManager())
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// LivenessErrorView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 30/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LivenessErrorView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
HStack {
|
||||
SQProgressIndicator(.constant(true))
|
||||
SQProgressIndicator(.constant(false))
|
||||
SQProgressIndicator(.constant(false))
|
||||
}
|
||||
SQIcon(.xmark, customSize: 60, color: .sqSemanticRed)
|
||||
SQText("Nous n’avons pas réussi à détecter votre visage.")
|
||||
.sqSize(20)
|
||||
.sqFont(.semiBold)
|
||||
SQText("Assurez-vous que les conditions d’éclairage sont bonnes et que votre téléphone est bien en face de vous.\nL’image ne doit pas être floue, coupée, ou présenter des reflets.")
|
||||
Spacer()
|
||||
SQButton("Recommencer") {
|
||||
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LivenessErrorView()
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// LivenessSuccessView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 30/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LivenessSuccessView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
HStack {
|
||||
SQProgressIndicator(.constant(true))
|
||||
SQProgressIndicator(.constant(false))
|
||||
SQProgressIndicator(.constant(false))
|
||||
}
|
||||
SQImage("lemonsieur", height: 365)
|
||||
.clipShape(Ellipse())
|
||||
.overlay(
|
||||
Ellipse()
|
||||
.stroke(Color.gray, lineWidth: 0)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
SQIcon(.check, color: .sqSemanticPositive)
|
||||
SQText("Votre visage est bien dans le cadre.")
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.check, color: .sqSemanticPositive)
|
||||
SQText("L’image n’est pas floue et n’a pas de reflet.")
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
SQIcon(.check, color: .sqSemanticPositive)
|
||||
SQText("Vous ne portez pas d’accessoires couvrant.")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
SQButton("Continuer") {
|
||||
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LivenessSuccessView()
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// LivenessTutorialView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 30/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LivenessTutorialView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
SQProgressIndicator(.constant(true))
|
||||
SQProgressIndicator(.constant(false))
|
||||
SQProgressIndicator(.constant(false))
|
||||
}
|
||||
VStack(spacing: 32) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Pour vérifier votre identité, vous allez devoir réaliser une courte vidéo de votre visage, nous permettant de valider que vous êtes une personne réelle.")
|
||||
.sqFont(.semiBold)
|
||||
SQText("Cette vidéo ne sera pas stockée par AlloVoisins.\nElle est éphémère et n’est utilisée que dans le cadre du processus de vérification d’identité.")
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SQText("Pour garantir le bon déroulement de la vérification d’identité :")
|
||||
.sqSize(18)
|
||||
.sqFont(.bold)
|
||||
HStack {
|
||||
SQImage("portrait_reflection", height: 80)
|
||||
SQText("Trouvez un endroit bien éclairé.\nUne bonne lumière est essentielle, mais évitez la lumière directe du soleil sur votre visage.")
|
||||
}
|
||||
HStack {
|
||||
SQImage("portrait_accessories", height: 80)
|
||||
SQText("Veuillez retirer vos lunettes de soleil, chapeau, masque ou tout autre accessoire. Ceux-ci rendent votre identification plus difficile.")
|
||||
}
|
||||
HStack {
|
||||
SQImage("portrait_angle", height: 80)
|
||||
SQText("Positionnez votre visage dans le cadre de l'écran et suivez les instructions.")
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
SQButton("Continuer") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.horizontal, .top])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LivenessTutorialView()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// LivenessView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 30/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LivenessView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@State private var isPresentingLiveness = true
|
||||
|
||||
var body: some View {
|
||||
// FaceLivenessDetectorView(
|
||||
// sessionID: "",
|
||||
// region: "",
|
||||
// isPresented: $isPresentingLiveness,
|
||||
// onCompletion: { result in
|
||||
// switch result {
|
||||
// case .success: break
|
||||
// case .failure(_): break
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LivenessView()
|
||||
}
|
||||
@@ -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") {}
|
||||
}
|
||||
@@ -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 d’identité"
|
||||
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()
|
||||
}
|
||||
@@ -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 d’identité", desc: "Carte d’identité, passeport, permis de conduire ou titre de séjour du représentant légal.", status: .pending)
|
||||
KYCDocumentButton(name: "Preuve d’immatriculation", 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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 d’appliquer 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 d’une 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 • L’utilisation d’un papier d’au moins
200 g/m2 ")
|
||||
SQButton("Imprimer") {}
|
||||
.frame(maxWidth: .infinity)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CardPrintView()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 d’appliquer 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)
|
||||
}
|
||||
@@ -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 d’expé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 j’engage ma responsabilité sur l’exhaustivité et l’authenticité des informations renseignées ci-dessus.", isChecked: $authentConfirm, error: .constant(.none))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
SQFooter {
|
||||
SQButton("Aperçu") {}
|
||||
.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()
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
//
|
||||
// TrialWarningCellView.swift
|
||||
// FlyerPrintView.swift
|
||||
// AlloVoisinsSwiftUI
|
||||
//
|
||||
// Created by Victor on 14/10/2024.
|
||||
// Created by Victor on 25/11/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TrialWarningCellView: View {
|
||||
struct FlyerPrintView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TrialWarningCellView()
|
||||
FlyerPrintView()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user