diff --git a/Hustle.xcodeproj/project.xcworkspace/xcuserdata/jay.xcuserdatad/UserInterfaceState.xcuserstate b/Hustle.xcodeproj/project.xcworkspace/xcuserdata/jay.xcuserdatad/UserInterfaceState.xcuserstate index b85d87e..149c3f4 100644 Binary files a/Hustle.xcodeproj/project.xcworkspace/xcuserdata/jay.xcuserdatad/UserInterfaceState.xcuserstate and b/Hustle.xcodeproj/project.xcworkspace/xcuserdata/jay.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Hustle/App/AppFeature.swift b/Hustle/App/AppFeature.swift index f982b5b..a0419e4 100644 --- a/Hustle/App/AppFeature.swift +++ b/Hustle/App/AppFeature.swift @@ -13,11 +13,13 @@ struct AppFeature { @ObservableState struct State: Equatable { var auth = AuthFeature.State() + var main = MainContentFeature.State() var isAuthenticated = false } enum Action { case auth(AuthFeature.Action) + case main(MainContentFeature.Action) case onAppear } @@ -26,8 +28,18 @@ struct AppFeature { AuthFeature() } + Scope(state: \.main, action: \.main) { + MainContentFeature() + } + Reduce { state, action in switch action { + case .onAppear: + return .none + + case .main(.profile(.logoutTapped)): + return .send(.auth(.signOutbuttonTapped)) + case .auth(.authenticationSucceeded): state.isAuthenticated = true return .none @@ -36,11 +48,15 @@ struct AppFeature { state.isAuthenticated = false return .none - case .onAppear: - return .send(.auth(.startListening)) + case .auth(.signOutSucceeded): + state.isAuthenticated = false + return .none case .auth: return .none + + case .main: + return .none } } } diff --git a/Hustle/App/AppView.swift b/Hustle/App/AppView.swift new file mode 100644 index 0000000..a7466fe --- /dev/null +++ b/Hustle/App/AppView.swift @@ -0,0 +1,35 @@ +// +// AppView.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import ComposableArchitecture +import SwiftUI + +struct AppView: View { + @Bindable var store: StoreOf + + var body: some View { + Group { + if store.isAuthenticated { + MainContentView(store: store.scope(state: \.main, action: \.main)) + } else { + AuthView(store: store.scope(state: \.auth, action: \.auth)) + } + } + .task { + await store.send(.auth(.startListening)).finish() + } + } +} + +#Preview { + AppView( + store: Store( + initialState: AppFeature.State(), + reducer: { AppFeature() } + ) + ) +} diff --git a/Hustle/App/MainView.swift b/Hustle/App/MainView.swift deleted file mode 100644 index 63d9fdc..0000000 --- a/Hustle/App/MainView.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// MainView.swift -// Center -// -// Created by Jidong Zheng on 9/26/25. -// - -import ComposableArchitecture -import SwiftUI - -struct MainView: View { - @Bindable var store: StoreOf - - var body: some View { - - Group { - switch store.auth.authState { - case .authenticated: - ContentView(store: store.scope(state: \.auth, action: \.auth)) - case .notAuthenticated: - AuthView(store: store.scope(state: \.auth, action: \.auth)) - } - } - .task { - store.send(.onAppear) - } - - } -} diff --git a/Hustle/Components/CategoryButton.swift b/Hustle/Components/CategoryButton.swift new file mode 100644 index 0000000..7518c95 --- /dev/null +++ b/Hustle/Components/CategoryButton.swift @@ -0,0 +1,39 @@ +// +// CategoryButton.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import SwiftUI + +struct CategoryButton: View { + let category: Category + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 8) { + Image(systemName: category.systemImage) + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(DesignConstants.Colors.white) + Text(category.title) + .font(DesignConstants.Fonts.title3Bold) + .foregroundColor(DesignConstants.Colors.white) + .fixedSize(horizontal: true, vertical: false) + } + .padding(.vertical, 10) + .padding(.horizontal, 14) + .frame(height: 32) + .background(DesignConstants.Colors.hustleGreen) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(DesignConstants.Colors.hustleGreen, lineWidth: 1.2) + ) + .cornerRadius(20) + } + .buttonStyle(.plain) + .shadow(color: Color.black.opacity(0.04), radius: 4, x: 0, y: 2) + } +} diff --git a/Hustle/Components/ResultsComponents.swift b/Hustle/Components/ResultsComponents.swift new file mode 100644 index 0000000..23c525a --- /dev/null +++ b/Hustle/Components/ResultsComponents.swift @@ -0,0 +1,71 @@ +// +// ResultsComponents.swift +// Hustle +// +// Created by Jay on 12/6/25. +// + +import SwiftUI + +struct FilterChipsRow: View { + let filters: [String] + var includeFilterIcon: Bool = false + + var body: some View { + HStack(spacing: 8) { + Image("Filter") + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 20) + .stroke(DesignConstants.Colors.hustleGreen.opacity(0.5), lineWidth: 1) + ) + + ForEach(filters, id: \.self) { filter in + HStack(spacing: 6) { + Text(filter) + .font(DesignConstants.Fonts.title3Bold) + .foregroundColor(DesignConstants.Colors.hustleGreen) + Image(systemName: "chevron.down") + .font(DesignConstants.Fonts.title3Bold) + .foregroundColor(DesignConstants.Colors.hustleGreen) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 20) + .stroke(DesignConstants.Colors.hustleGreen.opacity(0.5), lineWidth: 1) + ) + } + } + .padding(.bottom, 16) + .padding(.top, 8) + } +} + +struct ServiceResultsList: View { + let services: [Service] + var imageHeight: CGFloat = 240 + + var body: some View { + VStack(spacing: 16) { + ForEach(services) { service in + ServiceCardLarge( + service: service, + isFavorite: false, + ) + .frame(maxWidth: .infinity) + } + } + } +} + +#Preview { + VStack(spacing: 24) { + FilterChipsRow(filters: ["Price", "Location", "Ratings"], includeFilterIcon: true) + .padding(.horizontal, 16) + + ServiceResultsList(services: SampleData.services, imageHeight: 180) + .padding(.horizontal, 16) + } +} diff --git a/Hustle/Components/ServiceCard.swift b/Hustle/Components/ServiceCard.swift new file mode 100644 index 0000000..d919d1d --- /dev/null +++ b/Hustle/Components/ServiceCard.swift @@ -0,0 +1,86 @@ +// +// ServiceCard.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import SwiftUI + +struct ServiceCard: View { + let service: Service + var isFavorite: Bool = false + var cardWidth: CGFloat? = 178 + var cardHeight: CGFloat? = 280 + var imageHeight: CGFloat = 178 + + var body: some View { + VStack(alignment: .leading) { + ZStack(alignment: .topTrailing) { + Image(service.serviceImage) + .resizable() + .scaledToFill() + .frame(width: cardWidth, height: imageHeight) + .clipped() + + Image(systemName: isFavorite ? "heart.fill" : "heart") + .foregroundColor(isFavorite ? Color.red : Color.white) + .padding(10) + .shadow(color: Color.black.opacity(0.08), radius: 6, x: 0, y: 4) + } + + VStack(alignment: .leading){ + HStack{ + Image(service.providerPFP) + .resizable() + .frame(width: 24, height: 24) + .clipShape(Circle()) + + Text(service.providerName) + .font(DesignConstants.Fonts.title3Bold) + .foregroundColor(DesignConstants.Colors.black) + .lineLimit(1) + } + + Text(service.description) + .font(DesignConstants.Fonts.title4) + .foregroundColor(DesignConstants.Colors.black) + .lineLimit(2, reservesSpace: true) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack{ + Text(service.price) + .font(DesignConstants.Fonts.title3) + .foregroundColor(DesignConstants.Colors.black) + + Spacer() + + HStack() { + Image(systemName: "star.fill") + .foregroundColor(DesignConstants.Colors.accentGreen) + .font(.system(size: 16)) + + Text(String(format: "%.1f", service.rating)) + .font(DesignConstants.Fonts.subtitle1) + .foregroundColor(DesignConstants.Colors.black) + } + } + + } + .padding(.horizontal, 12) + .padding(.bottom, 16) + + } + .frame(width: cardWidth, height: cardHeight, alignment: .top) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(DesignConstants.Colors.white) + ) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .stroke(DesignConstants.Colors.stroke, lineWidth: 1) + ) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + .shadow(color: Color.black.opacity(0.05), radius: 6, x: 0, y: 4) + } +} diff --git a/Hustle/Components/ServiceCardLarge.swift b/Hustle/Components/ServiceCardLarge.swift new file mode 100644 index 0000000..6da3c12 --- /dev/null +++ b/Hustle/Components/ServiceCardLarge.swift @@ -0,0 +1,80 @@ +// +// ServiceCard.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import SwiftUI + +struct ServiceCardLarge: View { + let service: Service + var isFavorite: Bool = false + var cardWidth: CGFloat? = 364 + var cardHeight: CGFloat? = 436 + var imageHeight: CGFloat = 360 + + var body: some View { + VStack(alignment: .leading) { + Image(service.serviceImage) + .resizable() + .scaledToFill() + .frame(width: cardWidth, height: imageHeight) + .clipped() + + Spacer() + + HStack{ + Image(service.providerPFP) + .resizable() + .frame(width: 40, height: 40) + .clipShape(Circle()) + + VStack{ + HStack{ + Text(service.providerName) + .font(DesignConstants.Fonts.title3Bold) + .foregroundColor(DesignConstants.Colors.black) + .lineLimit(1) + Spacer() + + Image(systemName: "star.fill") + .foregroundColor(DesignConstants.Colors.accentGreen) + .font(.system(size: 16)) + + Text(String(format: "%.1f", service.rating)) + .font(DesignConstants.Fonts.subtitle1) + .foregroundColor(DesignConstants.Colors.secondaryGrey) + } + + HStack{ + Text(service.description) + .font(DesignConstants.Fonts.title4) + .foregroundColor(DesignConstants.Colors.secondaryGrey) + .lineLimit(1) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(service.price) + .font(DesignConstants.Fonts.title4) + .foregroundColor(DesignConstants.Colors.secondaryGrey) + } + } + } + .padding(.horizontal, 16) + + Spacer() + } + .frame(width: cardWidth, height: cardHeight, alignment: .top) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(DesignConstants.Colors.white) + ) + .overlay( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(DesignConstants.Colors.stroke, lineWidth: 1) + ) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + .shadow(color: Color.black.opacity(0.05), radius: 6, x: 0, y: 4) + } +} diff --git a/Hustle/Features/Auth/AuthFeature.swift b/Hustle/Features/Auth/AuthFeature.swift index e1947cb..408fa68 100644 --- a/Hustle/Features/Auth/AuthFeature.swift +++ b/Hustle/Features/Auth/AuthFeature.swift @@ -9,7 +9,7 @@ import ComposableArchitecture import FirebaseAuth import GoogleSignIn -public enum AuthState { +public enum AuthState: Equatable { case authenticated, notAuthenticated } @@ -20,9 +20,15 @@ struct AuthFeature { var authState: AuthState = .notAuthenticated var user: FirebaseAuth.User? var isLoading = false + + static func == (lhs: State, rhs: State) -> Bool { + lhs.authState == rhs.authState && + lhs.user?.uid == rhs.user?.uid && + lhs.isLoading == rhs.isLoading + } } - enum Action: Equatable { + enum Action { case startListening case signInButtonTapped case signOutbuttonTapped @@ -114,4 +120,3 @@ struct AuthFeature { } } } - diff --git a/Hustle/Features/Content/ContentView.swift b/Hustle/Features/Content/ContentView.swift deleted file mode 100644 index 2dbb746..0000000 --- a/Hustle/Features/Content/ContentView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ContentView.swift -// Center -// -// Created by Jidong Zheng on 10/20/25. -// - -import ComposableArchitecture -import SwiftUI - -struct ContentView: View { - @Bindable var store: StoreOf - - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello World!!!") - .background(DesignConstants.Colors.hustleGreen) - - Button("Sign out") { - store.send(.signOutbuttonTapped) - } - } - .padding() - } -} diff --git a/Hustle/Features/HomeTab/HomeFeature.swift b/Hustle/Features/HomeTab/HomeFeature.swift new file mode 100644 index 0000000..1d13020 --- /dev/null +++ b/Hustle/Features/HomeTab/HomeFeature.swift @@ -0,0 +1,66 @@ +// +// HomeFeature.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import Foundation +import ComposableArchitecture + +@Reducer +struct HomeFeature { + + @ObservableState + struct State: Equatable { + var searchQuery = "" + var isShowingSearch = false + var isShowingTagged = false + var selectedTaggedTitle: String = "" + var taggedServices: [Service] = [] + var search = SearchFeature.State() + var categories: [Category] = [] + var selectedCategoryID: Category.ID? + var sections: [ServiceSection] = [] + var isLoading = false + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case task + case categoryTapped(Category.ID) + case search(SearchFeature.Action) + } + + var body: some ReducerOf { + BindingReducer() + Scope(state: \.search, action: \.search) { + SearchFeature() + } + + Reduce { state, action in + switch action { + case .task: + state.isLoading = true + state.categories = SampleData.categories + state.sections = SampleData.sections + state.isLoading = false + return .none + + case let .categoryTapped(id): + state.selectedCategoryID = state.selectedCategoryID == id ? nil : id + + if let category = state.categories.first(where: { $0.id == id }) { + state.selectedTaggedTitle = category.title + state.taggedServices = SampleData.services.filter { $0.category == category.id } + state.isShowingTagged = true + return .none + } + return .none + + case .binding, .search: + return .none + } + } + } +} diff --git a/Hustle/Features/HomeTab/HomeView.swift b/Hustle/Features/HomeTab/HomeView.swift new file mode 100644 index 0000000..afb72a3 --- /dev/null +++ b/Hustle/Features/HomeTab/HomeView.swift @@ -0,0 +1,144 @@ +// +// ContentView.swift +// Center +// +// Created by Jidong Zheng on 10/20/25. +// + +import ComposableArchitecture +import SwiftUI +import ComposableArchitecture +import SwiftUI + +import ComposableArchitecture +import SwiftUI + +struct HomeView: View { + @Bindable var store: StoreOf + + var body: some View { + NavigationStack { + ScrollView(.vertical, showsIndicators: false) { + VStack(alignment: .leading) { + + Text("Hustle") + .font(DesignConstants.Fonts.h2Italic) + .foregroundColor(DesignConstants.Colors.hustleGreen) + .padding(.horizontal, 16) + + searchBar + .padding(.horizontal, 16) + + categoryRow + + ForEach(store.sections) { section in + sectionView(for: section) + } + } + .contentMargins(.horizontal, 16) + } + .background(DesignConstants.Colors.white.ignoresSafeArea()) + .navigationDestination(isPresented: $store.isShowingSearch) { + SearchView(store: store.scope(state: \.search, action: \.search)) + } + .navigationDestination(isPresented: $store.isShowingTagged) { + TaggedView( + title: store.selectedTaggedTitle, + services: store.taggedServices + ) { + store.isShowingTagged = false + } + } + } + .toolbar(.hidden, for: .navigationBar) + .task { + await store.send(.task).finish() + } + } + + private var searchBar: some View { + Button { + store.isShowingSearch = true + } label: { + HStack(spacing: 10) { + Image(systemName: "magnifyingglass") + .foregroundColor(DesignConstants.Colors.wash) + + Text("Find a service") + .font(DesignConstants.Fonts.body2) + .foregroundColor(DesignConstants.Colors.wash) + + Spacer() + } + .padding(.horizontal, 12) + .frame(height: 40) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(DesignConstants.Colors.shadedGray) + ) + } + .buttonStyle(.plain) + } + + private var categoryRow: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(store.categories) { category in + CategoryButton( + category: category, + isSelected: store.selectedCategoryID == category.id + ) { + store.send(.categoryTapped(category.id)) + } + } + } + .padding(.vertical, 4) + } + } + + private func sectionView(for section: ServiceSection) -> some View { + VStack(alignment: .leading, spacing: 12) { + + Button { + store.selectedTaggedTitle = section.title + store.taggedServices = section.items + store.isShowingTagged = true + } label: { + HStack { + Text(section.title) + .font(DesignConstants.Fonts.h3) + .foregroundColor(DesignConstants.Colors.black) + .padding(.leading, 16) + .padding(.trailing, 12) + + Image(systemName: "chevron.right") + .foregroundColor(DesignConstants.Colors.black) + .font(.system(size: 16, weight: .semibold)) + } + .padding(.vertical, 8) + } + .buttonStyle(.plain) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(Array(section.items.enumerated()), id: \.element.id) { index, service in + ServiceCard( + service: service, + isFavorite: index % 2 == 0 + ) + } + } + } + } + .padding(.bottom, 12) + } +} + +#Preview { + HomeView( + store: Store( + initialState: HomeFeature.State(), + reducer: { HomeFeature() } + ) + ) +} diff --git a/Hustle/Features/LearnTab/LearnFeature.swift b/Hustle/Features/LearnTab/LearnFeature.swift new file mode 100644 index 0000000..04c77a0 --- /dev/null +++ b/Hustle/Features/LearnTab/LearnFeature.swift @@ -0,0 +1,20 @@ +// +// LearnFeature.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import ComposableArchitecture + +@Reducer +struct LearnFeature { + @ObservableState + struct State: Equatable {} + + enum Action: Equatable {} + + var body: some ReducerOf { + Reduce { _, _ in .none } + } +} diff --git a/Hustle/Features/LearnTab/LearnView.swift b/Hustle/Features/LearnTab/LearnView.swift new file mode 100644 index 0000000..b98ea58 --- /dev/null +++ b/Hustle/Features/LearnTab/LearnView.swift @@ -0,0 +1,35 @@ +// +// LearnView.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import ComposableArchitecture +import SwiftUI + +struct LearnView: View { + @Bindable var store: StoreOf + + var body: some View { + VStack(spacing: 12) { + Image(systemName: "graduationcap.fill") + .font(.system(size: 40)) + .foregroundColor(DesignConstants.Colors.hustleGreen) + Text("Learn tab coming soon") + .font(DesignConstants.Fonts.title2) + .foregroundColor(DesignConstants.Colors.black) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(DesignConstants.Colors.white) + } +} + +#Preview { + LearnView( + store: Store( + initialState: LearnFeature.State(), + reducer: { LearnFeature() } + ) + ) +} diff --git a/Hustle/Features/MessagesTab/MessagesFeature.swift b/Hustle/Features/MessagesTab/MessagesFeature.swift new file mode 100644 index 0000000..8380955 --- /dev/null +++ b/Hustle/Features/MessagesTab/MessagesFeature.swift @@ -0,0 +1,20 @@ +// +// MessagesFeature.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import ComposableArchitecture + +@Reducer +struct MessagesFeature { + @ObservableState + struct State: Equatable {} + + enum Action: Equatable {} + + var body: some ReducerOf { + Reduce { _, _ in .none } + } +} diff --git a/Hustle/Features/MessagesTab/MessagesView.swift b/Hustle/Features/MessagesTab/MessagesView.swift new file mode 100644 index 0000000..acb64c2 --- /dev/null +++ b/Hustle/Features/MessagesTab/MessagesView.swift @@ -0,0 +1,35 @@ +// +// MessagesView.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import ComposableArchitecture +import SwiftUI + +struct MessagesView: View { + @Bindable var store: StoreOf + + var body: some View { + VStack(spacing: 12) { + Image(systemName: "bubble.left.and.bubble.right.fill") + .font(.system(size: 40)) + .foregroundColor(DesignConstants.Colors.hustleGreen) + Text("Messages coming soon") + .font(DesignConstants.Fonts.title2) + .foregroundColor(DesignConstants.Colors.black) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(DesignConstants.Colors.white) + } +} + +#Preview { + MessagesView( + store: Store( + initialState: MessagesFeature.State(), + reducer: { MessagesFeature() } + ) + ) +} diff --git a/Hustle/Features/ProfileTab/ProfileFeature.swift b/Hustle/Features/ProfileTab/ProfileFeature.swift new file mode 100644 index 0000000..c22ebb5 --- /dev/null +++ b/Hustle/Features/ProfileTab/ProfileFeature.swift @@ -0,0 +1,27 @@ +// +// ProfileFeature.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import ComposableArchitecture + +@Reducer +struct ProfileFeature { + @ObservableState + struct State: Equatable {} + + enum Action: Equatable { + case logoutTapped + } + + var body: some ReducerOf { + Reduce { _, action in + switch action { + case .logoutTapped: + return .none + } + } + } +} diff --git a/Hustle/Features/ProfileTab/ProfileView.swift b/Hustle/Features/ProfileTab/ProfileView.swift new file mode 100644 index 0000000..2677a86 --- /dev/null +++ b/Hustle/Features/ProfileTab/ProfileView.swift @@ -0,0 +1,51 @@ +// +// ProfileView.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import ComposableArchitecture +import SwiftUI + +struct ProfileView: View { + @Bindable var store: StoreOf + + var body: some View { + VStack(spacing: 20) { + VStack(spacing: 12) { + Image(systemName: "person.crop.circle.fill") + .font(.system(size: 40)) + .foregroundColor(DesignConstants.Colors.hustleGreen) + Text("Profile coming soon") + .font(DesignConstants.Fonts.title2) + .foregroundColor(DesignConstants.Colors.black) + } + + Button { + store.send(.logoutTapped) + } label: { + Text("Log out") + .font(DesignConstants.Fonts.title3Bold) + .foregroundColor(DesignConstants.Colors.white) + .frame(maxWidth: 220) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(DesignConstants.Colors.hustleGreen) + ) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(DesignConstants.Colors.white) + } +} + +#Preview { + ProfileView( + store: Store( + initialState: ProfileFeature.State(), + reducer: { ProfileFeature() } + ) + ) +} diff --git a/Hustle/Features/Search/SearchFeature.swift b/Hustle/Features/Search/SearchFeature.swift new file mode 100644 index 0000000..887a903 --- /dev/null +++ b/Hustle/Features/Search/SearchFeature.swift @@ -0,0 +1,83 @@ +// +// SearchFeature.swift +// Hustle +// +// Created by Jay on 12/4/25. +// + +import ComposableArchitecture +import Foundation + +@Reducer +struct SearchFeature { + @ObservableState + struct State: Equatable { + var query = "" + var isShowingResults = false + var recentQueries: [String] = SampleData.searchRecentQueries + var recentServices: [Service] = SampleData.searchRecentServices + var searchResults: [Service] = [] + var hasSearched = false + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case searchSubmitted + case resetSearch + } + + var body: some ReducerOf { + BindingReducer() + + Reduce { state, action in + switch action { + case .binding: + return .none + + case .resetSearch: + state.query = "" + state.hasSearched = false + state.searchResults = [] + state.isShowingResults = false + return .none + + case .searchSubmitted: + let trimmed = state.query.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + state.hasSearched = false + state.searchResults = [] + state.isShowingResults = false + return .none + } + + // Update recent queries (dedup, most recent first, max 10) + state.recentQueries.removeAll { $0.caseInsensitiveCompare(trimmed) == .orderedSame } + state.recentQueries.insert(trimmed, at: 0) + if state.recentQueries.count > 10 { + state.recentQueries = Array(state.recentQueries.prefix(10)) + } + + // Filter sample services by words contained in description or category + let tokens = trimmed + .lowercased() + .split(whereSeparator: \.isWhitespace) + + state.searchResults = SampleData.services.filter { service in + let description = service.description.lowercased() + let category = service.category.lowercased() + return tokens.allSatisfy { token in + let bare = token.hasSuffix("s") ? String(token.dropLast()) : String(token) + return description.contains(token) + || description.contains(bare) + || category.contains(token) + || category.contains(bare) + } + } + + state.hasSearched = true + state.isShowingResults = true + return .none + } + } + } +} diff --git a/Hustle/Features/Search/SearchResultsView.swift b/Hustle/Features/Search/SearchResultsView.swift new file mode 100644 index 0000000..3d0ddf9 --- /dev/null +++ b/Hustle/Features/Search/SearchResultsView.swift @@ -0,0 +1,133 @@ +// +// SearchResultsView.swift +// Hustle +// +// Created by Jay on 12/5/25. +// + +import ComposableArchitecture +import SwiftUI + +struct SearchResultsView: View { + @Bindable var store: StoreOf + @FocusState private var isSearchFieldFocused: Bool + + var body: some View { + VStack(spacing: 0) { + + header + .padding(.horizontal, 16) + .padding(.top, 12) + .padding(.bottom, 16) + + if store.searchResults.isEmpty { + noResultsView + } else { + ScrollView(.vertical, showsIndicators: false) { + FilterChipsRow(filters: ["Price", "Location", "Ratings"], includeFilterIcon: true) + .padding(.horizontal, 16) + + ServiceResultsList(services: store.searchResults, imageHeight: 240) + .padding(.horizontal, 16) + .padding(.bottom, 32) + } + .scrollDismissesKeyboard(.interactively) + } + } + .background(DesignConstants.Colors.white.ignoresSafeArea()) + .toolbar(.hidden, for: .navigationBar) + .onAppear { + isSearchFieldFocused = false + } + } + + private var header: some View { + HStack(spacing: 12) { + Button { + store.send(.binding(.set(\.isShowingResults, false))) + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(DesignConstants.Colors.black) + } + + TextField( + "", + text: $store.query, + prompt: Text("Search services") + .foregroundStyle(DesignConstants.Colors.wash) + ) + .onSubmit { + store.send(.searchSubmitted) + } + .focused($isSearchFieldFocused) + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(DesignConstants.Colors.shadedGray) + ) + .foregroundColor(DesignConstants.Colors.black) + .tint(DesignConstants.Colors.black) + .submitLabel(.search) + } + .frame(height: 28) + } + + private var noResultsView: some View { + VStack(alignment: .center, spacing: 16) { // Added spacing for better layout + Spacer() + + Image("SadFace") + .font(.system(size: 60)) // Made icon bigger + + VStack(spacing: 8) { + Text("No Results Found") + .font(DesignConstants.Fonts.h2Italic) + .foregroundColor(DesignConstants.Colors.hustleGreen) + Text("No results found. Please try again.") + .font(DesignConstants.Fonts.body2) + .foregroundColor(DesignConstants.Colors.black) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + } + + Button { + store.send(.resetSearch) + } label: { + Text("Go Back") + .font(DesignConstants.Fonts.h3) + .foregroundColor(DesignConstants.Colors.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background( + Capsule() + .fill(DesignConstants.Colors.hustleGreen) + ) + } + .padding(.horizontal, 32) + .padding(.top, 16) + + Spacer() + + Spacer().frame(height: 50) + } + .frame(maxWidth: .infinity) + } +} + +#Preview { + NavigationStack { + SearchResultsView( + store: Store( + initialState: SearchFeature.State( + query: "photos", + isShowingResults: true, + searchResults: SampleData.searchResults, + hasSearched: true + ), + reducer: { SearchFeature() } + ) + ) + } +} diff --git a/Hustle/Features/Search/SearchView.swift b/Hustle/Features/Search/SearchView.swift new file mode 100644 index 0000000..1d09a9e --- /dev/null +++ b/Hustle/Features/Search/SearchView.swift @@ -0,0 +1,137 @@ +// +// SearchView.swift +// Hustle +// +// Created by Jay on 12/4/25. +// + +import ComposableArchitecture +import SwiftUI + +struct SearchView: View { + @Environment(\.dismiss) private var dismiss + @Bindable var store: StoreOf + @FocusState private var isSearchFieldFocused: Bool + + var body: some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(alignment: .leading, spacing: 16) { + header + .padding(.top, 20) + + recentSection + + Text("Recently viewed") + .font(DesignConstants.Fonts.h3) + .foregroundColor(DesignConstants.Colors.black) + + recentServices + } + .padding(.horizontal, 16) + .padding(.bottom, 32) + } + .scrollDismissesKeyboard(.interactively) + .background(DesignConstants.Colors.white.ignoresSafeArea()) + .toolbar(.hidden, for: .navigationBar) + .onAppear { + isSearchFieldFocused = true + } + .onDisappear { + isSearchFieldFocused = false + } + .navigationDestination(isPresented: $store.isShowingResults) { + SearchResultsView(store: store) + } + } + + private var header: some View { + HStack(spacing: 12) { + Button(action: { dismiss() }) { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(DesignConstants.Colors.black) + } + + TextField( + "", + text: $store.query, + prompt: Text("Search services") + .foregroundStyle(DesignConstants.Colors.wash) + ) + .onSubmit { + store.send(.searchSubmitted) + } + .focused($isSearchFieldFocused) + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(DesignConstants.Colors.shadedGray) + ) + .foregroundColor(DesignConstants.Colors.black) + .tint(DesignConstants.Colors.black) + .submitLabel(.search) + } + .frame(height: 28) + } + + private var recentSection: some View { + VStack(alignment: .leading) { + Text("Recent") + .font(DesignConstants.Fonts.h3) + .foregroundColor(DesignConstants.Colors.black) + + let limitedQueries = Array(store.recentQueries.prefix(10)) + VStack(spacing: 0) { + ForEach(Array(limitedQueries.enumerated()), id: \.offset) { index, query in + Button { + store.query = query + store.send(.searchSubmitted) + } label: { + HStack(spacing: 12) { + Image(systemName: "clock.arrow.circlepath") + .foregroundColor(DesignConstants.Colors.hustleGreen) + .font(.system(size: 18, weight: .regular)) + + Text(query) + .font(DesignConstants.Fonts.body2) + .foregroundColor(DesignConstants.Colors.black) + + Spacer() + } + .padding(.vertical, 12) + } + .buttonStyle(.plain) + + if index < limitedQueries.count - 1 { + Divider() + .background(DesignConstants.Colors.stroke) + } + } + } + } + } + + private var recentServices: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(Array(store.recentServices.prefix(5))) { service in + ServiceCard( + service: service + ) + } + } + } + } +} + +#Preview { + NavigationStack { + SearchView( + store: Store( + initialState: SearchFeature.State(), + reducer: { SearchFeature() } + ) + ) + } +} diff --git a/Hustle/Features/Tagged/TaggedView.swift b/Hustle/Features/Tagged/TaggedView.swift new file mode 100644 index 0000000..acbb83f --- /dev/null +++ b/Hustle/Features/Tagged/TaggedView.swift @@ -0,0 +1,53 @@ +// +// TaggedView.swift +// Hustle +// +// Created by Jay on 12/6/25. +// + +import SwiftUI + +struct TaggedView: View { + let title: String + let services: [Service] + let onBack: () -> Void + + var body: some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(alignment: .leading, spacing: 16) { + header + FilterChipsRow(filters: ["Price", "Location", "Ratings"]) + ServiceResultsList(services: services, imageHeight: 240) + } + .padding(.horizontal, 16) + .padding(.bottom, 32) + } + .background(DesignConstants.Colors.white.ignoresSafeArea()) + .toolbar(.hidden, for: .navigationBar) + } + + private var header: some View { + Text(title) + .font(DesignConstants.Fonts.h3) + .foregroundColor(DesignConstants.Colors.black) + .frame(maxWidth: .infinity) + .overlay(alignment: .leading) { + Button(action: onBack) { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(DesignConstants.Colors.black) + .padding(.leading, 16) + .contentShape(Rectangle()) + } + } + .padding(.top, 8) + } +} + +#Preview { + TaggedView( + title: "Lessons", + services: SampleData.services, + onBack: {} + ) +} diff --git a/Hustle/HustleApp.swift b/Hustle/HustleApp.swift index 037b3f5..c2469d3 100644 --- a/Hustle/HustleApp.swift +++ b/Hustle/HustleApp.swift @@ -18,7 +18,7 @@ struct HustleApp: App { var body: some Scene { WindowGroup { - MainView(store: store) + AppView(store: store) } } } diff --git a/Hustle/MainContent/MainContentFeature.swift b/Hustle/MainContent/MainContentFeature.swift new file mode 100644 index 0000000..1e94a42 --- /dev/null +++ b/Hustle/MainContent/MainContentFeature.swift @@ -0,0 +1,82 @@ +// +// MainContentFeature.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import ComposableArchitecture + +@Reducer +struct MainContentFeature { + + @ObservableState + struct State: Equatable { + var selectedTab: Tab = .home + var home = HomeFeature.State() + var learn = LearnFeature.State() + var messages = MessagesFeature.State() + var profile = ProfileFeature.State() + } + + enum Tab: Hashable { + case home + case learn + case messages + case profile + + var title: String { + switch self { + case .home: return "Home" + case .learn: return "Learn" + case .messages: return "Messages" + case .profile: return "Profile" + } + } + + var systemImage: String { + switch self { + case .home: return "house.fill" + case .learn: return "graduationcap.fill" + case .messages: return "bubble.left.and.bubble.right.fill" + case .profile: return "person.crop.circle.fill" + } + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case home(HomeFeature.Action) + case learn(LearnFeature.Action) + case messages(MessagesFeature.Action) + case profile(ProfileFeature.Action) + case tabTapped(Tab) + } + + var body: some ReducerOf { + BindingReducer() + + Scope(state: \.home, action: \.home) { + HomeFeature() + } + Scope(state: \.learn, action: \.learn) { + LearnFeature() + } + Scope(state: \.messages, action: \.messages) { + MessagesFeature() + } + Scope(state: \.profile, action: \.profile) { + ProfileFeature() + } + + Reduce { state, action in + switch action { + case let .tabTapped(tab): + state.selectedTab = tab + return .none + case .binding, .home, .learn, .messages, .profile: + return .none + } + } + } +} diff --git a/Hustle/MainContent/MainContentView.swift b/Hustle/MainContent/MainContentView.swift new file mode 100644 index 0000000..8a974da --- /dev/null +++ b/Hustle/MainContent/MainContentView.swift @@ -0,0 +1,51 @@ +// +// MainView.swift +// Center +// +// Created by Jidong Zheng on 9/26/25. +// + +import ComposableArchitecture +import SwiftUI + +struct MainContentView: View { + @Bindable var store: StoreOf + + var body: some View { + TabView(selection: $store.selectedTab) { + HomeView(store: store.scope(state: \.home, action: \.home)) + .tabItem { + Label(MainContentFeature.Tab.home.title, systemImage: MainContentFeature.Tab.home.systemImage) + } + .tag(MainContentFeature.Tab.home) + + LearnView(store: store.scope(state: \.learn, action: \.learn)) + .tabItem { + Label(MainContentFeature.Tab.learn.title, systemImage: MainContentFeature.Tab.learn.systemImage) + } + .tag(MainContentFeature.Tab.learn) + + MessagesView(store: store.scope(state: \.messages, action: \.messages)) + .tabItem { + Label(MainContentFeature.Tab.messages.title, systemImage: MainContentFeature.Tab.messages.systemImage) + } + .tag(MainContentFeature.Tab.messages) + + ProfileView(store: store.scope(state: \.profile, action: \.profile)) + .tabItem { + Label(MainContentFeature.Tab.profile.title, systemImage: MainContentFeature.Tab.profile.systemImage) + } + .tag(MainContentFeature.Tab.profile) + } + .accentColor(DesignConstants.Colors.hustleGreen) + } +} + +#Preview { + MainContentView( + store: Store( + initialState: MainContentFeature.State(), + reducer: { MainContentFeature() } + ) + ) +} diff --git a/Hustle/Models/Category.swift b/Hustle/Models/Category.swift new file mode 100644 index 0000000..3e858ec --- /dev/null +++ b/Hustle/Models/Category.swift @@ -0,0 +1,15 @@ +// +// File.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import Foundation + +struct Category: Equatable, Identifiable { + let id: String + let title: String + let systemImage: String // Assuming backend sends an icon name, or you map it locally +} + diff --git a/Hustle/Models/SampleData.swift b/Hustle/Models/SampleData.swift new file mode 100644 index 0000000..5328bb5 --- /dev/null +++ b/Hustle/Models/SampleData.swift @@ -0,0 +1,91 @@ +// +// SampleData.swift +// Hustle +// +// Created by Jay on 12/5/25. +// + +import Foundation + +enum SampleData { + + static let categories: [Category] = [ + Category(id: "lessons", title: "Lessons", systemImage: "book.closed.fill"), + Category(id: "photo", title: "Photo", systemImage: "camera.fill"), + Category(id: "beauty", title: "Beauty", systemImage: "sparkles"), + Category(id: "professional", title: "Professional Services", systemImage: "briefcase.fill"), + Category(id: "homeCare", title: "Home Care", systemImage: "house.fill") + ] + + static let services: [Service] = [ + Service(id: UUID(), category: "photo", providerPFP: "Jay", providerName: "Jay Z.", serviceImage: "GradPhoto", description: "Dreamy fall grad photo session", price: "From $67/hour", rating: 4.1), + Service(id: UUID(), category: "lessons", providerPFP: "Jay", providerName: "Jay Z.", serviceImage: "MathTutor", description: "Calculus tutoring", price: "From $30/hour", rating: 4.8), + Service(id: UUID(), category: "beauty", providerPFP: "lauren", providerName: "Caroline S", serviceImage: "HairStyling", description: "Hair styling for events", price: "From $67/hour", rating: 4.6), + Service(id: UUID(), category: "beauty", providerPFP: "lauren", providerName: "Mia Nguyen", serviceImage: "NailWork", description: "Gel manicure and nail art", price: "From $40/session", rating: 4.8), + Service(id: UUID(), category: "beauty", providerPFP: "lauren", providerName: "Alex Fade", serviceImage: "Haircut", description: "Campus fades and haircuts", price: "From $35/service", rating: 4.7), + Service(id: UUID(), category: "photo", providerPFP: "lauren", providerName: "Casey Lens", serviceImage: "GradPhotoGirls", description: "Portrait and event photography", price: "From $80/hour", rating: 4.9), + Service(id: UUID(), category: "lessons", providerPFP: "lauren", providerName: "Devon Codes", serviceImage: "ProgrammingTutor", description: "Intro to programming tutoring", price: "From $45/hour", rating: 4.8), + Service(id: UUID(), category: "beauty", providerPFP: "Jay", providerName: "Jay Z.", serviceImage: "Makeup", description: "Event-ready makeup looks", price: "From $90/session", rating: 4.6) + ] + + static let sections: [ServiceSection] = [ + ServiceSection(title: "Popular right now", items: Array(services.prefix(3))), + ServiceSection(title: "New on Hustle", items: Array(services.suffix(3))), + ServiceSection(title: "Service near your", items: Array(services.suffix(3))), + ServiceSection(title: "Available this week", items: Array(services.suffix(3))) + ] + + static let searchRecentQueries: [String] = [ + "nails", + "photos", + "haircuts", + "plumbing", + "programming", + "tutoring", + "makeup", + "videography", + "editing", + "moving", + "painting" + ] + + static let searchRecentServices: [Service] = { + var services = SampleData.services + services.append( + Service( + id: UUID(), + category: "photo", + providerPFP: "lauren", + providerName: "Alex Kim", + serviceImage: "GradPhoto", + description: "Event videography and edits", + price: "From $120/hour", + rating: 4.7 + ) + ) + return services + }() + + static let searchResults: [Service] = [ + Service( + id: UUID(), + category: "photo", + providerPFP: "lauren", + providerName: "Jennifer Gu", + serviceImage: "GradPhoto", + description: "class of '26 photoshoots!", + price: "From $67/hour", + rating: 4.1 + ), + Service( + id: UUID(), + category: "photo", + providerPFP: "lauren", + providerName: "Jennifer Gu", + serviceImage: "GradPhoto", + description: "class of '26 photoshoots!", + price: "From $67/hour", + rating: 4.1 + ) + ] +} diff --git a/Hustle/Models/Service.swift b/Hustle/Models/Service.swift new file mode 100644 index 0000000..89c91b3 --- /dev/null +++ b/Hustle/Models/Service.swift @@ -0,0 +1,25 @@ +// +// Service.swift +// Hustle +// +// Created by Jay on 11/30/25. +// + +import Foundation + +struct Service: Equatable, Identifiable, Codable { + let id: UUID + let category: String + let providerPFP: String + let providerName: String + let serviceImage: String + let description: String + let price: String + let rating: Double +} + +struct ServiceSection: Identifiable, Equatable { + let id = UUID() + let title: String + let items: [Service] +} diff --git a/Hustle/Resource/Assets.xcassets/Filter.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/Filter.imageset/Contents.json new file mode 100644 index 0000000..8077807 --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/Filter.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "filter_list.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/Filter.imageset/filter_list.svg b/Hustle/Resource/Assets.xcassets/Filter.imageset/filter_list.svg new file mode 100644 index 0000000..ec259f7 --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/Filter.imageset/filter_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Contents.json new file mode 100644 index 0000000..414d177 --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Rectangle 122.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Rectangle 122-2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Rectangle 122-3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Rectangle 122-2.png b/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Rectangle 122-2.png new file mode 100644 index 0000000..3bd39ca Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Rectangle 122-2.png differ diff --git a/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Rectangle 122-3.png b/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Rectangle 122-3.png new file mode 100644 index 0000000..3bb626a Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Rectangle 122-3.png differ diff --git a/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Rectangle 122.png b/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Rectangle 122.png new file mode 100644 index 0000000..cd3a92b Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/GradPhoto.imageset/Rectangle 122.png differ diff --git a/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Contents.json new file mode 100644 index 0000000..fd1a67c --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Rectangle 122-4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Rectangle 122-5.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Rectangle 122-6.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Rectangle 122-4.png b/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Rectangle 122-4.png new file mode 100644 index 0000000..c124e20 Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Rectangle 122-4.png differ diff --git a/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Rectangle 122-5.png b/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Rectangle 122-5.png new file mode 100644 index 0000000..c31905d Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Rectangle 122-5.png differ diff --git a/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Rectangle 122-6.png b/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Rectangle 122-6.png new file mode 100644 index 0000000..37aa32d Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/GradPhotoGirls.imageset/Rectangle 122-6.png differ diff --git a/Hustle/Resource/Assets.xcassets/HairStyling.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/HairStyling.imageset/Contents.json new file mode 100644 index 0000000..605293f --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/HairStyling.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "HairStyle.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/HairStyling.imageset/HairStyle.png b/Hustle/Resource/Assets.xcassets/HairStyling.imageset/HairStyle.png new file mode 100644 index 0000000..622458b Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/HairStyling.imageset/HairStyle.png differ diff --git a/Hustle/Resource/Assets.xcassets/Haircut.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/Haircut.imageset/Contents.json new file mode 100644 index 0000000..0bbfb3a --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/Haircut.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Haircut.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/Haircut.imageset/Haircut.png b/Hustle/Resource/Assets.xcassets/Haircut.imageset/Haircut.png new file mode 100644 index 0000000..e34cb3e Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/Haircut.imageset/Haircut.png differ diff --git a/Hustle/Resource/Assets.xcassets/Jay.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/Jay.imageset/Contents.json new file mode 100644 index 0000000..49105c2 --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/Jay.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "HeadShot.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/Jay.imageset/HeadShot.png b/Hustle/Resource/Assets.xcassets/Jay.imageset/HeadShot.png new file mode 100644 index 0000000..1a5b29f Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/Jay.imageset/HeadShot.png differ diff --git a/Hustle/Resource/Assets.xcassets/Makeup.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/Makeup.imageset/Contents.json new file mode 100644 index 0000000..75d9ccb --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/Makeup.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Makeup.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/Makeup.imageset/Makeup.png b/Hustle/Resource/Assets.xcassets/Makeup.imageset/Makeup.png new file mode 100644 index 0000000..d877c9a Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/Makeup.imageset/Makeup.png differ diff --git a/Hustle/Resource/Assets.xcassets/MathTutor.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/MathTutor.imageset/Contents.json new file mode 100644 index 0000000..d0ba847 --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/MathTutor.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Tutor.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/MathTutor.imageset/Tutor.png b/Hustle/Resource/Assets.xcassets/MathTutor.imageset/Tutor.png new file mode 100644 index 0000000..45b8781 Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/MathTutor.imageset/Tutor.png differ diff --git a/Hustle/Resource/Assets.xcassets/NailWork.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/NailWork.imageset/Contents.json new file mode 100644 index 0000000..fd1e052 --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/NailWork.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "NailWork.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/NailWork.imageset/NailWork.png b/Hustle/Resource/Assets.xcassets/NailWork.imageset/NailWork.png new file mode 100644 index 0000000..1ce9b9e Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/NailWork.imageset/NailWork.png differ diff --git a/Hustle/Resource/Assets.xcassets/ProgrammingTutor.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/ProgrammingTutor.imageset/Contents.json new file mode 100644 index 0000000..17f6fd9 --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/ProgrammingTutor.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/ProgrammingTutor.imageset/Image.png b/Hustle/Resource/Assets.xcassets/ProgrammingTutor.imageset/Image.png new file mode 100644 index 0000000..8bb178a Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/ProgrammingTutor.imageset/Image.png differ diff --git a/Hustle/Resource/Assets.xcassets/SadFace.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/SadFace.imageset/Contents.json new file mode 100644 index 0000000..716d6d9 --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/SadFace.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Sad Face.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/SadFace.imageset/Sad Face.svg b/Hustle/Resource/Assets.xcassets/SadFace.imageset/Sad Face.svg new file mode 100644 index 0000000..aedd0bc --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/SadFace.imageset/Sad Face.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hustle/Resource/Assets.xcassets/lauren.imageset/Contents.json b/Hustle/Resource/Assets.xcassets/lauren.imageset/Contents.json new file mode 100644 index 0000000..fa9ee7a --- /dev/null +++ b/Hustle/Resource/Assets.xcassets/lauren.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Ellipse 5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hustle/Resource/Assets.xcassets/lauren.imageset/Ellipse 5.png b/Hustle/Resource/Assets.xcassets/lauren.imageset/Ellipse 5.png new file mode 100644 index 0000000..8899f0c Binary files /dev/null and b/Hustle/Resource/Assets.xcassets/lauren.imageset/Ellipse 5.png differ diff --git a/Hustle/Utils/DesignConstants.swift b/Hustle/Utils/DesignConstants.swift index 5584afd..3ae96b0 100644 --- a/Hustle/Utils/DesignConstants.swift +++ b/Hustle/Utils/DesignConstants.swift @@ -12,14 +12,15 @@ struct DesignConstants { enum Colors { // Defaults - static let primary = Color(hex: 0x222222) + static let black = Color(hex: 0x000000) static let tertiary = Color(hex: 0x2D2D2D) - static let secondary = Color(hex: 0x7D8288) + static let secondaryGrey = Color(hex: 0x636161) static let iconInactive = Color(hex: 0xE1E1E1) static let stroke = Color(hex: 0xE1E1E1) - static let wash = Color(hex: 0xF4F4F4) + static let wash = Color(hex: 0x958F8F) static let white = Color(hex: 0xFFFFFF) static let tint = Color(hex: 0xE1E1E1) + static let shadedGray = Color(hex: 0xD6D6D6) // Hustle static let hustleGreen = Color(hex: 0x004346) @@ -30,7 +31,9 @@ struct DesignConstants { //Header static let h1 = Font.custom("InstrumentSans-Medium", size: 36) + static let h1Italic = Font.custom("InstrumentSans-BoldItalic", size: 36).italic() static let h2 = Font.custom("InstrumentSans-Medium", size: 24) + static let h2Italic = Font.custom("InstrumentSans-BoldItalic", size: 24).italic() static let h3 = Font.custom("InstrumentSans-Bold", size: 18) //Body @@ -42,6 +45,7 @@ struct DesignConstants { static let title1 = Font.custom("HelveticaNeue", size: 24) static let title2 = Font.custom("HelveticaNeue-Medium", size: 16) static let title3 = Font.custom("HelveticaNeue-Medium", size: 14) + static let title3Bold = Font.custom("HelveticaNeue-Bold", size: 14) static let title4 = Font.custom("HelveticaNeue", size: 14) static let subtitle1 = Font.custom("HelveticaNeue", size: 12) }