Add rough UI for recommendation groups

This commit is contained in:
Jackson Harper 2022-12-05 15:08:47 +08:00
parent 55d4c89754
commit bdbad81fa9
9 changed files with 2601 additions and 0 deletions

View file

@ -107,6 +107,12 @@ struct ProfileView: View {
}
#endif
Section {
NavigationLink(destination: GroupsView()) {
Text("Recommendation Groups")
}
}
Section {
NavigationLink(
destination: BasicWebAppView.privacyPolicyWebView(baseURL: dataService.appEnvironment.webAppBaseURL)

View file

@ -0,0 +1,74 @@
import Models
import Services
import SwiftUI
import Views
@MainActor final class RecommendationsGroupViewModel: ObservableObject {
@Published var isLoading = false
@Published var networkError = true
@Published var recommendationGroup: InternalRecommendationGroup
init(recommendationGroup: InternalRecommendationGroup) {
self.recommendationGroup = recommendationGroup
}
func loadGroups(dataService _: DataService) async {
isLoading = true
// do {
// recommendationGroups = try await dataService.recommendationGroups()
// } catch {
// networkError = true
// }
isLoading = false
}
}
struct RecommendationGroupView: View {
@EnvironmentObject var dataService: DataService
@StateObject var viewModel: RecommendationsGroupViewModel
var body: some View {
Group {
#if os(iOS)
Form {
innerBody
}
#elseif os(macOS)
List {
innerBody
}
.listStyle(InsetListStyle())
#endif
}
.task { await viewModel.loadGroups(dataService: dataService) }
}
private var innerBody: some View {
Group {
Section("Name") {
Text(viewModel.recommendationGroup.name)
}
Section("Invite Link") {
Button(action: {
#if os(iOS)
UIPasteboard.general.string = viewModel.recommendationGroup.inviteUrl
#endif
#if os(macOS)
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.writeObjects([viewModel.recommendationGroup.inviteUrl as NSString])
#endif
Snackbar.show(message: "Invite link copied")
}, label: {
Text("[\(viewModel.recommendationGroup.inviteUrl)](\(viewModel.recommendationGroup.inviteUrl))")
})
}
}
.navigationTitle(viewModel.recommendationGroup.name)
}
}

View file

@ -0,0 +1,145 @@
import Models
import Services
import SwiftUI
import Views
@MainActor final class RecommendationsGroupsViewModel: ObservableObject {
@Published var isLoading = false
@Published var isCreating = false
@Published var networkError = true
@Published var recommendationGroups = [InternalRecommendationGroup]()
@Published var showCreateSheet = false
@Published var showCreateError = false
@Published var createGroupError: String?
func loadGroups(dataService: DataService) async {
isLoading = true
do {
recommendationGroups = try await dataService.recommendationGroups()
} catch {
networkError = true
}
isLoading = false
}
func createGroup(dataService: DataService, name: String) async {
isCreating = true
if let group = try? await dataService.createRecommendationGroup(name: name) {
print("CREATED GROUP: ", group)
await loadGroups(dataService: dataService)
showCreateSheet = false
} else {
createGroupError = "Error creating group"
showCreateError = true
}
isCreating = false
}
}
struct CreateRecommendationGroupView: View {
@State var name = ""
@EnvironmentObject var dataService: DataService
@StateObject var viewModel = RecommendationsGroupsViewModel()
var nextButton: some View {
if viewModel.isCreating {
return AnyView(ProgressView())
} else {
return AnyView(Button(action: {
Task {
await viewModel.createGroup(dataService: dataService, name: self.name)
}
}, label: {
Text("Next")
})
.disabled(name.isEmpty)
)
}
}
var body: some View {
NavigationView {
Form {
TextField("Name", text: $name, prompt: Text("Group Name"))
}
.alert(isPresented: $viewModel.showCreateError) {
Alert(
title: Text(viewModel.createGroupError ?? "Error creating group"),
dismissButton: .cancel(Text("Ok")) {
viewModel.createGroupError = nil
viewModel.showCreateError = false
}
)
}
}
.navigationViewStyle(.stack)
.navigationTitle("Create Group")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading:
Button(action: {
viewModel.showCreateSheet = false
}, label: { Text("Cancel") }),
trailing: nextButton)
}
}
struct GroupsView: View {
@EnvironmentObject var dataService: DataService
@StateObject var viewModel = RecommendationsGroupsViewModel()
var body: some View {
Group {
#if os(iOS)
Form {
innerBody
}
#elseif os(macOS)
List {
innerBody
}
.listStyle(InsetListStyle())
#endif
}
.sheet(isPresented: $viewModel.showCreateSheet) {
NavigationView {
CreateRecommendationGroupView(viewModel: self.viewModel)
}
}
.task { await viewModel.loadGroups(dataService: dataService) }
}
private var innerBody: some View {
Group {
Section {
Button(
action: { viewModel.showCreateSheet = true },
label: {
HStack {
Image(systemName: "plus.circle.fill").foregroundColor(.green)
Text("Create a new group")
Spacer()
}
}
)
.disabled(viewModel.isLoading)
}
Section(header: Text("Your recommendation groups")) {
ForEach(viewModel.recommendationGroups) { recommendationGroup in
NavigationLink(
destination: RecommendationGroupView(viewModel: RecommendationsGroupViewModel(recommendationGroup: recommendationGroup))
) {
Text(recommendationGroup.name)
}
}
}
}
.navigationTitle("Recommendation Groups")
}
}

View file

@ -91,6 +91,13 @@
<attribute name="savedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="term" optional="YES" attributeType="String"/>
</entity>
<entity name="RecommendationGroup" representedClassName="RecommendationGroup" syncable="YES" codeGenerationType="class">
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="inviteUrl" optional="YES" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<entity name="Viewer" representedClassName="Viewer" syncable="YES" codeGenerationType="class">
<attribute name="name" attributeType="String"/>
<attribute name="profileImageURL" optional="YES" attributeType="String"/>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
import CoreData
import Foundation
import Models
import SwiftGraphQL
public extension DataService {
func createRecommendationGroup(name: String) async throws -> InternalRecommendationGroup {
enum MutationResult {
case saved(recommendationGroup: InternalRecommendationGroup)
case error(errorCode: Enums.CreateGroupErrorCode)
}
let selection = Selection<MutationResult, Unions.CreateGroupResult> {
try $0.on(
createGroupError: .init {
.error(errorCode: try $0.errorCodes().first ?? .badRequest)
},
createGroupSuccess: .init {
.saved(recommendationGroup: try $0.group(selection: recommendationGroupSelection))
}
)
}
let input = InputObjects.CreateGroupInput(name: name)
let mutation = Selection.Mutation {
try $0.createGroup(input: input, selection: selection)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return try await withCheckedThrowingContinuation { continuation in
send(mutation, to: path, headers: headers) { [weak self] queryResult in
guard let payload = try? queryResult.get(), let self = self else {
continuation.resume(throwing: BasicError.message(messageText: "network error"))
return
}
switch payload.data {
case let .saved(recommendationGroup: recommendationGroup):
if [recommendationGroup].persist(context: self.backgroundContext) != nil {
continuation.resume(returning: recommendationGroup)
} else {
continuation.resume(throwing: BasicError.message(messageText: "CoreData error"))
}
case let .error(errorCode: errorCode):
continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue))
}
}
}
}
}

