diff --git a/HIAPI/Models/CartItem.swift b/HIAPI/Models/CartItem.swift new file mode 100644 index 00000000..04127e5d --- /dev/null +++ b/HIAPI/Models/CartItem.swift @@ -0,0 +1,38 @@ +// +// CartItem.swift +// HackIllinois +// +// Created by Anushka Sankaran on 2/14/25. +// Copyright © 2025 HackIllinois. All rights reserved. +// + +import Foundation +import APIManager + +public struct CartItemContainer: Decodable, APIReturnable { + public let items: [String: Int] + public let userId: String + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.items = try container.decode([String: Int].self, forKey: .items) + self.userId = try container.decode(String.self, forKey: .userId) + } + + private enum CodingKeys: String, CodingKey { + case items, userId + } +} + +public struct CartReturnItem: Codable, APIReturnable { + public let items: [String: Int]? // Return items upon success + public let userId: String? + public let error: String? + public let message: String? +} + +public struct QRItem: Codable, APIReturnable { + public let QRCode: String? // Return qr code upon success + public let error: String? + public let message: String? +} diff --git a/HIAPI/Models/Item.swift b/HIAPI/Models/Item.swift index ad821f8d..6076d597 100644 --- a/HIAPI/Models/Item.swift +++ b/HIAPI/Models/Item.swift @@ -20,12 +20,16 @@ public struct ItemContainer: Decodable, APIReturnable { public struct Item: Codable, Hashable { internal enum CodingKeys: String, CodingKey { + case _id + case itemId case name case price case isRaffle case quantity case imageURL } + public let _id: String + public let itemId: String public let name: String public let price: Int public let isRaffle: Bool diff --git a/HIAPI/Models/Profile.swift b/HIAPI/Models/Profile.swift index 9931c089..1d6db7c9 100644 --- a/HIAPI/Models/Profile.swift +++ b/HIAPI/Models/Profile.swift @@ -25,22 +25,24 @@ public struct ProfileContainer: Decodable, APIReturnable { public struct Profile: Codable, APIReturnable { internal enum CodingKeys: String, CodingKey { + case _id case userId + case discordTag case displayName - case points case foodWave - case discordTag + case points + case pointsAccumulated case avatarUrl - case coins } + public let _id: String public let userId: String + public let discordTag: String public let displayName: String - public let points: Int public let foodWave: Int - public let discordTag: String + public let points: Int + public let pointsAccumulated: Int public let avatarUrl: String - public let coins: Int } public struct Ranking: Codable, APIReturnable { diff --git a/HIAPI/Services/ShopService.swift b/HIAPI/Services/ShopService.swift index 78be7325..6b35abc5 100644 --- a/HIAPI/Services/ShopService.swift +++ b/HIAPI/Services/ShopService.swift @@ -18,6 +18,10 @@ public final class ShopService: BaseService { return APIRequest(service: self, endpoint: "shop/", headers: headers, method: .GET) } + public static func getCartItems() -> APIRequest { + return APIRequest(service: self, endpoint: "shop/cart/", headers: headers, method: .GET) + } + public static func redeemPrize(itemId: String, itemInstance: String, userToken: String) -> APIRequest { let jsonBody: [String: Any] = [ "itemId": itemId, @@ -27,4 +31,31 @@ public final class ShopService: BaseService { return APIRequest(service: self, endpoint: "shop/item/buy/", body: jsonBody, headers: headers, method: .POST) } + + public static func redeemCart(qrCode: String, userToken: String) -> APIRequest { + let jsonBody: [String: Any] = [ + "QRCode": qrCode + ] + let headers: HTTPParameters = ["Authorization": userToken] + + return APIRequest(service: self, endpoint: "shop/cart/redeem/", body: jsonBody, headers: headers, method: .POST) + } + + public static func addToCart(itemId: String, userToken: String) -> APIRequest { + let headers: HTTPParameters = ["Authorization": userToken] + + return APIRequest(service: self, endpoint: "shop/cart/\(itemId)/", headers: headers, method: .POST) + } + + public static func removeFromCart(itemId: String, userToken: String) -> APIRequest { + let headers: HTTPParameters = ["Authorization": userToken] + + return APIRequest(service: self, endpoint: "shop/cart/\(itemId)/", headers: headers, method: .DELETE) + } + + public static func getQR(userToken: String) -> APIRequest { + let headers: HTTPParameters = ["Authorization": userToken] + + return APIRequest(service: self, endpoint: "shop/cart/qr/", headers: headers, method: .GET) + } } diff --git a/HackIllinois.xcodeproj/project.pbxproj b/HackIllinois.xcodeproj/project.pbxproj index 121c7f3d..eaa5adaa 100644 --- a/HackIllinois.xcodeproj/project.pbxproj +++ b/HackIllinois.xcodeproj/project.pbxproj @@ -156,6 +156,7 @@ D1F1463A2B605C57004E7FC9 /* Hack_Mushroom_Loading.json in Resources */ = {isa = PBXBuildFile; fileRef = D1F146392B605C57004E7FC9 /* Hack_Mushroom_Loading.json */; }; D3A309BC2211175200CBA351 /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3A309BB2211175200CBA351 /* PassKit.framework */; }; D704DB322C73A8E200355019 /* URLImage in Frameworks */ = {isa = PBXBuildFile; productRef = D704DB312C73A8E200355019 /* URLImage */; }; + D7713B5D2D5FDEAA00389128 /* CartItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7713B5C2D5FDEA100389128 /* CartItem.swift */; }; DAACDC322D3B8F5E00D15118 /* HIEventListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAACDC312D3B8F5E00D15118 /* HIEventListViewController.swift */; }; DF3706382925DDAA000B4278 /* GoogleMapsBase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF3706372925DDAA000B4278 /* GoogleMapsBase.framework */; }; DF37063B2925DDB8000B4278 /* GoogleMaps.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF3706392925DDB7000B4278 /* GoogleMaps.framework */; }; @@ -375,6 +376,7 @@ D1F146392B605C57004E7FC9 /* Hack_Mushroom_Loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Hack_Mushroom_Loading.json; sourceTree = ""; }; D3A309BA221116A600CBA351 /* HackIllinois.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HackIllinois.entitlements; sourceTree = ""; }; D3A309BB2211175200CBA351 /* PassKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PassKit.framework; path = System/Library/Frameworks/PassKit.framework; sourceTree = SDKROOT; }; + D7713B5C2D5FDEA100389128 /* CartItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartItem.swift; sourceTree = ""; }; DAACDC312D3B8F5E00D15118 /* HIEventListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HIEventListViewController.swift; sourceTree = ""; }; DF3706372925DDAA000B4278 /* GoogleMapsBase.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GoogleMapsBase.framework; sourceTree = ""; }; DF3706392925DDB7000B4278 /* GoogleMaps.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GoogleMaps.framework; sourceTree = ""; }; @@ -760,6 +762,7 @@ 95E3146421FAF1740092C22E /* Models */ = { isa = PBXGroup; children = ( + D7713B5C2D5FDEA100389128 /* CartItem.swift */, 95E3146821FAF1740092C22E /* Announcement.swift */, 3521FFA82207C03E00634A63 /* Attendee.swift */, 95E3146521FAF1740092C22E /* Event.swift */, @@ -1194,6 +1197,7 @@ 3C8F62D4238F9639001A5DAF /* Time.swift in Sources */, 95E3147621FAF1740092C22E /* Announcement.swift in Sources */, D1D3351B2B81DF0300BBB596 /* NotificationService.swift in Sources */, + D7713B5D2D5FDEAA00389128 /* CartItem.swift in Sources */, 95E3146C21FAF1740092C22E /* UserService.swift in Sources */, 95B12043220BEA9D00E024BB /* CheckInService.swift in Sources */, 95E3147121FAF1740092C22E /* AuthService.swift in Sources */, diff --git a/HackIllinois/Assets.xcassets/Cart.imageset/Cart 2x.png b/HackIllinois/Assets.xcassets/Cart.imageset/Cart 2x.png new file mode 100644 index 00000000..90ecec98 Binary files /dev/null and b/HackIllinois/Assets.xcassets/Cart.imageset/Cart 2x.png differ diff --git a/HackIllinois/Assets.xcassets/Cart.imageset/Cart 3x.png b/HackIllinois/Assets.xcassets/Cart.imageset/Cart 3x.png new file mode 100644 index 00000000..4a9a3229 Binary files /dev/null and b/HackIllinois/Assets.xcassets/Cart.imageset/Cart 3x.png differ diff --git a/HackIllinois/Assets.xcassets/Cart.imageset/Cart.png b/HackIllinois/Assets.xcassets/Cart.imageset/Cart.png new file mode 100644 index 00000000..f4b7fcfb Binary files /dev/null and b/HackIllinois/Assets.xcassets/Cart.imageset/Cart.png differ diff --git a/HackIllinois/Assets.xcassets/Cart.imageset/Contents.json b/HackIllinois/Assets.xcassets/Cart.imageset/Contents.json new file mode 100644 index 00000000..3a1a83fb --- /dev/null +++ b/HackIllinois/Assets.xcassets/Cart.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Cart.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Cart 2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Cart 3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/CartBackground.imageset/Contents.json b/HackIllinois/Assets.xcassets/CartBackground.imageset/Contents.json new file mode 100644 index 00000000..c3013098 --- /dev/null +++ b/HackIllinois/Assets.xcassets/CartBackground.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "PS Cart Background.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "PS Cart Background 2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "PS Cart Background 3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/CartBackground.imageset/PS Cart Background 2x.png b/HackIllinois/Assets.xcassets/CartBackground.imageset/PS Cart Background 2x.png new file mode 100644 index 00000000..fd6bc13c Binary files /dev/null and b/HackIllinois/Assets.xcassets/CartBackground.imageset/PS Cart Background 2x.png differ diff --git a/HackIllinois/Assets.xcassets/CartBackground.imageset/PS Cart Background 3x.png b/HackIllinois/Assets.xcassets/CartBackground.imageset/PS Cart Background 3x.png new file mode 100644 index 00000000..6f406478 Binary files /dev/null and b/HackIllinois/Assets.xcassets/CartBackground.imageset/PS Cart Background 3x.png differ diff --git a/HackIllinois/Assets.xcassets/CartBackground.imageset/PS Cart Background.png b/HackIllinois/Assets.xcassets/CartBackground.imageset/PS Cart Background.png new file mode 100644 index 00000000..75b74bd9 Binary files /dev/null and b/HackIllinois/Assets.xcassets/CartBackground.imageset/PS Cart Background.png differ diff --git a/HackIllinois/Assets.xcassets/Home/HomePage/Contents 2.json b/HackIllinois/Assets.xcassets/Home/HomePage/Contents 2.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/HackIllinois/Assets.xcassets/Home/HomePage/Contents 2.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/Image 1.imageset/Contents 2.json b/HackIllinois/Assets.xcassets/Image 1.imageset/Contents 2.json new file mode 100644 index 00000000..a19a5492 --- /dev/null +++ b/HackIllinois/Assets.xcassets/Image 1.imageset/Contents 2.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/Image.imageset/Contents 2.json b/HackIllinois/Assets.xcassets/Image.imageset/Contents 2.json new file mode 100644 index 00000000..a19a5492 --- /dev/null +++ b/HackIllinois/Assets.xcassets/Image.imageset/Contents 2.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/PointShopBack.imageset/Back 2x.png b/HackIllinois/Assets.xcassets/PointShopBack.imageset/Back 2x.png new file mode 100644 index 00000000..005cac66 Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopBack.imageset/Back 2x.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopBack.imageset/Back 3x.png b/HackIllinois/Assets.xcassets/PointShopBack.imageset/Back 3x.png new file mode 100644 index 00000000..0a44538b Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopBack.imageset/Back 3x.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopBack.imageset/Back.png b/HackIllinois/Assets.xcassets/PointShopBack.imageset/Back.png new file mode 100644 index 00000000..91732698 Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopBack.imageset/Back.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopBack.imageset/Contents.json b/HackIllinois/Assets.xcassets/PointShopBack.imageset/Contents.json new file mode 100644 index 00000000..c6dab92e --- /dev/null +++ b/HackIllinois/Assets.xcassets/PointShopBack.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Back.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Back 2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Back 3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Contents.json b/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Contents.json new file mode 100644 index 00000000..0175a709 --- /dev/null +++ b/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Points Shop Background.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Points Shop Background 2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Points Shop Background 3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Points Shop Background 2x.png b/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Points Shop Background 2x.png new file mode 100644 index 00000000..86d7edfb Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Points Shop Background 2x.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Points Shop Background 3x.png b/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Points Shop Background 3x.png new file mode 100644 index 00000000..834b4a41 Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Points Shop Background 3x.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Points Shop Background.png b/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Points Shop Background.png new file mode 100644 index 00000000..a762b7f1 Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopBackground.imageset/Points Shop Background.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/Contents.json b/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/Contents.json new file mode 100644 index 00000000..9d7b5de9 --- /dev/null +++ b/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "PS Tab Selected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "PS Tab Selected 2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "PS Tab Selected 3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/PS Tab Selected 2x.png b/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/PS Tab Selected 2x.png new file mode 100644 index 00000000..1723b4c4 Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/PS Tab Selected 2x.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/PS Tab Selected 3x.png b/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/PS Tab Selected 3x.png new file mode 100644 index 00000000..88ccbc8e Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/PS Tab Selected 3x.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/PS Tab Selected.png b/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/PS Tab Selected.png new file mode 100644 index 00000000..03fe9b84 Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopTabSelected.imageset/PS Tab Selected.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/Contents.json b/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/Contents.json new file mode 100644 index 00000000..9cb59032 --- /dev/null +++ b/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "PS Tab Unselected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "PS Tab Unselected 2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "PS Tab Unselected 3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/PS Tab Unselected 2x.png b/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/PS Tab Unselected 2x.png new file mode 100644 index 00000000..2b617d74 Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/PS Tab Unselected 2x.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/PS Tab Unselected 3x.png b/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/PS Tab Unselected 3x.png new file mode 100644 index 00000000..fe72979c Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/PS Tab Unselected 3x.png differ diff --git a/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/PS Tab Unselected.png b/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/PS Tab Unselected.png new file mode 100644 index 00000000..3670c884 Binary files /dev/null and b/HackIllinois/Assets.xcassets/PointShopTabUnselected.imageset/PS Tab Unselected.png differ diff --git a/HackIllinois/Assets.xcassets/Redeem.imageset/Contents.json b/HackIllinois/Assets.xcassets/Redeem.imageset/Contents.json new file mode 100644 index 00000000..3b893eb6 --- /dev/null +++ b/HackIllinois/Assets.xcassets/Redeem.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Redeem.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Redeem 2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Redeem 3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/Redeem.imageset/Redeem 2x.png b/HackIllinois/Assets.xcassets/Redeem.imageset/Redeem 2x.png new file mode 100644 index 00000000..a08793cc Binary files /dev/null and b/HackIllinois/Assets.xcassets/Redeem.imageset/Redeem 2x.png differ diff --git a/HackIllinois/Assets.xcassets/Redeem.imageset/Redeem 3x.png b/HackIllinois/Assets.xcassets/Redeem.imageset/Redeem 3x.png new file mode 100644 index 00000000..4688f16d Binary files /dev/null and b/HackIllinois/Assets.xcassets/Redeem.imageset/Redeem 3x.png differ diff --git a/HackIllinois/Assets.xcassets/Redeem.imageset/Redeem.png b/HackIllinois/Assets.xcassets/Redeem.imageset/Redeem.png new file mode 100644 index 00000000..b6c968de Binary files /dev/null and b/HackIllinois/Assets.xcassets/Redeem.imageset/Redeem.png differ diff --git a/HackIllinois/Assets.xcassets/SandTimer.imageset/Contents 2.json b/HackIllinois/Assets.xcassets/SandTimer.imageset/Contents 2.json new file mode 100644 index 00000000..4deeb872 --- /dev/null +++ b/HackIllinois/Assets.xcassets/SandTimer.imageset/Contents 2.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "noun-hourglass-3018663 1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "noun-hourglass-3018663 1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "noun-hourglass-3018663 1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/SandTimer.imageset/noun-hourglass-3018663 1@2x 2.png b/HackIllinois/Assets.xcassets/SandTimer.imageset/noun-hourglass-3018663 1@2x 2.png new file mode 100644 index 00000000..79d63daa Binary files /dev/null and b/HackIllinois/Assets.xcassets/SandTimer.imageset/noun-hourglass-3018663 1@2x 2.png differ diff --git a/HackIllinois/Assets.xcassets/Sponsor.imageset/Contents 2.json b/HackIllinois/Assets.xcassets/Sponsor.imageset/Contents 2.json new file mode 100644 index 00000000..df240cf1 --- /dev/null +++ b/HackIllinois/Assets.xcassets/Sponsor.imageset/Contents 2.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Vector.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/Sponsor.imageset/Vector 2.png b/HackIllinois/Assets.xcassets/Sponsor.imageset/Vector 2.png new file mode 100644 index 00000000..72210685 Binary files /dev/null and b/HackIllinois/Assets.xcassets/Sponsor.imageset/Vector 2.png differ diff --git a/HackIllinois/Assets.xcassets/Sponsor.imageset/Vector@2x 2.png b/HackIllinois/Assets.xcassets/Sponsor.imageset/Vector@2x 2.png new file mode 100644 index 00000000..b7a5cac9 Binary files /dev/null and b/HackIllinois/Assets.xcassets/Sponsor.imageset/Vector@2x 2.png differ diff --git a/HackIllinois/Assets.xcassets/Sponsor.imageset/Vector@3x 2.png b/HackIllinois/Assets.xcassets/Sponsor.imageset/Vector@3x 2.png new file mode 100644 index 00000000..44482687 Binary files /dev/null and b/HackIllinois/Assets.xcassets/Sponsor.imageset/Vector@3x 2.png differ diff --git a/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Group 974 (1) 2.png b/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Group 974 (1) 2.png new file mode 100644 index 00000000..8572b6dd Binary files /dev/null and b/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Group 974 (1) 2.png differ diff --git a/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Group 974 (2) 2.png b/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Group 974 (2) 2.png new file mode 100644 index 00000000..664de71d Binary files /dev/null and b/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Group 974 (2) 2.png differ diff --git a/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Group 975.png b/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Group 975.png new file mode 100644 index 00000000..a94c518c Binary files /dev/null and b/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Group 975.png differ diff --git a/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Tear-Off Calendar 2.png b/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Tear-Off Calendar 2.png new file mode 100644 index 00000000..820c18d7 Binary files /dev/null and b/HackIllinois/Assets.xcassets/TabBarIcons/schedule.imageset/Tear-Off Calendar 2.png differ diff --git a/HackIllinois/Assets.xcassets/TabBarIcons/shop.imageset/image (2) 2.png b/HackIllinois/Assets.xcassets/TabBarIcons/shop.imageset/image (2) 2.png new file mode 100644 index 00000000..545996ff Binary files /dev/null and b/HackIllinois/Assets.xcassets/TabBarIcons/shop.imageset/image (2) 2.png differ diff --git a/HackIllinois/Assets.xcassets/TabBarIcons/shop.imageset/image 2.png b/HackIllinois/Assets.xcassets/TabBarIcons/shop.imageset/image 2.png new file mode 100644 index 00000000..e1137ea7 Binary files /dev/null and b/HackIllinois/Assets.xcassets/TabBarIcons/shop.imageset/image 2.png differ diff --git a/HackIllinois/Assets.xcassets/TabBarIcons/shopSelected.imageset/Group 978 (2) 2.png b/HackIllinois/Assets.xcassets/TabBarIcons/shopSelected.imageset/Group 978 (2) 2.png new file mode 100644 index 00000000..91e28aa2 Binary files /dev/null and b/HackIllinois/Assets.xcassets/TabBarIcons/shopSelected.imageset/Group 978 (2) 2.png differ diff --git a/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Contents 2.json b/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Contents 2.json new file mode 100644 index 00000000..df240cf1 --- /dev/null +++ b/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Contents 2.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Vector.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Vector 2.png b/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Vector 2.png new file mode 100644 index 00000000..6558d429 Binary files /dev/null and b/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Vector 2.png differ diff --git a/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Vector@2x 2.png b/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Vector@2x 2.png new file mode 100644 index 00000000..c6e2ef21 Binary files /dev/null and b/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Vector@2x 2.png differ diff --git a/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Vector@3x 2.png b/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Vector@3x 2.png new file mode 100644 index 00000000..990b6ca9 Binary files /dev/null and b/HackIllinois/Assets.xcassets/ToggleHomePage.imageset/Vector@3x 2.png differ diff --git a/HackIllinois/Assets.xcassets/Vase.imageset/Contents 2.json b/HackIllinois/Assets.xcassets/Vase.imageset/Contents 2.json new file mode 100644 index 00000000..a030dd7c --- /dev/null +++ b/HackIllinois/Assets.xcassets/Vase.imageset/Contents 2.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Other Day.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Other Day@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Other Day@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/Vase.imageset/Other Day 2.png b/HackIllinois/Assets.xcassets/Vase.imageset/Other Day 2.png new file mode 100644 index 00000000..efe89ceb Binary files /dev/null and b/HackIllinois/Assets.xcassets/Vase.imageset/Other Day 2.png differ diff --git a/HackIllinois/Assets.xcassets/Vase.imageset/Other Day@2x 2.png b/HackIllinois/Assets.xcassets/Vase.imageset/Other Day@2x 2.png new file mode 100644 index 00000000..4a4400cf Binary files /dev/null and b/HackIllinois/Assets.xcassets/Vase.imageset/Other Day@2x 2.png differ diff --git a/HackIllinois/Assets.xcassets/Vase.imageset/Other Day@3x 2.png b/HackIllinois/Assets.xcassets/Vase.imageset/Other Day@3x 2.png new file mode 100644 index 00000000..5c120e17 Binary files /dev/null and b/HackIllinois/Assets.xcassets/Vase.imageset/Other Day@3x 2.png differ diff --git a/HackIllinois/Assets.xcassets/VaseClicked.imageset/Contents 2.json b/HackIllinois/Assets.xcassets/VaseClicked.imageset/Contents 2.json new file mode 100644 index 00000000..0c604303 --- /dev/null +++ b/HackIllinois/Assets.xcassets/VaseClicked.imageset/Contents 2.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Vase (Active).png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vase (Active)@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vase (Active)@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/VaseClicked.imageset/Vase (Active) 2.png b/HackIllinois/Assets.xcassets/VaseClicked.imageset/Vase (Active) 2.png new file mode 100644 index 00000000..57193f6c Binary files /dev/null and b/HackIllinois/Assets.xcassets/VaseClicked.imageset/Vase (Active) 2.png differ diff --git a/HackIllinois/Assets.xcassets/VaseClicked.imageset/Vase (Active)@2x 2.png b/HackIllinois/Assets.xcassets/VaseClicked.imageset/Vase (Active)@2x 2.png new file mode 100644 index 00000000..15c3ac83 Binary files /dev/null and b/HackIllinois/Assets.xcassets/VaseClicked.imageset/Vase (Active)@2x 2.png differ diff --git a/HackIllinois/Assets.xcassets/VaseClicked.imageset/Vase (Active)@3x 2.png b/HackIllinois/Assets.xcassets/VaseClicked.imageset/Vase (Active)@3x 2.png new file mode 100644 index 00000000..16818cf1 Binary files /dev/null and b/HackIllinois/Assets.xcassets/VaseClicked.imageset/Vase (Active)@3x 2.png differ diff --git a/HackIllinois/Assets.xcassets/scanner-menu.imageset/Contents 2.json b/HackIllinois/Assets.xcassets/scanner-menu.imageset/Contents 2.json new file mode 100644 index 00000000..85560c29 --- /dev/null +++ b/HackIllinois/Assets.xcassets/scanner-menu.imageset/Contents 2.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Group 902.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 902@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 902@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HackIllinois/Assets.xcassets/scanner-menu.imageset/Group 902@2x 2.png b/HackIllinois/Assets.xcassets/scanner-menu.imageset/Group 902@2x 2.png new file mode 100644 index 00000000..c5409453 Binary files /dev/null and b/HackIllinois/Assets.xcassets/scanner-menu.imageset/Group 902@2x 2.png differ diff --git a/HackIllinois/Assets.xcassets/scanner-menu.imageset/Group 902@3x 2.png b/HackIllinois/Assets.xcassets/scanner-menu.imageset/Group 902@3x 2.png new file mode 100644 index 00000000..5ff2d72c Binary files /dev/null and b/HackIllinois/Assets.xcassets/scanner-menu.imageset/Group 902@3x 2.png differ diff --git a/HackIllinois/Assets.xcassets/scanner-menu.imageset/Group 903.png b/HackIllinois/Assets.xcassets/scanner-menu.imageset/Group 903.png new file mode 100644 index 00000000..cb82c185 Binary files /dev/null and b/HackIllinois/Assets.xcassets/scanner-menu.imageset/Group 903.png differ diff --git a/HackIllinois/ViewControllers/HIPointsShopSwiftUIView.swift b/HackIllinois/ViewControllers/HIPointsShopSwiftUIView.swift index 0fd48c86..109f987c 100644 --- a/HackIllinois/ViewControllers/HIPointsShopSwiftUIView.swift +++ b/HackIllinois/ViewControllers/HIPointsShopSwiftUIView.swift @@ -8,227 +8,604 @@ import Foundation import SwiftUI +import APIManager import HIAPI -class PointShopManager { +class PointShopManager: ObservableObject { + /// Singleton instance static let shared = PointShopManager() - var items: [Item] = [] + /// Published array of items that will notify SwiftUI of changes + @Published var items: [Item] = [] - private init() { - // Private initializer to enforce singleton pattern - } + private init() { } + /// Fetch items from the API and store them in `items`. + /// Publishing to `items` will automatically trigger UI updates in SwiftUI. func preloadItems() { - // Fetch and populate items HIAPI.ShopService.getAllItems() - .onCompletion { result in - do { - let (containedItem, _) = try result.get() - self.items = containedItem.items - } catch { - print("Failed to preload point shop items with the error: \(error)") + .onCompletion { [weak self] result in + // Always hop back onto the main thread before changing any + // Published properties, as they must update the UI on main. + DispatchQueue.main.async { + guard let self = self else { return } + do { + let (containedItem, _) = try result.get() + self.items = containedItem.items + } catch { + print("Failed to preload point shop items with error: \(error)") + } + } + } + .launch() + } +} + +class CartManager: ObservableObject { + /// Singleton instance + static let shared = CartManager() + + /// Published array of items that will notify SwiftUI of changes + @Published var items: [String: Int] = [:] + + private init() { + preloadCartItems() + } + + /// Fetch items from the API and store them in `items`. + /// Publishing to `items` will automatically trigger UI updates in SwiftUI. + func preloadCartItems() { + HIAPI.ShopService.getCartItems() + .onCompletion { [weak self] result in + // Always hop back onto the main thread before changing any + // Published properties, as they must update the UI on main. + DispatchQueue.main.async { + guard let self = self else { return } + do { + let (containedItem, _) = try result.get() + self.items = containedItem.items + } catch { + print("Failed to preload cart items with error: \(error)") + } } } + .authorize(with: HIApplicationStateController.shared.user) .launch() } } struct HIPointShopSwiftUIView: View { - @State var items=[] as [Item] - @State private var profile = HIProfile() - @State var coins = 0 + @ObservedObject var shopManager = PointShopManager.shared + @ObservedObject var cartManager = CartManager.shared + + @State private var coins = 0 @State var tabIndex = 0 + @Binding var title: String + @State var flowView = 0 + @State var startFetchingQR = false + @State var loading = true + @State var qrCode = "hackillinois://user?userToken=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + let isIpad = UIDevice.current.userInterfaceIdiom == .pad + let resizeFactor = [(UIScreen.main.bounds.width/428), (UIScreen.main.bounds.height/926)] // sizing done based on iPhone 13 pro max, resizing factor to modify spacing [width, height] + + @State var showError = false + @State var errorMessage = ["", ""] var body: some View { - // NavigationStack{ - - ZStack { - Image("PurpleBackground") - .resizable() - .edgesIgnoringSafeArea(.all) + if flowView == 0 { + ZStack { + // 1) Background + Image("PointShopBackground") + .resizable() + .ignoresSafeArea() + // 2) Coins display + tab bar VStack { - HStack{ - - HStack(alignment: .center, spacing: 7) { - Image("Coin") - .resizable() - .frame(width: isIpad ? 40 : 25, height:isIpad ? 40 : 25) - - Text("\(coins)") - .font(Font.custom("Montserrat", size: isIpad ? 26 : 16).weight(.bold)) - .foregroundColor(.white) - + HStack(alignment: .center, spacing: 7) { + Image("Coin") + .resizable() + .frame(width: 25 * resizeFactor[0], height: 25 * resizeFactor[0]) + Text("\(coins)") + .font(Font.custom("MontserratRoman-Bold", size: isIpad ? 26 : 16).weight(.bold)) + .foregroundColor(.black) + } + .padding(.horizontal, 11) + .padding(.vertical, 3) + .background(Color(red: 0.9607843137254902, green: 0.9411764705882353, blue: 0.8666666666666667)) + .cornerRadius(1000) + .frame(maxWidth: .infinity, alignment: .trailing) + .offset(x: -25, y: -38) + Spacer() + } + + CustomTopTabBar(tabIndex: $tabIndex) + .offset(y: -240 * (UIScreen.main.bounds.height/852)) + + // 3) BOTTOM VSTACK: two rows pinned to bottom + VStack(spacing: 16) { + let listedItems = filterShopItems(shopItems: shopManager.items, index: tabIndex) + + if listedItems.count >= 2 { + HStack(spacing: 16) { + ForEach(listedItems.prefix(2), id: \.name) { item in + PointShopItemCell(item: item, showError: $showError, errorMessage: $errorMessage) + } } - .padding(.horizontal, 11) - .padding(.vertical, 3) - .background(Color(red: 0.1647, green: 0.1647, blue: 0.1647)) - .cornerRadius(1000) - .frame(maxWidth: .infinity, alignment: .trailing) - .offset(y: isIpad ? -42 : -38) - Spacer() } - Image("KnickKnacks") - .resizable() - .frame(width: isIpad ? 590 : 355, height:isIpad ? 185 : 105) - .offset(y: -25) - VStack(spacing: 0) { - CustomTopTabBar(tabIndex: $tabIndex) - ScrollView(showsIndicators: false) { - if tabIndex == 0 { - VStack(spacing: 0) { - ForEach(PointShopManager.shared.items.filter { !$0.isRaffle }, id: \.self) { item in - PointShopItemCell(item: item) - } - Rectangle() - .foregroundColor(.clear) - .frame(width: UIScreen.main.bounds.width > 850 ? 800 : (isIpad ? 700 : 360), height: 10) - .background(Color(red: 0.4, green: 0.17, blue: 0.07)) - .cornerRadius(1) - } - .onAppear { - getItems() - PointShopManager.shared.preloadItems() - getCoins{coins in - self.coins=coins} - } - } else { - VStack(spacing: 0) { - ForEach(PointShopManager.shared.items.filter { $0.isRaffle }, id: \.self) { item in - PointShopItemCell(item: item) - } - Rectangle() - .foregroundColor(.clear) - .frame(width: UIScreen.main.bounds.width > 850 ? 800 : (isIpad ? 700 : 360), height: 10) - .background(Color(red: 0.4, green: 0.17, blue: 0.07)) - .cornerRadius(1) + // Row 2: horizontal scroll for the rest + if listedItems.count > 2 { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(listedItems.dropFirst(2), id: \.name) { item in + PointShopItemCell(item: item, showError: $showError, errorMessage: $errorMessage) } } - Spacer() + .padding(.horizontal, 8) } - .frame(width: UIScreen.main.bounds.width - 24, alignment: .center) - .padding(.horizontal, 12) } } + // Pin the VSTACK to the bottom of the screen + .frame(maxHeight: .infinity, alignment: .bottom) + .padding(.bottom, 35) // Adjust as needed + + VStack { + HStack { + Image("Cart") + .resizable() + .frame(width: 85 * resizeFactor[0], height: 30 * resizeFactor[1]) + .onTapGesture { + title = "CART" + flowView = 1 + } + } + .frame(height: 60 * resizeFactor[1]) + .frame(maxWidth: .infinity, alignment: .center) + } + } + .overlay(showError ? ErrorPopup(title: errorMessage[0], description: errorMessage[1], show: $showError) : nil) + .onAppear { + // Fetch coins + getCoins { newCoins in + coins = newCoins + } + QRFetchLoop() } + } else if flowView == 1 { + ZStack { + Image("CartBackground") + .resizable() + .edgesIgnoringSafeArea(.all) + // Top right coins display + VStack { + HStack(alignment: .center, spacing: 7) { + Image("Coin") + .resizable() + .frame(width: 25 * resizeFactor[0], height: 25 * resizeFactor[0]) + Text("\(coins)") + .font(Font.custom("MontserratRoman-Bold", size: isIpad ? 26 : 16).weight(.bold)) + .foregroundColor(.black) + } + .padding(.horizontal, 11) + .padding(.vertical, 3) + .background(Color(red: 0.9607843137254902, green: 0.9411764705882353, blue: 0.8666666666666667)) + .cornerRadius(1000) + .frame(maxWidth: .infinity, alignment: .trailing) + .offset(x: -25, y: -38) + Spacer() + } + // Back button to go back to point shop + ZStack { + Circle() + .frame(width: 34 * resizeFactor[0]) + .foregroundColor(Color(red: 139/255, green: 109/255, blue: 116/255)) + Image(systemName: "chevron.left") + .bold() + } + .frame(width: UIScreen.main.bounds.width - 50 * resizeFactor[0], height: UIScreen.main.bounds.height - 250 * resizeFactor[1], alignment: .topLeading) + .onTapGesture { + title = "POINT SHOP" + flowView = 0 + } + // Redeem button to go to QR + VStack { + Spacer() + Image("Redeem") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 119) + .padding(.bottom, 90) + .onTapGesture { + startFetchingQR = true + QRFetchLoop() + title = "" + flowView = 2 + } + } + + VStack(spacing: 5) { + ScrollView { + LazyVGrid(columns: [ + GridItem(.flexible(), spacing: 5), + GridItem(.flexible(), spacing: 5) + ], spacing: 16) { + ForEach(Array(cartManager.items.keys).sorted { $0 > $1 }, id: \.self) { key in + CartItemCell(count: cartManager.items[key] ?? 0, item: findItem(by: key, in: shopManager.items)!, showError: $showError, errorMessage: $errorMessage) + } + } + } + } + .frame(height: 500 * resizeFactor[1]) + .padding(.bottom, 60 * resizeFactor[0]) + } + .overlay(showError ? ErrorPopup(title: errorMessage[0], description: errorMessage[1], show: $showError) : nil) + .onAppear { + cartManager.preloadCartItems() + } + } else { + ZStack { + Image("CartBackground") + .resizable() + .edgesIgnoringSafeArea(.all) + VStack { + HStack { + Image("PointShopBack") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 80 * resizeFactor[0]) + .onTapGesture { + startFetchingQR = false + title = "POINT SHOP" + flowView = 0 + } + .padding(.leading, 35) + Spacer() + } + Spacer() + } + if startFetchingQR { + VStack { + Text("SCAN HERE TO COMPLETE PURCHASE") + .padding(.bottom, 50) + .multilineTextAlignment(.center) + .frame(width: 250 * resizeFactor[0]) + .font(Font.custom("Montserrat", size: 24).weight(.bold)) + Image(uiImage: UIImage(data: getQRCodeDate(text: qrCode)!)!) + .resizable() + .scaledToFit() + .frame(width: 250 * resizeFactor[0], height: 250 * resizeFactor[0]) + } + } + } + .overlay(showError ? ErrorPopup(title: errorMessage[0], description: errorMessage[1], show: $showError) : nil) + .onDisappear { + startFetchingQR = false + } + } } func getItems() { HIAPI.ShopService.getAllItems() - .onCompletion { [self] result in - do { - let (containedItem, _) = try result.get() - let apiItems = containedItem.items - items=[] - apiItems.forEach { apiItem in - items.append(apiItem) + .onCompletion { result in + DispatchQueue.main.async { + do { + let (containedItem, _) = try result.get() + // ✅ Update the manager’s items + shopManager.items = containedItem.items + } catch { + print("Failed to reload points shop: \(error)") } - } catch { - print("Failed to reload points shop with the error: \(error)") } } .launch() } - func getCoins(completion: @escaping (Int) -> Void) { - guard let user = HIApplicationStateController.shared.user else { return } - HIAPI.ProfileService.getUserProfile(userToken: user.token) - .onCompletion { result in - do { - let (apiProfile, _) = try result.get() - completion(apiProfile.coins) + + func getCartItems() { + HIAPI.ShopService.getCartItems() + .onCompletion { result in DispatchQueue.main.async { - NotificationCenter.default.post(name: .loginProfile, object: nil, userInfo: ["profile": self.profile]) + do { + let (containedItem, _) = try result.get() + // ✅ Update the manager’s items + cartManager.items = containedItem.items + } catch { + print("Failed to reload cart: \(error)") + } } - } catch { - print("Failed to reload coins with the error: \(error)") + } + .launch() + } + + func QRFetchLoop() { + if startFetchingQR { + getQRInfo() { _ in + print("QR code received") + } + DispatchQueue.main.asyncAfter(deadline: .now() + 15) { + QRFetchLoop() } } - .authorize(with: user) - .launch() + } + + func getCoins(completion: @escaping (Int) -> Void) { + guard let user = HIApplicationStateController.shared.user else { return } + HIAPI.ProfileService.getUserProfile(userToken: user.token) + .onCompletion { result in + do { + let (apiProfile, _) = try result.get() + DispatchQueue.main.async { + completion(apiProfile.points) + } + } catch { + print("Failed to reload coins with error: \(error)") + let errorDescription = "\(error)" + if let codeString = errorDescription.split(separator: ":").dropFirst().first?.trimmingCharacters(in: .whitespaces).prefix(3) { + print("Error code: \(codeString)") + if codeString == "404" { + errorMessage = ["INVALID USER", "Please sign out and login with an attendee account."] + } else { + errorMessage = ["ERROR: \(codeString)", "Something has gone wrong."] + } + } + showError = true + } + } + .authorize(with: user) + .launch() + } + + func getQRInfo(completion: @escaping (Int) -> Void) { + guard let user = HIApplicationStateController.shared.user else { return } + HIAPI.ShopService.getQR(userToken: user.token) + .onCompletion { result in + do { + let (qr, _) = try result.get() + DispatchQueue.main.async { + self.qrCode = qr.QRCode! + } + } catch APIRequestError.invalidHTTPReponse(code: let code, description: let description) { + if code == 400 { + errorMessage = ["INSUFFICIENT QUANTITY", "Not enough of that item in the shop."] + } else if code == 402 { + errorMessage = ["INSUFFICIENT FUNDS", "You don't have enough to purchase that item!"] + } else if code == 404 { + errorMessage = ["NOT FOUND", "Failed to find item."] + } else if code == 401 { + errorMessage = ["INVALID USER", "Please sign out and login with an attendee account."] + } else { + errorMessage = ["ERROR: \(code)", "\(description)"] + } + showError = true + startFetchingQR = false + } catch { + errorMessage = ["ERROR", "Something has gone wrong."] + showError = true + startFetchingQR = false + } + } + .authorize(with: user) + .launch() + } + + func getQRCodeDate(text: String) -> Data? { + guard let filter = CIFilter(name: "CIQRCodeGenerator") else { return nil } + let data = text.data(using: .ascii, allowLossyConversion: false) + filter.setValue(data, forKey: "inputMessage") + + // Change color of QR code + guard let colorFilter = CIFilter(name: "CIFalseColor") else { return nil } + colorFilter.setValue(filter.outputImage, forKey: "inputImage") + colorFilter.setValue(CIColor(red: 1, green: 1, blue: 1), forKey: "inputColor1") // Background off-white + colorFilter.setValue(CIColor(red: 0, green: 0, blue: 0), forKey: "inputColor0") // Barcode brown + + guard let ciimage = colorFilter.outputImage else { return nil } + let transform = CGAffineTransform(scaleX: 10, y: 10) + let scaledCIImage = ciimage.transformed(by: transform) + let uiimage = UIImage(ciImage: scaledCIImage) + return uiimage.pngData()! } } struct PointShopItemCell: View { + @ObservedObject var shopManager = PointShopManager.shared let item: Item - let isIpad = UIDevice.current.userInterfaceIdiom == .pad + @Binding var showError: Bool + @Binding var errorMessage: [String] + var body: some View { - VStack(spacing: 0) { - //brown bar - Rectangle() - .foregroundColor(.clear) - .frame(width: UIScreen.main.bounds.width > 850 ? 800 : (isIpad ? 700 : 360), height: 10) - .background(Color(red: 0.4, green: 0.17, blue: 0.07)) - .cornerRadius(1) - //transparent pane - ZStack { - Rectangle() - .fill(.white) - .frame(width: UIScreen.main.bounds.width > 850 ? 790 : (isIpad ? 690 : 350), height: 157) - .opacity(0.4) - HStack { - Spacer() - .frame(width: UIScreen.main.bounds.width > 850 ? 210 : (isIpad ? 120 : 30)) - //IMAGE - Image(systemName: "Profile0") - .data(url: URL(string: item.imageURL)!) - .resizable() - .scaledToFit() - .frame(width: 145, height: 145) - Spacer() + ZStack { + // Use a rounded rectangle for the background + RoundedRectangle(cornerRadius: 12) + .fill(Color(red:139/255,green:109/255,blue:117/255).opacity(0.89)) - //bubble view - VStack { - HStack{ - Spacer() - .frame(width:15) - Text(item.name) - .font( - Font.custom("Montserrat", size: 16) - .weight(.semibold) - ) - .foregroundColor(Color(red: 0.05, green: 0.25, blue: 0.25)) - .multilineTextAlignment(.center) - .lineLimit(nil) - .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: .infinity) - .padding(.trailing, 20) + VStack(spacing: 4) { + // Name + Text(item.name) + .font(.caption) + .foregroundColor(.white) + .multilineTextAlignment(.center) + .frame(width: 75 * (UIScreen.main.bounds.width/428)) + + // Item image + Image(systemName: "Profile0") + .data(url: URL(string: item.imageURL)!) + .resizable() + .scaledToFit() + .frame(width: 60 * (UIScreen.main.bounds.width/428), height: 60 * (UIScreen.main.bounds.width/428)) - } + // Price + quantity + HStack(spacing: 4) { + Image("Coin") + .resizable() + .frame(width: 15, height: 15) + if item.isRaffle { + Text("\(item.price)") + .font(.footnote).bold() + .foregroundColor(.black) + } else { + Text("\(item.price) | \(item.quantity) Left") + .font(.footnote) + .foregroundColor(.black) + } + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color(red:245/255, green:240/255, blue:221/255)) + .clipShape(Capsule()) + } + .padding() + } + // Force a square cell + .frame(width: 140 * (UIScreen.main.bounds.width/428), height: 140 * (UIScreen.main.bounds.width/428)) + .cornerRadius(12) + .overlay( + // Add button in the top right corner + Button(action: { + addItemToCart(view: 0, showError: $showError, errorMessage: $errorMessage, itemId: item.itemId) { itemName in + print("Added \(itemName) to cart") + } + }) { + ZStack { + Circle() + .frame(width: 18 * (UIScreen.main.bounds.width/428)) + .foregroundColor(.white) + Image(systemName: "plus") + .foregroundColor(.black) + } + .padding(6) + }, + alignment: .topTrailing + ) + } +} - HStack(alignment: .center, spacing: 7) { - Image("Coin") - .resizable() - .frame(width: 25, height: 25) - if(item.isRaffle) { - Text("\(item.price)") - .font(Font.custom("Montserrat", size: 16).weight(.bold)) - .foregroundColor(.white) - } else { - Group { - Text("\(item.price)") - .font(Font.custom("Montserrat", size: 16).weight(.bold)) - .foregroundColor(.white) + - Text(" | \(item.quantity) Left") - .font(Font.custom("Montserrat", size: 16).weight(.regular)) - .foregroundColor(.white) - } - } +struct CartItemCell: View { + let count: Int + let item: Item + @Binding var showError: Bool + @Binding var errorMessage: [String] + + var body: some View { + ZStack { + // Use a rounded rectangle for the background + RoundedRectangle(cornerRadius: 12) + .fill(Color(red:139/255,green:109/255,blue:117/255).opacity(0.89)) + VStack(spacing: 4) { + // Name + Text(item.name) + .font(.caption) + .foregroundColor(.white) + .multilineTextAlignment(.center) + .frame(width: 75 * (UIScreen.main.bounds.width/428)) + + // Item image + Image(systemName: "Profile0") + .data(url: URL(string: item.imageURL)!) + .resizable() + .scaledToFit() + .frame(width: 60 * (UIScreen.main.bounds.width/428), height: 60 * (UIScreen.main.bounds.width/428)) + + // Price + quantity + HStack(spacing: 4) { + Button(action: { + removeItemFromCart(itemId: item.itemId) { itemName in + print("Removed \(itemName) from cart") } - .padding(.horizontal, 11) - .padding(.vertical, 3) - .background(Color(red: 0.05, green: 0.25, blue: 0.25).opacity(0.5)) - .cornerRadius(1000) + }) { + Image(systemName: "minus") + .foregroundColor(.black) + .frame(width: 6, height: 6) + .padding(.leading, 3) + .background( + Rectangle() + .frame(width: 6, height: 6) + .foregroundColor(Color(.white).opacity(0)) + ) + } + Text(" | \(count) | ") + .foregroundColor(.black) + .font(Font.custom("Montserrat", size: 18).weight(.bold)) + Button(action: { + addItemToCart(view: 1, showError: $showError, errorMessage: $errorMessage, itemId: item.itemId) { itemName in + print("Added \(itemName) to cart") + } + }) { + Image(systemName: "plus") + .foregroundColor(.black) + .frame(width: 6, height: 6) + .padding(.trailing, 3) + .background( + Rectangle() + .frame(width: 6, height: 6) + .foregroundColor(Color(.white).opacity(0)) + ) } - Spacer() } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color(red:245/255, green:240/255, blue:221/255)) + .clipShape(Capsule()) } + .padding() } + // Force a square cell + .frame(width: 140 * (UIScreen.main.bounds.width/428), height: 140 * (UIScreen.main.bounds.width/428)) + .cornerRadius(12) } } +func addItemToCart(view: Int, showError: Binding, errorMessage: Binding<[String]>, itemId: String, completion: @escaping (String) -> Void) { + guard let user = HIApplicationStateController.shared.user else { return } + HIAPI.ShopService.addToCart(itemId: itemId, userToken: user.token) + .onCompletion { result in + do { + let (codeResult, _) = try result.get() + CartManager.shared.items = codeResult.items ?? CartManager.shared.items + if view == 0 { // Points shop --> want popup + let itemName = findItem(by: itemId, in: PointShopManager.shared.items)?.name + errorMessage.wrappedValue = ["SUCCESS!", "\(itemName ?? "Item") added to cart."] + showError.wrappedValue = true + } + } catch APIRequestError.invalidHTTPReponse(code: let code, description: let description) { + print("Failed to add to cart \(code): \(description)") + if code == 400 { + errorMessage.wrappedValue = ["INSUFFICIENT QUANTITY", "Not enough of that item in the shop."] + } else if code == 402 { + errorMessage.wrappedValue = ["INSUFFICIENT FUNDS", "You don't have enough to purchase that item!"] + } else if code == 404 { + errorMessage.wrappedValue = ["NOT FOUND", "Failed to find item."] + } else if code == 401 { + errorMessage.wrappedValue = ["INVALID USER", "Please sign out and login with an attendee account."] + } else { + errorMessage.wrappedValue = ["ERROR: \(code)", "\(description)"] + } + showError.wrappedValue = true + } catch { + print("Failed to add to cart: \(error)") + errorMessage.wrappedValue = ["ERROR", "Something has gone wrong."] + showError.wrappedValue = true + } + } + .authorize(with: user) + .launch() +} + +func removeItemFromCart(itemId: String, completion: @escaping (String) -> Void) { + guard let user = HIApplicationStateController.shared.user else { return } + HIAPI.ShopService.removeFromCart(itemId: itemId, userToken: user.token) + .onCompletion { result in + do { + let (codeResult, _) = try result.get() + CartManager.shared.items = codeResult.items ?? CartManager.shared.items + } catch { + print("Failed to remove from cart: \(error)") + } + } + .authorize(with: user) + .launch() +} + struct CustomTopTabBar: View { @Binding var tabIndex: Int let isIpad = UIDevice.current.userInterfaceIdiom == .pad @@ -238,7 +615,7 @@ struct CustomTopTabBar: View { TabBarButton(text: "MERCH", isSelected: .constant(tabIndex == 0)) .onTapGesture { onButtonTapped(index: 0) } Spacer() - .frame(width: isIpad ? 100: 40) + .frame(width: isIpad ? 100: 30) TabBarButton(text: "RAFFLE", isSelected: .constant(tabIndex == 1)) .onTapGesture { onButtonTapped(index: 1) } } @@ -249,6 +626,14 @@ struct CustomTopTabBar: View { } } +func findItem(by itemId: String, in shopItems: [Item]) -> Item? { + return shopItems.first(where: { $0.itemId == itemId }) +} + +func filterShopItems(shopItems: [Item], index: Int) -> [Item] { + return shopItems.filter { $0.isRaffle == (index == 1) } +} + struct TabBarButton: View { let text: String @Binding var isSelected: Bool @@ -256,21 +641,64 @@ struct TabBarButton: View { var body: some View { ZStack(alignment: .center) { if isSelected { - Rectangle() - .fill(Color(red: 0.85, green: 0.25, blue: 0.47)) + Image("PointShopTabSelected") + .resizable() + .aspectRatio(contentMode: .fit) .frame(width: UIScreen.main.bounds.width > 850 ? 350 : (isIpad ? 295 : 155), height: isIpad ? 90: 50) - .cornerRadius(10, corners: [.topLeft, .topRight]) }else{ - Rectangle() - .fill(Color(red: 0.85, green: 0.25, blue: 0.47)) + Image("PointShopTabUnselected") + .resizable() + .aspectRatio(contentMode: .fit) .frame(width: UIScreen.main.bounds.width > 850 ? 350 : (isIpad ? 295 : 155), height: isIpad ? 90: 50) - .cornerRadius(10, corners: [.topLeft, .topRight]) - .opacity(0) } Text(text) - .foregroundColor(.white) + .foregroundColor(Color(HIAppearance.metallicCopper)) .fontWeight(.heavy) - .font(.custom("MontserratRoman-SemiBold", size: UIDevice.current.userInterfaceIdiom == .pad ? 36 : 18)) + .font(.custom("MontserratRoman-Bold", size: UIDevice.current.userInterfaceIdiom == .pad ? 36 : 16)) + } + } +} + +struct ErrorPopup: View { + let title: String + let description: String + @Binding var show: Bool + var body: some View { + ZStack { + Rectangle() + .ignoresSafeArea() + .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) + .foregroundColor(Color(.black).opacity(0.2)) + VStack { + Text(title) + .font(Font.custom("Montserrat", size: 18).weight(.bold)) + .foregroundColor(Color(red:109/255, green:41/255, blue:26/255)) + .padding(.top, 20) + .padding(.bottom, 10) + Text(description) + .font(Font.custom("Montserrat", size: 15).weight(.medium)) + .frame(width: 275) + .multilineTextAlignment(.center) + Text("OK") + .font(Font.custom("Montserrat", size: 18).weight(.bold)) + .foregroundColor(Color(red:109/255, green:41/255, blue:26/255)) + .padding(.vertical, 5) + .onTapGesture { + show = false + } + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(red: 255 / 255, green: 178 / 255, blue: 62 / 255)) + .frame(width: (300 - 16) * (UIScreen.main.bounds.width/393)) + ) + .padding(.bottom, 8 * UIScreen.main.bounds.width/393) + .padding(.top, 10) + } + .frame(width: 300 * (UIScreen.main.bounds.width/393)) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(red: 255 / 255, green: 247 / 255, blue: 240 / 255)) + ) } } } diff --git a/HackIllinois/ViewControllers/HIPointsShopViewController.swift b/HackIllinois/ViewControllers/HIPointsShopViewController.swift index b7aae3c4..e3e449ff 100644 --- a/HackIllinois/ViewControllers/HIPointsShopViewController.swift +++ b/HackIllinois/ViewControllers/HIPointsShopViewController.swift @@ -16,6 +16,7 @@ import SwiftUI class HIPointsShopViewController: HIBaseViewController { // MARK: - Properties private var profile = HIProfile() + private var titleBinding = "POINT SHOP" } // MARK: - UITabBarItem Setup @@ -29,8 +30,14 @@ extension HIPointsShopViewController { extension HIPointsShopViewController { override func loadView() { super.loadView() - let swiftUIView = HIPointShopSwiftUIView() - let hostingController = UIHostingController(rootView: swiftUIView) + let swiftUIView = HIPointShopSwiftUIView(title: Binding( + get: { self.titleBinding }, + set: { newValue in + self.titleBinding = newValue + self.setCustomTitle(customTitle: newValue) + } + )) + let hostingController = UIHostingController(rootView: swiftUIView) addChild(hostingController) hostingController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(hostingController.view) diff --git a/HackIllinois/ViewControllers/HIScanPointsShopViewController.swift b/HackIllinois/ViewControllers/HIScanPointsShopViewController.swift index f809f4aa..9e1184d8 100644 --- a/HackIllinois/ViewControllers/HIScanPointsShopViewController.swift +++ b/HackIllinois/ViewControllers/HIScanPointsShopViewController.swift @@ -271,38 +271,29 @@ extension HIScanPointsShopViewController: AVCaptureMetadataOutputObjectsDelegate let meta = metadataObjects.first as? AVMetadataMachineReadableCodeObject let code = meta?.stringValue ?? "" guard let user = HIApplicationStateController.shared.user else { return } - if let rangeItemId = code.range(of: "itemId="), let rangeInstance = code.range(of: "&instance=") { - let itemId = code[rangeItemId.upperBound.. [String: AnyObject]? { let string = token.components(separatedBy: ".") if string.count == 1 { return nil }