From ee72d5b8e741c0e10e7549663b47e2a0faa0b18f Mon Sep 17 00:00:00 2001 From: Mauricio Cardozo Date: Fri, 17 Nov 2023 14:15:37 -0300 Subject: [PATCH 1/8] pulls the medium widget up to the main app with a separate module --- app/LandinhoLib/Package.swift | 8 +++++ .../Sources/Common}/EventByDate.swift | 25 +++++++-------- .../Sources/Common/RaceBundle.swift | 18 +++++++++++ .../WidgetUI/NextRaceMediumWidgetView.swift} | 31 +++++++------------ app/VroomVroom.xcodeproj/project.pbxproj | 28 +++++++++++------ .../NextRaceWidget/NextRaceWidget.swift | 17 +++++++++- 6 files changed, 84 insertions(+), 43 deletions(-) rename app/{Widgets/NextRaceWidget => LandinhoLib/Sources/Common}/EventByDate.swift (58%) create mode 100644 app/LandinhoLib/Sources/Common/RaceBundle.swift rename app/{Widgets/NextRaceWidget/NextRaceMediumWidget.swift => LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift} (74%) diff --git a/app/LandinhoLib/Package.swift b/app/LandinhoLib/Package.swift index f85dade..aaa3758 100644 --- a/app/LandinhoLib/Package.swift +++ b/app/LandinhoLib/Package.swift @@ -24,6 +24,7 @@ let package = Package( .library(name: "RacesAdmin", targets: ["RacesAdmin"]), .library(name: "ScheduleList", targets: ["ScheduleList"]), .library(name: "Settings", targets: ["Settings"]), + .library(name: "WidgetUI", targets: ["WidgetUI"]), ], dependencies: [ .package( @@ -110,5 +111,12 @@ let package = Package( "APIClient", composable ]), + + .target( + name: "WidgetUI", + dependencies: [ + "Common", + composable + ]), ] ) diff --git a/app/Widgets/NextRaceWidget/EventByDate.swift b/app/LandinhoLib/Sources/Common/EventByDate.swift similarity index 58% rename from app/Widgets/NextRaceWidget/EventByDate.swift rename to app/LandinhoLib/Sources/Common/EventByDate.swift index 99f8492..d2d0111 100644 --- a/app/Widgets/NextRaceWidget/EventByDate.swift +++ b/app/LandinhoLib/Sources/Common/EventByDate.swift @@ -1,19 +1,18 @@ // // EventByDate.swift -// WidgetsExtension // -// Created by Mauricio Cardozo on 16/11/23. +// +// Created by Mauricio Cardozo on 17/11/23. // -import Common import Foundation -struct EventByDate: Identifiable { - let date: String - let events: [Event] - var id: String { date } +public struct EventByDate: Identifiable { + public let date: String + public let events: [Event] + public var id: String { date } - struct Event: Identifiable { + public struct Event: Identifiable { init(raceEvent: RaceEvent) { title = raceEvent.title time = raceEvent.date.formatted( @@ -23,14 +22,14 @@ struct EventByDate: Identifiable { ) } - let title: String - let time: String - var id: String { title } + public let title: String + public let time: String + public var id: String { title } } } -struct EventByDateFactory { - static func convert(events: [RaceEvent]) -> [EventByDate] { +public struct EventByDateFactory { + public static func convert(events: [RaceEvent]) -> [EventByDate] { guard !events.isEmpty else { return [] } return Dictionary(grouping: events) { diff --git a/app/LandinhoLib/Sources/Common/RaceBundle.swift b/app/LandinhoLib/Sources/Common/RaceBundle.swift new file mode 100644 index 0000000..ab71050 --- /dev/null +++ b/app/LandinhoLib/Sources/Common/RaceBundle.swift @@ -0,0 +1,18 @@ +// +// File.swift +// +// +// Created by Mauricio Cardozo on 17/11/23. +// + +import Foundation + +public struct RaceBundle: Codable, Equatable { + public init(category: RaceCategory, nextRace: Race) { + self.category = category + self.nextRace = nextRace + } + + public let category: RaceCategory + public let nextRace: Race +} diff --git a/app/Widgets/NextRaceWidget/NextRaceMediumWidget.swift b/app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift similarity index 74% rename from app/Widgets/NextRaceWidget/NextRaceMediumWidget.swift rename to app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift index e261c71..e8031f1 100644 --- a/app/Widgets/NextRaceWidget/NextRaceMediumWidget.swift +++ b/app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift @@ -1,23 +1,26 @@ // -// NextRaceMediumWidget.swift -// WidgetsExtension +// File.swift +// // -// Created by Mauricio Cardozo on 16/11/23. +// Created by Mauricio Cardozo on 17/11/23. // -import ComposableArchitecture +import Common import Foundation import SwiftUI import WidgetKit -import ScheduleList -import Common -struct NextRaceMediumWidgetView: View { +public struct NextRaceMediumWidgetView: View { + + public init(response: RaceBundle, lastUpdatedDate: Date) { + self.response = response + self.lastUpdatedDate = lastUpdatedDate + } - let response: ScheduleList.ScheduleListResponse + let response: RaceBundle let lastUpdatedDate: Date - var body: some View { + public var body: some View { VStack { HStack { VStack(alignment: .leading) { @@ -52,7 +55,6 @@ struct NextRaceMediumWidgetView: View { .foregroundStyle(.secondary) .frame(maxWidth: .infinity) } - } var eventsByDate: [EventByDate] { @@ -60,13 +62,4 @@ struct NextRaceMediumWidgetView: View { } } -#Preview(as: .systemMedium) { - NextRaceWidget() -} timeline: { - NextRaceEntry(date: Date()) - NextRaceEntry.empty - NextRaceEntry.placeholder -} - - diff --git a/app/VroomVroom.xcodeproj/project.pbxproj b/app/VroomVroom.xcodeproj/project.pbxproj index f41c7a8..cec08fa 100644 --- a/app/VroomVroom.xcodeproj/project.pbxproj +++ b/app/VroomVroom.xcodeproj/project.pbxproj @@ -13,14 +13,14 @@ B42CA9F42AFE8B67009B3FFB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B42CA9F32AFE8B67009B3FFB /* Preview Assets.xcassets */; }; B450025C2AFE91EF003CDE6A /* APIClient in Frameworks */ = {isa = PBXBuildFile; productRef = B450025B2AFE91EF003CDE6A /* APIClient */; }; B450025E2AFE91F1003CDE6A /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = B450025D2AFE91F1003CDE6A /* Common */; }; + B453CE022B07B07F00B0CEA5 /* WidgetUI in Frameworks */ = {isa = PBXBuildFile; productRef = B453CE012B07B07F00B0CEA5 /* WidgetUI */; }; + B453CE042B07B08700B0CEA5 /* WidgetUI in Frameworks */ = {isa = PBXBuildFile; productRef = B453CE032B07B08700B0CEA5 /* WidgetUI */; }; B4877C222B0692C30009760C /* NextRaceSmallWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C1E2B0692240009760C /* NextRaceSmallWidget.swift */; }; B4877C232B0692C60009760C /* NextEventIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C202B06928F0009760C /* NextEventIntent.swift */; }; B4877C272B0692F40009760C /* NextRaceTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */; }; B4877C2A2B0694350009760C /* NextRaceEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C292B0694350009760C /* NextRaceEntry.swift */; }; - B4877C2C2B06A0C70009760C /* NextRaceMediumWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C2B2B06A0C70009760C /* NextRaceMediumWidget.swift */; }; B4877C2E2B06A0D40009760C /* NextRaceLargeWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C2D2B06A0D40009760C /* NextRaceLargeWidget.swift */; }; B4877C302B06A0E60009760C /* NextRaceExtraLargeWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C2F2B06A0E60009760C /* NextRaceExtraLargeWidget.swift */; }; - B4877C322B06AB700009760C /* EventByDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C312B06AB700009760C /* EventByDate.swift */; }; B4877C342B06B4C60009760C /* NextRaceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C332B06B4C60009760C /* NextRaceWidget.swift */; }; B48A501F2AFECEEB00A01B7E /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48A501E2AFECEEB00A01B7E /* Root.swift */; }; B4CBD1632B0401C100BF6776 /* Home in Frameworks */ = {isa = PBXBuildFile; productRef = B4CBD1622B0401C100BF6776 /* Home */; }; @@ -76,10 +76,8 @@ B4877C202B06928F0009760C /* NextEventIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextEventIntent.swift; sourceTree = ""; }; B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceTimelineProvider.swift; sourceTree = ""; }; B4877C292B0694350009760C /* NextRaceEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceEntry.swift; sourceTree = ""; }; - B4877C2B2B06A0C70009760C /* NextRaceMediumWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceMediumWidget.swift; sourceTree = ""; }; B4877C2D2B06A0D40009760C /* NextRaceLargeWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceLargeWidget.swift; sourceTree = ""; }; B4877C2F2B06A0E60009760C /* NextRaceExtraLargeWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceExtraLargeWidget.swift; sourceTree = ""; }; - B4877C312B06AB700009760C /* EventByDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventByDate.swift; sourceTree = ""; }; B4877C332B06B4C60009760C /* NextRaceWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceWidget.swift; sourceTree = ""; }; B48A501E2AFECEEB00A01B7E /* Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Root.swift; sourceTree = ""; }; B4CBD16C2B05570F00BF6776 /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -103,6 +101,7 @@ B4D87C482B01AF07000EBEF9 /* Categories in Frameworks */, B4D87C4C2B01AF0B000EBEF9 /* Settings in Frameworks */, B4D87C4A2B01AF09000EBEF9 /* ScheduleList in Frameworks */, + B453CE022B07B07F00B0CEA5 /* WidgetUI in Frameworks */, B450025C2AFE91EF003CDE6A /* APIClient in Frameworks */, B4D87C4E2B01B12B000EBEF9 /* EventDetail in Frameworks */, B450025E2AFE91F1003CDE6A /* Common in Frameworks */, @@ -119,6 +118,7 @@ B4CBD1AD2B05582300BF6776 /* ScheduleList in Frameworks */, B4CBD16E2B05570F00BF6776 /* WidgetKit.framework in Frameworks */, B4CBD1AB2B05582000BF6776 /* Common in Frameworks */, + B453CE042B07B08700B0CEA5 /* WidgetUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -181,13 +181,11 @@ children = ( B4877C332B06B4C60009760C /* NextRaceWidget.swift */, B4877C1E2B0692240009760C /* NextRaceSmallWidget.swift */, - B4877C2B2B06A0C70009760C /* NextRaceMediumWidget.swift */, B4877C2D2B06A0D40009760C /* NextRaceLargeWidget.swift */, B4877C2F2B06A0E60009760C /* NextRaceExtraLargeWidget.swift */, B4877C202B06928F0009760C /* NextEventIntent.swift */, B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */, B4877C292B0694350009760C /* NextRaceEntry.swift */, - B4877C312B06AB700009760C /* EventByDate.swift */, ); path = NextRaceWidget; sourceTree = ""; @@ -233,6 +231,7 @@ B4CBD1622B0401C100BF6776 /* Home */, B4CBD1642B0409FF00BF6776 /* Admin */, B4CBD1662B040A0300BF6776 /* CategoriesAdmin */, + B453CE012B07B07F00B0CEA5 /* WidgetUI */, ); productName = VroomVroom; productReference = B42CA9E92AFE8B66009B3FFB /* VroomVroom.app */; @@ -254,6 +253,7 @@ packageProductDependencies = ( B4CBD1AA2B05582000BF6776 /* Common */, B4CBD1AC2B05582300BF6776 /* ScheduleList */, + B453CE032B07B08700B0CEA5 /* WidgetUI */, ); productName = WidgetsExtension; productReference = B4CBD16C2B05570F00BF6776 /* WidgetsExtension.appex */; @@ -333,7 +333,6 @@ files = ( B4CBD1752B05570F00BF6776 /* WidgetsLiveActivity.swift in Sources */, B4877C2A2B0694350009760C /* NextRaceEntry.swift in Sources */, - B4877C2C2B06A0C70009760C /* NextRaceMediumWidget.swift in Sources */, B4CBD1792B05570F00BF6776 /* AppIntent.swift in Sources */, B4877C342B06B4C60009760C /* NextRaceWidget.swift in Sources */, B4877C232B0692C60009760C /* NextEventIntent.swift in Sources */, @@ -342,7 +341,6 @@ B4877C272B0692F40009760C /* NextRaceTimelineProvider.swift in Sources */, B4877C302B06A0E60009760C /* NextRaceExtraLargeWidget.swift in Sources */, B4877C2E2B06A0D40009760C /* NextRaceLargeWidget.swift in Sources */, - B4877C322B06AB700009760C /* EventByDate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -483,12 +481,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"VroomVroom/Preview Content\""; DEVELOPMENT_TEAM = UQCQ667RNK; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VroomVroom/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -514,12 +513,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"VroomVroom/Preview Content\""; DEVELOPMENT_TEAM = UQCQ667RNK; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VroomVroom/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -635,6 +635,14 @@ isa = XCSwiftPackageProductDependency; productName = Common; }; + B453CE012B07B07F00B0CEA5 /* WidgetUI */ = { + isa = XCSwiftPackageProductDependency; + productName = WidgetUI; + }; + B453CE032B07B08700B0CEA5 /* WidgetUI */ = { + isa = XCSwiftPackageProductDependency; + productName = WidgetUI; + }; B4CBD1622B0401C100BF6776 /* Home */ = { isa = XCSwiftPackageProductDependency; productName = Home; diff --git a/app/Widgets/NextRaceWidget/NextRaceWidget.swift b/app/Widgets/NextRaceWidget/NextRaceWidget.swift index ad214e8..bbadee5 100644 --- a/app/Widgets/NextRaceWidget/NextRaceWidget.swift +++ b/app/Widgets/NextRaceWidget/NextRaceWidget.swift @@ -9,6 +9,7 @@ import ComposableArchitecture import Foundation import SwiftUI import WidgetKit +import WidgetUI import ScheduleList import Common @@ -52,8 +53,11 @@ struct NextRaceWidgetView: View { response: response, lastUpdatedDate: lastUpdatedDate) case .systemMedium: + #warning("TODO remove RaceBundle initializer") NextRaceMediumWidgetView( - response: response, + response: .init( + category: response.category, + nextRace: response.nextRace), lastUpdatedDate: lastUpdatedDate) case .systemLarge: NextRaceLargeWidgetView( @@ -74,3 +78,14 @@ struct NextRaceWidgetView: View { } } +import Foundation +import WidgetKit +import WidgetUI + +#Preview(as: .systemMedium) { + NextRaceWidget() +} timeline: { + NextRaceEntry(date: Date()) + NextRaceEntry.empty + NextRaceEntry.placeholder +} From 5bf24daab20aeac90c4fd7cb77bb134ec41a2dfe Mon Sep 17 00:00:00 2001 From: Mauricio Cardozo Date: Fri, 17 Nov 2023 14:24:15 -0300 Subject: [PATCH 2/8] modularizes large and extra large --- .../NextRaceExtraLargeWidgetView.swift} | 28 ++++++----------- .../WidgetUI/NextRaceLargeWidgetView.swift} | 31 +++++++------------ .../WidgetUI/NextRaceMediumWidgetView.swift | 1 - app/VroomVroom.xcodeproj/project.pbxproj | 8 ----- .../NextRaceWidget/NextRaceWidget.swift | 31 ++++++++++++------- 5 files changed, 41 insertions(+), 58 deletions(-) rename app/{Widgets/NextRaceWidget/NextRaceExtraLargeWidget.swift => LandinhoLib/Sources/WidgetUI/NextRaceExtraLargeWidgetView.swift} (76%) rename app/{Widgets/NextRaceWidget/NextRaceLargeWidget.swift => LandinhoLib/Sources/WidgetUI/NextRaceLargeWidgetView.swift} (75%) diff --git a/app/Widgets/NextRaceWidget/NextRaceExtraLargeWidget.swift b/app/LandinhoLib/Sources/WidgetUI/NextRaceExtraLargeWidgetView.swift similarity index 76% rename from app/Widgets/NextRaceWidget/NextRaceExtraLargeWidget.swift rename to app/LandinhoLib/Sources/WidgetUI/NextRaceExtraLargeWidgetView.swift index 181ac02..d7580cd 100644 --- a/app/Widgets/NextRaceWidget/NextRaceExtraLargeWidget.swift +++ b/app/LandinhoLib/Sources/WidgetUI/NextRaceExtraLargeWidgetView.swift @@ -1,23 +1,22 @@ // -// NextRaceExtraLargeWidget.swift -// WidgetsExtension +// NextRaceExtraLargeWidgetView.swift // -// Created by Mauricio Cardozo on 16/11/23. +// +// Created by Mauricio Cardozo on 17/11/23. // -import ComposableArchitecture +import Common import Foundation import SwiftUI -import WidgetKit -import ScheduleList -import Common -struct NextRaceExtraLargeWidgetView: View { +// TODO: Finish this Widget once we have more a `next-races` endpoint + +public struct NextRaceExtraLargeWidgetView: View { - let response: ScheduleList.ScheduleListResponse + let response: RaceBundle let lastUpdatedDate: Date - var body: some View { + public var body: some View { VStack(alignment: .leading, spacing: 10) { Text("Próximas corridas") .font(.title) @@ -75,12 +74,3 @@ struct NextRaceExtraLargeWidgetView: View { EventByDateFactory.convert(events: response.nextRace.events) } } - -#Preview(as: .systemExtraLarge) { - NextRaceWidget() -} timeline: { - NextRaceEntry(date: Date()) - NextRaceEntry.empty - NextRaceEntry.placeholder -} - diff --git a/app/Widgets/NextRaceWidget/NextRaceLargeWidget.swift b/app/LandinhoLib/Sources/WidgetUI/NextRaceLargeWidgetView.swift similarity index 75% rename from app/Widgets/NextRaceWidget/NextRaceLargeWidget.swift rename to app/LandinhoLib/Sources/WidgetUI/NextRaceLargeWidgetView.swift index 6610e55..45b44d0 100644 --- a/app/Widgets/NextRaceWidget/NextRaceLargeWidget.swift +++ b/app/LandinhoLib/Sources/WidgetUI/NextRaceLargeWidgetView.swift @@ -1,23 +1,25 @@ // -// NextRaceLargeWidget.swift -// WidgetsExtension +// NextRaceLargeWidgetView.swift // -// Created by Mauricio Cardozo on 16/11/23. +// +// Created by Mauricio Cardozo on 17/11/23. // -import ComposableArchitecture +import Common import Foundation import SwiftUI -import WidgetKit -import ScheduleList -import Common -struct NextRaceLargeWidgetView: View { +public struct NextRaceLargeWidgetView: View { + + public init(response: RaceBundle, lastUpdatedDate: Date) { + self.response = response + self.lastUpdatedDate = lastUpdatedDate + } - let response: ScheduleList.ScheduleListResponse + let response: RaceBundle let lastUpdatedDate: Date - var body: some View { + public var body: some View { VStack { VStack(alignment: .leading) { Text(response.category.title) @@ -69,12 +71,3 @@ struct NextRaceLargeWidgetView: View { EventByDateFactory.convert(events: response.nextRace.events) } } - -#Preview(as: .systemLarge) { - NextRaceWidget() -} timeline: { - NextRaceEntry(date: Date()) - NextRaceEntry.empty - NextRaceEntry.placeholder -} - diff --git a/app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift b/app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift index e8031f1..bf67fed 100644 --- a/app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift +++ b/app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift @@ -8,7 +8,6 @@ import Common import Foundation import SwiftUI -import WidgetKit public struct NextRaceMediumWidgetView: View { diff --git a/app/VroomVroom.xcodeproj/project.pbxproj b/app/VroomVroom.xcodeproj/project.pbxproj index cec08fa..1398e13 100644 --- a/app/VroomVroom.xcodeproj/project.pbxproj +++ b/app/VroomVroom.xcodeproj/project.pbxproj @@ -19,8 +19,6 @@ B4877C232B0692C60009760C /* NextEventIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C202B06928F0009760C /* NextEventIntent.swift */; }; B4877C272B0692F40009760C /* NextRaceTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */; }; B4877C2A2B0694350009760C /* NextRaceEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C292B0694350009760C /* NextRaceEntry.swift */; }; - B4877C2E2B06A0D40009760C /* NextRaceLargeWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C2D2B06A0D40009760C /* NextRaceLargeWidget.swift */; }; - B4877C302B06A0E60009760C /* NextRaceExtraLargeWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C2F2B06A0E60009760C /* NextRaceExtraLargeWidget.swift */; }; B4877C342B06B4C60009760C /* NextRaceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C332B06B4C60009760C /* NextRaceWidget.swift */; }; B48A501F2AFECEEB00A01B7E /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48A501E2AFECEEB00A01B7E /* Root.swift */; }; B4CBD1632B0401C100BF6776 /* Home in Frameworks */ = {isa = PBXBuildFile; productRef = B4CBD1622B0401C100BF6776 /* Home */; }; @@ -76,8 +74,6 @@ B4877C202B06928F0009760C /* NextEventIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextEventIntent.swift; sourceTree = ""; }; B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceTimelineProvider.swift; sourceTree = ""; }; B4877C292B0694350009760C /* NextRaceEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceEntry.swift; sourceTree = ""; }; - B4877C2D2B06A0D40009760C /* NextRaceLargeWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceLargeWidget.swift; sourceTree = ""; }; - B4877C2F2B06A0E60009760C /* NextRaceExtraLargeWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceExtraLargeWidget.swift; sourceTree = ""; }; B4877C332B06B4C60009760C /* NextRaceWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceWidget.swift; sourceTree = ""; }; B48A501E2AFECEEB00A01B7E /* Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Root.swift; sourceTree = ""; }; B4CBD16C2B05570F00BF6776 /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -181,8 +177,6 @@ children = ( B4877C332B06B4C60009760C /* NextRaceWidget.swift */, B4877C1E2B0692240009760C /* NextRaceSmallWidget.swift */, - B4877C2D2B06A0D40009760C /* NextRaceLargeWidget.swift */, - B4877C2F2B06A0E60009760C /* NextRaceExtraLargeWidget.swift */, B4877C202B06928F0009760C /* NextEventIntent.swift */, B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */, B4877C292B0694350009760C /* NextRaceEntry.swift */, @@ -339,8 +333,6 @@ B4CBD1732B05570F00BF6776 /* WidgetsBundle.swift in Sources */, B4877C222B0692C30009760C /* NextRaceSmallWidget.swift in Sources */, B4877C272B0692F40009760C /* NextRaceTimelineProvider.swift in Sources */, - B4877C302B06A0E60009760C /* NextRaceExtraLargeWidget.swift in Sources */, - B4877C2E2B06A0D40009760C /* NextRaceLargeWidget.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/app/Widgets/NextRaceWidget/NextRaceWidget.swift b/app/Widgets/NextRaceWidget/NextRaceWidget.swift index bbadee5..84a346d 100644 --- a/app/Widgets/NextRaceWidget/NextRaceWidget.swift +++ b/app/Widgets/NextRaceWidget/NextRaceWidget.swift @@ -60,16 +60,13 @@ struct NextRaceWidgetView: View { nextRace: response.nextRace), lastUpdatedDate: lastUpdatedDate) case .systemLarge: + #warning("TODO remove RaceBundle initializer") NextRaceLargeWidgetView( - response: response, - lastUpdatedDate: lastUpdatedDate) - case .systemExtraLarge: - // Disabled for now -- maybe this isn't what - // TODO: Enable when Timeline provider fetches more than a single race - NextRaceExtraLargeWidgetView( - response: response, + response: .init( + category: response.category, + nextRace: response.nextRace), lastUpdatedDate: lastUpdatedDate) - case .accessoryCircular, .accessoryRectangular, .accessoryInline: + case .systemExtraLarge, .accessoryCircular, .accessoryRectangular, .accessoryInline: // TODO: Accessory Widgets EmptyView() @unknown default: @@ -78,9 +75,13 @@ struct NextRaceWidgetView: View { } } -import Foundation -import WidgetKit -import WidgetUI +#Preview(as: .systemSmall) { + NextRaceWidget() +} timeline: { + NextRaceEntry(date: Date()) + NextRaceEntry.empty + NextRaceEntry.placeholder +} #Preview(as: .systemMedium) { NextRaceWidget() @@ -89,3 +90,11 @@ import WidgetUI NextRaceEntry.empty NextRaceEntry.placeholder } + +#Preview(as: .systemLarge) { + NextRaceWidget() +} timeline: { + NextRaceEntry(date: Date()) + NextRaceEntry.empty + NextRaceEntry.placeholder +} From 06d48d495764949d181e7206c40f116a7ff3ce5a Mon Sep 17 00:00:00 2001 From: Mauricio Cardozo Date: Fri, 17 Nov 2023 14:37:37 -0300 Subject: [PATCH 3/8] reorganizes files around, brings next race small size to widgetui --- .../Sources/Common/RaceBundle.swift | 18 ++++++- .../NextRaceExtraLargeWidgetView.swift | 6 +-- .../NextRaceLargeWidgetView.swift | 12 ++--- .../NextRaceMediumWidgetView.swift | 12 ++--- .../NextRaceSmallWidgetView+Intents.swift | 48 +++++++++++++++++++ .../NextRace/NextRaceSmallWidgetView.swift} | 43 +++++++---------- app/VroomVroom.xcodeproj/project.pbxproj | 8 ---- .../NextRaceWidget/NextEventIntent.swift | 46 ------------------ .../NextRaceWidget/NextRaceWidget.swift | 33 +++++++------ 9 files changed, 117 insertions(+), 109 deletions(-) rename app/LandinhoLib/Sources/WidgetUI/{ => NextRace}/NextRaceExtraLargeWidgetView.swift (92%) rename app/LandinhoLib/Sources/WidgetUI/{ => NextRace}/NextRaceLargeWidgetView.swift (84%) rename app/LandinhoLib/Sources/WidgetUI/{ => NextRace}/NextRaceMediumWidgetView.swift (81%) create mode 100644 app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceSmallWidgetView+Intents.swift rename app/{Widgets/NextRaceWidget/NextRaceSmallWidget.swift => LandinhoLib/Sources/WidgetUI/NextRace/NextRaceSmallWidgetView.swift} (69%) delete mode 100644 app/Widgets/NextRaceWidget/NextEventIntent.swift diff --git a/app/LandinhoLib/Sources/Common/RaceBundle.swift b/app/LandinhoLib/Sources/Common/RaceBundle.swift index ab71050..9bc7aaa 100644 --- a/app/LandinhoLib/Sources/Common/RaceBundle.swift +++ b/app/LandinhoLib/Sources/Common/RaceBundle.swift @@ -12,7 +12,23 @@ public struct RaceBundle: Codable, Equatable { self.category = category self.nextRace = nextRace } - + + public init() { + // init meant for placeholder widget views + category = RaceCategory(id: "", title: "Formula 1", tag: "") + nextRace = Race( + id: UUID(), + title: "Placeholder", + shortTitle: "Placeholder", + events: [ + .init(id: UUID(), title: "Placeholder", date: Date(), isMainEvent: false), + .init(id: UUID(), title: "Placeholder", date: Date(), isMainEvent: false), + .init(id: UUID(), title: "Placeholder", date: Date().advanced(by: 100000), isMainEvent: false), + .init(id: UUID(), title: "Placeholder", date: Date().advanced(by: 100000), isMainEvent: false), + .init(id: UUID(), title: "Placeholder", date: Date().advanced(by: 200000), isMainEvent: true), + ]) + } + public let category: RaceCategory public let nextRace: Race } diff --git a/app/LandinhoLib/Sources/WidgetUI/NextRaceExtraLargeWidgetView.swift b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceExtraLargeWidgetView.swift similarity index 92% rename from app/LandinhoLib/Sources/WidgetUI/NextRaceExtraLargeWidgetView.swift rename to app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceExtraLargeWidgetView.swift index d7580cd..d425c32 100644 --- a/app/LandinhoLib/Sources/WidgetUI/NextRaceExtraLargeWidgetView.swift +++ b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceExtraLargeWidgetView.swift @@ -13,7 +13,7 @@ import SwiftUI public struct NextRaceExtraLargeWidgetView: View { - let response: RaceBundle + let bundle: RaceBundle let lastUpdatedDate: Date public var body: some View { @@ -41,7 +41,7 @@ public struct NextRaceExtraLargeWidgetView: View { Text("Formula 1") .font(.callout) - Text(response.nextRace.title) + Text(bundle.nextRace.title) .font(.title3) } @@ -71,6 +71,6 @@ public struct NextRaceExtraLargeWidgetView: View { } var eventsByDate: [EventByDate] { - EventByDateFactory.convert(events: response.nextRace.events) + EventByDateFactory.convert(events: bundle.nextRace.events) } } diff --git a/app/LandinhoLib/Sources/WidgetUI/NextRaceLargeWidgetView.swift b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceLargeWidgetView.swift similarity index 84% rename from app/LandinhoLib/Sources/WidgetUI/NextRaceLargeWidgetView.swift rename to app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceLargeWidgetView.swift index 45b44d0..b9b194b 100644 --- a/app/LandinhoLib/Sources/WidgetUI/NextRaceLargeWidgetView.swift +++ b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceLargeWidgetView.swift @@ -11,20 +11,20 @@ import SwiftUI public struct NextRaceLargeWidgetView: View { - public init(response: RaceBundle, lastUpdatedDate: Date) { - self.response = response + public init(bundle: RaceBundle, lastUpdatedDate: Date) { + self.bundle = bundle self.lastUpdatedDate = lastUpdatedDate } - let response: RaceBundle + let bundle: RaceBundle let lastUpdatedDate: Date public var body: some View { VStack { VStack(alignment: .leading) { - Text(response.category.title) + Text(bundle.category.title) .font(.callout) - Text(response.nextRace.title) + Text(bundle.nextRace.title) .font(.title3) } .frame(maxWidth: .infinity, alignment: .leading) @@ -68,6 +68,6 @@ public struct NextRaceLargeWidgetView: View { } var eventsByDate: [EventByDate] { - EventByDateFactory.convert(events: response.nextRace.events) + EventByDateFactory.convert(events: bundle.nextRace.events) } } diff --git a/app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceMediumWidgetView.swift similarity index 81% rename from app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift rename to app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceMediumWidgetView.swift index bf67fed..e5c1816 100644 --- a/app/LandinhoLib/Sources/WidgetUI/NextRaceMediumWidgetView.swift +++ b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceMediumWidgetView.swift @@ -11,21 +11,21 @@ import SwiftUI public struct NextRaceMediumWidgetView: View { - public init(response: RaceBundle, lastUpdatedDate: Date) { - self.response = response + public init(bundle: RaceBundle, lastUpdatedDate: Date) { + self.bundle = bundle self.lastUpdatedDate = lastUpdatedDate } - let response: RaceBundle + let bundle: RaceBundle let lastUpdatedDate: Date public var body: some View { VStack { HStack { VStack(alignment: .leading) { - Text(response.category.title) + Text(bundle.category.title) .font(.callout) - Text(response.nextRace.shortTitle) + Text(bundle.nextRace.shortTitle) .font(.title3) } .frame(maxHeight: .infinity) @@ -57,7 +57,7 @@ public struct NextRaceMediumWidgetView: View { } var eventsByDate: [EventByDate] { - EventByDateFactory.convert(events: response.nextRace.events) + EventByDateFactory.convert(events: bundle.nextRace.events) } } diff --git a/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceSmallWidgetView+Intents.swift b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceSmallWidgetView+Intents.swift new file mode 100644 index 0000000..152946e --- /dev/null +++ b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceSmallWidgetView+Intents.swift @@ -0,0 +1,48 @@ +// +// NextEventIntent.swift +// +// +// Created by Mauricio Cardozo on 17/11/23. +// + +import Common +import Foundation +import AppIntents + +extension NextRaceSmallWidgetView { + struct NextEventIntent: AppIntent { + init(race: Race) { + eventCount = race.events.count + } + + init() { + fatalError("This init should not be called") + } + + let eventCount: Int + + func perform() async throws -> some IntentResult { + WidgetPositionManager.nextButtonTapped(maxEvents: eventCount) + return .result() + } + + static var title: LocalizedStringResource = "Next Event" + static var description: IntentDescription? = "Mostra o próximo evento de uma corrida" + } + + class WidgetPositionManager: ObservableObject { + static let live = WidgetPositionManager() + + @Published var currentPosition = 0 + + static func nextButtonTapped(maxEvents: Int) { + // TODO: take in the race and compare to the current one to reset + // TODO: reset if its the end of the array + if Self.live.currentPosition >= maxEvents - 1 { + Self.live.currentPosition = 0 + } else { + Self.live.currentPosition += 1 + } + } + } +} diff --git a/app/Widgets/NextRaceWidget/NextRaceSmallWidget.swift b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceSmallWidgetView.swift similarity index 69% rename from app/Widgets/NextRaceWidget/NextRaceSmallWidget.swift rename to app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceSmallWidgetView.swift index 039da31..cc051b8 100644 --- a/app/Widgets/NextRaceWidget/NextRaceSmallWidget.swift +++ b/app/LandinhoLib/Sources/WidgetUI/NextRace/NextRaceSmallWidgetView.swift @@ -1,28 +1,31 @@ // -// NextRaceWidget.swift -// VroomVroom +// File.swift +// // -// Created by Mauricio Cardozo on 16/11/23. +// Created by Mauricio Cardozo on 17/11/23. // -import ComposableArchitecture +import Common import Foundation import SwiftUI import WidgetKit -import ScheduleList -import Common -struct NextRaceSmallWidgetView: View { - @ObservedObject var positionManager = WidgetPositionManager.live +public struct NextRaceSmallWidgetView: View { + + public init(bundle: RaceBundle, lastUpdatedDate: Date) { + self.bundle = bundle + self.lastUpdatedDate = lastUpdatedDate + } - let response: ScheduleList.ScheduleListResponse + @ObservedObject var positionManager = WidgetPositionManager.live + let bundle: RaceBundle let lastUpdatedDate: Date - var body: some View { + public var body: some View { VStack(alignment: .leading) { - Text(response.category.title) + Text(bundle.category.title) .font(.callout) - Text(response.nextRace.shortTitle) + Text(bundle.nextRace.shortTitle) .font(.title3) Spacer() @@ -35,7 +38,7 @@ struct NextRaceSmallWidgetView: View { } .frame(maxWidth: .infinity, alignment: .trailing) HStack { - Button(intent: NextEventIntent(race: response.nextRace)) { + Button(intent: NextEventIntent(race: bundle.nextRace)) { Image(systemName: "chevron.right") } Spacer() @@ -54,11 +57,11 @@ struct NextRaceSmallWidgetView: View { // TODO: Bring this to `EventByDate` to standardize date formatting var currentEvent: RaceEvent? { - guard response.nextRace.events.indices.contains(positionManager.currentPosition) else { + guard bundle.nextRace.events.indices.contains(positionManager.currentPosition) else { return nil } - return response.nextRace.events[positionManager.currentPosition] + return bundle.nextRace.events[positionManager.currentPosition] } var currentEventDate: String { @@ -89,13 +92,3 @@ struct NextRaceSmallWidgetView: View { ) } } - -#Preview(as: .systemSmall) { - NextRaceWidget() -} timeline: { - NextRaceEntry(date: Date()) - NextRaceEntry.empty - NextRaceEntry.placeholder -} - - diff --git a/app/VroomVroom.xcodeproj/project.pbxproj b/app/VroomVroom.xcodeproj/project.pbxproj index 1398e13..b152fc8 100644 --- a/app/VroomVroom.xcodeproj/project.pbxproj +++ b/app/VroomVroom.xcodeproj/project.pbxproj @@ -15,8 +15,6 @@ B450025E2AFE91F1003CDE6A /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = B450025D2AFE91F1003CDE6A /* Common */; }; B453CE022B07B07F00B0CEA5 /* WidgetUI in Frameworks */ = {isa = PBXBuildFile; productRef = B453CE012B07B07F00B0CEA5 /* WidgetUI */; }; B453CE042B07B08700B0CEA5 /* WidgetUI in Frameworks */ = {isa = PBXBuildFile; productRef = B453CE032B07B08700B0CEA5 /* WidgetUI */; }; - B4877C222B0692C30009760C /* NextRaceSmallWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C1E2B0692240009760C /* NextRaceSmallWidget.swift */; }; - B4877C232B0692C60009760C /* NextEventIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C202B06928F0009760C /* NextEventIntent.swift */; }; B4877C272B0692F40009760C /* NextRaceTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */; }; B4877C2A2B0694350009760C /* NextRaceEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C292B0694350009760C /* NextRaceEntry.swift */; }; B4877C342B06B4C60009760C /* NextRaceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C332B06B4C60009760C /* NextRaceWidget.swift */; }; @@ -70,8 +68,6 @@ B42CA9F02AFE8B67009B3FFB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B42CA9F32AFE8B67009B3FFB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; B45002592AFE91A1003CDE6A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - B4877C1E2B0692240009760C /* NextRaceSmallWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceSmallWidget.swift; sourceTree = ""; }; - B4877C202B06928F0009760C /* NextEventIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextEventIntent.swift; sourceTree = ""; }; B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceTimelineProvider.swift; sourceTree = ""; }; B4877C292B0694350009760C /* NextRaceEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceEntry.swift; sourceTree = ""; }; B4877C332B06B4C60009760C /* NextRaceWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceWidget.swift; sourceTree = ""; }; @@ -176,8 +172,6 @@ isa = PBXGroup; children = ( B4877C332B06B4C60009760C /* NextRaceWidget.swift */, - B4877C1E2B0692240009760C /* NextRaceSmallWidget.swift */, - B4877C202B06928F0009760C /* NextEventIntent.swift */, B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */, B4877C292B0694350009760C /* NextRaceEntry.swift */, ); @@ -329,9 +323,7 @@ B4877C2A2B0694350009760C /* NextRaceEntry.swift in Sources */, B4CBD1792B05570F00BF6776 /* AppIntent.swift in Sources */, B4877C342B06B4C60009760C /* NextRaceWidget.swift in Sources */, - B4877C232B0692C60009760C /* NextEventIntent.swift in Sources */, B4CBD1732B05570F00BF6776 /* WidgetsBundle.swift in Sources */, - B4877C222B0692C30009760C /* NextRaceSmallWidget.swift in Sources */, B4877C272B0692F40009760C /* NextRaceTimelineProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/app/Widgets/NextRaceWidget/NextEventIntent.swift b/app/Widgets/NextRaceWidget/NextEventIntent.swift deleted file mode 100644 index b6298cf..0000000 --- a/app/Widgets/NextRaceWidget/NextEventIntent.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// NextEventIntent.swift -// VroomVroom -// -// Created by Mauricio Cardozo on 16/11/23. -// - -import Common -import Foundation -import AppIntents - -struct NextEventIntent: AppIntent { - init(race: Race) { - eventCount = race.events.count - } - - init() { - fatalError("This init should not be called") - } - - let eventCount: Int - - func perform() async throws -> some IntentResult { - WidgetPositionManager.nextButtonTapped(maxEvents: eventCount) - return .result() - } - - static var title: LocalizedStringResource = "Next Event" - static var description: IntentDescription? = "Mostra o próximo evento de uma corrida" -} - -class WidgetPositionManager: ObservableObject { - static let live = WidgetPositionManager() - - @Published var currentPosition = 0 - - static func nextButtonTapped(maxEvents: Int) { - // TODO: take in the race and compare to the current one to reset - // TODO: reset if its the end of the array - if Self.live.currentPosition >= maxEvents - 1 { - Self.live.currentPosition = 0 - } else { - Self.live.currentPosition += 1 - } - } -} diff --git a/app/Widgets/NextRaceWidget/NextRaceWidget.swift b/app/Widgets/NextRaceWidget/NextRaceWidget.swift index 84a346d..d007ed4 100644 --- a/app/Widgets/NextRaceWidget/NextRaceWidget.swift +++ b/app/Widgets/NextRaceWidget/NextRaceWidget.swift @@ -5,13 +5,13 @@ // Created by Mauricio Cardozo on 16/11/23. // +import Common import ComposableArchitecture import Foundation +import ScheduleList import SwiftUI import WidgetKit import WidgetUI -import ScheduleList -import Common // TODO: extract views to a module so we can visualize them in the full app @@ -22,12 +22,13 @@ struct NextRaceWidget: Widget { provider: NextRaceTimelineProvider()) { entry in Group { if let response = entry.response { + #warning("TODO remove RaceBundle initializer") NextRaceWidgetView( - response: response, + bundle: .init(category: response.category, nextRace: response.nextRace), lastUpdatedDate: entry.date) } else { NextRaceWidgetView( - response: .init(), + bundle: .init(), lastUpdatedDate: entry.date) .redacted(reason: .placeholder) } @@ -43,28 +44,22 @@ struct NextRaceWidget: Widget { struct NextRaceWidgetView: View { @Environment(\.widgetFamily) var family - let response: ScheduleList.ScheduleListResponse + let bundle: RaceBundle let lastUpdatedDate: Date var body: some View { switch family { case .systemSmall: NextRaceSmallWidgetView( - response: response, + bundle: bundle, lastUpdatedDate: lastUpdatedDate) case .systemMedium: - #warning("TODO remove RaceBundle initializer") NextRaceMediumWidgetView( - response: .init( - category: response.category, - nextRace: response.nextRace), + bundle: bundle, lastUpdatedDate: lastUpdatedDate) case .systemLarge: - #warning("TODO remove RaceBundle initializer") NextRaceLargeWidgetView( - response: .init( - category: response.category, - nextRace: response.nextRace), + bundle: bundle, lastUpdatedDate: lastUpdatedDate) case .systemExtraLarge, .accessoryCircular, .accessoryRectangular, .accessoryInline: // TODO: Accessory Widgets @@ -75,6 +70,16 @@ struct NextRaceWidgetView: View { } } +// MARK: Previews + +#Preview(as: .systemSmall) { + NextRaceWidget() +} timeline: { + NextRaceEntry(date: Date()) + NextRaceEntry.empty + NextRaceEntry.placeholder +} + #Preview(as: .systemSmall) { NextRaceWidget() } timeline: { From 99ef1ff9febb139295410d099e669130500df37a Mon Sep 17 00:00:00 2001 From: Mauricio Cardozo Date: Fri, 17 Nov 2023 15:22:49 -0300 Subject: [PATCH 4/8] removes schedulelist from widgets --- app/LandinhoLib/Package.swift | 10 ++++- .../Sources/ScheduleList/ScheduleList.swift | 38 +++------------- app/LandinhoLib/Sources/Widgets/Widgets.swift | 44 +++++++++++++++++++ app/VroomVroom.xcodeproj/project.pbxproj | 14 +++--- .../NextRaceWidget/NextRaceEntry.swift | 6 ++- .../NextRaceTimelineProvider.swift | 21 ++++++--- .../NextRaceWidget/NextRaceWidget.swift | 31 ++++++++----- backend/Sources/App/pages/next-race.swift | 16 ++++--- 8 files changed, 115 insertions(+), 65 deletions(-) create mode 100644 app/LandinhoLib/Sources/Widgets/Widgets.swift diff --git a/app/LandinhoLib/Package.swift b/app/LandinhoLib/Package.swift index aaa3758..9fb4c1c 100644 --- a/app/LandinhoLib/Package.swift +++ b/app/LandinhoLib/Package.swift @@ -24,6 +24,7 @@ let package = Package( .library(name: "RacesAdmin", targets: ["RacesAdmin"]), .library(name: "ScheduleList", targets: ["ScheduleList"]), .library(name: "Settings", targets: ["Settings"]), + .library(name: "Widgets", targets: ["Widgets"]), .library(name: "WidgetUI", targets: ["WidgetUI"]), ], dependencies: [ @@ -113,10 +114,17 @@ let package = Package( ]), .target( - name: "WidgetUI", + name: "Widgets", dependencies: [ "Common", + "APIClient", composable ]), + + .target( + name: "WidgetUI", + dependencies: [ + "Common" + ]), ] ) diff --git a/app/LandinhoLib/Sources/ScheduleList/ScheduleList.swift b/app/LandinhoLib/Sources/ScheduleList/ScheduleList.swift index ce14711..ed8bb42 100644 --- a/app/LandinhoLib/Sources/ScheduleList/ScheduleList.swift +++ b/app/LandinhoLib/Sources/ScheduleList/ScheduleList.swift @@ -11,6 +11,7 @@ import Foundation import ComposableArchitecture import SwiftUI +// TODO: Fetch next races, paginate public struct ScheduleList: Reducer { public init() {} @@ -21,14 +22,12 @@ public struct ScheduleList: Reducer { } let categoryTag: String? - - public var racesState = APIClient.State(endpoint: "next-race") + public var racesState = APIClient.State(endpoint: "next-race") } public enum Action: Equatable { case onAppear - - case racesRequest(APIClient.Action) + case racesRequest(APIClient.Action) } public var body: some ReducerOf { @@ -45,32 +44,6 @@ public struct ScheduleList: Reducer { APIClient() } } - - public struct ScheduleListResponse: Codable, Equatable { - public init(category: RaceCategory, nextRace: Race) { - self.category = category - self.nextRace = nextRace - } - - public init() { - // init for placeholder widget views - category = RaceCategory(id: "", title: "Formula 1", tag: "") - nextRace = Race( - id: UUID(), - title: "Placeholder", - shortTitle: "Placeholder", - events: [ - .init(id: UUID(), title: "Placeholder", date: Date(), isMainEvent: false), - .init(id: UUID(), title: "Placeholder", date: Date(), isMainEvent: false), - .init(id: UUID(), title: "Placeholder", date: Date().advanced(by: 100000), isMainEvent: false), - .init(id: UUID(), title: "Placeholder", date: Date().advanced(by: 100000), isMainEvent: false), - .init(id: UUID(), title: "Placeholder", date: Date().advanced(by: 200000), isMainEvent: true), - ]) - } - - public let category: RaceCategory - public let nextRace: Race - } } public struct ScheduleListView: View { @@ -102,13 +75,12 @@ public struct ScheduleListView: View { } } -// TODO: This should not take in an 'internal' model public struct ScheduleListItem: View { - public init(_ response: ScheduleList.ScheduleListResponse) { + public init(_ response: RaceBundle) { self.response = response } - let response: ScheduleList.ScheduleListResponse + let response: RaceBundle public var body: some View { VStack(alignment: .leading) { diff --git a/app/LandinhoLib/Sources/Widgets/Widgets.swift b/app/LandinhoLib/Sources/Widgets/Widgets.swift new file mode 100644 index 0000000..96525ed --- /dev/null +++ b/app/LandinhoLib/Sources/Widgets/Widgets.swift @@ -0,0 +1,44 @@ +// +// Widgets.swift +// +// +// Created by Mauricio Cardozo on 17/11/23. +// + +import APIClient +import Common +import ComposableArchitecture +import Foundation + +public struct Widgets: Reducer { + public init() {} + + public struct State: Equatable { + public init(shouldFilterNonMainEvents: Bool, categoryTag: String?) { + self.shouldFilterNonMainEvents = shouldFilterNonMainEvents + self.categoryTag = categoryTag + } + + let categoryTag: String? + // TODO: Filter non-main events + let shouldFilterNonMainEvents: Bool + public var racesState = APIClient.State(endpoint: "next-race") + } + + public enum Action: Equatable { + case racesRequest(APIClient.Action) + } + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .racesRequest: + return .none + } + } + Scope(state: \.racesState, action: /Action.racesRequest) { + APIClient() + } + } +} + diff --git a/app/VroomVroom.xcodeproj/project.pbxproj b/app/VroomVroom.xcodeproj/project.pbxproj index b152fc8..ae3cc68 100644 --- a/app/VroomVroom.xcodeproj/project.pbxproj +++ b/app/VroomVroom.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ B450025E2AFE91F1003CDE6A /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = B450025D2AFE91F1003CDE6A /* Common */; }; B453CE022B07B07F00B0CEA5 /* WidgetUI in Frameworks */ = {isa = PBXBuildFile; productRef = B453CE012B07B07F00B0CEA5 /* WidgetUI */; }; B453CE042B07B08700B0CEA5 /* WidgetUI in Frameworks */ = {isa = PBXBuildFile; productRef = B453CE032B07B08700B0CEA5 /* WidgetUI */; }; + B453CE082B07DDBE00B0CEA5 /* Widgets in Frameworks */ = {isa = PBXBuildFile; productRef = B453CE072B07DDBE00B0CEA5 /* Widgets */; }; B4877C272B0692F40009760C /* NextRaceTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C262B0692F40009760C /* NextRaceTimelineProvider.swift */; }; B4877C2A2B0694350009760C /* NextRaceEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C292B0694350009760C /* NextRaceEntry.swift */; }; B4877C342B06B4C60009760C /* NextRaceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C332B06B4C60009760C /* NextRaceWidget.swift */; }; @@ -30,7 +31,6 @@ B4CBD17B2B05571100BF6776 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B4CBD17A2B05571100BF6776 /* Assets.xcassets */; }; B4CBD17F2B05571100BF6776 /* WidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B4CBD16C2B05570F00BF6776 /* WidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B4CBD1AB2B05582000BF6776 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = B4CBD1AA2B05582000BF6776 /* Common */; }; - B4CBD1AD2B05582300BF6776 /* ScheduleList in Frameworks */ = {isa = PBXBuildFile; productRef = B4CBD1AC2B05582300BF6776 /* ScheduleList */; }; B4D87C482B01AF07000EBEF9 /* Categories in Frameworks */ = {isa = PBXBuildFile; productRef = B4D87C472B01AF07000EBEF9 /* Categories */; }; B4D87C4A2B01AF09000EBEF9 /* ScheduleList in Frameworks */ = {isa = PBXBuildFile; productRef = B4D87C492B01AF09000EBEF9 /* ScheduleList */; }; B4D87C4C2B01AF0B000EBEF9 /* Settings in Frameworks */ = {isa = PBXBuildFile; productRef = B4D87C4B2B01AF0B000EBEF9 /* Settings */; }; @@ -107,8 +107,8 @@ buildActionMask = 2147483647; files = ( B4CBD1702B05570F00BF6776 /* SwiftUI.framework in Frameworks */, - B4CBD1AD2B05582300BF6776 /* ScheduleList in Frameworks */, B4CBD16E2B05570F00BF6776 /* WidgetKit.framework in Frameworks */, + B453CE082B07DDBE00B0CEA5 /* Widgets in Frameworks */, B4CBD1AB2B05582000BF6776 /* Common in Frameworks */, B453CE042B07B08700B0CEA5 /* WidgetUI in Frameworks */, ); @@ -240,8 +240,8 @@ name = WidgetsExtension; packageProductDependencies = ( B4CBD1AA2B05582000BF6776 /* Common */, - B4CBD1AC2B05582300BF6776 /* ScheduleList */, B453CE032B07B08700B0CEA5 /* WidgetUI */, + B453CE072B07DDBE00B0CEA5 /* Widgets */, ); productName = WidgetsExtension; productReference = B4CBD16C2B05570F00BF6776 /* WidgetsExtension.appex */; @@ -627,6 +627,10 @@ isa = XCSwiftPackageProductDependency; productName = WidgetUI; }; + B453CE072B07DDBE00B0CEA5 /* Widgets */ = { + isa = XCSwiftPackageProductDependency; + productName = Widgets; + }; B4CBD1622B0401C100BF6776 /* Home */ = { isa = XCSwiftPackageProductDependency; productName = Home; @@ -643,10 +647,6 @@ isa = XCSwiftPackageProductDependency; productName = Common; }; - B4CBD1AC2B05582300BF6776 /* ScheduleList */ = { - isa = XCSwiftPackageProductDependency; - productName = ScheduleList; - }; B4D87C472B01AF07000EBEF9 /* Categories */ = { isa = XCSwiftPackageProductDependency; productName = Categories; diff --git a/app/Widgets/NextRaceWidget/NextRaceEntry.swift b/app/Widgets/NextRaceWidget/NextRaceEntry.swift index 6a2ef52..5bfd0d8 100644 --- a/app/Widgets/NextRaceWidget/NextRaceEntry.swift +++ b/app/Widgets/NextRaceWidget/NextRaceEntry.swift @@ -5,13 +5,15 @@ // Created by Mauricio Cardozo on 16/11/23. // +import Common import Foundation import WidgetKit -import ScheduleList +import Widgets struct NextRaceEntry: TimelineEntry { var date: Date - var response: ScheduleList.ScheduleListResponse? + var response: RaceBundle + var error: Error? } extension NextRaceEntry { diff --git a/app/Widgets/NextRaceWidget/NextRaceTimelineProvider.swift b/app/Widgets/NextRaceWidget/NextRaceTimelineProvider.swift index 77aac93..7ad02b6 100644 --- a/app/Widgets/NextRaceWidget/NextRaceTimelineProvider.swift +++ b/app/Widgets/NextRaceWidget/NextRaceTimelineProvider.swift @@ -7,15 +7,17 @@ import ComposableArchitecture import Foundation -import ScheduleList +import Widgets import WidgetKit -// TODO: We should remove ScheduleList from this. Completely different use cases. - struct NextRaceTimelineProvider: AppIntentTimelineProvider { - let store = Store(initialState: ScheduleList.State(categoryTag: nil)) { - ScheduleList() + enum TimelineError: LocalizedError { + case failure + } + + let store = Store(initialState: Widgets.State(shouldFilterNonMainEvents: false, categoryTag: nil)) { + Widgets() } func placeholder(in context: Context) -> NextRaceEntry { @@ -36,7 +38,14 @@ struct NextRaceTimelineProvider: AppIntentTimelineProvider { await viewStore.send(.racesRequest(.request(.get))).finish() guard let nextRace = viewStore.racesState.response.value else { - return Timeline(entries: [], policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + response: .init(), + error: TimelineError.failure) + ], + policy: .atEnd) } let entries: [NextRaceEntry] = [ diff --git a/app/Widgets/NextRaceWidget/NextRaceWidget.swift b/app/Widgets/NextRaceWidget/NextRaceWidget.swift index d007ed4..5c8a715 100644 --- a/app/Widgets/NextRaceWidget/NextRaceWidget.swift +++ b/app/Widgets/NextRaceWidget/NextRaceWidget.swift @@ -8,23 +8,20 @@ import Common import ComposableArchitecture import Foundation -import ScheduleList import SwiftUI +import Widgets import WidgetKit import WidgetUI -// TODO: extract views to a module so we can visualize them in the full app - struct NextRaceWidget: Widget { var body: some WidgetConfiguration { AppIntentConfiguration( kind: "NextRaceWidget", provider: NextRaceTimelineProvider()) { entry in Group { - if let response = entry.response { - #warning("TODO remove RaceBundle initializer") + if entry.error == nil { NextRaceWidgetView( - bundle: .init(category: response.category, nextRace: response.nextRace), + bundle: entry.response, lastUpdatedDate: entry.date) } else { NextRaceWidgetView( @@ -70,12 +67,15 @@ struct NextRaceWidgetView: View { } } -// MARK: Previews +// MARK: - Previews #Preview(as: .systemSmall) { NextRaceWidget() } timeline: { - NextRaceEntry(date: Date()) + NextRaceEntry( + date: Date(), + response: .init(), + error: NextRaceTimelineProvider.TimelineError.failure) NextRaceEntry.empty NextRaceEntry.placeholder } @@ -83,7 +83,10 @@ struct NextRaceWidgetView: View { #Preview(as: .systemSmall) { NextRaceWidget() } timeline: { - NextRaceEntry(date: Date()) + NextRaceEntry( + date: Date(), + response: .init(), + error: NextRaceTimelineProvider.TimelineError.failure) NextRaceEntry.empty NextRaceEntry.placeholder } @@ -91,7 +94,10 @@ struct NextRaceWidgetView: View { #Preview(as: .systemMedium) { NextRaceWidget() } timeline: { - NextRaceEntry(date: Date()) + NextRaceEntry( + date: Date(), + response: .init(), + error: NextRaceTimelineProvider.TimelineError.failure) NextRaceEntry.empty NextRaceEntry.placeholder } @@ -99,7 +105,10 @@ struct NextRaceWidgetView: View { #Preview(as: .systemLarge) { NextRaceWidget() } timeline: { - NextRaceEntry(date: Date()) + NextRaceEntry( + date: Date(), + response: .init(), + error: NextRaceTimelineProvider.TimelineError.failure) NextRaceEntry.empty NextRaceEntry.placeholder } diff --git a/backend/Sources/App/pages/next-race.swift b/backend/Sources/App/pages/next-race.swift index 4fe79f6..67919f4 100644 --- a/backend/Sources/App/pages/next-race.swift +++ b/backend/Sources/App/pages/next-race.swift @@ -35,20 +35,26 @@ struct NextRaceHandler: AsyncRequestHandler { query.filter(Category.self, \.$tag, .equal, args) } - let nextRace = try await query.first() + guard let nextRace = try await query.first() else { + throw Abort(.notFound) + } return NextRaceResponse( nextRace: nextRace, - category: nextRace?.category) + category: nextRace.category) } +} + +// MARK: - Request/Response + +extension NextRaceHandler { struct NextRaceRequest: Content { let argument: String } struct NextRaceResponse: Content { - let nextRace: Race? - let category: Category? + let nextRace: Race + let category: Category } } - From c15e06518f45e423b671354b3c1f85a268e8fc6a Mon Sep 17 00:00:00 2001 From: Mauricio Cardozo Date: Fri, 17 Nov 2023 16:12:53 -0300 Subject: [PATCH 5/8] updates tca --- app/LandinhoLib/Package.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 21 +++++++++++++------ app/VroomVroom/RootView.swift | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/LandinhoLib/Package.swift b/app/LandinhoLib/Package.swift index 9fb4c1c..8db9c8d 100644 --- a/app/LandinhoLib/Package.swift +++ b/app/LandinhoLib/Package.swift @@ -30,7 +30,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/pointfreeco/swift-composable-architecture", - from: Version(1, 0, 0)), + from: Version(1, 4, 2)), ], targets: [ .target( diff --git a/app/VroomVroom.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/app/VroomVroom.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f827e57..9105051 100644 --- a/app/VroomVroom.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/app/VroomVroom.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", - "version" : "1.0.0" + "revision" : "ed7facdd4a361514b46e3bbc6238cd41c84be4ec", + "version" : "1.1.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "revision" : "9b0f600253f467f61cbd53f60ccc243cc4ff27cd", - "version" : "1.3.0" + "revision" : "eddc1869b37ce42e3b34e72b1e1355d049e73970", + "version" : "1.4.2" } }, { @@ -81,13 +81,22 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version" : "509.0.2" + } + }, { "identity" : "swiftui-navigation", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation", "state" : { - "revision" : "74adfb8e48724c50d0ac148c658ae5fa7dfd6b6d", - "version" : "1.0.3" + "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", + "version" : "1.2.0" } }, { diff --git a/app/VroomVroom/RootView.swift b/app/VroomVroom/RootView.swift index 7cb41b0..f13ae37 100644 --- a/app/VroomVroom/RootView.swift +++ b/app/VroomVroom/RootView.swift @@ -21,7 +21,7 @@ struct RootView: View { store: store.scope(state: \.homeState, action: Root.Action.home) ) .tabItem { - Label("Home", systemImage: "house") + Label("Home", systemImage: "flag.checkered") } CategoriesView( From 7b986864ae8bf520837456405e22d0a093fda84d Mon Sep 17 00:00:00 2001 From: Mauricio Cardozo Date: Fri, 17 Nov 2023 17:14:56 -0300 Subject: [PATCH 6/8] refactors admin navigation --- app/LandinhoLib/Package.swift | 23 ++-- app/LandinhoLib/Sources/Admin/Admin.swift | 100 ++---------------- .../CategoriesAdmin/CategoriesAdmin.swift | 95 +++++++++-------- .../Views/CategoriesAdminView.swift | 20 ++-- .../Sources/EventsAdmin/EventsAdmin.swift | 36 +------ .../EventsAdmin/UploadRaceEventBundle.swift | 52 +++++++++ .../EventsAdmin/Views/EventsAdminView.swift | 22 ++-- .../Sources/RacesAdmin/RacesAdmin.swift | 57 +++++++--- .../RacesAdmin/Views/RacesAdminView.swift | 31 ++++-- backend/Sources/App/pages/race.swift | 7 ++ 10 files changed, 216 insertions(+), 227 deletions(-) create mode 100644 app/LandinhoLib/Sources/EventsAdmin/UploadRaceEventBundle.swift diff --git a/app/LandinhoLib/Package.swift b/app/LandinhoLib/Package.swift index 8db9c8d..7614e0f 100644 --- a/app/LandinhoLib/Package.swift +++ b/app/LandinhoLib/Package.swift @@ -40,11 +40,11 @@ let package = Package( composable ]), - .target( - name: "APIClient", - dependencies: [ - composable - ]), + .target( + name: "APIClient", + dependencies: [ + composable + ]), .target( name: "Common"), @@ -61,17 +61,16 @@ let package = Package( dependencies: [ "APIClient", "Common", - "EventsAdmin", "RacesAdmin", composable ]), - .target( - name: "EventDetail", - dependencies: [ - "APIClient", - composable - ]), + .target( + name: "EventDetail", + dependencies: [ + "APIClient", + composable + ]), .target( name: "EventsAdmin", diff --git a/app/LandinhoLib/Sources/Admin/Admin.swift b/app/LandinhoLib/Sources/Admin/Admin.swift index 60a4aee..d8dcaf7 100644 --- a/app/LandinhoLib/Sources/Admin/Admin.swift +++ b/app/LandinhoLib/Sources/Admin/Admin.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// Admin.swift +// // // Created by Mauricio Cardozo on 14/11/23. // @@ -8,8 +8,8 @@ import ComposableArchitecture import Foundation import CategoriesAdmin -import EventsAdmin -import RacesAdmin + +// This layer is reserved for future use public struct Admin: Reducer { public init() {} @@ -17,87 +17,20 @@ public struct Admin: Reducer { public struct State: Equatable { public init() {} - var path = StackState() - var categoriesAdminState = CategoriesAdmin.State() } public enum Action: Equatable { case categoriesAdmin(CategoriesAdmin.Action) - - // TODO: This should probably be refactored to tree-based navigation, no point in keeping it a stack - // but also no point in refactoring it now - case path(StackAction) } public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .categoriesAdmin(.onCategoryTap(let id)): - guard - let categories = state.categoriesAdminState.categoryList.response.value, - let selectedCategory = categories.first(where: { $0.id == id }) - else { - return .none - } - - state.path.append( - .racesAdmin( - .init(title: selectedCategory.title, tag: selectedCategory.tag) - ) - ) - return .none - - case .path(.element(id: _, action: .racesAdmin(.onRaceTap(let race)))): - state.path.append( - .eventsAdmin( - .init( - id: race.id, - title: race.title) - ) - ) - return .none - - case .categoriesAdmin, .path: - return .none - } - } - .forEach(\.path, action: /Action.path) { - Path() - } - Scope(state: \.categoriesAdminState, action: /Action.categoriesAdmin) { CategoriesAdmin() } } - - public struct Path: Reducer { - public init() {} - - public enum State: Equatable { - case racesAdmin(RacesAdmin.State) - case eventsAdmin(EventsAdmin.State) - } - - public enum Action: Equatable { - case racesAdmin(RacesAdmin.Action) - case eventsAdmin(EventsAdmin.Action) - } - - public var body: some ReducerOf { - Scope(state: /State.racesAdmin, action: /Action.racesAdmin) { - RacesAdmin() - } - - Scope(state: /State.eventsAdmin, action: /Action.eventsAdmin) { - EventsAdmin() - } - } - } } - - import SwiftUI public struct AdminView: View { @@ -108,27 +41,8 @@ public struct AdminView: View { let store: StoreOf public var body: some View { - NavigationStackStore(store.scope(state: \.path, action: Admin.Action.path)) { - CategoriesAdminView(store: store.scope( - state: \.categoriesAdminState, - action: Admin.Action.categoriesAdmin)) - } destination: { state in - switch state { - case .racesAdmin: - CaseLet( - /Admin.Path.State.racesAdmin, - action: Admin.Path.Action.racesAdmin, - then: RacesAdminView.init(store:) - ) - - case .eventsAdmin: - CaseLet( - /Admin.Path.State.eventsAdmin, - action: Admin.Path.Action.eventsAdmin, - then: EventsAdminView.init(store:) - ) - - } - } + CategoriesAdminView(store: store.scope( + state: \.categoriesAdminState, + action: Admin.Action.categoriesAdmin)) } } diff --git a/app/LandinhoLib/Sources/CategoriesAdmin/CategoriesAdmin.swift b/app/LandinhoLib/Sources/CategoriesAdmin/CategoriesAdmin.swift index 16b4d4b..ea856b6 100644 --- a/app/LandinhoLib/Sources/CategoriesAdmin/CategoriesAdmin.swift +++ b/app/LandinhoLib/Sources/CategoriesAdmin/CategoriesAdmin.swift @@ -9,19 +9,47 @@ import APIClient import Common import ComposableArchitecture import Foundation -import EventsAdmin import RacesAdmin -public struct CategoriesAdmin: Reducer { +import EventsAdmin + +extension CategoriesAdmin { + @Reducer + public struct Destination { + public init() {} + + public enum State: Equatable { + case categoryEditor(CategoryEditor.State) + case racesAdmin(RacesAdmin.State) + } + + public enum Action: Equatable { + case categoryEditor(CategoryEditor.Action) + case racesAdmin(RacesAdmin.Action) + } + + public var body: some ReducerOf { + Scope(state: \.categoryEditor, action: \.categoryEditor) { + CategoryEditor() + } + + Scope(state: \.racesAdmin, action: \.racesAdmin) { + RacesAdmin() + } + } + } +} + +@Reducer +public struct CategoriesAdmin { public init() {} public struct State: Equatable { public init() {} - @PresentationState var categoryEditorState: CategoryEditor.State? + @PresentationState var destination: Destination.State? public var categoryList = APIClient<[RaceCategory]>.State(endpoint: "category") - var path = StackState() } public enum Action: Equatable { @@ -31,8 +59,7 @@ public struct CategoriesAdmin: Reducer { case onCategoryEditorTap(String) case categoryRequest(APIClient<[RaceCategory]>.Action) - case categoryEditor(PresentationAction) - case path(StackAction) + case destination(PresentationAction) } public var body: some ReducerOf { @@ -43,13 +70,22 @@ public struct CategoriesAdmin: Reducer { await send(.categoryRequest(.request(.get))) } - case .onCategoryTap: - // TODO: Add a DelegateAction - // Used to let parent feature know which one has been tapped + case .onCategoryTap(let id): + guard + let categories = state.categoryList.response.value, + let selectedCategory = categories.first(where: { $0.id == id }) + else { + return .none + } + + state.destination = .racesAdmin( + .init( + title: selectedCategory.title, + tag: selectedCategory.tag)) return .none case .onPlusTap: - state.categoryEditorState = .init() + state.destination = .categoryEditor(.init()) return .none case .onCategoryEditorTap(let id): @@ -60,52 +96,25 @@ public struct CategoriesAdmin: Reducer { return .none } - state.categoryEditorState = .init(category: selectedCategory) + state.destination = .categoryEditor(.init(category: selectedCategory)) return .none - case .categoryEditor(.presented(.categoryRequest(.response(.finished(.success))))): + case .destination(.presented(.categoryEditor(.categoryRequest(.response(.finished(.success)))))): return .merge( .send(.onAppear), - .send(.categoryEditor(.dismiss)) + .send(.destination(.dismiss)) ) - case .categoryRequest, .categoryEditor, .path: + case .categoryRequest, .destination: return .none } } - .ifLet(\.$categoryEditorState, action: /Action.categoryEditor) { - CategoryEditor() - } - .forEach(\.path, action: /Action.path) { - Path() + .ifLet(\.$destination, action: \.destination) { + Destination() } Scope(state: \.categoryList, action: /Action.categoryRequest) { APIClient() } } - - public struct Path: Reducer { - public init() {} - - public enum State: Equatable { - case races(RacesAdmin.State) - case events(EventsAdmin.State) - } - - public enum Action: Equatable { - case races(RacesAdmin.Action) - case events(EventsAdmin.Action) - } - - public var body: some ReducerOf { - Scope(state: /State.races, action: /Action.races) { - RacesAdmin() - } - - Scope(state: /State.events, action: /Action.events) { - EventsAdmin() - } - } - } } diff --git a/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoriesAdminView.swift b/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoriesAdminView.swift index 490d83c..321191d 100644 --- a/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoriesAdminView.swift +++ b/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoriesAdminView.swift @@ -9,6 +9,7 @@ import APIClient import ComposableArchitecture import Foundation import SwiftUI +import RacesAdmin public struct CategoriesAdminView: View { public init(store: StoreOf) { @@ -73,12 +74,19 @@ public struct CategoriesAdminView: View { } } .sheet( - store: store - .scope( - state: { $0.$categoryEditorState }, - action: CategoriesAdmin.Action.categoryEditor)) { store in - CategoryEditorView(store: store) - } + store: store.scope(state: \.$destination, action: { .destination($0) }), + state: \.categoryEditor, + action: { .categoryEditor($0) } + ) { store in + CategoryEditorView(store: store) + } + .navigationDestination( + store: store.scope(state: \.$destination, action: { .destination($0) }), + state: \.racesAdmin, + action: { .racesAdmin($0) } + ) { store in + RacesAdminView(store: store) + } } } diff --git a/app/LandinhoLib/Sources/EventsAdmin/EventsAdmin.swift b/app/LandinhoLib/Sources/EventsAdmin/EventsAdmin.swift index 69384f4..ce6ad4e 100644 --- a/app/LandinhoLib/Sources/EventsAdmin/EventsAdmin.swift +++ b/app/LandinhoLib/Sources/EventsAdmin/EventsAdmin.swift @@ -24,40 +24,8 @@ public struct UploadRaceEvent: Codable, Equatable, Identifiable { var isMainEvent: Bool } -public enum UploadRaceEventBundle: Equatable { - case F1Sprint - case F1Regular - - var tag: String { - switch self { - case .F1Sprint, .F1Regular: - "f1" - } - } - - var events: [UploadRaceEvent] { - switch self { - case .F1Sprint: - [ - .init(title: "Treino Livre", date: Date(), isMainEvent: false), - .init(title: "Classificação", date: Date(), isMainEvent: false), - .init(title: "Sprint Shootout", date: Date(), isMainEvent: false), - .init(title: "Sprint", date: Date(), isMainEvent: true), - .init(title: "Corrida", date: Date(), isMainEvent: true), - ] - case .F1Regular: - [ - .init(title: "Treino Livre 1", date: Date(), isMainEvent: false), - .init(title: "Treino Livre 2", date: Date(), isMainEvent: false), - .init(title: "Treino Livre 3", date: Date(), isMainEvent: false), - .init(title: "Classificação", date: Date(), isMainEvent: false), - .init(title: "Corrida", date: Date(), isMainEvent: true), - ] - } - } -} - -public struct EventsAdmin: Reducer { +@Reducer +public struct EventsAdmin { public init() {} public struct State: Equatable { diff --git a/app/LandinhoLib/Sources/EventsAdmin/UploadRaceEventBundle.swift b/app/LandinhoLib/Sources/EventsAdmin/UploadRaceEventBundle.swift new file mode 100644 index 0000000..ba15d98 --- /dev/null +++ b/app/LandinhoLib/Sources/EventsAdmin/UploadRaceEventBundle.swift @@ -0,0 +1,52 @@ +// +// UploadRaceEventBundle.swift +// +// +// Created by Mauricio Cardozo on 17/11/23. +// + +import Foundation + +public enum UploadRaceEventBundle: Equatable, CaseIterable, Identifiable { + case F1Sprint + case F1Regular + + var tag: String { + switch self { + case .F1Sprint, .F1Regular: + "f1" + } + } + + var title: String { + switch self { + case .F1Regular: "fim de semana da F1" + case .F1Sprint: "fim de semana Sprint da F1" + } + } + + public var id: String { + title + } + + var events: [UploadRaceEvent] { + switch self { + case .F1Sprint: + [ + .init(title: "Treino Livre", date: Date(), isMainEvent: false), + .init(title: "Classificação", date: Date(), isMainEvent: false), + .init(title: "Sprint Shootout", date: Date(), isMainEvent: false), + .init(title: "Sprint", date: Date(), isMainEvent: true), + .init(title: "Corrida", date: Date(), isMainEvent: true), + ] + case .F1Regular: + [ + .init(title: "Treino Livre 1", date: Date(), isMainEvent: false), + .init(title: "Treino Livre 2", date: Date(), isMainEvent: false), + .init(title: "Treino Livre 3", date: Date(), isMainEvent: false), + .init(title: "Classificação", date: Date(), isMainEvent: false), + .init(title: "Corrida", date: Date(), isMainEvent: true), + ] + } + } +} diff --git a/app/LandinhoLib/Sources/EventsAdmin/Views/EventsAdminView.swift b/app/LandinhoLib/Sources/EventsAdmin/Views/EventsAdminView.swift index b37ee12..6787809 100644 --- a/app/LandinhoLib/Sources/EventsAdmin/Views/EventsAdminView.swift +++ b/app/LandinhoLib/Sources/EventsAdmin/Views/EventsAdminView.swift @@ -21,13 +21,7 @@ public struct EventsAdminView: View { WithViewStore(store, observe: { $0 }) { viewStore in Group { switch viewStore.eventList.response { - case .idle: - Color(.systemBackground) - .opacity(0.01) - .onAppear { - viewStore.send(.onAppear) - } - case .loading, .reloading: + case .idle, .loading, .reloading: ProgressView() case .finished(.failure(let error)): APIErrorView(error: error) @@ -42,15 +36,11 @@ public struct EventsAdminView: View { .navigationTitle(viewStore.title) .toolbarTitleDisplayMode(.large) .toolbar { - ToolbarItem(placement: .secondaryAction) { - Button("Adicionar fim de semana da F1") { - viewStore.send(.addBundle(.F1Regular)) - } - } - - ToolbarItem(placement: .secondaryAction) { - Button("Adicionar fim de semana Sprint da F1") { - viewStore.send(.addBundle(.F1Sprint)) + ToolbarItemGroup(placement: .secondaryAction) { + ForEach(UploadRaceEventBundle.allCases) { item in + Button("Adicionar \(item.title)") { + viewStore.send(.addBundle(item)) + } } } diff --git a/app/LandinhoLib/Sources/RacesAdmin/RacesAdmin.swift b/app/LandinhoLib/Sources/RacesAdmin/RacesAdmin.swift index 8493221..37f3c73 100644 --- a/app/LandinhoLib/Sources/RacesAdmin/RacesAdmin.swift +++ b/app/LandinhoLib/Sources/RacesAdmin/RacesAdmin.swift @@ -8,12 +8,40 @@ import APIClient import Common import ComposableArchitecture +import EventsAdmin import Foundation import SwiftUI -import EventsAdmin +extension RacesAdmin { + @Reducer + public struct Destination { + public init() {} + + public enum State: Equatable { + case raceEditor(RaceEditor.State) + case eventsAdmin(EventsAdmin.State) + } + + public enum Action: Equatable { + case raceEditor(RaceEditor.Action) + case eventsAdmin(EventsAdmin.Action) + } + + public var body: some ReducerOf { + Scope(state: \.raceEditor, action: \.raceEditor) { + RaceEditor() + } -public struct RacesAdmin: Reducer { + Scope(state: \.eventsAdmin, action: \.eventsAdmin) { + EventsAdmin() + } + } + } +} + + +@Reducer +public struct RacesAdmin { public init() {} public struct State: Equatable { @@ -28,7 +56,7 @@ public struct RacesAdmin: Reducer { var raceList = APIClient<[Race]>.State(endpoint: "race") var removePastRacesState = APIClient<[Race]>.State(endpoint: "prune-race") - @PresentationState var raceEditorState: RaceEditor.State? + @PresentationState var destination: Destination.State? } public enum Action: Equatable { @@ -41,14 +69,14 @@ public struct RacesAdmin: Reducer { case removePastRacesRequest(APIClient<[Race]>.Action) case raceRequest(APIClient<[Race]>.Action) - case raceEditor(PresentationAction) + case destination(PresentationAction) } public var body: some ReducerOf { Reduce { state, action in switch action { case .onPlusTap: - state.raceEditorState = .init(tag: state.tag) + state.destination = .raceEditor(.init(tag: state.tag)) return .none case .onAppear: @@ -60,9 +88,13 @@ public struct RacesAdmin: Reducer { } case .onEditTap(let race): - state.raceEditorState = .init(race: race, tag: state.tag) + state.destination = .raceEditor(.init(race: race, tag: state.tag)) return .none + case .onRaceTap(let race): + state.destination = .eventsAdmin(.init(id: race.id, title: race.title)) + return .send(.destination(.presented(.eventsAdmin(.onAppear)))) + case .onPruneTap: let tag = state.tag return .run { send in @@ -71,13 +103,13 @@ public struct RacesAdmin: Reducer { ])))) } - case .raceEditor(.presented(.raceRequest(.response(.finished(.success))))): + case .destination(.presented(.raceEditor(.raceRequest(.response(.finished(.success)))))): return .merge( .send(.onAppear), - .send(.raceEditor(.dismiss)) + .send(.destination(.dismiss)) ) - case .raceEditor(.presented(.raceRequest(.response(.finished(.failure(let error)))))): + case .destination(.presented(.raceEditor(.raceRequest(.response(.finished(.failure(let error))))))): print(error) return .none @@ -85,11 +117,12 @@ public struct RacesAdmin: Reducer { state.raceList.response = .finished(.success(races)) return .none - case .raceRequest, .raceEditor, .removePastRacesRequest, .onRaceTap: + case .raceRequest, .destination, .removePastRacesRequest: return .none } - }.ifLet(\.$raceEditorState, action: /Action.raceEditor) { - RaceEditor() + } + .ifLet(\.$destination, action: \.destination) { + Destination() } Scope(state: \.raceList, action: /Action.raceRequest) { diff --git a/app/LandinhoLib/Sources/RacesAdmin/Views/RacesAdminView.swift b/app/LandinhoLib/Sources/RacesAdmin/Views/RacesAdminView.swift index 084c389..010985f 100644 --- a/app/LandinhoLib/Sources/RacesAdmin/Views/RacesAdminView.swift +++ b/app/LandinhoLib/Sources/RacesAdmin/Views/RacesAdminView.swift @@ -7,6 +7,7 @@ import APIClient import ComposableArchitecture +import EventsAdmin import Foundation import SwiftUI @@ -21,7 +22,12 @@ public struct RacesAdminView: View { WithViewStore(store, observe: { $0 }) { viewStore in Group { switch viewStore.raceList.response { - case .idle, .loading, .reloading: + case .idle: + Color(.systemBackground) + .onAppear { + store.send(.onAppear) + } + case .loading, .reloading: ProgressView() case .finished(.failure(let error)): APIErrorView(error: error) @@ -55,18 +61,21 @@ public struct RacesAdminView: View { Image(systemName: "plus") }) } - - } - .onAppear { - store.send(.onAppear) } .sheet( - store: store - .scope( - state: { $0.$raceEditorState }, - action: RacesAdmin.Action.raceEditor)) { store in - RaceEditorView(store: store) - } + store: store.scope(state: \.$destination, action: { .destination($0) }), + state: \.raceEditor, + action: { .raceEditor($0) } + ) { store in + RaceEditorView(store: store) + } + .navigationDestination( + store: store.scope(state: \.$destination, action: { .destination($0) }), + state: \.eventsAdmin, + action: { .eventsAdmin($0) } + ) { store in + EventsAdminView(store: store) + } } @MainActor diff --git a/backend/Sources/App/pages/race.swift b/backend/Sources/App/pages/race.swift index e2cbac8..d51a52d 100644 --- a/backend/Sources/App/pages/race.swift +++ b/backend/Sources/App/pages/race.swift @@ -106,8 +106,14 @@ struct UpdateRaceHandler: AsyncRequestHandler { throw Abort(.badRequest) } + guard request.shortTitle.count <= 23 else { + // 23 is the exact number of characters in "Circuit of the Americas" + throw Abort(.badRequest, reason: "Short title is too long!") + } + try await Race.query(on: req.db) .set(\.$title, to: request.title) + .set(\.$shortTitle, to: request.shortTitle) .filter(\.$id, .equal, id) .update() @@ -117,5 +123,6 @@ struct UpdateRaceHandler: AsyncRequestHandler { struct UpdateRaceRequest: Content { let id: String let title: String + let shortTitle: String } } From 6768872d0d9ff2df580cfd02d1a064a7c3530b0d Mon Sep 17 00:00:00 2001 From: Mauricio Cardozo Date: Fri, 17 Nov 2023 17:59:45 -0300 Subject: [PATCH 7/8] adds an actual settings screen before admin, fixes a couple of strings, adds todos file --- .../Views/CategoriesAdminView.swift | 2 +- .../Views/CategoryEditorView.swift | 4 +- .../RacesAdmin/Views/RacesAdminView.swift | 2 +- .../Sources/Settings/Settings.swift | 47 ++++++++++++++----- app/VroomVroom.xcodeproj/project.pbxproj | 4 ++ app/VroomVroom/TODOs.swift | 12 +++++ 6 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 app/VroomVroom/TODOs.swift diff --git a/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoriesAdminView.swift b/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoriesAdminView.swift index 321191d..39d2a86 100644 --- a/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoriesAdminView.swift +++ b/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoriesAdminView.swift @@ -53,7 +53,7 @@ public struct CategoriesAdminView: View { } .foregroundStyle(.primary) .contextMenu { - Button("Edit") { + Button("Editar") { viewStore.send(.onCategoryEditorTap(category.id)) } } diff --git a/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoryEditorView.swift b/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoryEditorView.swift index a8f11a2..257087c 100644 --- a/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoryEditorView.swift +++ b/app/LandinhoLib/Sources/CategoriesAdmin/Views/CategoryEditorView.swift @@ -28,9 +28,9 @@ public struct CategoryEditorView: View { Text("Tag") TextField("Tag", text: viewStore.$tag) } - HStack { + VStack(alignment: .leading) { Text("Comentário") - TextField("Comentário", text: viewStore.$comment) + TextField("Comentário", text: viewStore.$comment, axis: .vertical) } } diff --git a/app/LandinhoLib/Sources/RacesAdmin/Views/RacesAdminView.swift b/app/LandinhoLib/Sources/RacesAdmin/Views/RacesAdminView.swift index 010985f..6eff1c5 100644 --- a/app/LandinhoLib/Sources/RacesAdmin/Views/RacesAdminView.swift +++ b/app/LandinhoLib/Sources/RacesAdmin/Views/RacesAdminView.swift @@ -98,7 +98,7 @@ public struct RacesAdminView: View { } .foregroundStyle(.primary) .contextMenu(menuItems: { - Button("Edit") { + Button("Editar") { store.send(.onEditTap(race)) } }) diff --git a/app/LandinhoLib/Sources/Settings/Settings.swift b/app/LandinhoLib/Sources/Settings/Settings.swift index a7e5314..82a3255 100644 --- a/app/LandinhoLib/Sources/Settings/Settings.swift +++ b/app/LandinhoLib/Sources/Settings/Settings.swift @@ -10,28 +10,33 @@ import Foundation import ComposableArchitecture import SwiftUI -public struct Settings: Reducer { +@Reducer +public struct Settings { public init() {} public struct State: Equatable { public init() {} - var adminState = Admin.State() + @PresentationState var adminState: Admin.State? } public enum Action: Equatable { - case admin(Admin.Action) + case showAdmin + case admin(PresentationAction) } public var body: some ReducerOf { Reduce { state, action in switch action { + case .showAdmin: + state.adminState = .init() + return .none + case .admin: return .none } } - - Scope(state: \.adminState, action: /Action.admin) { + .ifLet(\.$adminState, action: \.admin) { Admin() } } @@ -45,11 +50,31 @@ public struct SettingsView: View { let store: StoreOf public var body: some View { - // TODO: Create an actual settings view instead of just encapsulating Admin - AdminView(store: store.scope( - state: \.adminState, - action: Settings.Action.admin) - ) + NavigationStack { + List { + Button { + + } label: { + Label("Changelog", systemImage: "") + } + Button { + + } label: { + Label("Termos de Serviço", systemImage: "") + } + Button { + store.send(.showAdmin) + } label: { + Label("Admin", systemImage: "fuel.pump") + } + } + .navigationTitle("Ajustes") + .navigationDestination(store: store.scope( + state: \.$adminState, + action: { .admin($0) } ) + ) { store in + AdminView(store: store) + } + } } } - diff --git a/app/VroomVroom.xcodeproj/project.pbxproj b/app/VroomVroom.xcodeproj/project.pbxproj index ae3cc68..ee3e100 100644 --- a/app/VroomVroom.xcodeproj/project.pbxproj +++ b/app/VroomVroom.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ B4877C2A2B0694350009760C /* NextRaceEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C292B0694350009760C /* NextRaceEntry.swift */; }; B4877C342B06B4C60009760C /* NextRaceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4877C332B06B4C60009760C /* NextRaceWidget.swift */; }; B48A501F2AFECEEB00A01B7E /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48A501E2AFECEEB00A01B7E /* Root.swift */; }; + B48D11D62B080B86003A05B3 /* TODOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48D11D52B080B86003A05B3 /* TODOs.swift */; }; B4CBD1632B0401C100BF6776 /* Home in Frameworks */ = {isa = PBXBuildFile; productRef = B4CBD1622B0401C100BF6776 /* Home */; }; B4CBD1652B0409FF00BF6776 /* Admin in Frameworks */ = {isa = PBXBuildFile; productRef = B4CBD1642B0409FF00BF6776 /* Admin */; }; B4CBD1672B040A0300BF6776 /* CategoriesAdmin in Frameworks */ = {isa = PBXBuildFile; productRef = B4CBD1662B040A0300BF6776 /* CategoriesAdmin */; }; @@ -72,6 +73,7 @@ B4877C292B0694350009760C /* NextRaceEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceEntry.swift; sourceTree = ""; }; B4877C332B06B4C60009760C /* NextRaceWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextRaceWidget.swift; sourceTree = ""; }; B48A501E2AFECEEB00A01B7E /* Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Root.swift; sourceTree = ""; }; + B48D11D52B080B86003A05B3 /* TODOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TODOs.swift; sourceTree = ""; }; B4CBD16C2B05570F00BF6776 /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; B4CBD16D2B05570F00BF6776 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; B4CBD16F2B05570F00BF6776 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; @@ -145,6 +147,7 @@ B42CA9EC2AFE8B66009B3FFB /* VroomVroomApp.swift */, B42CA9EE2AFE8B66009B3FFB /* RootView.swift */, B48A501E2AFECEEB00A01B7E /* Root.swift */, + B48D11D52B080B86003A05B3 /* TODOs.swift */, B42CA9F02AFE8B67009B3FFB /* Assets.xcassets */, B42CA9F22AFE8B67009B3FFB /* Preview Content */, ); @@ -312,6 +315,7 @@ B42CA9EF2AFE8B66009B3FFB /* RootView.swift in Sources */, B42CA9ED2AFE8B66009B3FFB /* VroomVroomApp.swift in Sources */, B48A501F2AFECEEB00A01B7E /* Root.swift in Sources */, + B48D11D62B080B86003A05B3 /* TODOs.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/app/VroomVroom/TODOs.swift b/app/VroomVroom/TODOs.swift new file mode 100644 index 0000000..284e3e1 --- /dev/null +++ b/app/VroomVroom/TODOs.swift @@ -0,0 +1,12 @@ +// +// TODOs.swift +// VroomVroom +// +// Created by Mauricio Cardozo on 17/11/23. +// + +import Foundation + +// TODO: Add notification support +// TODO: Add ErrorQueue +// TODO: Add BetaSheet From 3dbcd465922d50dbc920b6cb6b82d714a3381f07 Mon Sep 17 00:00:00 2001 From: Mauricio Cardozo Date: Fri, 17 Nov 2023 20:11:44 -0300 Subject: [PATCH 8/8] adds a home that shows the widgets --- app/LandinhoLib/Package.swift | 2 + .../Sources/APIClient/APIErrorView.swift | 5 +- .../Sources/APIClient/Request.swift | 4 + app/LandinhoLib/Sources/Common/Race.swift | 27 +++ .../Sources/Common/RaceBundle.swift | 3 +- app/LandinhoLib/Sources/Home/Home.swift | 11 +- .../Sources/ScheduleList/ScheduleList.swift | 95 +++------- .../ScheduleList/Views/ScheduleItem.swift | 25 --- .../ScheduleList/Views/ScheduleListView.swift | 175 ++++++++++++++++++ .../Sources/Settings/Settings.swift | 2 +- backend/Sources/App/configure.swift | 3 + backend/Sources/App/pages/next-race.swift | 1 - backend/Sources/App/pages/next-races.swift | 29 +++ 13 files changed, 279 insertions(+), 103 deletions(-) delete mode 100644 app/LandinhoLib/Sources/ScheduleList/Views/ScheduleItem.swift create mode 100644 app/LandinhoLib/Sources/ScheduleList/Views/ScheduleListView.swift create mode 100644 backend/Sources/App/pages/next-races.swift diff --git a/app/LandinhoLib/Package.swift b/app/LandinhoLib/Package.swift index 7614e0f..fa3e732 100644 --- a/app/LandinhoLib/Package.swift +++ b/app/LandinhoLib/Package.swift @@ -100,7 +100,9 @@ let package = Package( .target( name: "ScheduleList", dependencies: [ + "Common", "EventDetail", + "WidgetUI", composable ]), diff --git a/app/LandinhoLib/Sources/APIClient/APIErrorView.swift b/app/LandinhoLib/Sources/APIClient/APIErrorView.swift index a6e8625..9418ffd 100644 --- a/app/LandinhoLib/Sources/APIClient/APIErrorView.swift +++ b/app/LandinhoLib/Sources/APIClient/APIErrorView.swift @@ -19,8 +19,11 @@ public struct APIErrorView: View { public var body: some View { // TODO: Don't show description on RELEASE ContentUnavailableView( - "Something went wrong", + "Algo de errado aconteceu", systemImage: "xmark.octagon", description: Text((error as? APIError)?.jsonString ?? "")) + .onAppear { + dump(error as? APIError) + } } } diff --git a/app/LandinhoLib/Sources/APIClient/Request.swift b/app/LandinhoLib/Sources/APIClient/Request.swift index 1635dca..013cff6 100644 --- a/app/LandinhoLib/Sources/APIClient/Request.swift +++ b/app/LandinhoLib/Sources/APIClient/Request.swift @@ -43,6 +43,10 @@ public extension Request { method: "GET") } + static func get(_ queryItems: [String: String] = [:]) -> Self { + return .get(queryItems: queryItems.map { URLQueryItem(name: $0.key, value: $0.value)}) + } + static var get: Self { Request( data: nil, diff --git a/app/LandinhoLib/Sources/Common/Race.swift b/app/LandinhoLib/Sources/Common/Race.swift index 10c0669..4fdaebb 100644 --- a/app/LandinhoLib/Sources/Common/Race.swift +++ b/app/LandinhoLib/Sources/Common/Race.swift @@ -20,3 +20,30 @@ public struct Race: Codable, Equatable, Identifiable, Hashable { public let shortTitle: String public let events: [RaceEvent] } + +// TODO: Consolidate this into Race instead +public struct MegaRace: Codable, Equatable, Identifiable, Hashable { + public init(id: UUID, title: String, shortTitle: String, events: [RaceEvent], category: RaceCategory) { + self.id = id + self.title = title + self.shortTitle = shortTitle + self.events = events + self.category = category + } + + public let id: UUID + public let title: String + public let shortTitle: String + public let events: [RaceEvent] + public let category: RaceCategory + + public var bundled: RaceBundle { + .init( + category: category, + nextRace: .init( + id: id, + title: title, + shortTitle: shortTitle, + events: events)) + } +} diff --git a/app/LandinhoLib/Sources/Common/RaceBundle.swift b/app/LandinhoLib/Sources/Common/RaceBundle.swift index 9bc7aaa..4eb84e5 100644 --- a/app/LandinhoLib/Sources/Common/RaceBundle.swift +++ b/app/LandinhoLib/Sources/Common/RaceBundle.swift @@ -7,7 +7,7 @@ import Foundation -public struct RaceBundle: Codable, Equatable { +public struct RaceBundle: Codable, Equatable, Identifiable { public init(category: RaceCategory, nextRace: Race) { self.category = category self.nextRace = nextRace @@ -31,4 +31,5 @@ public struct RaceBundle: Codable, Equatable { public let category: RaceCategory public let nextRace: Race + public var id: UUID { nextRace.id } } diff --git a/app/LandinhoLib/Sources/Home/Home.swift b/app/LandinhoLib/Sources/Home/Home.swift index 2324e0b..1a31105 100644 --- a/app/LandinhoLib/Sources/Home/Home.swift +++ b/app/LandinhoLib/Sources/Home/Home.swift @@ -48,10 +48,13 @@ public struct HomeView: View { let store: StoreOf public var body: some View { + NavigationStack { + ScheduleListView( + store: store.scope( + state: \.scheduleListState, + action: Home.Action.scheduleList)) + .navigationTitle("Home") + } - ScheduleListView( - store: store.scope( - state: \.scheduleListState, - action: Home.Action.scheduleList)) } } diff --git a/app/LandinhoLib/Sources/ScheduleList/ScheduleList.swift b/app/LandinhoLib/Sources/ScheduleList/ScheduleList.swift index ed8bb42..5c0665a 100644 --- a/app/LandinhoLib/Sources/ScheduleList/ScheduleList.swift +++ b/app/LandinhoLib/Sources/ScheduleList/ScheduleList.swift @@ -10,6 +10,19 @@ import Common import Foundation import ComposableArchitecture import SwiftUI +import WidgetUI + +// TODO: Move to Common +public struct Page: Codable, Equatable { + let items: [T] + let metadata: Metadata + + public struct Metadata: Codable, Equatable { + let page: Int + let per: Int + let total: Int + } +} // TODO: Fetch next races, paginate public struct ScheduleList: Reducer { @@ -22,20 +35,28 @@ public struct ScheduleList: Reducer { } let categoryTag: String? - public var racesState = APIClient.State(endpoint: "next-race") + public var racesState = APIClient>.State(endpoint: "next-races") } public enum Action: Equatable { case onAppear - case racesRequest(APIClient.Action) + case delegate(DelegateAction) + case racesRequest(APIClient>.Action) + } + + public enum DelegateAction: Equatable { + case onWidgetTap(MegaRace) } public var body: some ReducerOf { Reduce { state, action in switch action { case .onAppear: - return .send(.racesRequest(.request(.get))) - case .racesRequest: + return .send(.racesRequest(.request(.get([ + "page": "0", + "per": "5" + ])))) + case .racesRequest, .delegate: return .none } } @@ -46,69 +67,3 @@ public struct ScheduleList: Reducer { } } -public struct ScheduleListView: View { - public init(store: StoreOf) { - self.store = store - } - - let store: StoreOf - - public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - List { - switch viewStore.racesState.response { - case .idle: - EmptyView() - case .loading: - ProgressView() - case .reloading(let response), .finished(.success(let response)): - ScheduleListItem(response) - .padding(.vertical) - case .finished(.failure(let error)): - APIErrorView(error: error) - } - } - } - .task { - store.send(.onAppear) - } - } -} - -public struct ScheduleListItem: View { - public init(_ response: RaceBundle) { - self.response = response - } - - let response: RaceBundle - - public var body: some View { - VStack(alignment: .leading) { - Text("Category title") - .font(.callout) - - Text(response.nextRace.title) - .font(.title3) - - HStack { - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .frame(maxWidth: 100) - - Spacer() - VStack(alignment: .leading) { - - - ForEach(response.nextRace.events) { event in - HStack { - Text(event.title) - Text(event.date.formatted()) - } - .font(.caption) - } - } - } - } - } -} - - diff --git a/app/LandinhoLib/Sources/ScheduleList/Views/ScheduleItem.swift b/app/LandinhoLib/Sources/ScheduleList/Views/ScheduleItem.swift deleted file mode 100644 index d1392df..0000000 --- a/app/LandinhoLib/Sources/ScheduleList/Views/ScheduleItem.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// File.swift -// -// -// Created by Mauricio Cardozo on 14/11/23. -// - -import ComposableArchitecture -import Foundation -import SwiftUI - -public struct ScheduleItem: View { - public init(store: StoreOf) { - self.store = store - } - - let store: StoreOf - - public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - Text("Placeholder") - } - } -} - diff --git a/app/LandinhoLib/Sources/ScheduleList/Views/ScheduleListView.swift b/app/LandinhoLib/Sources/ScheduleList/Views/ScheduleListView.swift new file mode 100644 index 0000000..3d33321 --- /dev/null +++ b/app/LandinhoLib/Sources/ScheduleList/Views/ScheduleListView.swift @@ -0,0 +1,175 @@ +// +// ScheduleListView.swift +// +// +// Created by Mauricio Cardozo on 17/11/23. +// + +import APIClient +import Common +import ComposableArchitecture +import Foundation +import SwiftUI +import WidgetUI + +public struct ScheduleListView: View { + public init(store: StoreOf) { + self.store = store + } + + let store: StoreOf + + public var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + switch viewStore.racesState.response { + case .idle: + Color(.systemBackground) + .task { + viewStore.send(.onAppear) + } + case .loading: + ProgressView() + case .reloading(let response), .finished(.success(let response)): + ScrollView { + VStack(spacing: 20) { + Text("Ainda não tem nada por aqui, mas se você quiser ver, esse são os Widgets do app por enquanto:") + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + ScrollView(.horizontal) { + HStack { + Spacer().padding(.leading, 5) + ForEach(response.items) { item in + smallWidget(for: item) + .onTapAnimate { + viewStore.send(.delegate(.onWidgetTap(item))) + } + } + Spacer().padding(.trailing, 5) + } + } + .scrollClipDisabled() + .scrollIndicators(.hidden) + + ScrollView(.horizontal) { + HStack { + Spacer().padding(.leading, 5) + ForEach(response.items) { item in + NextRaceMediumWidgetView(bundle: item.bundled, lastUpdatedDate: Date()) + .padding() + .background(Color(.systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous)) + .frame(width: 364, height: 170) + .shadow(color: .black.opacity(0.1), radius: 1) + .onTapAnimate { + viewStore.send(.delegate(.onWidgetTap(item))) + } + } + Spacer().padding(.trailing, 5) + } + } + .scrollClipDisabled() + .scrollIndicators(.hidden) + + ScrollView(.horizontal) { + HStack { + Spacer().padding(.leading, 5) + ForEach(response.items) { item in + NextRaceLargeWidgetView(bundle: item.bundled, lastUpdatedDate: Date()) + .padding() + .background(Color(.systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous)) + .frame(width: 364, height: 384) + .shadow(color: .black.opacity(0.1), radius: 1) + .onTapAnimate { + viewStore.send(.delegate(.onWidgetTap(item))) + } + } + Spacer().padding(.trailing, 5) + } + } + .scrollClipDisabled() + .scrollIndicators(.hidden) + + Spacer().frame(height: 50) + } + } + .background( + .background.secondary + ) + .refreshable { + viewStore.send(.onAppear) + } + case .finished(.failure(let error)): + APIErrorView(error: error) + } + } + } + + @MainActor + func smallWidget(for bundle: MegaRace) -> some View { + NextRaceSmallWidgetView(bundle: bundle.bundled, lastUpdatedDate: Date()) + .padding() + .background(Color(.systemBackground)) + .frame(width: 170, height: 170) + .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous)) + .shadow(color: .black.opacity(0.1), radius: 1) + } +} + +#Preview { + let store = Store(initialState: ScheduleList.State(categoryTag: nil)) { + ScheduleList() + } + store.send(.racesRequest(.response(.finished(.success(.init(items: [ + .init( + id: .init(), + title: "Race", + shortTitle: "Race", + events: [ + .init( + id: .init(), + title: "Treino Livre 1", + date: Date(), + isMainEvent: false) + ], + category: .init( + id: .init(), + title: "Formula 1", + tag: "f1")) + ], metadata: .init(page: 0, per: 0, total: 0))))))) + + return NavigationStack { + ScheduleListView(store: store) + .navigationTitle("ScheduleList") + } +} + +// TODO: Move to a new module, CommonUI or whatever +struct TapAnimationModifier: ViewModifier { + @State private var isTapped = false + var completion: () -> Void + var animation: Animation = .default + var delay: TimeInterval = 0.2 // Default dela + + func body(content: Content) -> some View { + content + .scaleEffect(isTapped ? 0.95 : 1.0) + .onTapGesture { + withAnimation(.spring(duration: delay)) { + isTapped = true + } + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + completion() + withAnimation(.spring(duration: 0.1)) { + isTapped = false + } + } + } + } +} + +extension View { + func onTapAnimate(animation: Animation = .default, delay: TimeInterval = 0.5, completion: @escaping () -> Void) -> some View { + self.modifier(TapAnimationModifier(completion: completion, animation: animation, delay: delay)) + } +} diff --git a/app/LandinhoLib/Sources/Settings/Settings.swift b/app/LandinhoLib/Sources/Settings/Settings.swift index 82a3255..e816f69 100644 --- a/app/LandinhoLib/Sources/Settings/Settings.swift +++ b/app/LandinhoLib/Sources/Settings/Settings.swift @@ -65,7 +65,7 @@ public struct SettingsView: View { Button { store.send(.showAdmin) } label: { - Label("Admin", systemImage: "fuel.pump") + Label("Admin", systemImage: "fuelpump") } } .navigationTitle("Ajustes") diff --git a/backend/Sources/App/configure.swift b/backend/Sources/App/configure.swift index 8003409..ce86439 100644 --- a/backend/Sources/App/configure.swift +++ b/backend/Sources/App/configure.swift @@ -29,6 +29,9 @@ public func configure(_ app: Application) async throws { // next-race NextRaceHandler(), + // next-races + NextRacesHandler(), + // race RaceListHandler(), UpdateRaceHandler(), diff --git a/backend/Sources/App/pages/next-race.swift b/backend/Sources/App/pages/next-race.swift index 67919f4..be48c52 100644 --- a/backend/Sources/App/pages/next-race.swift +++ b/backend/Sources/App/pages/next-race.swift @@ -12,7 +12,6 @@ struct NextRaceHandler: AsyncRequestHandler { var method: HTTPMethod { .GET } var path: String { "next-race" } - @Sendable func handle(req: Request) async throws -> some AsyncResponseEncodable { var args: String do { diff --git a/backend/Sources/App/pages/next-races.swift b/backend/Sources/App/pages/next-races.swift new file mode 100644 index 0000000..c49b0a8 --- /dev/null +++ b/backend/Sources/App/pages/next-races.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by Mauricio Cardozo on 17/11/23. +// + +import Foundation +import Vapor +import Fluent + +struct NextRacesHandler: AsyncRequestHandler { + var method: HTTPMethod { .GET } + var path: String { "next-races" } + + func handle(req: Request) async throws -> some AsyncResponseEncodable { + + let currentDate = Date() + + return try await Race + .query(on: req.db) + .filter(\.$earliestEventDate, .greaterThanOrEqual, currentDate) + .join(parent: \.$category) + .sort(\.$earliestEventDate, .ascending) + .with(\.$events) + .with(\.$category) + .paginate(for: req) + } +}