✨New adds
This commit is contained in:
62
AlloVoisinsSwiftUI/Core/Views/MessageNotificationView.swift
Normal file
62
AlloVoisinsSwiftUI/Core/Views/MessageNotificationView.swift
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// 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, font: .demiBold)
|
||||||
|
SQText(notification.message, size: 14, textColor: .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))
|
||||||
|
}
|
||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,8 +40,13 @@ struct SQPicker: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 51)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.tint(.sqNeutral(100))
|
.tint(.sqNeutral(100))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.white)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerPickerMenu() {
|
func triggerPickerMenu() {
|
||||||
@@ -114,5 +119,8 @@ struct SQPickerPreview: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
SQPickerPreview()
|
ZStack {
|
||||||
|
Color.black
|
||||||
|
SQPickerPreview()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ struct SQTextField: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.white)
|
||||||
|
}
|
||||||
.foregroundStyle(Color.sqNeutral())
|
.foregroundStyle(Color.sqNeutral())
|
||||||
.background(isDisabled ? Color.sqNeutral(10) : .clear)
|
.background(isDisabled ? Color.sqNeutral(10) : .clear)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|||||||
@@ -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, textColor: .sqNeutral(50))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding()
|
||||||
|
.background(Color.sqNeutral(10))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.onTapGesture {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SQSearchBarButton(placeholder: "Rechercher") {
|
||||||
|
print("search")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,8 +52,10 @@ struct BoosterSubscriptionSelectionScreen: View {
|
|||||||
VStack(spacing: 48) {
|
VStack(spacing: 48) {
|
||||||
ZStack {
|
ZStack {
|
||||||
ElipseShape()
|
ElipseShape()
|
||||||
|
.fill(Color.sqPurple(60))
|
||||||
|
Rectangle()
|
||||||
.fill(backgroundGradient)
|
.fill(backgroundGradient)
|
||||||
|
|
||||||
VStack(alignment: .trailing) {
|
VStack(alignment: .trailing) {
|
||||||
BoosterLockedToPremierView()
|
BoosterLockedToPremierView()
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//
|
||||||
|
// 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", size: 12, font: .demiBold)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.sqPurple(20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
SQText("UserID:", font: .demiBold)
|
||||||
|
Spacer()
|
||||||
|
SQText("103135")
|
||||||
|
Button {} label: {
|
||||||
|
SQIcon(.files)
|
||||||
|
.padding(8)
|
||||||
|
.background {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.sqNeutral(20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
SQText("Email:", font: .demiBold)
|
||||||
|
Spacer()
|
||||||
|
SQText("test@test.com")
|
||||||
|
Button {} label: {
|
||||||
|
SQIcon(.files)
|
||||||
|
.padding(8)
|
||||||
|
.background {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.sqNeutral(20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
SQText("Téléphone:", font: .demiBold)
|
||||||
|
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,58 @@
|
|||||||
|
//
|
||||||
|
// 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", size: 18, font: .bold)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
Divider()
|
||||||
|
SQText("Types à rechercher :", font: .demiBold)
|
||||||
|
SQPicker(selection: $selectedSearchType, options: searchTypes)
|
||||||
|
Toggle(isOn: $showSuggested) {
|
||||||
|
SQText("Afficher une suggestion", font: .demiBold)
|
||||||
|
}
|
||||||
|
Toggle(isOn: $showAllCategories) {
|
||||||
|
SQText("Afficher \"Toutes les catégories\"", font: .demiBold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.fill(Color.sqNeutral(10))
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
Spacer()
|
||||||
|
SQFooter {
|
||||||
|
SQButton("Afficher la vue", color: .sqNeutral(100), textColor: .white) {
|
||||||
|
showSearchView.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showSearchView) {}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(.container, edges: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ConfigPrestationSearchView()
|
||||||
|
}
|
||||||
100
AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift
Normal file
100
AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//
|
||||||
|
// 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 :", font: .demiBold)
|
||||||
|
SQPicker(selection: $currentEnv, options: pickerOptions)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.fill(Color.sqNeutral(10))
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
SQText("User ID :", font: .demiBold)
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
VStack {
|
||||||
|
SQTextField("Visiter le profil :", placeholder: "UserID", text: $userId)
|
||||||
|
SQButton("Visiter le profil", color: .sqNeutral(100), textColor: .white) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
SQTextField("Se connecter sur :", placeholder: "UserID", text: $userId)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
SQButton("Se connecter", color: .sqNeutral(100), textColor: .white) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.fill(Color.sqNeutral(10))
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .principal) {
|
||||||
|
SQText("🦄 Debug Land 🦄", font: .bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button {} label: {
|
||||||
|
SQIcon(.user_group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationStack {
|
||||||
|
DebugLandView()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ struct CategorySelectorView: View {
|
|||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.sqNeutral(100))
|
.fill(Color.sqNeutral(100))
|
||||||
.frame(height: 2)
|
.frame(height: 2)
|
||||||
|
.padding(.top, 1)
|
||||||
}
|
}
|
||||||
VStack {
|
VStack {
|
||||||
SQText("Objets", font: .demiBold)
|
SQText("Objets", font: .demiBold)
|
||||||
@@ -26,6 +27,7 @@ struct CategorySelectorView: View {
|
|||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.sqNeutral(20))
|
.fill(Color.sqNeutral(20))
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
|
.padding(.top, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// SubCategoryCell.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 08/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SubCategoryCell: View {
|
||||||
|
var text: String
|
||||||
|
@Binding var isChecked: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack {
|
||||||
|
if isChecked {
|
||||||
|
SQImage("checked_neutral", height: 20)
|
||||||
|
} else {
|
||||||
|
SQImage("checkbox_unchecked", height: 20)
|
||||||
|
}
|
||||||
|
SQText("Bricolage - Travaux")
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
Divider()
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SubCategoryCell(text: "Bricolage - Petits travaux", isChecked: .constant(true))
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ struct PrestationSearchView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.padding(.vertical)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|||||||
53
AlloVoisinsSwiftUI/Features/Subscriptions/Models/FAQ.swift
Normal file
53
AlloVoisinsSwiftUI/Features/Subscriptions/Models/FAQ.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// FAQ.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 22/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Modèle pour une question fréquente
|
||||||
|
struct FAQItem: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let question: String
|
||||||
|
let answer: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension de Pricing pour gérer les FAQs selon l'abonnement
|
||||||
|
extension Pricing {
|
||||||
|
var faqItems: [FAQItem] {
|
||||||
|
switch self {
|
||||||
|
case .standard:
|
||||||
|
return []
|
||||||
|
case .premier:
|
||||||
|
return [
|
||||||
|
FAQItem(
|
||||||
|
question: "Comment résilier mon abonnement Premier ?",
|
||||||
|
answer: "L'abonnement Premier est sans engagement. Vous pouvez le résilier à tout moment, depuis le menu Abonnement > Gérer mes abonnements > Demander la résiliation."
|
||||||
|
)
|
||||||
|
] + commonFAQItems
|
||||||
|
case .premierPro:
|
||||||
|
return [
|
||||||
|
FAQItem(
|
||||||
|
question: "Comment résilier mon abonnement Premier ?",
|
||||||
|
answer: "Pendant votre essai gratuit de 14 jours, la résiliation est possible à tout moment. Passé ce délai, la résiliation sera effective à la fin de votre engagement de 12 mois. Vous pouvez résilier votre abonnement depuis le menu Abonnement > Gérer mes abonnements > Demander la résiliation."
|
||||||
|
)
|
||||||
|
] + commonFAQItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Questions communes à tous les abonnements
|
||||||
|
private var commonFAQItems: [FAQItem] {
|
||||||
|
[
|
||||||
|
FAQItem(
|
||||||
|
question: "Comment me faire payer mes prestations ?",
|
||||||
|
answer: "Paiement en ligne ou paiement en direct lors de la prestation, vous choisissez le mode de paiement qui vous convient. En optant pour le paiement en ligne, vous bénéficiez d'un paiement réalisé en toute sécurité, sans commission."
|
||||||
|
),
|
||||||
|
FAQItem(
|
||||||
|
question: "Comment contacter le Service Clients ?",
|
||||||
|
answer: "Pour nous contacter, il suffit de vous rendre sur le lien \"Contactez-nous\" présent au bas de tous nos articles de notre aide en ligne.\nNotre Service Clients est disponible pour répondre à toutes vos questions, du lundi au vendredi, de 9h à 18h."
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
//
|
||||||
|
// Pricing.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 21/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum Pricing: String, CaseIterable, Identifiable {
|
||||||
|
case standard
|
||||||
|
case premier
|
||||||
|
case premierPro
|
||||||
|
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
var headerTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .standard, .premier:
|
||||||
|
return "Abonnements"
|
||||||
|
case .premierPro:
|
||||||
|
return "Abonnement Premier"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var footerButtonColor: Color {
|
||||||
|
switch self {
|
||||||
|
case .standard:
|
||||||
|
return .sqNeutral(100)
|
||||||
|
case .premier, .premierPro:
|
||||||
|
return .sqOrange(50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var footerTextColor: Color {
|
||||||
|
switch self {
|
||||||
|
case .standard:
|
||||||
|
return .sqNeutral(100)
|
||||||
|
case .premier, .premierPro:
|
||||||
|
return .sqOrange(70)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var footerBackgroundColor: Color {
|
||||||
|
switch self {
|
||||||
|
case .standard:
|
||||||
|
return .sqNeutral(15)
|
||||||
|
case .premier, .premierPro:
|
||||||
|
return .sqOrange(20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var footerPrimaryText: String {
|
||||||
|
switch self {
|
||||||
|
case .standard:
|
||||||
|
return "Gratuit"
|
||||||
|
case .premier:
|
||||||
|
return "À partir de 9,99 € / mois"
|
||||||
|
case .premierPro:
|
||||||
|
return "Essai gratuit* de 14 jours"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var footerSecondaryText: String {
|
||||||
|
switch self {
|
||||||
|
case .standard:
|
||||||
|
return ""
|
||||||
|
case .premier:
|
||||||
|
return "Sans engagement"
|
||||||
|
case .premierPro:
|
||||||
|
return "À partir de 29,99 € / mois"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var footerTertiaryText: String {
|
||||||
|
switch self {
|
||||||
|
case .standard, .premier:
|
||||||
|
return ""
|
||||||
|
case .premierPro:
|
||||||
|
return "* Offre d’essai valable une seule fois par utilisateur."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
//
|
||||||
|
// PricingFeature.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 22/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Représente une fonctionnalité individuelle
|
||||||
|
struct PricingFeature {
|
||||||
|
let description: String
|
||||||
|
let value: FeatureValue
|
||||||
|
|
||||||
|
enum FeatureValue {
|
||||||
|
case boolean(Bool)
|
||||||
|
case text(String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Représente une section de fonctionnalités
|
||||||
|
struct PricingFeatureSection {
|
||||||
|
let title: String
|
||||||
|
let subtitle: String?
|
||||||
|
let features: [PricingFeature]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension de Pricing pour gérer les sections de fonctionnalités
|
||||||
|
extension Pricing {
|
||||||
|
var featureSections: [PricingFeatureSection] {
|
||||||
|
switch self {
|
||||||
|
case .standard:
|
||||||
|
// MARK: - Standard
|
||||||
|
return [
|
||||||
|
PricingFeatureSection(
|
||||||
|
title: "Proposer mes services",
|
||||||
|
subtitle: "Répondez aux demandes dans votre périmètre d'intervention",
|
||||||
|
features: [
|
||||||
|
PricingFeature(description: "Nombre de réponses", value: .text("jusqu'à 4 / mois")),
|
||||||
|
PricingFeature(description: "Demandes ayant déjà reçu 3 réponses", value: .boolean(false)),
|
||||||
|
PricingFeature(description: "Demandes réservées aux abonnés Premier", value: .boolean(false))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
PricingFeatureSection(
|
||||||
|
title: "Gérer la visibilité de mon profil",
|
||||||
|
subtitle: nil,
|
||||||
|
features: [
|
||||||
|
PricingFeature(description: "Possibilité d'afficher mon numéro de téléphone", value: .boolean(false)),
|
||||||
|
PricingFeature(description: "Affichage des photos de mes réalisations", value: .text("3")),
|
||||||
|
PricingFeature(description: "Collecte d'avis auprès de mes anciens contacts", value: .boolean(false)),
|
||||||
|
PricingFeature(description: "Référencement prioritaire sur Google", value: .boolean(false))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
case .premier:
|
||||||
|
// MARK: - Premier
|
||||||
|
return [
|
||||||
|
PricingFeatureSection(
|
||||||
|
title: "Proposer mes services",
|
||||||
|
subtitle: "Répondez aux demandes dans votre périmètre d'intervention",
|
||||||
|
features: [
|
||||||
|
PricingFeature(description: "Nombre de réponses", value: .text("illimité")),
|
||||||
|
PricingFeature(description: "Demandes ayant déjà reçu 3 réponses", value: .boolean(true)),
|
||||||
|
PricingFeature(description: "Demandes réservées aux abonnés Premier", value: .boolean(true))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
PricingFeatureSection(
|
||||||
|
title: "Gérer la visibilité de mon profil",
|
||||||
|
subtitle: nil,
|
||||||
|
features: [
|
||||||
|
PricingFeature(description: "Possibilité d'afficher mon numéro de téléphone", value: .boolean(true)),
|
||||||
|
PricingFeature(description: "Affichage des photos de mes réalisations", value: .text("50")),
|
||||||
|
PricingFeature(description: "Collecte d'avis auprès de mes anciens contacts", value: .boolean(true)),
|
||||||
|
PricingFeature(description: "Référencement prioritaire sur Google", value: .boolean(true))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
case .premierPro:
|
||||||
|
// MARK: - Premier Pro
|
||||||
|
return [
|
||||||
|
PricingFeatureSection(
|
||||||
|
title: "Développer ma clientèle",
|
||||||
|
subtitle: "Répondez aux demandes dans votre périmètre d'intervention",
|
||||||
|
features: [
|
||||||
|
PricingFeature(description: "Nombre de réponses", value: .text("illimité")),
|
||||||
|
PricingFeature(description: "Demandes ayant déjà reçu 3 réponses", value: .boolean(true)),
|
||||||
|
PricingFeature(description: "Demandes réservées aux Pros", value: .boolean(true))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
PricingFeatureSection(
|
||||||
|
title: "Augmenter ma visibilité",
|
||||||
|
subtitle: nil,
|
||||||
|
features: [
|
||||||
|
PricingFeature(description: "Référencement prioritaire sur Google", value: .boolean(true)),
|
||||||
|
PricingFeature(description: "Affichage de mon numéro de téléphone\nsur mon profil", value: .boolean(true)),
|
||||||
|
PricingFeature(description: "Collecte d'avis auprès de mes anciens\nclients (hors AlloVoisins)", value: .boolean(true)),
|
||||||
|
PricingFeature(description: "Cartes de visite et prospectus\npersonnalisés", value: .boolean(true))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
PricingFeatureSection(
|
||||||
|
title: "Gagner du temps",
|
||||||
|
subtitle: nil,
|
||||||
|
features: [
|
||||||
|
PricingFeature(description: "Logiciel de facturation intégré", value: .boolean(true)),
|
||||||
|
PricingFeature(description: "Signature électronique des documents", value: .boolean(true)),
|
||||||
|
PricingFeature(description: "Relance client automatisée", value: .boolean(true))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// ReassuranceIndicator.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 22/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Modèle pour un indicateur de réassurance
|
||||||
|
struct ReassuranceIndicator: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let icon: SQIconName
|
||||||
|
let text: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension de Pricing pour gérer les indicateurs selon l'abonnement
|
||||||
|
extension Pricing {
|
||||||
|
var reassuranceIndicators: [ReassuranceIndicator] {
|
||||||
|
switch self {
|
||||||
|
case .standard, .premier:
|
||||||
|
return [
|
||||||
|
ReassuranceIndicator(icon: .lock_keyhole_open, text: "Sans engagement"),
|
||||||
|
ReassuranceIndicator(icon: .euro_sign, text: "Pas de commission sur vos prestations"),
|
||||||
|
ReassuranceIndicator(icon: .comments, text: "Assistance prioritaire")
|
||||||
|
]
|
||||||
|
case .premierPro:
|
||||||
|
return [
|
||||||
|
ReassuranceIndicator(icon: .users, text: "4 millions de membres"),
|
||||||
|
ReassuranceIndicator(icon: .euro_sign, text: "Pas de commission sur vos prestations"),
|
||||||
|
ReassuranceIndicator(icon: .comments, text: "Assistance prioritaire")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// FAQRow.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 22/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FAQRow: View {
|
||||||
|
let item: FAQItem
|
||||||
|
@State private var isExpanded = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Button(action: {
|
||||||
|
withAnimation {
|
||||||
|
isExpanded.toggle()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
SQText(item.question, size: 13, font: .demiBold)
|
||||||
|
Spacer()
|
||||||
|
SQIcon(isExpanded ? .chevron_up : .chevron_down)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isExpanded {
|
||||||
|
SQText(item.answer, size: 14)
|
||||||
|
.padding(.bottom, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// FAQSection.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 22/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FAQSection: View {
|
||||||
|
let items: [FAQItem]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
SQText("Questions fréquentes", size: 16, font: .demiBold)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
ForEach(items) { item in
|
||||||
|
FAQRow(item: item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// FeatureRow.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 22/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FeatureRow: View {
|
||||||
|
let feature: PricingFeature
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
SQText(" • ", size: 14)
|
||||||
|
SQText("\(feature.description)", size: 14)
|
||||||
|
Spacer()
|
||||||
|
switch feature.value {
|
||||||
|
case .boolean(let isIncluded):
|
||||||
|
SQIcon(isIncluded ? .check : .xmark,
|
||||||
|
size: .l,
|
||||||
|
type: .solid,
|
||||||
|
color: isIncluded ? .sqSemanticGreen : .sqSemanticRed)
|
||||||
|
case .text(let text):
|
||||||
|
SQText(text, size: 14, font: .demiBold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// FeatureSection.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 22/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FeatureSection: View {
|
||||||
|
let section: PricingFeatureSection
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
SQText(section.title, size: 18, font: .bold)
|
||||||
|
if let subtitle = section.subtitle {
|
||||||
|
SQText(subtitle, size: 13, font: .demiBold)
|
||||||
|
}
|
||||||
|
ForEach(section.features, id: \.description) { feature in
|
||||||
|
FeatureRow(feature: feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// PricingSubscribeFooter.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 21/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PricingSubscribeFooter: View {
|
||||||
|
var pricing: Pricing
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
SQText(pricing.footerPrimaryText, size: 18, font: .bold, textColor: pricing.footerTextColor)
|
||||||
|
if !pricing.footerSecondaryText.isEmpty {
|
||||||
|
SQText(pricing.footerSecondaryText, size: 13, textColor: pricing.footerTextColor)
|
||||||
|
}
|
||||||
|
SQButton("Continuer", color: pricing.footerButtonColor, textColor: .white) {
|
||||||
|
|
||||||
|
}
|
||||||
|
if !pricing.footerTertiaryText.isEmpty {
|
||||||
|
SQText(pricing.footerTertiaryText, size: 12, textColor: .sqOrange(70))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.background(pricing.footerBackgroundColor)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.shadow(color: Color(red: 0.09, green: 0.14, blue: 0.2).opacity(0.1), radius: 8, x: 0, y: -4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PricingSubscribeFooter(pricing: .standard)
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// PricingSubscribeHeader.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 21/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PricingSubscribeHeader: View {
|
||||||
|
var pricing: Pricing
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Button {
|
||||||
|
|
||||||
|
} label: {
|
||||||
|
SQIcon(.xmark, size: .l)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
SQText(pricing.headerTitle, size: 18, font: .bold)
|
||||||
|
Spacer()
|
||||||
|
if pricing == .premierPro {
|
||||||
|
Button {
|
||||||
|
|
||||||
|
} label: {
|
||||||
|
SQIcon(.user_group, size: .l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PricingSubscribeHeader(pricing: .premier)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// ReassuranceIndicator.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 22/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ReassuranceIndicatorSection: View {
|
||||||
|
let indicator: ReassuranceIndicator
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.sqNeutral(15))
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
|
||||||
|
SQIcon(indicator.icon, size: .s, type: .solid)
|
||||||
|
}
|
||||||
|
|
||||||
|
SQText(indicator.text, size: 14)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// ReassuranceIndicatorsRow.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 22/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ReassuranceIndicatorsRow: View {
|
||||||
|
let indicators: [ReassuranceIndicator]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
ForEach(indicators) { indicator in
|
||||||
|
ReassuranceIndicatorSection(indicator: indicator)
|
||||||
|
if indicator.id != indicators.last?.id {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// SwitchAndInfo.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 21/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SwitchAndInfo: View {
|
||||||
|
@Binding var pricing: Pricing
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
if pricing == .premierPro {
|
||||||
|
HStack {
|
||||||
|
SQIcon(.chart_line_up, size: .l)
|
||||||
|
SQText("L’abonnement conçu pour augmenter votre chiffre d’affaires et développer votre entreprise.", font: .demiBold)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.foregroundStyle( Color.sqNeutral(10))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Picker("", selection: $pricing) {
|
||||||
|
SQText("Standard").tag(Pricing.standard)
|
||||||
|
SQText("Premier").tag(Pricing.premier)
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
if pricing == .standard {
|
||||||
|
SQText("Idéal pour proposer vos services ponctuellement, sans objectif de complément de revenus.", font: .demiBold)
|
||||||
|
} else {
|
||||||
|
SQText("Idéal pour vous générer un complément de revenus régulier.", font: .demiBold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SwitchAndInfo(pricing: .constant(.premier))
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// SubscriptionsPricingView.swift
|
||||||
|
// AlloVoisinsSwiftUI
|
||||||
|
//
|
||||||
|
// Created by Victor on 21/01/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SubscriptionsPricingView: View {
|
||||||
|
@State var pricing: Pricing
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
PricingSubscribeHeader(pricing: pricing)
|
||||||
|
Divider()
|
||||||
|
ScrollView {
|
||||||
|
SwitchAndInfo(pricing: $pricing)
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ForEach(pricing.featureSections, id: \.title) { section in
|
||||||
|
FeatureSection(section: section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pricing != .standard {
|
||||||
|
Divider()
|
||||||
|
ReassuranceIndicatorsRow(indicators: pricing.reassuranceIndicators)
|
||||||
|
.padding()
|
||||||
|
Divider()
|
||||||
|
FAQSection(items: pricing.faqItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PricingSubscribeFooter(pricing: pricing)
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(.container, edges: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
Color.white
|
||||||
|
.sheet(isPresented: .constant(true)) {
|
||||||
|
SubscriptionsPricingView(pricing: .premierPro)
|
||||||
|
}
|
||||||
|
}
|
||||||
185
AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Avatar0.pdf
vendored
Normal file
185
AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Avatar0.pdf
vendored
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< /Type /XObject
|
||||||
|
/Length 2 0 R
|
||||||
|
/Group << /Type /Group
|
||||||
|
/S /Transparency
|
||||||
|
>>
|
||||||
|
/Subtype /Form
|
||||||
|
/Resources << >>
|
||||||
|
/BBox [ 0.000000 0.000000 300.000000 300.000000 ]
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||||
|
0.799255 0.843708 0.898039 scn
|
||||||
|
0.000000 150.000000 m
|
||||||
|
0.000000 232.842712 67.157288 300.000000 150.000000 300.000000 c
|
||||||
|
232.842712 300.000000 300.000000 232.842712 300.000000 150.000000 c
|
||||||
|
300.000000 67.157288 232.842712 0.000000 150.000000 0.000000 c
|
||||||
|
67.157288 0.000000 0.000000 67.157288 0.000000 150.000000 c
|
||||||
|
h
|
||||||
|
f
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 -16.041626 -56.863281 cm
|
||||||
|
0.467200 0.544662 0.640000 scn
|
||||||
|
148.459488 268.987030 m
|
||||||
|
162.539291 275.160522 177.380173 271.215851 186.499435 258.885834 c
|
||||||
|
249.168854 174.124954 l
|
||||||
|
257.207672 163.255920 259.575836 147.484070 255.444305 132.350998 c
|
||||||
|
229.972214 43.073334 l
|
||||||
|
223.329834 18.725861 202.148956 3.297180 182.670288 8.614502 c
|
||||||
|
25.247545 51.567444 l
|
||||||
|
5.768888 56.884735 -4.638077 80.929825 2.004305 105.277298 c
|
||||||
|
27.564735 194.867538 l
|
||||||
|
31.648695 209.830734 41.525627 222.113205 53.794498 227.491669 c
|
||||||
|
148.459488 268.987030 l
|
||||||
|
h
|
||||||
|
178.071045 156.238922 m
|
||||||
|
179.628311 155.843689 181.186005 155.448364 182.744797 155.053558 c
|
||||||
|
185.099365 154.458969 187.620422 155.801025 188.198013 158.162369 c
|
||||||
|
188.408676 159.032166 188.513992 159.959717 188.490219 160.945007 c
|
||||||
|
188.395081 164.791107 186.132248 168.423157 182.670059 170.101593 c
|
||||||
|
176.452377 173.122070 169.405670 169.632721 167.768005 163.309753 c
|
||||||
|
167.764603 163.292770 167.761200 163.279175 167.754410 163.262192 c
|
||||||
|
167.166626 160.934814 168.753311 158.600662 171.084091 158.009476 c
|
||||||
|
173.413559 157.421005 175.741806 156.830093 178.071045 156.238922 c
|
||||||
|
h
|
||||||
|
127.326241 105.638535 m
|
||||||
|
112.848907 109.389526 102.557457 121.807877 99.061287 137.311295 c
|
||||||
|
98.208481 141.089462 101.412453 144.843842 104.697968 143.991028 c
|
||||||
|
165.733154 128.178452 l
|
||||||
|
169.018661 127.325638 169.997192 122.490814 167.418381 119.602814 c
|
||||||
|
156.834732 107.748474 141.806976 101.887558 127.326241 105.638535 c
|
||||||
|
h
|
||||||
|
114.798615 187.724869 m
|
||||||
|
108.625107 190.592484 101.697327 187.109894 100.073257 180.844635 c
|
||||||
|
100.039284 180.712128 100.005302 180.583008 99.978127 180.450500 c
|
||||||
|
99.478668 178.116333 101.000809 175.816116 103.314606 175.221527 c
|
||||||
|
104.788635 174.843964 106.262665 174.465424 107.736877 174.086838 c
|
||||||
|
110.136459 173.470612 112.536537 172.854248 114.937920 172.241791 c
|
||||||
|
117.248314 171.647202 119.691216 172.934906 120.374138 175.221527 c
|
||||||
|
120.737686 176.437897 120.880386 177.779953 120.730896 179.254532 c
|
||||||
|
120.360550 182.873032 118.101120 186.189133 114.798615 187.724869 c
|
||||||
|
h
|
||||||
|
f*
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
2432
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
<< /Type /XObject
|
||||||
|
/Length 4 0 R
|
||||||
|
/Group << /Type /Group
|
||||||
|
/S /Transparency
|
||||||
|
>>
|
||||||
|
/Subtype /Form
|
||||||
|
/Resources << >>
|
||||||
|
/BBox [ 0.000000 0.000000 300.000000 300.000000 ]
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||||
|
0.000000 0.000000 0.000000 scn
|
||||||
|
0.000000 150.000000 m
|
||||||
|
0.000000 232.842712 67.157288 300.000000 150.000000 300.000000 c
|
||||||
|
232.842712 300.000000 300.000000 232.842712 300.000000 150.000000 c
|
||||||
|
300.000000 67.157288 232.842712 0.000000 150.000000 0.000000 c
|
||||||
|
67.157288 0.000000 0.000000 67.157288 0.000000 150.000000 c
|
||||||
|
h
|
||||||
|
f
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
405
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /XObject << /X1 1 0 R >>
|
||||||
|
/ExtGState << /E1 << /SMask << /Type /Mask
|
||||||
|
/G 3 0 R
|
||||||
|
/S /Alpha
|
||||||
|
>>
|
||||||
|
/Type /ExtGState
|
||||||
|
>> >>
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Length 7 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
/E1 gs
|
||||||
|
/X1 Do
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
7 0 obj
|
||||||
|
46
|
||||||
|
endobj
|
||||||
|
|
||||||
|
8 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 300.000000 300.000000 ]
|
||||||
|
/Resources 5 0 R
|
||||||
|
/Contents 6 0 R
|
||||||
|
/Parent 9 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
9 0 obj
|
||||||
|
<< /Kids [ 8 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
10 0 obj
|
||||||
|
<< /Pages 9 0 R
|
||||||
|
/Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 11
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000002692 00000 n
|
||||||
|
0000002715 00000 n
|
||||||
|
0000003370 00000 n
|
||||||
|
0000003392 00000 n
|
||||||
|
0000003690 00000 n
|
||||||
|
0000003792 00000 n
|
||||||
|
0000003813 00000 n
|
||||||
|
0000003988 00000 n
|
||||||
|
0000004062 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 10 0 R
|
||||||
|
/Size 11
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
4122
|
||||||
|
%%EOF
|
||||||
12
AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Contents.json
vendored
Normal file
12
AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Avatar0.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user