diff --git a/AppPackage/Package.swift b/AppPackage/Package.swift index db555f0..9fd1a9c 100644 --- a/AppPackage/Package.swift +++ b/AppPackage/Package.swift @@ -78,7 +78,10 @@ let package = Package( .target( name: "TimeTableFeature", dependencies: [ - .product(name: "ComposableArchitecture", package: "swift-composable-architecture") + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + "APIClient", + "APIClientLive", + "DesignSystem" ] ), .target( diff --git a/AppPackage/Sources/Entity/Model.swift b/AppPackage/Sources/Entity/Model.swift index 1b6279a..e6fa12b 100644 --- a/AppPackage/Sources/Entity/Model.swift +++ b/AppPackage/Sources/Entity/Model.swift @@ -30,31 +30,31 @@ public struct UpdateUsernameRequest: Encodable, Equatable { public struct CreatePromisingRequest: Encodable, Equatable { private enum CodingKeys: String, CodingKey { case name = "promisingName" - case startDate = "minTime" - case endDate = "maxTime" + case minTime + case maxTime case categoryID = "categoryId" case availableDates case place = "placeName" } public let name: String - public let startDate: Date - public let endDate: Date + public let minTime: String + public let maxTime: String public let categoryID: Int - public let availableDates: [Date] + public let availableDates: [String] public let place: String public init( name: String, - startDate: Date, - endDate: Date, + minTime: String, + maxTime: String, categoryID: Int, - availableDates: [Date], + availableDates: [String], place: String ) { self.name = name - self.startDate = startDate - self.endDate = endDate + self.minTime = minTime + self.maxTime = maxTime self.categoryID = categoryID self.availableDates = availableDates self.place = place @@ -74,29 +74,21 @@ public struct CreatePromisingResponse: Decodable, Equatable { } public struct PromisingSessionResponse: Decodable, Equatable { - private enum CodingKeys: String, CodingKey { - case startDate = "minTime" - case endDate = "maxTime" - case totalCount - case unit - case availableDates - } - - public let startDate: Date - public let endDate: Date + public let minTime: String + public let maxTime: String public let totalCount: Int - public let unit: Int - public let availableDates: [Date] + public let unit: Double + public let availableDates: [String] public init( - startDate: Date, - endDate: Date, + minTime: String, + maxTime: String, totalCount: Int, - unit: Int, - availableDates: [Date] + unit: Double, + availableDates: [String] ) { - self.startDate = startDate - self.endDate = endDate + self.minTime = minTime + self.maxTime = maxTime self.totalCount = totalCount self.unit = unit self.availableDates = availableDates @@ -172,12 +164,28 @@ public struct PromisingSession: Codable, Equatable { } public struct PromisingTime: Codable, Equatable { - public let unit: Int - public let timeTable: TimeTable + public let unit: Double + public let timeTable: [TimeTable] public struct TimeTable: Codable, Equatable { - public let date: Date + public let date: String public let times: [Bool] + + public init( + date: String, + times: [Bool] + ) { + self.date = date + self.times = times + } + } + + public init( + unit: Double, + timeTable: [TimeTable] + ) { + self.unit = unit + self.timeTable = timeTable } } diff --git a/AppPackage/Sources/HomeContainerFeature/HomeContainerCore.swift b/AppPackage/Sources/HomeContainerFeature/HomeContainerCore.swift index fede91f..5cfc90b 100644 --- a/AppPackage/Sources/HomeContainerFeature/HomeContainerCore.swift +++ b/AppPackage/Sources/HomeContainerFeature/HomeContainerCore.swift @@ -39,12 +39,12 @@ public struct HomeContainerCore: ReducerProtocol { } public enum DestinationState: Equatable { - case makePromise(MakePromiseState) + case makePromise(MakePromise.State) case promiseList(PromiseListCore.State) } public enum DestinationAction: Equatable { - case makePromise(MakePromiseAction) + case makePromise(MakePromise.Action) case promiseList(PromiseListCore.Action) } @@ -135,14 +135,8 @@ public struct HomeContainerCore: ReducerProtocol { Scope( state: /DestinationState.makePromise, action: /DestinationAction.makePromise, - child: { - Reduce( - makePromiseReducer, - environment: .init() - ) - } + child: MakePromise.init ) - Scope( state: /DestinationState.promiseList, action: /DestinationAction.promiseList, diff --git a/AppPackage/Sources/MakePromise/MakePromiseAction.swift b/AppPackage/Sources/MakePromise/MakePromiseAction.swift deleted file mode 100644 index 0756787..0000000 --- a/AppPackage/Sources/MakePromise/MakePromiseAction.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// File.swift -// -// -// Created by 한상준 on 2023/02/25. -// - -import CalendarFeature -import Foundation -import TimeTableFeature - -public enum MakePromiseAction: Equatable { - case dismiss - case nextButtonTapped - case backButtonTapped - - case selectTheme(SelectThemeAction) - case setNameAndPlace(SetNameAndPlaceAction) - case calendar(CalendarCore.Action) - case timeSelection(TimeSelection.Action) - case timeTable(TimeTableAction) -} diff --git a/AppPackage/Sources/MakePromise/MakePromiseState.swift b/AppPackage/Sources/MakePromise/MakePromiseState.swift deleted file mode 100644 index a754090..0000000 --- a/AppPackage/Sources/MakePromise/MakePromiseState.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// File.swift -// -// -// Created by 한상준 on 2023/02/25. -// - -import CalendarFeature -import ComposableArchitecture -import TimeTableFeature - -public struct MakePromiseState: Equatable { - var shouldShowBackButton: Bool - var steps: [Step] - var currentStep: Step? { - if index < steps.count { - return steps[index] - } - return nil - } - - var index: Int = 0 - - var selectTheme: SelectThemeState? { - get { - getSelectThemeState() - } - set { - setSelectThemeState(newValue) - } - } - - var setNameAndPlace: SetNameAndPlaceState? { - get { - steps.compactMap { step in - guard case let .setNameAndPlace(state) = step else { - return nil - } - return state - }.first - } - set { - guard let newState = newValue else { - return - } - guard let index = steps.firstIndex(where: { - guard case .setNameAndPlace = $0 else { - return false - } - return true - }) else { return } - steps[index] = .setNameAndPlace(newState) - } - } - - var timeSelection: TimeSelection.State? { - get { - steps.compactMap { step in - guard case let .timeSelection(state) = step else { - return nil - } - return state - }.first - } - set { - guard let newState = newValue else { - return - } - guard let index = steps.firstIndex(where: { - guard case .timeSelection = $0 else { - return false - } - return true - }) else { return } - steps[index] = .timeSelection(newState) - } - } - - var calendar: CalendarCore.State? { - get { - steps.compactMap { step in - guard case let .calendar(state) = step else { - return nil - } - return state - }.first - } - set { - guard let newState = newValue else { - return - } - guard let index = steps.firstIndex(where: { - guard case .calendar = $0 else { - return false - } - return true - }) else { return } - steps[index] = .calendar(newState) - } - } - - var timeTable: TimeTableState? { - get { - steps.compactMap { step in - guard case let .timeTable(state) = step else { - return nil - } - return state - }.first - } - set { - guard let newState = newValue else { - return - } - guard let index = steps.firstIndex(where: { - guard case .timeTable = $0 else { - return false - } - return true - }) else { return } - steps[index] = .timeTable(newState) - } - } - - public init( - shouldShowBackButton: Bool = false, - steps: [Step] = [ - .selectTheme(.init()), - .setNameAndPlace(.init()), - .timeSelection(.init(timeRange: .init(start: 9, end: 23))), - .calendar(.init()), - .timeTable(.mock) - ] - ) { - self.shouldShowBackButton = shouldShowBackButton - self.steps = steps - } - - public enum Step: Equatable { - case selectTheme(SelectThemeState) - case setNameAndPlace(SetNameAndPlaceState) - case calendar(CalendarCore.State) - case timeSelection(TimeSelection.State) - case timeTable(TimeTableState) - } - - var isNextButtonEnable: Bool { - guard let currentStep else { return false } - switch currentStep { - case let .selectTheme(selectTheme): - return selectTheme.selectedType != nil - case .setNameAndPlace: - return true - case let .calendar(calendar): - return calendar.selectedDates.count > 0 - case let .timeSelection(timeSelection): - return timeSelection.isTimeRangeValid - case let .timeTable(timeTable): - return timeTable.isTimeSelected - } - } - - mutating func moveNextStep() { - guard index + 1 < steps.count else { return } - index += 1 - } - - mutating func movePastStep() { - let newIndex = index - 1 - guard newIndex >= 0, newIndex < steps.count else { return } - index = newIndex - } - - mutating func updateBackButtonVisibleState() { - shouldShowBackButton = index > 0 - } - - func getSelectThemeState() -> SelectThemeState? { - steps - .compactMap { - guard case let .selectTheme(state) = $0 else { - return nil - } - return state - } - .first - } - - mutating func setSelectThemeState(_ newState: SelectThemeState?) { - guard let newState else { - return - } - steps[index] = Step.selectTheme(newState) - } - - mutating func fetchCurrentStep() {} -} diff --git a/AppPackage/Sources/MakePromise/MakePromiseStore.swift b/AppPackage/Sources/MakePromise/MakePromiseStore.swift index 0af0469..6ef0124 100644 --- a/AppPackage/Sources/MakePromise/MakePromiseStore.swift +++ b/AppPackage/Sources/MakePromise/MakePromiseStore.swift @@ -6,97 +6,399 @@ // Copyright © 2022 Team-Planz. All rights reserved. // +import APIClient import CalendarFeature import ComposableArchitecture +import Entity import Foundation import TimeTableFeature -public struct MakePromiseEnvironment { - public init() {} -} +public struct MakePromise: ReducerProtocol { + public struct State: Equatable { + @PresentationState var alert: AlertState? + var shouldShowBackButton: Bool + var steps: [Step] + var currentStep: Step? { + if index < steps.count { + return steps[index] + } + return nil + } -public let makePromiseReducer = AnyReducer.combine( - makePromiseSelectThemeReducer - .optional() - .pullback( - state: \.selectTheme, - action: /MakePromiseAction.selectTheme, - environment: { _ in SelectThemeEnvironment() } - ), - makePromiseSetNameAndPlaceReducer - .optional() - .pullback( - state: \.setNameAndPlace, - action: /MakePromiseAction.setNameAndPlace, - environment: { _ in SetNameAndPlaceEnvironment() } - ), - AnyReducer { _ in - CalendarCore() - } - .optional() - .pullback( - state: \.calendar, - action: /MakePromiseAction.calendar, - environment: { _ in } - ), - AnyReducer { _ in - TimeSelection() - } - .optional() - .pullback( - state: \.timeSelection, - action: /MakePromiseAction.timeSelection, - environment: { _ in } - ), - timeTableReducer - .optional() - .pullback( - state: \.timeTable, - action: /MakePromiseAction.timeTable, - environment: { _ in () } - ), - AnyReducer { state, action, _ in - switch action { - case .dismiss: - return .none - - case .nextButtonTapped: - if case let .timeSelection(timeSelection) = state.currentStep, - let startTime = timeSelection.startTime, - let endTime = timeSelection.endTime { - state.timeTable?.startTime = TimeInterval(startTime * 3600) - state.timeTable?.endTime = TimeInterval(endTime * 3600) - state.timeTable?.reload() + var index: Int = 0 + + var selectTheme: SelectTheme.State? { + get { + getSelectThemeState() + } + set { + setSelectThemeState(newValue) + } + } + + var setNameAndPlace: SetNameAndPlace.State? { + get { + steps.compactMap { step in + guard case let .setNameAndPlace(state) = step else { + return nil + } + return state + }.first + } + set { + guard let newState = newValue else { + return + } + guard let index = steps.firstIndex(where: { + guard case .setNameAndPlace = $0 else { + return false + } + return true + }) else { return } + steps[index] = .setNameAndPlace(newState) + } + } + + var timeSelection: TimeSelection.State? { + get { + steps.compactMap { step in + guard case let .timeSelection(state) = step else { + return nil + } + return state + }.first + } + set { + guard let newState = newValue else { + return + } + guard let index = steps.firstIndex(where: { + guard case .timeSelection = $0 else { + return false + } + return true + }) else { return } + steps[index] = .timeSelection(newState) + } + } + + var calendar: CalendarCore.State? { + get { + steps.compactMap { step in + guard case let .calendar(state) = step else { + return nil + } + return state + }.first + } + set { + guard let newState = newValue else { + return + } + guard let index = steps.firstIndex(where: { + guard case .calendar = $0 else { + return false + } + return true + }) else { return } + steps[index] = .calendar(newState) + } + } + + var timeTable: TimeTableFeature.TimeTable.State? { + get { + steps.compactMap { step in + guard case let .timeTable(state) = step else { + return nil + } + return state + }.first + } + set { + guard let newState = newValue else { + return + } + guard let index = steps.firstIndex(where: { + guard case .timeTable = $0 else { + return false + } + return true + }) else { return } + steps[index] = .timeTable(newState) + } + } + + public init( + alert: AlertState? = nil, + shouldShowBackButton: Bool = false, + steps: [Step] = [ + .selectTheme(.init()), + .setNameAndPlace(.init()), + .calendar(.init()), + .timeSelection(.init(timeRange: .init(start: 9, end: 23))), + .timeTable(.init()) + ] + ) { + self.alert = alert + self.shouldShowBackButton = shouldShowBackButton + self.steps = steps + } + + public enum Step: Equatable { + case selectTheme(SelectTheme.State) + case setNameAndPlace(SetNameAndPlace.State) + case calendar(CalendarCore.State) + case timeSelection(TimeSelection.State) + case timeTable(TimeTableFeature.TimeTable.State) + } + + var isNextButtonEnable: Bool { + guard let currentStep else { return false } + switch currentStep { + case let .selectTheme(selectTheme): + return selectTheme.selectThemeItems.contains(where: { $0.isSelected }) + case .setNameAndPlace: + return true + case let .calendar(calendar): + return calendar.selectedDates.count > 0 + case let .timeSelection(timeSelection): + return timeSelection.isTimeRangeValid + case let .timeTable(timeTable): + return timeTable.isTimeSelected } + } + + mutating func moveNextStep() { + guard index + 1 < steps.count else { return } + index += 1 + } + + mutating func movePastStep() { + let newIndex = index - 1 + guard newIndex >= 0, newIndex < steps.count else { return } + index = newIndex + } - if case let .calendar(calendarState) = state.currentStep { - let days: [TimeTableState.Day] = calendarState.selectedDates.map { - .init(date: $0) + mutating func updateBackButtonVisibleState() { + shouldShowBackButton = index > 0 + } + + func getSelectThemeState() -> SelectTheme.State? { + steps + .compactMap { + guard case let .selectTheme(state) = $0 else { + return nil + } + return state } - state.timeTable?.days = days + .first + } + + mutating func setSelectThemeState(_ newState: SelectTheme.State?) { + guard let newState else { + return } + steps[index] = Step.selectTheme(newState) + } + + mutating func fetchCurrentStep() {} + } + + @Dependency(\.apiClient) var apiClient + + public enum Action: Equatable { + case dismiss + case nextButtonTapped + case backButtonTapped + case temporaryPromisingResponse(TaskResult) + case updatePromiseTimeRespose(TaskResult) + case selectTheme(SelectTheme.Action) + case setNameAndPlace(SetNameAndPlace.Action) + case calendar(CalendarCore.Action) + case timeSelection(TimeSelection.Action) + case timeTable(TimeTableFeature.TimeTable.Action) + case alert(PresentationAction) + } - if state.isNextButtonEnable { + public enum AlertAction: Equatable { + case confirmButtonTapped + case dismiss + } + + public var body: some ReducerProtocolOf { + Reduce { state, action in + switch action { + case .dismiss: + return .none + + case .nextButtonTapped: + if case let .selectTheme(selectTheme) = state.currentStep { + state.setNameAndPlace?.id = selectTheme.selectThemeItems + .filter { $0.isSelected } + .first?.id ?? 0 + } + if case .timeSelection = state.currentStep { + state.alert = .init( + title: .init(Resource.string.alert), + message: .init(Resource.string.warning), + primaryButton: .cancel(.init(Resource.string.cancel), action: .send(.dismiss)), + secondaryButton: .default(.init(Resource.string.confirm), action: .send(.confirmButtonTapped)) + ) + return .none + } + + if case let .timeTable(timeTable) = state.currentStep { + guard let sessionID = timeTable.sessionID else { + return .none + } + return .task { [state = state] in + await .updatePromiseTimeRespose( + TaskResult { + try await apiClient.request( + route: .promising( + .respondTimeByHost( + sessionID, + .init( + unit: 0.5, + timeTable: state.timeTable?.days.enumerated().map { index, day in + .init( + date: dateFormatter.string(from: day.date), + times: state.timeTable?.timeCells[index].map { $0 == .selected } ?? [] + ) + } ?? [] + ) + ) + ), + as: UpdatePromiseTimeResponse.self + ) + } + ) + } + } + + if state.isNextButtonEnable { + state.moveNextStep() + state.updateBackButtonVisibleState() + } + return .none + case .backButtonTapped: + if case .timeTable = state.currentStep { + return .send(.dismiss) + } + state.movePastStep() + state.updateBackButtonVisibleState() + return .none + case .selectTheme: + return .none + case .setNameAndPlace: + return .none + case .calendar: + return .none + case .timeSelection: + return .none + case .timeTable: + return .none + case .alert(.presented(.confirmButtonTapped)): + guard case let .timeSelection(timeSelection) = state.currentStep else { + return .none + } + guard let categoryID = state.selectTheme?.selectThemeItems + .filter({ $0.isSelected }).first?.id + else { + return .none + } + guard let startTime: Date = .today(hour: timeSelection.startTime), + let endTime: Date = .today(hour: timeSelection.endTime) + else { + return .none + } + state.alert = nil + return .task { [state = state] in + await .temporaryPromisingResponse( + TaskResult { + try await apiClient.request( + route: .promising( + .create( + .init( + name: state.setNameAndPlace?.promiseName ?? "", + minTime: dateFormatter.string(from: startTime), + maxTime: dateFormatter.string(from: endTime), + categoryID: categoryID, + availableDates: state.calendar?.selectedDates + .map { dateFormatter.string(from: $0) } ?? [], + place: state.setNameAndPlace?.promisePlace ?? "" + ) + ) + ), + as: CreatePromisingResponse.self + ) + } + ) + } + case let .temporaryPromisingResponse(.success(response)): + state.timeTable?.sessionID = response.id state.moveNextStep() state.updateBackButtonVisibleState() + return .none + case .temporaryPromisingResponse(.failure): + return .none + case .updatePromiseTimeRespose: + return .none + case .alert: + return .none } - return .none - case .backButtonTapped: - state.movePastStep() - state.updateBackButtonVisibleState() - return .none - case let .selectTheme(.promiseTypeListItemTapped(promiseType)): - return .none - case let .setNameAndPlace(.filledPromiseName(name)): - return .none - case let .setNameAndPlace(.filledPromisePlace(place)): - return .none - case .calendar: - return .none - case .timeSelection: - return .none - case .timeTable: - return .none + } + .ifLet(\.selectTheme, action: /MakePromise.Action.selectTheme) { + SelectTheme() + } + .ifLet(\.setNameAndPlace, action: /MakePromise.Action.setNameAndPlace) { + SetNameAndPlace() + } + .ifLet(\.calendar, action: /MakePromise.Action.calendar) { + CalendarCore() + } + .ifLet(\.timeSelection, action: /MakePromise.Action.timeSelection) { + TimeSelection() + } + .ifLet(\.timeTable, action: /MakePromise.Action.timeTable) { + TimeTable() } } -) + + public init() {} +} + +private enum Resource { + enum string { + static let alert = "알림" + static let warning = "다음을 누르시면 이전 단계로 돌아갈 수 없습니다. 진행하시겠습니까?" + static let cancel = "취소" + static let confirm = "확인" + } +} + +private var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + return dateFormatter +}() + +private extension Date { + static func today( + hour: Int? = nil, + minute: Int? = nil, + second: Int? = nil + ) -> Self? { + let today: Date = .now + let calendar = Calendar.current + let dateComponents = DateComponents( + year: calendar.component(.year, from: today), + month: calendar.component(.month, from: today), + day: calendar.component(.day, from: today), + hour: hour, + minute: minute, + second: second + ) + return calendar.date(from: dateComponents) + } +} diff --git a/AppPackage/Sources/MakePromise/MakePromiseView.swift b/AppPackage/Sources/MakePromise/MakePromiseView.swift index f9d921d..e373877 100644 --- a/AppPackage/Sources/MakePromise/MakePromiseView.swift +++ b/AppPackage/Sources/MakePromise/MakePromiseView.swift @@ -13,7 +13,7 @@ import SwiftUI import TimeTableFeature public struct MakePromiseView: View { - let store: Store + let store: StoreOf public var body: some View { VStack { @@ -25,34 +25,34 @@ public struct MakePromiseView: View { } } - public init(store: Store) { + public init(store: StoreOf) { self.store = store } } struct PromiseContentView: View { - var store: Store + var store: StoreOf public var body: some View { IfLetStore(store.scope(state: \.currentStep)) { store in SwitchStore(store) { CaseLet( - state: /MakePromiseState.Step.selectTheme, - action: MakePromiseAction.selectTheme, + state: /MakePromise.State.Step.selectTheme, + action: MakePromise.Action.selectTheme, then: SelectThemeView.init ) CaseLet( - state: /MakePromiseState.Step.setNameAndPlace, - action: MakePromiseAction.setNameAndPlace, + state: /MakePromise.State.Step.setNameAndPlace, + action: MakePromise.Action.setNameAndPlace, then: NameAndPlaceView.init ) CaseLet( - state: /MakePromiseState.Step.timeSelection, - action: MakePromiseAction.timeSelection, + state: /MakePromise.State.Step.timeSelection, + action: MakePromise.Action.timeSelection, then: TimeSelectionView.init ) CaseLet( - state: /MakePromiseState.Step.calendar, - action: MakePromiseAction.calendar, + state: /MakePromise.State.Step.calendar, + action: MakePromise.Action.calendar, then: { CalendarView( type: .promise, @@ -61,13 +61,19 @@ struct PromiseContentView: View { } ) CaseLet( - state: /MakePromiseState.Step.timeTable, - action: MakePromiseAction.timeTable, + state: /MakePromise.State.Step.timeTable, + action: MakePromise.Action.timeTable, then: TimeTableView.init ) } } .frame(alignment: .top) .navigationBarBackButtonHidden() + .alert( + store: self.store.scope( + state: \.$alert, + action: MakePromise.Action.alert + ) + ) } } diff --git a/AppPackage/Sources/MakePromise/SubViews/MakePromiseBottomButton.swift b/AppPackage/Sources/MakePromise/SubViews/MakePromiseBottomButton.swift index 3c196de..09c6c25 100644 --- a/AppPackage/Sources/MakePromise/SubViews/MakePromiseBottomButton.swift +++ b/AppPackage/Sources/MakePromise/SubViews/MakePromiseBottomButton.swift @@ -11,7 +11,7 @@ import DesignSystem import SwiftUI public struct MakePromiseBottomButton: View { - var store: Store + var store: StoreOf public var body: some View { WithViewStore(self.store) { viewStore in HStack { @@ -34,7 +34,7 @@ public struct MakePromiseBottomButton: View { } public struct PromiseNextButton: View { - var store: Store + var store: StoreOf public var body: some View { WithViewStore(self.store) { viewStore in @@ -56,7 +56,7 @@ public struct PromiseNextButton: View { } public struct PromiseBackButton: View { - var store: Store + var store: StoreOf public var body: some View { WithViewStore(self.store) { viewStore in Button { diff --git a/AppPackage/Sources/MakePromise/SubViews/SelectThemeStore.swift b/AppPackage/Sources/MakePromise/SubViews/SelectThemeStore.swift index ec75ae7..a7b82e3 100644 --- a/AppPackage/Sources/MakePromise/SubViews/SelectThemeStore.swift +++ b/AppPackage/Sources/MakePromise/SubViews/SelectThemeStore.swift @@ -6,43 +6,67 @@ // Copyright © 2022 Team-Planz. All rights reserved. // +import APIClient +import APIClientLive import ComposableArchitecture +import Entity import Foundation import SwiftUI -public enum PromiseType: String, CaseIterable, Equatable { - case meal = "식사 약속" - case meeting = "미팅 약속" - case travel = "여행 약속" - case etc = "기타 약속" +public struct SelectTheme: ReducerProtocol { + public struct State: Equatable { + var selectThemeItems: IdentifiedArrayOf - var withEmoji: String { - switch self { - case .meal: return rawValue + " 🍚" - case .meeting: return rawValue + " ☕️" - case .travel: return rawValue + " ✈️" - case .etc: return rawValue + " ☺️" + public init( + selectThemeItems: IdentifiedArrayOf = [] + ) { + self.selectThemeItems = selectThemeItems } } -} -public struct SelectThemeState: Equatable { - var selectedType: PromiseType? - public init(selectedType: PromiseType? = nil) { - self.selectedType = selectedType + public enum Action: Equatable { + case task + case categoriesResponse(TaskResult<[Entity.Category]>) + case selectThemeItem(id: Int, action: SelectThemeItem.Action) } -} - -public enum SelectThemeAction: Equatable { - case promiseTypeListItemTapped(PromiseType) -} -public struct SelectThemeEnvironment {} + @Dependency(\.apiClient) var apiClient -public let makePromiseSelectThemeReducer = AnyReducer { state, action, _ in - switch action { - case let .promiseTypeListItemTapped(type): - state.selectedType = (state.selectedType == type) ? nil : type - return .none + public var body: some ReducerProtocolOf { + Reduce { state, action in + switch action { + case .task: + return .task { + await .categoriesResponse( + TaskResult { + try await apiClient.request( + route: .promising(.fetchCategories), + as: [Entity.Category].self + ) + } + ) + } + case let .categoriesResponse(.success(categories)): + state.selectThemeItems = .init( + uniqueElements: categories.map { + SelectThemeItem.State( + id: $0.id, + title: $0.keyword + ) + } + ) + return .none + case .categoriesResponse(.failure): + return .none + case let .selectThemeItem(id: id, action: .tapped): + state.selectThemeItems.map(\.id).forEach { + state.selectThemeItems[id: $0]?.isSelected = ($0 == id) + } + return .none + } + } + .forEach(\.selectThemeItems, action: /Action.selectThemeItem(id:action:)) { + SelectThemeItem() + } } } diff --git a/AppPackage/Sources/MakePromise/SubViews/SelectThemeView.swift b/AppPackage/Sources/MakePromise/SubViews/SelectThemeView.swift index b89184b..6afd751 100644 --- a/AppPackage/Sources/MakePromise/SubViews/SelectThemeView.swift +++ b/AppPackage/Sources/MakePromise/SubViews/SelectThemeView.swift @@ -11,24 +11,64 @@ import DesignSystem import SwiftUI public struct SelectThemeView: View { - var store: Store + var store: StoreOf let listItemEdgePadding = EdgeInsets(top: 6, leading: 20, bottom: 6, trailing: 20) public var body: some View { - VStack { - Spacer() - ForEach(PromiseType.allCases, id: \.self) { - SelectThemeItemView(promiseType: $0, store: store) - .padding(listItemEdgePadding) + WithViewStore(store) { viewStore in + VStack { + Spacer() + ForEachStore( + store.scope( + state: \.selectThemeItems, + action: SelectTheme.Action.selectThemeItem(id:action:) + ) + ) { + SelectThemeItemView(store: $0) + .padding(listItemEdgePadding) + } + Spacer() + } + .background(Color.white) + .task { + viewStore.send(.task) + } + } + } +} + +public struct SelectThemeItem: ReducerProtocol { + public struct State: Equatable, Identifiable { + public let id: Int + let title: String + public var isSelected: Bool + + init( + id: Int, + title: String, + isSelected: Bool = false + ) { + self.id = id + self.title = title + self.isSelected = isSelected + } + } + + public enum Action { + case tapped + } + + public var body: some ReducerProtocolOf { + Reduce { _, action in + switch action { + case .tapped: + return .none } - Spacer() } - .background(Color.white) } } public struct SelectThemeItemView: View { - var promiseType: PromiseType - var store: Store + var store: StoreOf let itemCornerRadius: CGFloat = 16 let checkMarkCircle = "checkmark.circle" let checkmarkCircleFill = "checkmark.circle.fill" @@ -36,27 +76,27 @@ public struct SelectThemeItemView: View { public var body: some View { WithViewStore(self.store) { viewStore in HStack { - Text(promiseType.withEmoji) + Text(viewStore.title) .foregroundColor( - viewStore.selectedType == promiseType ? + viewStore.isSelected ? PDS.COLOR.purple9.scale : PDS.COLOR.gray5.scale ) Spacer() Image(systemName: - viewStore.selectedType == promiseType ? + viewStore.isSelected ? checkmarkCircleFill : checkMarkCircle ) .foregroundColor( - viewStore.selectedType == promiseType ? + viewStore.isSelected ? PDS.COLOR.purple9.scale : PDS.COLOR.gray5.scale ) } .padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)) .background( - viewStore.selectedType == promiseType ? + viewStore.isSelected ? PDS.COLOR.purple9.scale.opacity(0.15) : PDS.COLOR.gray1.scale ) @@ -65,11 +105,11 @@ public struct SelectThemeItemView: View { RoundedRectangle(cornerRadius: itemCornerRadius) .stroke( PDS.COLOR.purple9.scale, - lineWidth: viewStore.selectedType == promiseType ? 0.7 : 0 + lineWidth: viewStore.isSelected ? 0.7 : 0 ) ) .onTapGesture { - viewStore.send(.promiseTypeListItemTapped(promiseType)) + viewStore.send(.tapped) } } } diff --git a/AppPackage/Sources/MakePromise/SubViews/SetNameAndPlaceStore.swift b/AppPackage/Sources/MakePromise/SubViews/SetNameAndPlaceStore.swift index 78c7680..69c4c86 100644 --- a/AppPackage/Sources/MakePromise/SubViews/SetNameAndPlaceStore.swift +++ b/AppPackage/Sources/MakePromise/SubViews/SetNameAndPlaceStore.swift @@ -6,59 +6,98 @@ // Copyright © 2022 Team-Planz. All rights reserved. // +import APIClient +import APIClientLive import ComposableArchitecture +import Entity import Foundation -public struct SetNameAndPlaceState: Equatable { - var maxCharacter = 10 - var promiseName: String = "" - var promisePlace: String = "" +public struct SetNameAndPlace: ReducerProtocol { + public struct State: Equatable { + public var id: Int + var maxCharacter: Int + var promiseNamePlaceholder: String + var promiseName: String + var promisePlace: String - var numberOfCharacterInNameText: Int { - if promiseName.count <= maxCharacter { - return promiseName.count - } else { - return maxCharacter + public init( + id: Int = .init(), + maxCharacter: Int = 10, + promiseNamePlaceholder: String = .init(), + promiseName: String = .init(), + promisePlace: String = .init() + ) { + self.id = id + self.maxCharacter = maxCharacter + self.promiseNamePlaceholder = promiseNamePlaceholder + self.promiseName = promiseName + self.promisePlace = promisePlace } - } - var numberOfCharacterInPlaceText: Int { - if promisePlace.count <= maxCharacter { - return promisePlace.count - } else { - return maxCharacter + var numberOfCharacterInNameText: Int { + if promiseName.count <= maxCharacter { + return promiseName.count + } else { + return maxCharacter + } } - } - var shouldShowNameTextCountWarning: Bool { - promiseName.count > maxCharacter - } + var numberOfCharacterInPlaceText: Int { + if promisePlace.count <= maxCharacter { + return promisePlace.count + } else { + return maxCharacter + } + } - var shouldShowPlaceTextCountWarning: Bool { - promisePlace.count > maxCharacter - } + var shouldShowNameTextCountWarning: Bool { + promiseName.count > maxCharacter + } - var isNextButtonEnable: Bool { - (numberOfCharacterInNameText > 0 && !shouldShowNameTextCountWarning) && (numberOfCharacterInPlaceText > 0 && !shouldShowPlaceTextCountWarning) - } + var shouldShowPlaceTextCountWarning: Bool { + promisePlace.count > maxCharacter + } - public init() {} -} + var isNextButtonEnable: Bool { + (numberOfCharacterInNameText > 0 && !shouldShowNameTextCountWarning) && (numberOfCharacterInPlaceText > 0 && !shouldShowPlaceTextCountWarning) + } + } -public enum SetNameAndPlaceAction: Equatable { - case filledPromiseName(String) - case filledPromisePlace(String) -} + public enum Action: Equatable { + case task + case placeHintResponse(TaskResult) + case filledPromiseName(String) + case filledPromisePlace(String) + } -public struct SetNameAndPlaceEnvironment {} + @Dependency(\.apiClient) var apiClient -public let makePromiseSetNameAndPlaceReducer = AnyReducer { state, action, _ in - switch action { - case let .filledPromiseName(name): - state.promiseName = name - case let .filledPromisePlace(place): - state.promisePlace = place + public var body: some ReducerProtocolOf { + Reduce { state, action in + switch action { + case .task: + return .task { [id = state.id] in + await .placeHintResponse( + TaskResult { + try await apiClient.request( + route: .promising(.randomName(id)), + as: CategoryName.self + ) + } + ) + } + case let .placeHintResponse(.success(placeHint)): + state.promiseNamePlaceholder = placeHint.name + return .none + case .placeHintResponse(.failure): + return .none + case let .filledPromiseName(name): + state.promiseName = name + return .none + case let .filledPromisePlace(place): + state.promisePlace = place + return .none + } + } } - - return .none } diff --git a/AppPackage/Sources/MakePromise/SubViews/SetNameAndPlaceView.swift b/AppPackage/Sources/MakePromise/SubViews/SetNameAndPlaceView.swift index 088b4c0..d27fcd1 100644 --- a/AppPackage/Sources/MakePromise/SubViews/SetNameAndPlaceView.swift +++ b/AppPackage/Sources/MakePromise/SubViews/SetNameAndPlaceView.swift @@ -13,7 +13,7 @@ import SwiftUI typealias NameAndPlaceView = SetNameAndPlaceView public struct SetNameAndPlaceView: View { - var store: Store + var store: StoreOf public enum TextFieldType { case name @@ -35,27 +35,32 @@ public struct SetNameAndPlaceView: View { } public var body: some View { - VStack { - Spacer() - VStack(spacing: 24) { - TextFieldWithTitleView( - type: .name, store: self.store - ) - TextFieldWithTitleView( - type: .place, store: self.store - ) + WithViewStore(store) { viewStore in + VStack { + Spacer() + VStack(spacing: 24) { + TextFieldWithTitleView( + type: .name, store: self.store + ) + TextFieldWithTitleView( + type: .place, store: self.store + ) + } + Spacer() + } + .task { + viewStore.send(.task) } - Spacer() } } } public struct TextFieldWithTitleView: View { var type: SetNameAndPlaceView.TextFieldType - var store: Store + var store: StoreOf - @ObservedObject var viewStore: ViewStore - init(type: SetNameAndPlaceView.TextFieldType, store: Store) { + @ObservedObject var viewStore: ViewStore + init(type: SetNameAndPlaceView.TextFieldType, store: StoreOf) { self.type = type self.store = store viewStore = ViewStore( @@ -67,27 +72,29 @@ public struct TextFieldWithTitleView: View { struct ViewState: Equatable { let showWarningMessage: Bool + let placeholder: String let textFieldText: String let numberOfCharacter: Int let maxNumberOfCharacter: Int - init(_ type: SetNameAndPlaceView.TextFieldType, state: SetNameAndPlaceState) { + init(_ type: SetNameAndPlaceView.TextFieldType, state: SetNameAndPlace.State) { switch type { case .name: showWarningMessage = state.shouldShowNameTextCountWarning textFieldText = state.promiseName numberOfCharacter = state.numberOfCharacterInNameText + placeholder = state.promiseNamePlaceholder case .place: showWarningMessage = state.shouldShowPlaceTextCountWarning textFieldText = state.promisePlace numberOfCharacter = state.numberOfCharacterInPlaceText + placeholder = .init() } - maxNumberOfCharacter = state.maxCharacter } } - typealias SetNameAndPlaceStore = ViewStore + typealias SetNameAndPlaceStore = ViewStoreOf public var body: some View { HStack { Spacer(minLength: 20) @@ -97,7 +104,7 @@ public struct TextFieldWithTitleView: View { Spacer() } TextField( - type.placeHolder, + viewStore.placeholder, text: viewStore.binding( get: { $0.textFieldText }, send: { type == .name ? .filledPromiseName($0) : .filledPromisePlace($0) } @@ -125,12 +132,12 @@ public struct TextFieldWithTitleView: View { } } - func getBorderColor(_ viewStore: ViewStore, type _: SetNameAndPlaceView.TextFieldType) -> Color { + func getBorderColor(_ viewStore: ViewStore, type _: SetNameAndPlaceView.TextFieldType) -> Color { return viewStore.showWarningMessage ? PDS.COLOR.scarlet1.scale : PDS.COLOR.purple9.scale } - func getTextCountColor(_ viewStore: ViewStore, type _: SetNameAndPlaceView.TextFieldType) -> Color { + func getTextCountColor(_ viewStore: ViewStore, type _: SetNameAndPlaceView.TextFieldType) -> Color { return viewStore.showWarningMessage ? PDS.COLOR.scarlet1.scale : PDS.COLOR.gray4.scale } diff --git a/AppPackage/Sources/MakePromise/SubViews/TopInformationView.swift b/AppPackage/Sources/MakePromise/SubViews/TopInformationView.swift index 90a8683..e606843 100644 --- a/AppPackage/Sources/MakePromise/SubViews/TopInformationView.swift +++ b/AppPackage/Sources/MakePromise/SubViews/TopInformationView.swift @@ -11,7 +11,7 @@ import DesignSystem import SwiftUI public struct TopInformationView: View { - var store: Store + var store: StoreOf public var body: some View { WithViewStore(self.store) { viewStore in VStack(spacing: 4) { @@ -52,7 +52,7 @@ public struct TopInformationView: View { } } -private extension MakePromiseState.Step { +private extension MakePromise.State.Step { var title: String { switch self { case .selectTheme: diff --git a/AppPackage/Sources/TimeTableFeature/Day+Extensions.swift b/AppPackage/Sources/TimeTableFeature/Day+Extensions.swift index b762aff..14a428d 100644 --- a/AppPackage/Sources/TimeTableFeature/Day+Extensions.swift +++ b/AppPackage/Sources/TimeTableFeature/Day+Extensions.swift @@ -8,7 +8,7 @@ import Foundation -extension TimeTableState.Day { +extension TimeTable.State.Day { func formatted(with formatter: DateFormatter) -> String { formatter.string(from: date) } @@ -30,7 +30,7 @@ extension DateFormatter { }() } -extension Array where Element == TimeTableState.Day { +extension Array where Element == TimeTable.State.Day { static var weekend: Self { return (0 ..< 7).map { index -> Element in .init(date: .init(timeIntervalSinceNow: .day * TimeInterval(index))) diff --git a/AppPackage/Sources/TimeTableFeature/TimeTableView.swift b/AppPackage/Sources/TimeTableFeature/TimeTableView.swift index 67eafe7..42e6536 100644 --- a/AppPackage/Sources/TimeTableFeature/TimeTableView.swift +++ b/AppPackage/Sources/TimeTableFeature/TimeTableView.swift @@ -6,116 +6,158 @@ // Copyright © 2022 Team-Planz. All rights reserved. // +import APIClient +import APIClientLive import ComposableArchitecture +import Entity import SwiftUI +import DesignSystem -public struct TimeTableState: Equatable { - public struct Day: Hashable, Equatable, Identifiable { - public let id: Int - let date: Date +public struct TimeTable: ReducerProtocol { + public struct State: Equatable { + public var sessionID: String? + public var days: [Day] + public var startTime: TimeInterval + public var endTime: TimeInterval + public var timeInterval: TimeInterval + public var timeMarkerInterval: TimeInterval + public var timeRanges: [TimeRange] = [] + public var timeCells: [[TimeCell]] = [] - public init(date: Date) { - id = date.hashValue - self.date = date + public struct Day: Hashable, Equatable, Identifiable { + public let id: Int + public let date: Date + + public init(date: Date) { + id = date.hashValue + self.date = date + } } - } - public enum TimeCell { - case selected - case deselected + public enum TimeCell { + case selected + case deselected - mutating func toggle() { - switch self { - case .deselected: - self = .selected - case .selected: - self = .deselected + mutating func toggle() { + switch self { + case .deselected: + self = .selected + case .selected: + self = .deselected + } } } - } - public struct TimeRange: Equatable, Hashable { - let startTime: TimeInterval - let endTime: TimeInterval - let isStartTimeVisible: Bool - } - - public var days: [Day] - public var startTime: TimeInterval - public var endTime: TimeInterval - public var timeInterval: TimeInterval - public var timeMarkerInterval: TimeInterval - public var timeRanges: [TimeRange] = [] - public var timeCells: [[TimeCell]] = [] + public struct TimeRange: Equatable, Hashable { + let startTime: TimeInterval + let endTime: TimeInterval + let isStartTimeVisible: Bool + } - public init( - days: [Day] = [], - startTime: TimeInterval = .init(), - endTime: TimeInterval = .init(), - timeInterval: TimeInterval = .init(), - timeMarkerInterval: TimeInterval = .init() - ) { - self.days = days - self.startTime = startTime - self.endTime = endTime - self.timeInterval = timeInterval - self.timeMarkerInterval = timeMarkerInterval - reload() - } + public init( + days: [Day] = [], + startTime: TimeInterval = .init(), + endTime: TimeInterval = .init(), + timeInterval: TimeInterval = .init(), + timeMarkerInterval: TimeInterval = .init() + ) { + self.days = days + self.startTime = startTime + self.endTime = endTime + self.timeInterval = timeInterval + self.timeMarkerInterval = timeMarkerInterval + } - public var isTimeSelected: Bool { - timeCells - .flatMap { $0 } - .contains { $0 == .selected } - } + public var isTimeSelected: Bool { + timeCells + .flatMap { $0 } + .contains { $0 == .selected } + } - public var isGridLoadable: Bool { - timeRanges.count > 0 && days.count > 0 && timeCells.count > 0 - } + public var isGridLoadable: Bool { + timeRanges.count > 0 && days.count > 0 && timeCells.count > 0 + } - public mutating func reload() { - timeRanges = stride( - from: startTime, - to: endTime, - by: timeInterval - ) - .map { - .init( - startTime: $0, - endTime: $0 + timeInterval, - isStartTimeVisible: $0.truncatingRemainder(dividingBy: timeMarkerInterval) == 0 + public mutating func reload() { + timeRanges = stride( + from: startTime, + to: endTime, + by: timeInterval + ) + .map { + .init( + startTime: $0, + endTime: $0 + timeInterval, + isStartTimeVisible: $0.truncatingRemainder(dividingBy: timeMarkerInterval) == 0 + ) + } + timeCells = .init( + repeating: .init(repeating: .deselected, count: timeRanges.count), + count: days.count ) } - timeCells = .init( - repeating: .init(repeating: .deselected, count: timeRanges.count), - count: days.count - ) } -} -public enum TimeTableAction: Equatable { - case timeCellTapped(row: Int, column: Int) -} + public enum Action: Equatable { + case task + case fetchSessionResponse(TaskResult) + case timeCellTapped(row: Int, column: Int) + } + + @Dependency(\.apiClient) var apiClient -public let timeTableReducer = Reducer< - TimeTableState, - TimeTableAction, - Void -> { state, action, _ in - switch action { - case let .timeCellTapped(row, column): - guard row >= 0, row < state.timeRanges.count else { return .none } - guard column >= 0, column < state.days.count else { return .none } - state.timeCells[column][row].toggle() - return .none + public var body: some ReducerProtocolOf { + Reduce { state, action in + switch action { + case .task: + guard let id = state.sessionID else { + return .none + } + return .task { + await .fetchSessionResponse( + TaskResult { + try await apiClient.request( + route: .promising(.fetchSession(id)), + as: PromisingSessionResponse.self + ) + } + ) + } + case let .fetchSessionResponse(.success(response)): + guard let startTime: Date = dateFormatter.date(from: response.minTime), + let endTime: Date = dateFormatter.date(from: response.maxTime) + else { + return .none + } + state.startTime = TimeInterval(Calendar.current.component(.hour, from: startTime) * 3600) + state.endTime = TimeInterval(Calendar.current.component(.hour, from: endTime) * 3600) + state.days = response.availableDates + .compactMap { + dateFormatter.date(from: $0) + } + .map { .init(date: $0) } + state.timeInterval = response.unit * .hour + state.reload() + return .none + case .fetchSessionResponse(.failure): + return .none + case let .timeCellTapped(row, column): + guard row >= 0, row < state.timeRanges.count else { return .none } + guard column >= 0, column < state.days.count else { return .none } + state.timeCells[column][row].toggle() + return .none + } + } } + + public init() {} } public struct TimeTableView: View { - let store: Store - @ObservedObject var viewStore: ViewStore + let store: StoreOf + @ObservedObject var viewStore: ViewStoreOf - public init(store: Store) { + public init(store: StoreOf) { self.store = store viewStore = ViewStore(store) } @@ -135,11 +177,11 @@ public struct TimeTableView: View { width: dayCellWidth * CGFloat(viewStore.days.count), height: LayoutConstant.headerHeight ) - .background(Resource.PlanzColor.white200) + .background(PDS.COLOR.white2.scale) Divider() .frame(height: LayoutConstant.lineWidth) - .overlay(Resource.PlanzColor.gray200) + .overlay(PDS.COLOR.gray4.scale) if viewStore.isGridLoadable { grid .frame( @@ -149,13 +191,16 @@ public struct TimeTableView: View { } Divider() .frame(height: LayoutConstant.lineWidth) - .overlay(Resource.PlanzColor.gray200) + .overlay(PDS.COLOR.gray4.scale) } } .overlay( gradient, alignment: .topLeading ) } + .task { + viewStore.send(.task) + } } } @@ -166,11 +211,11 @@ public struct TimeTableView: View { VStack(alignment: .center) { Text(day.formatted(with: .dayOnly)) .font(.system(size: 12)) - .foregroundColor(Resource.PlanzColor.gray800) + .foregroundColor(PDS.COLOR.gray8.scale) Text(day.formatted(with: .monthAndDay)) .font(.system(size: 14)) - .foregroundColor(Resource.PlanzColor.purple900) + .foregroundColor(PDS.COLOR.purple9.scale) } .frame( width: proxy.size.width / CGFloat(viewStore.days.count), @@ -181,7 +226,7 @@ public struct TimeTableView: View { .stroke(Resource.PlanzColor.dayCellBorder) .background( RoundedRectangle(cornerRadius: LayoutConstant.dayCellCornerRadius) - .fill(Resource.PlanzColor.dayCellBackground) + .fill(PDS.COLOR.purple1.scale) ) .frame( width: LayoutConstant.dayCellSize.width, @@ -201,7 +246,7 @@ public struct TimeTableView: View { if timeRange.isStartTimeVisible { Text(timeRange.startTime.formatted(with: .hhmm)) .font(.system(size: 12)) - .foregroundColor(Resource.PlanzColor.gray900) + .foregroundColor(PDS.COLOR.cGray2.scale) .minimumScaleFactor(0.5) } else { Spacer() @@ -211,12 +256,12 @@ public struct TimeTableView: View { } } .frame(width: LayoutConstant.timelineWidth) - .background(Resource.PlanzColor.white200) + .background(PDS.COLOR.white2.scale) .overlay( HStack { Divider() .frame(width: LayoutConstant.lineWidth) - .overlay(Resource.PlanzColor.gray200) + .overlay(PDS.COLOR.gray4.scale) }, alignment: .trailing ) @@ -243,17 +288,17 @@ public struct TimeTableView: View { Rectangle() .frame(width: (proxy.size.width - LayoutConstant.timelineWidth) / CGFloat(columns)) .foregroundColor(viewStore.timeCells[column][row] == .selected - ? Resource.PlanzColor.purple900 : Resource.PlanzColor.white200) + ? PDS.COLOR.purple9.scale : PDS.COLOR.white2.scale) .clipShape(Rectangle()) .overlay( VerticalLine() - .stroke(Resource.PlanzColor.timeCellBorder) + .stroke(PDS.COLOR.gray3.scale) .frame(width: LayoutConstant.lineWidth), alignment: .trailing ) .overlay( HorizontalLine() - .stroke(Resource.PlanzColor.timeCellBorder, + .stroke(PDS.COLOR.gray3.scale, style: row % 2 == 0 ? .init(dash: [2]) : .init()) .frame(height: LayoutConstant.lineWidth), alignment: .bottom @@ -271,7 +316,7 @@ public struct TimeTableView: View { var gradient: some View { LinearGradient( - colors: [Resource.PlanzColor.white200.opacity(0.1), Resource.PlanzColor.white200], + colors: [PDS.COLOR.white2.scale.opacity(0.1), PDS.COLOR.white2.scale], startPoint: .trailing, endPoint: .leading ) @@ -295,20 +340,11 @@ private enum LayoutConstant { private enum Resource { enum PlanzColor { - static let gray200: Color = .init(red: 205 / 255, green: 210 / 255, blue: 217 / 255) - static let gray500: Color = .init(red: 156 / 255, green: 163 / 255, blue: 173 / 255) - static let gray800: Color = .init(red: 2 / 255, green: 2 / 255, blue: 2 / 255) - static let gray900: Color = .init(red: 91 / 255, green: 104 / 255, blue: 122 / 255) - static let white200: Color = .init(red: 251 / 255, green: 251 / 255, blue: 251 / 255) - static let purple100: Color = .init(red: 251 / 255, green: 251 / 255, blue: 251 / 255) - static let purple900: Color = .init(red: 102 / 255, green: 113 / 255, blue: 246 / 255) - static let dayCellBackground: Color = .init(red: 232 / 255, green: 234 / 255, blue: 254 / 255) static let dayCellBorder: Color = .init(red: 206 / 255, green: 210 / 255, blue: 252 / 255) - static let timeCellBorder: Color = .init(red: 232 / 255, green: 234 / 255, blue: 237 / 255) } } -public extension TimeTableState { +public extension TimeTable.State { static var mock: Self { let startTime: TimeInterval = 9 * .hour let endTime: TimeInterval = 24 * .hour @@ -328,9 +364,14 @@ struct TimeTableView_Previews: PreviewProvider { TimeTableView( store: .init( initialState: .mock, - reducer: timeTableReducer, - environment: () + reducer: TimeTable() ) ) } } + +private var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + return dateFormatter +}()