View file

@ -0,0 +1,52 @@
import CoreData
import Foundation
import Models
import SwiftGraphQL
public extension DataService {
func recommendationGroups() async throws -> [InternalRecommendationGroup] {
enum QueryResult {
case success(result: [InternalRecommendationGroup])
case error(error: String)
}
let selection = Selection<QueryResult, Unions.GroupsResult> {
try $0.on(
groupsError: .init {
QueryResult.error(error: try $0.errorCodes().description)
},
groupsSuccess: .init {
QueryResult.success(result: try $0.groups(selection: recommendationGroupSelection.list))
}
)
}
let query = Selection.Query {
try $0.groups(selection: selection)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
let context = backgroundContext
return try await withCheckedThrowingContinuation { continuation in
send(query, to: path, headers: headers) { queryResult in
guard let payload = try? queryResult.get() else {
continuation.resume(throwing: BasicError.message(messageText: "network request failed"))
return
}
switch payload.data {
case let .success(result: result):
if result.persist(context: context) != nil {
continuation.resume(returning: result)
} else {
continuation.resume(throwing: BasicError.message(messageText: "CoreData error"))
}
case .error:
continuation.resume(throwing: BasicError.message(messageText: "Recommendation Groups fetch error"))
}
}
}
}
}

View file

@ -0,0 +1,10 @@
import Models
import SwiftGraphQL
let recommendationGroupSelection = Selection.RecommendationGroup {
InternalRecommendationGroup(
id: try $0.id(),
name: try $0.name(),
inviteUrl: try $0.inviteUrl()
)
}

View file

@ -0,0 +1,65 @@
//
// InternalRecommendationGroup.swift
//
//
// Created by Jackson Harper on 12/5/22.
//
import CoreData
import Foundation
import Models
public struct InternalRecommendationGroup: Encodable, Identifiable {
public let id: String
public let name: String
public let inviteUrl: String
func asManagedObject(inContext context: NSManagedObjectContext) -> RecommendationGroup {
let fetchRequest: NSFetchRequest<Models.RecommendationGroup> = RecommendationGroup.fetchRequest()
fetchRequest.predicate = NSPredicate(
format: "id == %@", id
)
let existing = (try? context.fetch(fetchRequest))?.first
let recommendationGroup = existing ?? RecommendationGroup(entity: RecommendationGroup.entity(), insertInto: context)
recommendationGroup.id = id
recommendationGroup.name = name
recommendationGroup.inviteUrl = inviteUrl
return recommendationGroup
}
static func make(from recommendationGroup: RecommendationGroup) -> InternalRecommendationGroup? {
if let id = recommendationGroup.id,
let name = recommendationGroup.name,
let inviteUrl = recommendationGroup.inviteUrl
{
return InternalRecommendationGroup(
id: id,
name: name,
inviteUrl: inviteUrl
)
}
return nil
}
}
extension Sequence where Element == InternalRecommendationGroup {
func persist(context: NSManagedObjectContext) -> [NSManagedObjectID]? {
var result: [NSManagedObjectID]?
context.performAndWait {
let recommendationGroups = map { $0.asManagedObject(inContext: context) }
do {
try context.save()
logger.debug("InternalRecommendationGroup saved succesfully")
result = recommendationGroups.map(\.objectID)
} catch {
context.rollback()
logger.debug("Failed to save InternalRecommendationGroups: \(error.localizedDescription)")
}
}
return result
}
}