diff --git a/yobble.xcodeproj/project.pbxproj b/yobble.xcodeproj/project.pbxproj index 605aa74..2c8a383 100644 --- a/yobble.xcodeproj/project.pbxproj +++ b/yobble.xcodeproj/project.pbxproj @@ -395,7 +395,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = V22H44W47J; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -435,7 +435,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = V22H44W47J; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; diff --git a/yobble/Components/TopBarView.swift b/yobble/Components/TopBarView.swift index e69de29..0a9b9b2 100644 --- a/yobble/Components/TopBarView.swift +++ b/yobble/Components/TopBarView.swift @@ -0,0 +1,104 @@ +import SwiftUI + +struct TopBarView: View { + var title: String + + // Состояния для ProfileTab + @Binding var selectedAccount: String +// @Binding var sheetType: ProfileTab.SheetType? + var accounts: [String] + var viewModel: LoginViewModel + + // Привязка для управления боковым меню + @Binding var isSideMenuPresented: Bool + + var isHomeTab: Bool { + return title == "Home" + } + + var isProfileTab: Bool { + return title == "Profile" + } + + var body: some View { + VStack(spacing: 0) { + HStack { + // Кнопка "Гамбургер" для открытия меню + Button(action: { + withAnimation { + isSideMenuPresented.toggle() + } + }) { + Image(systemName: "line.horizontal.3") + .imageScale(.large) + .foregroundColor(.primary) + } + +// Spacer() + + if isHomeTab{ + Text("Yobble") + .font(.largeTitle) + .fontWeight(.bold) + Spacer() + } else if isProfileTab { + Spacer() + Button(action: { }) { + HStack(spacing: 4) { + Text(selectedAccount) + .font(.headline) + .foregroundColor(.primary) + Image(systemName: "chevron.down") + .font(.subheadline) + .foregroundColor(.gray) + } + } + Spacer() + } else { + Text(title) + .font(.largeTitle) + .fontWeight(.bold) + Spacer() + } + + if isHomeTab{ + HStack(spacing: 20) { + // Кнопка поиска + Button(action: { + // пока ничего не делаем + }) { + Image(systemName: "magnifyingglass") + .imageScale(.large) + .foregroundColor(.primary) + } + // Кнопка уведомлений + Button(action: { + // пока ничего не делаем + }) { + Image(systemName: "bell") + .imageScale(.large) + .foregroundColor(.primary) + } + } + } else if isProfileTab { + NavigationLink(destination: SettingsView(viewModel: viewModel)) { + Image(systemName: "wrench") + .imageScale(.large) + .foregroundColor(.primary) + } + } + } + .padding() + .frame(height: 50) // Стандартная высота для нав. бара + + Divider() + } + .background(Color(UIColor.systemBackground)) + } +} + +struct TopBarView_Previews: PreviewProvider { + static var previews: some View { + /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/ + } +} diff --git a/yobble/Network/AuthService.swift b/yobble/Network/AuthService.swift index 556994d..8d7328b 100644 --- a/yobble/Network/AuthService.swift +++ b/yobble/Network/AuthService.swift @@ -99,11 +99,12 @@ class AuthService { do { let decoder = JSONDecoder() - let loginResponse = try decoder.decode(LoginResponse.self, from: data) + let loginResponse = try decoder.decode(APIResponse.self, from: data).data // Сохраняем токены в Keychain KeychainService.shared.save(loginResponse.access_token, forKey: "access_token", service: username) KeychainService.shared.save(loginResponse.refresh_token, forKey: "refresh_token", service: username) + KeychainService.shared.save(loginResponse.user_id, forKey: "user_id", service: username) UserDefaults.standard.set(username, forKey: "currentUser") completion(true, nil) @@ -160,7 +161,7 @@ class AuthService { if (200...299).contains(httpResponse.statusCode) { do { - let _ = try decoder.decode(RegisterResponse.self, from: data) + let _ = try decoder.decode(APIResponse.self, from: data) if AppConfig.DEBUG{ print("Регистрация успешна. Пытаемся сразу войти...")} // Сразу логинимся @@ -177,8 +178,8 @@ class AuthService { } } else { // Ошибка сервера — пробуем распарсить message - if let errorResponseMessage = try? decoder.decode(ErrorResponseMessage.self, from: data), - let message = errorResponseMessage.message { + if let errorResponseMessage = try? decoder.decode(ErrorResponse.self, from: data), + let message = errorResponseMessage.data?.message { if let jsonString = String(data: data, encoding: .utf8) { if AppConfig.DEBUG{ print("Raw JSON:", jsonString)} @@ -270,25 +271,31 @@ class AuthService { } } -struct LoginResponse: Decodable { +struct APIResponse: Decodable { let status: String + let data: T +} + +struct LoginPayload: Decodable { let access_token: String let refresh_token: String - let token_type: String + let user_id: String } -struct TokenRefreshResponse: Decodable { - let status: String +struct TokenRefreshPayload: Decodable { let access_token: String let token_type: String } -struct RegisterResponse: Decodable { - let status: String +struct RegisterPayload: Decodable { let message: String } -struct ErrorResponseMessage: Decodable { - let status: String? +struct ErrorPayload: Decodable { let message: String? } + +struct ErrorResponse: Decodable { + let status: String? + let data: ErrorPayload? +} diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 595175b..ce871e8 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -1,58 +1,13 @@ { "sourceLanguage" : "en", "strings" : { + "@yourusername" : { + + }, "🌍" : { }, - "AuthService_error_empty_response" : { - - }, - "AuthService_error_invalid_invitation_code" : { - - }, - "AuthService_error_invalid_request" : { - - }, - "AuthService_error_invalid_response" : { - - }, - "AuthService_error_invitation_expired" : { - - }, - "AuthService_error_invitation_not_active" : { - - }, - "AuthService_error_invitation_usage_limit" : { - - }, - "AuthService_error_login_already_registered" : { - - }, - "AuthService_error_network" : { - - }, - "AuthService_error_parsing_response" : { - - }, - "AuthService_error_registration_disabled" : { - - }, - "AuthService_error_registration_forbidden" : { - - }, - "AuthService_error_serialization" : { - - }, - "AuthService_error_server_error" : { - - }, - "AuthService_error_server_unavailable" : { - - }, - "AuthService_error_too_many_requests" : { - - }, - "AuthService_login_success_but_failed" : { + "CATEGORY" : { }, "Hello, world!" : { @@ -94,15 +49,75 @@ }, "profile_down_text_3" : { + }, + "Push-уведомления" : { + + }, + "SERVICES" : { + + }, + "Yobble" : { + + }, + "Your Name" : { + + }, + "Активные сессии" : { + + }, + "Безопасность" : { + }, "Войти" : { + }, + "Выйти из аккаунта" : { + + }, + "Данные" : { + + }, + "Двухфакторная аутентификация" : { + + }, + "Другое" : { + + }, + "Заглушка: Push-уведомления" : { + + }, + "Заглушка: Активные сессии" : { + + }, + "Заглушка: Двухфакторная аутентификация" : { + + }, + "Заглушка: Другие настройки" : { + + }, + "Заглушка: Обратная связь" : { + + }, + "Заглушка: Сменить пароль" : { + + }, + "Заглушка: Хранилище данных" : { + + }, + "Заглушка: Частые вопросы" : { + }, "Закрыть" : { "comment" : "Закрыть" }, "Зарегистрироваться" : { "comment" : "Зарегистрироваться" + }, + "Здесь будут чаты" : { + + }, + "Здесь не будут чаты" : { + }, "Инвайт-код (необязательно)" : { "comment" : "Инвайт-код" @@ -112,9 +127,27 @@ }, "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)" : { "comment" : "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)" + }, + "Логин уже занят." : { + + }, + "Мой профиль" : { + + }, + "Настройки" : { + + }, + "Не удалось обработать ответ сервера." : { + }, "Не удалось сериализовать данные запроса." : { + }, + "Неверный запрос (400)." : { + + }, + "Неверный код приглашения." : { + }, "Неверный логин" : { "comment" : "Неверный логин" @@ -136,6 +169,18 @@ }, "Нет аккаунта? Регистрация" : { "comment" : "Регистрация" + }, + "О приложении" : { + + }, + "Обратная связь" : { + + }, + "Описание" : { + + }, + "Отображаемое имя" : { + }, "Ошибка авторизации" : { @@ -160,21 +205,75 @@ }, "Пароль должен быть от 8 до 128 символов" : { "comment" : "Пароль должен быть от 6 до 32 символов" + }, + "Поддержка" : { + }, "Подтверждение пароля" : { "comment" : "Подтверждение пароля" + }, + "Приглашение достигло лимита использования." : { + + }, + "Приглашение истекло." : { + + }, + "Приглашение не активно." : { + + }, + "Приложение" : { + + }, + "Применить" : { + }, "Произошла ошибка." : { + }, + "Профиль" : { + + }, + "Публичная информация" : { + + }, + "Пустой ответ от сервера." : { + }, "Регистрация" : { "comment" : "Регистрация" + }, + "Регистрация временно недоступна." : { + + }, + "Регистрация выполнена, но вход не удался." : { + + }, + "Регистрация запрещена." : { + + }, + "Редактировать профиль" : { + }, "Сервер не отвечает. Попробуйте позже." : { }, "Слишком много запросов." : { + }, + "Сменить пароль" : { + + }, + "Тёмная тема" : { + + }, + "Уведомления" : { + + }, + "Частые вопросы" : { + + }, + "Язык" : { + } }, "version" : "1.0" diff --git a/yobble/Views/Tab/ChatsTab.swift b/yobble/Views/Tab/ChatsTab.swift index e69de29..4b22288 100644 --- a/yobble/Views/Tab/ChatsTab.swift +++ b/yobble/Views/Tab/ChatsTab.swift @@ -0,0 +1,20 @@ +// +// ChatsTab.swift +// VolnahubApp +// +// Created by cheykrym on 09/06/2025. +// + +import SwiftUI + +struct ChatsTab: View { + var body: some View { + VStack { + Text("Здесь будут чаты") + .font(.title) + .foregroundColor(.gray) + + Spacer() + } + } +} diff --git a/yobble/Views/Tab/CustomTabBar.swift b/yobble/Views/Tab/CustomTabBar.swift index e69de29..fdf0751 100644 --- a/yobble/Views/Tab/CustomTabBar.swift +++ b/yobble/Views/Tab/CustomTabBar.swift @@ -0,0 +1,84 @@ +import SwiftUI + +struct CustomTabBar: View { + @Binding var selectedTab: Int + var onCreate: () -> Void + + var body: some View { + HStack { + // Tab 1: Feed + TabBarButton(systemName: "list.bullet.rectangle", text: "Лента", isSelected: selectedTab == 0) { + selectedTab = 0 + } + + // Tab 2: Search + TabBarButton(systemName: "magnifyingglass", text: "Поиск", isSelected: selectedTab == 1) { + selectedTab = 1 + } + + // Create Button + CreateButton { + onCreate() + } + + // Tab 3: Chats + TabBarButton(systemName: "bubble.left.and.bubble.right.fill", text: "Чаты", isSelected: selectedTab == 2) { + selectedTab = 2 + } + + // Tab 4: Profile + TabBarButton(systemName: "person.crop.square", text: "Лицо", isSelected: selectedTab == 3) { + selectedTab = 3 + } + } + .padding(.horizontal) + .padding(.top, 1) + .padding(.bottom, 30) // Добавляем отступ снизу +// .background(Color(.systemGray6)) + } +} + +struct TabBarButton: View { + let systemName: String + let text: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 4) { + Image(systemName: systemName) + .font(.system(size: 22)) + Text(text) +// .font(.caption) + .font(.system(size: 12)) + } + .foregroundColor(isSelected ? .accentColor : .gray) + } + .frame(maxWidth: .infinity) + } +} + +struct CreateButton: View { + let action: () -> Void + + var body: some View { + Button(action: action) { + Image(systemName: "plus") + .font(.system(size: 24, weight: .bold)) + .foregroundColor(.white) + .padding(16) +// .background(Color.accentColor) + .background( + LinearGradient( + gradient: Gradient(colors: [.blue, .white]), + startPoint: .top, + endPoint: .bottom + ) + ) + .clipShape(Circle()) + .shadow(radius: 4) + } + .offset(y: -3) + } +} diff --git a/yobble/Views/Tab/MainView.swift b/yobble/Views/Tab/MainView.swift index e69de29..8acfbde 100644 --- a/yobble/Views/Tab/MainView.swift +++ b/yobble/Views/Tab/MainView.swift @@ -0,0 +1,136 @@ +import SwiftUI + +struct MainView: View { + @ObservedObject var viewModel: LoginViewModel + @State private var selectedTab: Int = 0 +// @StateObject private var newHomeTabViewModel = NewHomeTabViewModel() + + // Состояния для TopBarView + @State private var selectedAccount = "@user1" + @State private var accounts = ["@user1", "@user2", "@user3"] +// @State private var sheetType: ProfileTab.SheetType? = nil + + // Состояния для бокового меню + @State private var isSideMenuPresented = false + @State private var menuOffset: CGFloat = 0 + + private var tabTitle: String { + switch selectedTab { + case 0: return "Home" + case 1: return "Search" + case 2: return "Chats" + case 3: return "Profile" + default: return "Home" + } + } + + private var menuWidth: CGFloat { + UIScreen.main.bounds.width * 0.8 + } + + var body: some View { + NavigationView { + ZStack(alignment: .leading) { // Выравниваем ZStack по левому краю + // Основной контент + VStack(spacing: 0) { + TopBarView( + title: tabTitle, + selectedAccount: $selectedAccount, + accounts: accounts, + viewModel: viewModel, + isSideMenuPresented: $isSideMenuPresented + ) + + ZStack { + NewHomeTab() + .opacity(selectedTab == 0 ? 1 : 0) + + SearchTab() + .opacity(selectedTab == 1 ? 1 : 0) + + ChatsTab() + .opacity(selectedTab == 2 ? 1 : 0) + + ProfileTab() + .opacity(selectedTab == 3 ? 1 : 0) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + CustomTabBar(selectedTab: $selectedTab) { + print("Create button tapped") + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) // Убедимся, что основной контент занимает все пространство + .ignoresSafeArea(edges: .bottom) + .navigationBarHidden(true) +// .sheet(item: $sheetType) { type in +// // ... sheet presentation logic +// } + + // Затемнение и закрытие по тапу + Color.black + .opacity(Double(menuOffset / menuWidth) * 0.4) + .ignoresSafeArea() + .onTapGesture { + withAnimation(.easeInOut) { + isSideMenuPresented = false + } + } + .allowsHitTesting(menuOffset > 0) + + // Боковое меню + SideMenuView(isPresented: $isSideMenuPresented) + .frame(width: menuWidth) + .offset(x: -menuWidth + menuOffset) // Новая логика смещения + .ignoresSafeArea(edges: .vertical) + } + .gesture( + DragGesture() + .onChanged { gesture in + if !isSideMenuPresented && gesture.startLocation.x > 60 { return } + + let translation = gesture.translation.width + + // Определяем базовое смещение в зависимости от того, открыто меню или нет + let baseOffset = isSideMenuPresented ? menuWidth : 0 + + // Новое смещение — это база плюс текущий свайп + let newOffset = baseOffset + translation + + // Жестко ограничиваем итоговое смещение между 0 и шириной меню + self.menuOffset = max(0, min(menuWidth, newOffset)) + } + .onEnded { gesture in + if !isSideMenuPresented && gesture.startLocation.x > 60 { return } + + let threshold = menuWidth * 0.4 + + withAnimation(.easeInOut) { + if self.menuOffset > threshold { + isSideMenuPresented = true + } else { + isSideMenuPresented = false + } + // Устанавливаем финальное смещение после анимации + self.menuOffset = isSideMenuPresented ? menuWidth : 0 + } + } + ) + } + .navigationViewStyle(StackNavigationViewStyle()) + .onChange(of: isSideMenuPresented) { presented in + // Плавная анимация при нажатии на кнопку, а не только при жесте + withAnimation(.easeInOut) { + menuOffset = presented ? menuWidth : 0 + } + } + } +} + +struct MainView_Previews: PreviewProvider { + static var previews: some View { + let mockViewModel = LoginViewModel() + MainView(viewModel: mockViewModel) + .environmentObject(ThemeManager()) + } +} diff --git a/yobble/Views/Tab/NewHomeTab.swift b/yobble/Views/Tab/NewHomeTab.swift index e69de29..57f0f02 100644 --- a/yobble/Views/Tab/NewHomeTab.swift +++ b/yobble/Views/Tab/NewHomeTab.swift @@ -0,0 +1,17 @@ +import SwiftUI + +struct NewHomeTab: View { + + var body: some View { + VStack { + VStack { + Text("Здесь не будут чаты") + .font(.title) + .foregroundColor(.gray) + + Spacer() + } + } +// .background(Color(.secondarySystemBackground)) // Фон для всей вкладки + } +} diff --git a/yobble/Views/Tab/ProfileTab.swift b/yobble/Views/Tab/ProfileTab.swift index e69de29..2bdcf9b 100644 --- a/yobble/Views/Tab/ProfileTab.swift +++ b/yobble/Views/Tab/ProfileTab.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct ProfileTab: View { +// @Binding var sheetType: SheetType? + +// enum SheetType: Identifiable { +// case accountShare +// var id: Int { self.hashValue } +// } + + var body: some View { + VStack { + Text("Здесь не будут чаты") + .font(.title) + .foregroundColor(.gray) + + Spacer() + } + } +} diff --git a/yobble/Views/Tab/SearchTab.swift b/yobble/Views/Tab/SearchTab.swift index e69de29..1638074 100644 --- a/yobble/Views/Tab/SearchTab.swift +++ b/yobble/Views/Tab/SearchTab.swift @@ -0,0 +1,14 @@ +import SwiftUI + +struct SearchTab: View { + + var body: some View { + VStack { + Text("Здесь не будут чаты") + .font(.title) + .foregroundColor(.gray) + + Spacer() + } + } +} diff --git a/yobble/Views/Tab/Settings/EditProfileView.swift b/yobble/Views/Tab/Settings/EditProfileView.swift index e69de29..8f869be 100644 --- a/yobble/Views/Tab/Settings/EditProfileView.swift +++ b/yobble/Views/Tab/Settings/EditProfileView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct EditProfileView: View { + @State private var displayName = "" + @State private var description = "" + + var body: some View { + Form { + Section(header: Text("Публичная информация")) { + TextField("Отображаемое имя", text: $displayName) + TextField("Описание", text: $description) + } + + Button(action: { + // Действие для сохранения профиля + print("DisplayName: \(displayName)") + print("Description: \(description)") + }) { + Text("Применить") + } + } + .navigationTitle("Редактировать профиль") + } +} diff --git a/yobble/Views/Tab/Settings/SettingsView.swift b/yobble/Views/Tab/Settings/SettingsView.swift index e69de29..9e2bd40 100644 --- a/yobble/Views/Tab/Settings/SettingsView.swift +++ b/yobble/Views/Tab/Settings/SettingsView.swift @@ -0,0 +1,98 @@ +import SwiftUI + +struct SettingsView: View { + @ObservedObject var viewModel: LoginViewModel + @AppStorage("isDarkMode") private var isDarkMode: Bool = true + + var body: some View { + Form { + // MARK: - Профиль + Section(header: Text("Профиль")) { + NavigationLink(destination: EditProfileView()) { + Label("Мой профиль", systemImage: "person.crop.circle") + } + } + + // MARK: - Безопасность + Section(header: Text("Безопасность")) { + NavigationLink(destination: Text("Заглушка: Сменить пароль")) { + Label("Сменить пароль", systemImage: "key") + } + NavigationLink(destination: Text("Заглушка: Двухфакторная аутентификация")) { + Label("Двухфакторная аутентификация", systemImage: "lock.shield") + } + NavigationLink(destination: Text("Заглушка: Активные сессии")) { + Label("Активные сессии", systemImage: "iphone") + } + } + + // MARK: - Приложение + Section(header: Text("Приложение")) { + Button(action: openLanguageSettings) { + Label("Язык", systemImage: "globe") + } + + Toggle(isOn: $isDarkMode) { + Label("Тёмная тема", systemImage: "moon.fill") + } + + NavigationLink(destination: Text("Заглушка: Хранилище данных")) { + Label("Данные", systemImage: "externaldrive") + } + + NavigationLink(destination: Text("Заглушка: Другие настройки")) { + Label("Другое", systemImage: "ellipsis.circle") + } + } + + // MARK: - Уведомления + Section(header: Text("Уведомления")) { + NavigationLink(destination: Text("Заглушка: Push-уведомления")) { + Label("Push-уведомления", systemImage: "bell") + } + } + + // MARK: - Поддержка + Section(header: Text("Поддержка")) { + NavigationLink(destination: Text("Заглушка: Частые вопросы")) { + Label("Частые вопросы", systemImage: "questionmark.circle") + } + NavigationLink(destination: Text("Заглушка: Обратная связь")) { + Label("Обратная связь", systemImage: "paperplane") + } + } + + // MARK: - О приложении + Section(header: Text("О приложении")) { + VStack(alignment: .leading, spacing: 6) { + Text(AppInfo.text_1) + Text(AppInfo.text_2) + Text(AppInfo.text_3) + } + .font(.footnote) + .foregroundColor(.gray) + .padding(.vertical, 4) + } + + // MARK: - Выход + Section { + Button(action: { + viewModel.logoutCurrentUser() + }) { + HStack { + Image(systemName: "arrow.backward.square") + Text("Выйти из аккаунта") + } + .foregroundColor(.red) + } + } + } + .navigationTitle("Настройки") + } + + private func openLanguageSettings() { + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + UIApplication.shared.open(url) + } + +} diff --git a/yobble/Views/Tab/SideMenuView.swift b/yobble/Views/Tab/SideMenuView.swift index e69de29..6acb5ee 100644 --- a/yobble/Views/Tab/SideMenuView.swift +++ b/yobble/Views/Tab/SideMenuView.swift @@ -0,0 +1,259 @@ +import SwiftUI + +// --- HELPER STRUCTS & EXTENSIONS --- + +// Dummy data for the account list +struct Account: Identifiable { + let id = UUID() + let name: String + let username: String + let isCurrent: Bool +} + +// Custom Transition for older Xcode versions +extension AnyTransition { + static var slideAndFade: AnyTransition { + let insertion = AnyTransition.move(edge: .top).combined(with: .opacity) + let removal = AnyTransition.move(edge: .top).combined(with: .opacity) + return .asymmetric(insertion: insertion, removal: removal) + } +} + +// Helper Views for buttons +struct SideMenuButton: View { + let icon: String + let title: String + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 15) { + Image(systemName: icon) + .font(.title3) + .frame(width: 30) + Text(title) + .font(.subheadline) + } + .foregroundColor(.primary) + .padding(.vertical, 8) + } + } +} + +struct SideMenuFooterButton: View { + let icon: String + let title: String + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack { + Image(systemName: icon) + .font(.title2) + Text(title) + .font(.caption2) + } + .foregroundColor(.primary) + } + } +} + + +// --- MAIN VIEW --- + +struct SideMenuView: View { + @EnvironmentObject var themeManager: ThemeManager + @Environment(\.colorScheme) var colorScheme + @Binding var isPresented: Bool + @State private var isAccountListExpanded = false + + // Adjustable paddings + private let topPadding: CGFloat = 66 + private let bottomPadding: CGFloat = 34 + + // Dummy account data + private let accounts: [Account] = [ + Account(name: "Your Name", username: "@yourusername", isCurrent: true), + Account(name: "Second Account", username: "@second", isCurrent: false), + Account(name: "Another One", username: "@another", isCurrent: false), + Account(name: "Test User", username: "@test", isCurrent: false), + Account(name: "Creative Profile", username: "@creative", isCurrent: false) + ] + + private var themeToggleButton: some View { + Button(action: { + themeManager.toggleTheme(from: colorScheme) + }) { + Image(systemName: iconName) + .font(.title2) + .foregroundColor(.primary) + } + } + + private var iconName: String { + let effectiveScheme: ColorScheme + switch themeManager.theme { + case .system: + effectiveScheme = colorScheme + case .light: + effectiveScheme = .light + case .dark: + effectiveScheme = .dark + } + + return effectiveScheme == .dark ? "sun.max.fill" : "moon.fill" + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + ScrollView { + VStack(alignment: .leading, spacing: 0) { // Parent VStack + + // --- Header --- + HStack(alignment: .top) { + Button(action: { }) { + Image(systemName: "person.circle.fill") + .resizable() + .frame(width: 60, height: 60) + .foregroundColor(.gray) + } + Spacer() + themeToggleButton + } + .padding(.horizontal, 20) + .padding(.top, topPadding) + .padding(.bottom, 10) + + // --- Header Button --- + Button(action: { + withAnimation(.spring()) { + isAccountListExpanded.toggle() + } + }) { + HStack { + VStack(alignment: .leading) { + Text("Your Name") + .font(.title3).bold() + Text("@yourusername") + .font(.footnote) + } + .foregroundColor(.primary) + + Spacer() + + Image(systemName: isAccountListExpanded ? "chevron.up" : "chevron.down") + .font(.headline) + .foregroundColor(.secondary) + } + } + .padding(.horizontal, 20) + .padding(.bottom, 10) + + // --- Collapsible Account List in a clipped container --- + VStack { + if isAccountListExpanded { + VStack(alignment: .leading, spacing: 15) { + ForEach(accounts) { account in + HStack { + Button(action: { }) { + ZStack { + Image(systemName: "person.circle.fill") + .resizable() + .frame(width: 32, height: 32) // Smaller icon + .foregroundColor(.secondary) + + if account.isCurrent { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.blue) + .background(Circle().fill(Color(UIColor.systemBackground))) + .font(.body) // Smaller checkmark + .offset(x: 11, y: 11) // Adjusted offset + } + } + + VStack(alignment: .leading) { + Text(account.name).font(.footnote).bold() // Smaller text + Text(account.username).font(.caption2) // Smaller text + } + .foregroundColor(.primary) + } + + Spacer() + } + } + } + .padding(.horizontal, 20) + .padding(.vertical, 10) + .transition(.slideAndFade) + } + } + .clipped() + + // Menu Items + VStack(alignment: .leading, spacing: 20) { + // Section 1 + VStack(alignment: .leading, spacing: 7) { + SideMenuButton(icon: "person.2.fill", title: "People You May Like", action: {}) + SideMenuButton(icon: "star.fill", title: "Fun Fest", action: {}) + SideMenuButton(icon: "lightbulb.fill", title: "Creator Center", action: {}) + } + + Divider() + + // Section 2 + VStack(alignment: .leading, spacing: 7) { + Text("CATEGORY").font(.caption2).foregroundColor(.secondary) + SideMenuButton(icon: "doc.text", title: "Drafts", action: {}) + SideMenuButton(icon: "bubble.left", title: "My Comments", action: {}) + SideMenuButton(icon: "clock", title: "History", action: {}) + SideMenuButton(icon: "arrow.down.circle", title: "My Downloads", action: {}) + } + + Divider() + + // Section 3 + VStack(alignment: .leading, spacing: 7) { + Text("SERVICES").font(.caption2).foregroundColor(.secondary) + SideMenuButton(icon: "shippingbox", title: "Orders", action: {}) + SideMenuButton(icon: "cart", title: "Cart", action: {}) + SideMenuButton(icon: "wallet.pass", title: "Wallet", action: {}) + } + + Divider() + + // Section 4 + VStack(alignment: .leading, spacing: 15) { + SideMenuButton(icon: "square.grid.2x2", title: "Applets", action: {}) + } + } + .padding() + } + .frame(maxWidth: .infinity, alignment: .leading) // Align to the left + } + .clipped() + + Spacer() + + // Footer + HStack(spacing: 20) { + Spacer() + SideMenuFooterButton(icon: "qrcode.viewfinder", title: "Scan", action: {}) + SideMenuFooterButton(icon: "questionmark.circle", title: "Help Center", action: {}) + SideMenuFooterButton(icon: "gear", title: "Settings", action: {}) + Spacer() + } + .padding() + .padding(.bottom, bottomPadding) + } + .background(Color(UIColor.systemBackground)) + } +} + +// --- PREVIEW --- + +struct SideMenuView_Previews: PreviewProvider { + static var previews: some View { + SideMenuView(isPresented: .constant(true)) + .environmentObject(ThemeManager()) + } +} diff --git a/yobble/yobbleApp.swift b/yobble/yobbleApp.swift index d9486c7..a72abf1 100644 --- a/yobble/yobbleApp.swift +++ b/yobble/yobbleApp.swift @@ -18,8 +18,7 @@ struct yobbleApp: App { if viewModel.isLoading { SplashScreenView() } else if viewModel.isLoggedIn { -// MainView(viewModel: viewModel) - LoginView(viewModel: viewModel) + MainView(viewModel: viewModel) } else { LoginView(viewModel: viewModel) }