commit 88b392e5e2024f6690c812b09d41de06b1f05ff5 Author: umer Date: Wed Apr 23 20:14:15 2025 +0500 updated code diff --git a/Club_portal/Club Portal/Club Portal.xcodeproj/project.pbxproj b/Club_portal/Club Portal/Club Portal.xcodeproj/project.pbxproj new file mode 100644 index 0000000..19e74b6 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal.xcodeproj/project.pbxproj @@ -0,0 +1,365 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + ACAC2DEB2D9F2C1900869E5C /* CalendarKit in Frameworks */ = {isa = PBXBuildFile; productRef = ACAC2DEA2D9F2C1900869E5C /* CalendarKit */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + ACAC2DCF2D9F271300869E5C /* Club Portal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Club Portal.app"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + ACAC2DD12D9F271300869E5C /* Club Portal */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "Club Portal"; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + ACAC2DCC2D9F271300869E5C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ACAC2DEB2D9F2C1900869E5C /* CalendarKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + ACAC2DC62D9F271300869E5C = { + isa = PBXGroup; + children = ( + ACAC2DD12D9F271300869E5C /* Club Portal */, + ACAC2DD02D9F271300869E5C /* Products */, + ); + sourceTree = ""; + }; + ACAC2DD02D9F271300869E5C /* Products */ = { + isa = PBXGroup; + children = ( + ACAC2DCF2D9F271300869E5C /* Club Portal.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + ACAC2DCE2D9F271300869E5C /* Club Portal */ = { + isa = PBXNativeTarget; + buildConfigurationList = ACAC2DDD2D9F271500869E5C /* Build configuration list for PBXNativeTarget "Club Portal" */; + buildPhases = ( + ACAC2DCB2D9F271300869E5C /* Sources */, + ACAC2DCC2D9F271300869E5C /* Frameworks */, + ACAC2DCD2D9F271300869E5C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + ACAC2DD12D9F271300869E5C /* Club Portal */, + ); + name = "Club Portal"; + packageProductDependencies = ( + ACAC2DEA2D9F2C1900869E5C /* CalendarKit */, + ); + productName = "Club Portal"; + productReference = ACAC2DCF2D9F271300869E5C /* Club Portal.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + ACAC2DC72D9F271300869E5C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + ACAC2DCE2D9F271300869E5C = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = ACAC2DCA2D9F271300869E5C /* Build configuration list for PBXProject "Club Portal" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = ACAC2DC62D9F271300869E5C; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + ACAC2DE92D9F2C1900869E5C /* XCRemoteSwiftPackageReference "CalendarKit" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = ACAC2DD02D9F271300869E5C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + ACAC2DCE2D9F271300869E5C /* Club Portal */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + ACAC2DCD2D9F271300869E5C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + ACAC2DCB2D9F271300869E5C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + ACAC2DDB2D9F271500869E5C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + ACAC2DDC2D9F271500869E5C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + ACAC2DDE2D9F271500869E5C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Club Portal/Preview Content\""; + DEVELOPMENT_TEAM = BSGYUG5U29; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.app.Club-Portal"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + ACAC2DDF2D9F271500869E5C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Club Portal/Preview Content\""; + DEVELOPMENT_TEAM = BSGYUG5U29; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.app.Club-Portal"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + ACAC2DCA2D9F271300869E5C /* Build configuration list for PBXProject "Club Portal" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ACAC2DDB2D9F271500869E5C /* Debug */, + ACAC2DDC2D9F271500869E5C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + ACAC2DDD2D9F271500869E5C /* Build configuration list for PBXNativeTarget "Club Portal" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ACAC2DDE2D9F271500869E5C /* Debug */, + ACAC2DDF2D9F271500869E5C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + ACAC2DE92D9F2C1900869E5C /* XCRemoteSwiftPackageReference "CalendarKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/richardtop/CalendarKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.11; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + ACAC2DEA2D9F2C1900869E5C /* CalendarKit */ = { + isa = XCSwiftPackageProductDependency; + package = ACAC2DE92D9F2C1900869E5C /* XCRemoteSwiftPackageReference "CalendarKit" */; + productName = CalendarKit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = ACAC2DC72D9F271300869E5C /* Project object */; +} diff --git a/Club_portal/Club Portal/Club Portal.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Club_portal/Club Portal/Club Portal.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Club_portal/Club Portal/Club Portal.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Club_portal/Club Portal/Club Portal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Club_portal/Club Portal/Club Portal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..45d1f78 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "67a1c5e835fc1360427d7e9a548368960eed766e429f7c9f11a95a0e99d682d9", + "pins" : [ + { + "identity" : "calendarkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/richardtop/CalendarKit", + "state" : { + "revision" : "89e02a1472e9ad815ae72b62fd15430144fd1c4c", + "version" : "1.1.11" + } + } + ], + "version" : 3 +} diff --git a/Club_portal/Club Portal/Club Portal.xcodeproj/xcuserdata/umertahir.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Club_portal/Club Portal/Club Portal.xcodeproj/xcuserdata/umertahir.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..19e5bda --- /dev/null +++ b/Club_portal/Club Portal/Club Portal.xcodeproj/xcuserdata/umertahir.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/AccentColor.colorset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/AppIcon.appiconset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/Color/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/Color/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/Color/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/Color/blue.colorset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/Color/blue.colorset/Contents.json new file mode 100644 index 0000000..fb61520 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/Color/blue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x64", + "green" : "0x26", + "red" : "0x16" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x64", + "green" : "0x26", + "red" : "0x16" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/Color/green.colorset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/Color/green.colorset/Contents.json new file mode 100644 index 0000000..95e1e06 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/Color/green.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x48", + "green" : "0x64", + "red" : "0x17" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x48", + "green" : "0x64", + "red" : "0x17" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/ball.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/ball.imageset/Contents.json new file mode 100644 index 0000000..a6874b6 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/ball.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Custom Icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Custom Icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/ball.imageset/Custom Icon.png b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/ball.imageset/Custom Icon.png new file mode 100644 index 0000000..7410d59 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/ball.imageset/Custom Icon.png differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/ball.imageset/Custom Icon@2x.png b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/ball.imageset/Custom Icon@2x.png new file mode 100644 index 0000000..f51125a Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/ball.imageset/Custom Icon@2x.png differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/cash.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/cash.imageset/Contents.json new file mode 100644 index 0000000..8124221 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/cash.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/cash.imageset/Icon (1).pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/cash.imageset/Icon (1).pdf new file mode 100644 index 0000000..2a59d27 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/cash.imageset/Icon (1).pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/clock.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/clock.imageset/Contents.json new file mode 100644 index 0000000..e899d63 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/clock.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "clock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/clock.imageset/clock.pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/clock.imageset/clock.pdf new file mode 100644 index 0000000..f4b4185 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/clock.imageset/clock.pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter 1.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter 1.imageset/Contents.json new file mode 100644 index 0000000..7d33427 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter 1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Frame 11.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter 1.imageset/Frame 11.pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter 1.imageset/Frame 11.pdf new file mode 100644 index 0000000..f699672 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter 1.imageset/Frame 11.pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter.imageset/Contents.json new file mode 100644 index 0000000..e4fa4c3 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "filter.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter.imageset/filter.png b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter.imageset/filter.png new file mode 100644 index 0000000..7e3c8b5 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/filter.imageset/filter.png differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/left_arrow.imageset/Compact Button [1.0] (1).pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/left_arrow.imageset/Compact Button [1.0] (1).pdf new file mode 100644 index 0000000..f2840c0 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/left_arrow.imageset/Compact Button [1.0] (1).pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/left_arrow.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/left_arrow.imageset/Contents.json new file mode 100644 index 0000000..4d29721 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/left_arrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Compact Button [1.0] (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/lock.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/lock.imageset/Contents.json new file mode 100644 index 0000000..2636b8f --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/lock.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Custom Icon (1).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/lock.imageset/Custom Icon (1).png b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/lock.imageset/Custom Icon (1).png new file mode 100644 index 0000000..1fc358e Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/lock.imageset/Custom Icon (1).png differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/logout.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/logout.imageset/Contents.json new file mode 100644 index 0000000..e8f5450 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/logout.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon-right.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/logout.imageset/Icon-right.pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/logout.imageset/Icon-right.pdf new file mode 100644 index 0000000..6c7ac50 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/logout.imageset/Icon-right.pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/right_arrow.imageset/Compact Button [1.0].pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/right_arrow.imageset/Compact Button [1.0].pdf new file mode 100644 index 0000000..c69e01f Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/right_arrow.imageset/Compact Button [1.0].pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/right_arrow.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/right_arrow.imageset/Contents.json new file mode 100644 index 0000000..97e6326 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/right_arrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Compact Button [1.0].pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/send.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/send.imageset/Contents.json new file mode 100644 index 0000000..59f885f --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/send.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Frame 4 (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/send.imageset/Frame 4 (1).pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/send.imageset/Frame 4 (1).pdf new file mode 100644 index 0000000..06c0b4c Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/send.imageset/Frame 4 (1).pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1.imageset/Contents.json new file mode 100644 index 0000000..813d5fd --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Frame 595 (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1.imageset/Frame 595 (2).pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1.imageset/Frame 595 (2).pdf new file mode 100644 index 0000000..385d468 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1.imageset/Frame 595 (2).pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1_.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1_.imageset/Contents.json new file mode 100644 index 0000000..020b3f9 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1_.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Frame 595.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1_.imageset/Frame 595.pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1_.imageset/Frame 595.pdf new file mode 100644 index 0000000..fcf8e2b Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab1_.imageset/Frame 595.pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2.imageset/Contents.json new file mode 100644 index 0000000..2921b0b --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "calendar-clock, date-time.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2.imageset/calendar-clock, date-time.pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2.imageset/calendar-clock, date-time.pdf new file mode 100644 index 0000000..207d03c Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2.imageset/calendar-clock, date-time.pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2_.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2_.imageset/Contents.json new file mode 100644 index 0000000..a44bfc3 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2_.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "calendar-clock, date-time (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2_.imageset/calendar-clock, date-time (1).pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2_.imageset/calendar-clock, date-time (1).pdf new file mode 100644 index 0000000..14430d2 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab2_.imageset/calendar-clock, date-time (1).pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3.imageset/Contents.json new file mode 100644 index 0000000..d2ffc4e --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Frame 595 (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3.imageset/Frame 595 (1).pdf b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3.imageset/Frame 595 (1).pdf new file mode 100644 index 0000000..f7cd623 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3.imageset/Frame 595 (1).pdf differ diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3_.imageset/Contents.json b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3_.imageset/Contents.json new file mode 100644 index 0000000..bfa0c83 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3_.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 595.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3_.imageset/Frame 595.png b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3_.imageset/Frame 595.png new file mode 100644 index 0000000..f0762e2 Binary files /dev/null and b/Club_portal/Club Portal/Club Portal/Assets.xcassets/icons/tab3_.imageset/Frame 595.png differ diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/APIConstants.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/APIConstants.swift new file mode 100644 index 0000000..e752b5f --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/APIConstants.swift @@ -0,0 +1,36 @@ +// +// APIConstants.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + +enum APIConstants { + static let baseURL = "https://courtmatchup.mkdlabs.com" + static let xProject = "Y291cnRtYXRjaHVwOmczbmp1OTlpZjR3enk4Z3V0bHEwYmUwZDI1Nzk1MTNsbTg=" + static var commonHeaders: [String: String] { + var headers = ["Content-Type": "application/json", + "x-project": "Y291cnRtYXRjaHVwOmczbmp1OTlpZjR3enk4Z3V0bHEwYmUwZDI1Nzk1MTNsbTg=" + ] + if let token = AuthToken.current { + headers["Authorization"] = "Bearer \(token)" + } + return headers + } +} + +struct AuthToken { + static var current: String? { + // Replace with Keychain token + return AppSettings.token + } +} + + +enum HTTPMethod: String { + case get = "GET" + case post = "POST" +} + + diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/APIError.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/APIError.swift new file mode 100644 index 0000000..7f5e094 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/APIError.swift @@ -0,0 +1,122 @@ +import Foundation + +import SwiftUI + +enum APIError: Error, CustomStringConvertible { + case invalidURL + case invalidResponse(Int, String?) // Include status code and optional error message + case decodingError(Error) + case serverError(String) + case unknownError + case logout + + var description: String { + switch self { + case .invalidURL: + return "Invalid URL" + case .invalidResponse(let statusCode, let message): + return "Invalid response with status code: \(statusCode)\nMessage: \(message ?? "No additional message")" + case .decodingError(let error): + return "Decoding error: \(error.localizedDescription)" + case .serverError(let message): + return "Server error: \(message)" + case .unknownError: + return "An unknown error occurred" + case .logout: + return "Token expired" + } + } +} + +//struct Endpoint { +// let urlRequest: URLRequest? +// let isMultipart: Bool +// let body: Data? +//} + +final class APIService { + static let shared = APIService() + private init() {} + + func request(_ endpoint: Endpoint, responseType: T.Type) async throws -> T { + guard let request = endpoint.urlRequest else { + throw APIError.invalidURL + } + + print("Request URL: \(request.url?.absoluteString ?? "Invalid URL")") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.invalidResponse(0, "No HTTP response received") + } + + print("HTTP Status Code: \(httpResponse.statusCode)") + + if let responseString = String(data: data, encoding: .utf8) { + print("Response: \(responseString)") + } else { + print("Failed to convert response data to string") + } + + if !(200..<300).contains(httpResponse.statusCode) { + if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) { + if errorResponse.message == "TOKEN_EXPIRED" { + // Perform logout + throw APIError.logout + } + throw APIError.serverError(errorResponse.message) + } else { + throw APIError.invalidResponse(httpResponse.statusCode, "Unknown server error") + } + } + + do { + return try JSONDecoder().decode(T.self, from: data) + } catch { + throw APIError.decodingError(error) + } + } + + private func uploadMultipart(endpoint: Endpoint, responseType: T.Type) async throws -> T { + guard var request = endpoint.urlRequest else { throw APIError.invalidURL } + + let boundary = "Boundary-\(UUID().uuidString)" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + request.httpBody = endpoint.body + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.invalidResponse(0, "No HTTP response received") + } + + print("HTTP Status Code: \(httpResponse.statusCode)") + + if let responseString = String(data: data, encoding: .utf8) { + print("Response: \(responseString)") + } else { + print("Failed to convert response data to string") + } + + guard (200..<300).contains(httpResponse.statusCode) else { + if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) { + throw APIError.serverError(errorResponse.message) + } else { + throw APIError.invalidResponse(httpResponse.statusCode, "Unknown server error") + } + } + + do { + return try JSONDecoder().decode(T.self, from: data) + } catch { + throw APIError.decodingError(error) + } + } + + +} + +struct ErrorResponse: Codable { + let message: String +} diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/AvailabilityListEndpoint.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/AvailabilityListEndpoint.swift new file mode 100644 index 0000000..435dfdf --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/AvailabilityListEndpoint.swift @@ -0,0 +1,26 @@ +// +// AvailabilityListEndpoint.swift +// Club Portal +// +// Created by Umer Tahir on 15/04/2025. +// + +import SwiftUI + +struct AvailabilityListEndpoint: Endpoint { + var urlQueryItems: [URLQueryItem] = [] + + var path: String { + "/v3/api/custom/courtmatchup/club/reservations/availability" + } + + var method: HTTPMethod { .get } + + + + var customHeaders: [String: String]? { nil } + + var body: Data? { nil } + + var isMultipart: Bool { false } +} diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ClubStatisticsEndpoint.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ClubStatisticsEndpoint.swift new file mode 100644 index 0000000..8382931 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ClubStatisticsEndpoint.swift @@ -0,0 +1,73 @@ +// +// ClubStatisticsEndpoint.swift +// Club Portal +// +// Created by Umer Tahir on 11/04/2025. +// +import SwiftUI + +struct ClubStatisticsEndpoint: Endpoint { + var path: String { + "/v3/api/custom/courtmatchup/club/statistics" + } + + var method: HTTPMethod { .get } + + let startDate: String + let endDate: String + let startTime: String + let endTime: String + + var customHeaders: [String: String]? { nil } + + var urlQueryItems: [URLQueryItem] { + [ + URLQueryItem(name: "start_date", value: startDate), + URLQueryItem(name: "end_date", value: endDate), + URLQueryItem(name: "start_time", value: startTime), + URLQueryItem(name: "end_time", value: endTime) + ] + } + + var body: Data? { nil } + + var isMultipart: Bool { false } +} +struct GetClubEndpoint: Endpoint { + var path: String { + "/v3/api/custom/courtmatchup/user/club/\(clubId)" + } + + var method: HTTPMethod { .get } + + let clubId: String + + var customHeaders: [String: String]? { nil } + + var urlQueryItems: [URLQueryItem] { [] } // no query items needed + + var body: Data? { nil } + + var isMultipart: Bool { false } +} + +struct GetprofileEndpoint: Endpoint { + var path: String { + "/v3/api/custom/courtmatchup/club/profile" + } + + var method: HTTPMethod { .get } + + + var customHeaders: [String: String]? { nil } + + var urlQueryItems: [URLQueryItem] { + [ + + ] + } + + var body: Data? { nil } + + var isMultipart: Bool { false } +} diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ForgotPasswordEndpoint.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ForgotPasswordEndpoint.swift new file mode 100644 index 0000000..3341629 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ForgotPasswordEndpoint.swift @@ -0,0 +1,29 @@ +// +// ForgotPasswordEndpoint.swift +// Club Portal +// +// Created by Umer Tahir on 10/04/2025. +// + +import SwiftUI + +struct ForgotPasswordEndpoint: Endpoint { + var urlQueryItems: [URLQueryItem] = [] + + var path: String { "/v2/api/lambda/mobile/forgot" } + var method: HTTPMethod { .post } + + let email: String + let role: String + + var customHeaders: [String: String]? { nil } + + var body: Data? { + try? JSONEncoder().encode([ + "email": email, + "role": role + ]) + } + + var isMultipart: Bool { false } +} diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/LoginEndpoint.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/LoginEndpoint.swift new file mode 100644 index 0000000..9703de7 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/LoginEndpoint.swift @@ -0,0 +1,33 @@ +// +// LoginEndpoint.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + +import SwiftUI + +struct LoginEndpoint: Endpoint { + var urlQueryItems: [URLQueryItem] = [] + + var path: String { "/v2/api/lambda/login" } + var method: HTTPMethod { .post } + + let email: String + let password: String + let role: String = "club" + let isRefresh: Bool + + var customHeaders: [String: String]? { nil } + + var body: Data? { + try? JSONEncoder().encode([ + "email": email, + "password": password, + "role": role, + "is_refresh": isRefresh ? "true" : "false" // Convert Bool to String + ]) + } + + var isMultipart: Bool { false } +} diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ReservationListEndpoint.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ReservationListEndpoint.swift new file mode 100644 index 0000000..108887a --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ReservationListEndpoint.swift @@ -0,0 +1,38 @@ +// +// ReservationListEndpoint.swift +// Club Portal +// +// Created by Umer Tahir on 12/04/2025. +// +import SwiftUI + +struct ReservationListEndpoint: Endpoint { + var urlQueryItems: [URLQueryItem] = [] + + var path: String { + "/v4/api/records/reservation?join=booking|booking_id&join=user|user_id&join=buddy|buddy_id&join=clubs|club_id&order=id,desc&filter=courtmatchup_booking.date,cs,\(clubID)" + } + + var method: HTTPMethod { .get } + + //let date: String + let clubID : Int + +// var queryItems: [URLQueryItem]? { +// [ +// URLQueryItem(name: "join", value: "booking|booking_id"), +// URLQueryItem(name: "join", value: "user|user_id"), +// URLQueryItem(name: "join", value: "buddy|buddy_id"), +// URLQueryItem(name: "join", value: "clubs|club_id"), +// URLQueryItem(name: "order", value: "id,desc"), +// URLQueryItem(name: "filter", value: "courtmatchup_booking.date,cs,\(date)") +// ] +// } + + var customHeaders: [String: String]? { nil } + + var body: Data? { nil } + + var isMultipart: Bool { false } +} + diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ResetPasswordEndpoint.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ResetPasswordEndpoint.swift new file mode 100644 index 0000000..4908a46 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/EndPoints/ResetPasswordEndpoint.swift @@ -0,0 +1,31 @@ +// +// ResetPasswordEndpoint.swift +// Club Portal +// +// Created by Umer Tahir on 10/04/2025. +// + +import SwiftUI + +struct ResetPasswordEndpoint: Endpoint { + var urlQueryItems: [URLQueryItem] = [] + + var path: String { "/v2/api/lambda/mobile/reset" } + var method: HTTPMethod { .post } + + let email: String + let code: String + let password: String + + var customHeaders: [String: String]? { nil } + + var body: Data? { + try? JSONEncoder().encode([ + "email": email, + "code": code, + "password": password + ]) + } + + var isMultipart: Bool { false } +} diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Endpoint.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Endpoint.swift new file mode 100644 index 0000000..4692659 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Endpoint.swift @@ -0,0 +1,47 @@ +// +// Endpoint.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + +import SwiftUI + +protocol Endpoint { + var path: String { get } + var method: HTTPMethod { get } + var customHeaders: [String: String]? { get } + var body: Data? { get } + var isMultipart: Bool { get } + + var urlQueryItems: [URLQueryItem] { get } // 👈 Add this + var urlRequest: URLRequest? { get } +} + +extension Endpoint { + var urlRequest: URLRequest? { + // Build full URL with query parameters + guard var components = URLComponents(string: APIConstants.baseURL + path) else { + return nil + } + + if !urlQueryItems.isEmpty { + components.queryItems = urlQueryItems + } + + guard let url = components.url else { + return nil + } + + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + + // Merge headers + var headers = APIConstants.commonHeaders + customHeaders?.forEach { headers[$0] = $1 } + request.allHTTPHeaderFields = headers + + request.httpBody = body + return request + } +} diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/AvailabliltyRsp.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/AvailabliltyRsp.swift new file mode 100644 index 0000000..8d435ad --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/AvailabliltyRsp.swift @@ -0,0 +1,444 @@ +// This file was generated from JSON Schema using quicktype, do not modify it directly. +// To parse the JSON, add this file to your project and do: +// +// let availabliltyRsp = try? JSONDecoder().decode(AvailabliltyRsp.self, from: jsonData) + +import Foundation + +// MARK: - AvailabliltyRsp +struct AvailabliltyRsp: Codable { + let error: Bool? + let courts: Courts? + let hours: Hours? + let coaches: Coaches? + let staff: Staff? + let sports: [JSONAny]? + let dateRange, timeRange: Range? + + enum CodingKeys: String, CodingKey { + case error, courts, hours, coaches, staff, sports + case dateRange = "date_range" + case timeRange = "time_range" + } +} + +// MARK: - Coaches +struct Coaches: Codable { + let available: [CoachesAvailable]? + let unavailable: [CoachesAvailable]? + let total: Int? +} + +// MARK: - CoachesAvailable +struct CoachesAvailable: Codable , Identifiable, Hashable { + let id: Int? + let coachID, userID: Int? + let bio, hourlyRate, availability, firstName: String? + let lastName, phone, email: String? + let photo: String? + let coachSports: String? + let sports: [Sport]? + let availabilityData: AvailabilityData? + + enum CodingKeys: String, CodingKey { + case coachID = "coach_id" + case userID = "user_id" + case bio + case hourlyRate = "hourly_rate" + case availability + case firstName = "first_name" + case lastName = "last_name" + case email, photo, phone + case coachSports = "coach_sports" + case sports, availabilityData + case id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: CoachesAvailable, rhs: CoachesAvailable) -> Bool { + lhs.id == rhs.id + } +} + +// MARK: - AvailabilityData +struct AvailabilityData: Codable, Identifiable, Hashable { + + let id: Int? + let availability: [Availability]? + let unavailability: [JSONAny]? + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: AvailabilityData, rhs: AvailabilityData) -> Bool { + lhs.id == rhs.id + } + +} + +// MARK: - Availability +struct Availability: Codable, Identifiable, Hashable { + let start, end, date, time: String? + let id: Int? + +} + +// MARK: - Sport +struct Sport: Codable, Identifiable, Hashable{ + let id: Int? + let sportID: Int? + let type, subType: String? + let price: Int? + let sportName: String? + + enum CodingKeys: String, CodingKey { + case sportID = "sport_id" + case type + case subType = "sub_type" + case price + case sportName = "sport_name" + case id + } + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: Sport, rhs: Sport) -> Bool { + lhs.id == rhs.id + } +} + +// MARK: - Courts +struct Courts: Codable { + let available: [CourtsAvailable]? + let unavailable: [JSONAny]? + let total: Int? +} + +// MARK: - CourtsAvailable +struct CourtsAvailable: Codable, Identifiable , Hashable{ + let id: Int? + let name, type, subtype: String? + let sportID, surfaceID: Int? + let availability, sportName: String? + let surfaceName: String? + let availabilityData: AvailabilityData? + + enum CodingKeys: String, CodingKey { + case id, name, type, subtype + case sportID = "sport_id" + case surfaceID = "surface_id" + case availability + case sportName = "sport_name" + case surfaceName = "surface_name" + case availabilityData + } + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: CourtsAvailable, rhs: CourtsAvailable) -> Bool { + lhs.id == rhs.id + } +} + +// MARK: - Range +struct Range: Codable { + let from, until: String? +} + +// MARK: - Hours +struct Hours: Codable { + let available: [String]? + let total: Int? + let range: Range? + let byDate: [ByDate]? +} + +// MARK: - ByDate +struct ByDate: Codable { + let date: String? + let slots: [String]? + let from, until: String? +} + +// MARK: - Staff +struct Staff: Codable, Identifiable, Hashable { + var id: Int? + let available: [StaffAvailable]? + let unavailable: [JSONAny]? + let total: Int? + + static func == (lhs: Staff, rhs: Staff) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +// MARK: - StaffAvailable +struct StaffAvailable: Codable, Identifiable, Hashable { + var id: Int? + let staffID, userID: Int? + let availability, staffRole, firstName, lastName: String? + let email, phone, photo: String? + let availabilityData: AvailabilityData? + + enum CodingKeys: String, CodingKey { + case staffID = "staff_id" + case userID = "user_id" + case availability + case staffRole = "staff_role" + case firstName = "first_name" + case lastName = "last_name" + case email, photo, availabilityData, phone + } +} + +// MARK: - Encode/decode helpers + +class JSONNull: Codable, Hashable { + + public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool { + return true + } + + public var hashValue: Int { + return 0 + } + + public init() {} + + public required init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if !container.decodeNil() { + throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encodeNil() + } +} + +class JSONCodingKey: CodingKey { + let key: String + + required init?(intValue: Int) { + return nil + } + + required init?(stringValue: String) { + key = stringValue + } + + var intValue: Int? { + return nil + } + + var stringValue: String { + return key + } +} + +class JSONAny: Codable { + + let value: Any + + static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { + let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") + return DecodingError.typeMismatch(JSONAny.self, context) + } + + static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError { + let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny") + return EncodingError.invalidValue(value, context) + } + + static func decode(from container: SingleValueDecodingContainer) throws -> Any { + if let value = try? container.decode(Bool.self) { + return value + } + if let value = try? container.decode(Int64.self) { + return value + } + if let value = try? container.decode(Double.self) { + return value + } + if let value = try? container.decode(String.self) { + return value + } + if container.decodeNil() { + return JSONNull() + } + throw decodingError(forCodingPath: container.codingPath) + } + + static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any { + if let value = try? container.decode(Bool.self) { + return value + } + if let value = try? container.decode(Int64.self) { + return value + } + if let value = try? container.decode(Double.self) { + return value + } + if let value = try? container.decode(String.self) { + return value + } + if let value = try? container.decodeNil() { + if value { + return JSONNull() + } + } + if var container = try? container.nestedUnkeyedContainer() { + return try decodeArray(from: &container) + } + if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) { + return try decodeDictionary(from: &container) + } + throw decodingError(forCodingPath: container.codingPath) + } + + static func decode(from container: inout KeyedDecodingContainer, forKey key: JSONCodingKey) throws -> Any { + if let value = try? container.decode(Bool.self, forKey: key) { + return value + } + if let value = try? container.decode(Int64.self, forKey: key) { + return value + } + if let value = try? container.decode(Double.self, forKey: key) { + return value + } + if let value = try? container.decode(String.self, forKey: key) { + return value + } + if let value = try? container.decodeNil(forKey: key) { + if value { + return JSONNull() + } + } + if var container = try? container.nestedUnkeyedContainer(forKey: key) { + return try decodeArray(from: &container) + } + if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) { + return try decodeDictionary(from: &container) + } + throw decodingError(forCodingPath: container.codingPath) + } + + static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] { + var arr: [Any] = [] + while !container.isAtEnd { + let value = try decode(from: &container) + arr.append(value) + } + return arr + } + + static func decodeDictionary(from container: inout KeyedDecodingContainer) throws -> [String: Any] { + var dict = [String: Any]() + for key in container.allKeys { + let value = try decode(from: &container, forKey: key) + dict[key.stringValue] = value + } + return dict + } + + static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws { + for value in array { + if let value = value as? Bool { + try container.encode(value) + } else if let value = value as? Int64 { + try container.encode(value) + } else if let value = value as? Double { + try container.encode(value) + } else if let value = value as? String { + try container.encode(value) + } else if value is JSONNull { + try container.encodeNil() + } else if let value = value as? [Any] { + var container = container.nestedUnkeyedContainer() + try encode(to: &container, array: value) + } else if let value = value as? [String: Any] { + var container = container.nestedContainer(keyedBy: JSONCodingKey.self) + try encode(to: &container, dictionary: value) + } else { + throw encodingError(forValue: value, codingPath: container.codingPath) + } + } + } + + static func encode(to container: inout KeyedEncodingContainer, dictionary: [String: Any]) throws { + for (key, value) in dictionary { + let key = JSONCodingKey(stringValue: key)! + if let value = value as? Bool { + try container.encode(value, forKey: key) + } else if let value = value as? Int64 { + try container.encode(value, forKey: key) + } else if let value = value as? Double { + try container.encode(value, forKey: key) + } else if let value = value as? String { + try container.encode(value, forKey: key) + } else if value is JSONNull { + try container.encodeNil(forKey: key) + } else if let value = value as? [Any] { + var container = container.nestedUnkeyedContainer(forKey: key) + try encode(to: &container, array: value) + } else if let value = value as? [String: Any] { + var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) + try encode(to: &container, dictionary: value) + } else { + throw encodingError(forValue: value, codingPath: container.codingPath) + } + } + } + + static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws { + if let value = value as? Bool { + try container.encode(value) + } else if let value = value as? Int64 { + try container.encode(value) + } else if let value = value as? Double { + try container.encode(value) + } else if let value = value as? String { + try container.encode(value) + } else if value is JSONNull { + try container.encodeNil() + } else { + throw encodingError(forValue: value, codingPath: container.codingPath) + } + } + + public required init(from decoder: Decoder) throws { + if var arrayContainer = try? decoder.unkeyedContainer() { + self.value = try JSONAny.decodeArray(from: &arrayContainer) + } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) { + self.value = try JSONAny.decodeDictionary(from: &container) + } else { + let container = try decoder.singleValueContainer() + self.value = try JSONAny.decode(from: container) + } + } + + public func encode(to encoder: Encoder) throws { + if let arr = self.value as? [Any] { + var container = encoder.unkeyedContainer() + try JSONAny.encode(to: &container, array: arr) + } else if let dict = self.value as? [String: Any] { + var container = encoder.container(keyedBy: JSONCodingKey.self) + try JSONAny.encode(to: &container, dictionary: dict) + } else { + var container = encoder.singleValueContainer() + try JSONAny.encode(to: &container, value: self.value) + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ClubDetailsResponse.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ClubDetailsResponse.swift new file mode 100644 index 0000000..625aa0c --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ClubDetailsResponse.swift @@ -0,0 +1,62 @@ +// +// ClubDetailsResponse.swift +// Club Portal +// +// Created by Umer Tahir on 19/04/2025. +// + + +import Foundation + +// MARK: - Main Response +struct ClubDetailsResponse: Codable { + let error: Bool + let model: ClubDetailsModel +} + +// MARK: - Club Details Model +struct ClubDetailsModel: Codable { + let user: ClubUser + let club: Club + let courts: [Court] + let sports: [Sport] + let pricing: [Pricing] +} + +// MARK: - Club User +struct ClubUser: Codable { + let id: Int + let oauth: String? + let role: String + let firstName, lastName, email, password: String + let type: Int + let alternativePhone, ageGroup, familyRole, defaultPaymentMethod: String? + let guardian, passwordLogin, verify: Int + let clubID: Int? + let phone, photo, bio, refer, stripeUID, paypalUID, twoFactorAuthentication: String? + let status: Int + let createAt, updateAt: String + + enum CodingKeys: String, CodingKey { + case id, oauth, role + case firstName = "first_name" + case lastName = "last_name" + case email, password, type + case alternativePhone = "alternative_phone" + case ageGroup = "age_group" + case familyRole = "family_role" + case defaultPaymentMethod = "default_payment_method" + case guardian + case passwordLogin = "password_login" + case verify + case clubID = "club_id" + case phone, photo, bio, refer + case stripeUID = "stripe_uid" + case paypalUID = "paypal_uid" + case twoFactorAuthentication = "two_factor_authentication" + case status + case createAt = "create_at" + case updateAt = "update_at" + } +} + diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ClubResponse.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ClubResponse.swift new file mode 100644 index 0000000..148558d --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ClubResponse.swift @@ -0,0 +1,200 @@ +// This file was generated from JSON Schema using quicktype, do not modify it directly. +// To parse the JSON, add this file to your project and do: +// +// let clubDetail = try? JSONDecoder().decode(ClubDetail.self, from: jsonData) + +import Foundation + +// MARK: - ClubDetail +struct ClubDetail: Codable { + let error: Bool? + let model: Model? + let sports: [Club]? + let courts: [Court]? + let pricing: [Pricing]? +} + +// MARK: - Court +struct Court: Codable, Identifiable, Hashable { + let id, clubID: Int? + let name: String? + let sportID: Int? + let type: String? + let surfaceID: Int? + let subType, availability: String? + let createAt, updateAt: String? + + enum CodingKeys: String, CodingKey { + case id + case clubID = "club_id" + case name + case sportID = "sport_id" + case type + case surfaceID = "surface_id" + case subType = "sub_type" + case availability + case createAt = "create_at" + case updateAt = "update_at" + } +} + +enum TypeEnum: String, Codable { + case empty = "" + case indoor = "Indoor" + case outdoor = "Outdoor" + case typeOutdoor = "outdoor" +} + +// MARK: - Model +struct Model: Codable { + let id: Int? + let name, slug, title, description: String? + let userID, completed: Int? + let bio: JSONNull? + let openingTime, closingTime, times, splashScreen: String? + let showClinic, showNotification: Int? + let clubLogo: String? + let feeSettings: String? + let customFields: [CustomField]? + let homeImage: String? + let showBuddy: Int? + let exceptions, clubLocation: String? + let showCoach, showGroups, showCourt: Int? + let buddyDescription, courtDescription, coachDescription, clinicDescription: String? + let lessonDescription: String? + let customRequestThreshold, clubFee, serviceFee, maxPlayers: Int? + let accountDetails, accountSettings, membershipSettings, dailyBreaks: String? + let daysOff, createAt, updateAt: String? + + enum CodingKeys: String, CodingKey { + case id, name, slug, title, description + case userID = "user_id" + case completed, bio + case openingTime = "opening_time" + case closingTime = "closing_time" + case times + case splashScreen = "splash_screen" + case showClinic = "show_clinic" + case showNotification = "show_notification" + case clubLogo = "club_logo" + case feeSettings = "fee_settings" + case customFields = "custom_fields" + case homeImage = "home_image" + case showBuddy = "show_buddy" + case exceptions + case clubLocation = "club_location" + case showCoach = "show_coach" + case showGroups = "show_groups" + case showCourt = "show_court" + case buddyDescription = "buddy_description" + case courtDescription = "court_description" + case coachDescription = "coach_description" + case clinicDescription = "clinic_description" + case lessonDescription = "lesson_description" + case customRequestThreshold = "custom_request_threshold" + case clubFee = "club_fee" + case serviceFee = "service_fee" + case maxPlayers = "max_players" + case accountDetails = "account_details" + case accountSettings = "account_settings" + case membershipSettings = "membership_settings" + case dailyBreaks = "daily_breaks" + case daysOff = "days_off" + case createAt = "create_at" + case updateAt = "update_at" + } +} + +// MARK: - CustomField +struct CustomField: Codable { + let value, label, type: String? + let customFieldRequired: Bool? + let options: [Option]? + let min, max, step: String? + + enum CodingKeys: String, CodingKey { + case value, label, type + case customFieldRequired = "required" + case options, min, max, step + } +} + +enum Option: Codable { + case double(Double) + case string(String) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let x = try? container.decode(Double.self) { + self = .double(x) + return + } + if let x = try? container.decode(String.self) { + self = .string(x) + return + } + throw DecodingError.typeMismatch(Option.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Option")) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .double(let x): + try container.encode(x) + case .string(let x): + try container.encode(x) + } + } +} + +// MARK: - Pricing +struct Pricing: Codable { + let id: Int? + let surfaceID: Int? + let clubID: Int? + let type, subtype: String? + let sportID: Int? + let status, isGeneral, generalRate: Int? + let priceByHours, createAt, updateAt: String? + + enum CodingKeys: String, CodingKey { + case id + case surfaceID = "surface_id" + case clubID = "club_id" + case type, subtype + case sportID = "sport_id" + case status + case isGeneral = "is_general" + case generalRate = "general_rate" + case priceByHours = "price_by_hours" + case createAt = "create_at" + case updateAt = "update_at" + } +} + +// MARK: - Sport +struct ClubSport: Codable { + let id, status: Int? + let name: String? + let clubID: Int? + let sportTypes: [SportType]? + + enum CodingKeys: String, CodingKey { + case id, status, name + case clubID = "club_id" + case sportTypes = "sport_types" + } +} + +// MARK: - SportType +struct SportType: Codable { + let type: String? + let subtype: [String]? + let clubSportTypeID: String? + + enum CodingKeys: String, CodingKey { + case type, subtype + case clubSportTypeID = "club_sport_type_id" + } +} + diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ClubStatisticsResponse.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ClubStatisticsResponse.swift new file mode 100644 index 0000000..fcd15cd --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ClubStatisticsResponse.swift @@ -0,0 +1,68 @@ +// +// ClubStatisticsResponse.swift +// Club Portal +// +// Created by Umer Tahir on 11/04/2025. +// + + +struct ClubStatisticsResponse: Decodable { + let error: Bool + let model: ClubStatisticsModel +} + +struct ClubStatisticsModel: Decodable { + let clinicReservations: [ReservationStats] + let coachReservations: [ReservationStats] + let totalReservations: [ReservationStats] + let revenueHeatMap: [RevenueHeatMap] + let revenueByDateRange: [RevenueByDateRange] + let revenueByModule: [RevenueByModule] + let buddyStatistics: [ReservationStats] + let totalExpenses: [ExpenseStats] + let totalRevenue: [RevenueStats] + let totalProfit: [ProfitStats] + let courtUtilization: [CourtUtilizationStats] + let lessonReservations: [ReservationStats] +} + +struct ReservationStats: Decodable { + let total_hours: Double? + let total_revenue: Double? +} + +struct RevenueHeatMap: Decodable { + let date: String + let total_revenue: Double +} + +struct RevenueByDateRange: Decodable { + // Add fields if needed +} + +struct RevenueByModule: Decodable { + let module: String + let revenue: Double? +} + +struct ExpenseStats: Decodable { + let total_hours: Double? + let total_expense: Double? +} + +struct RevenueStats: Decodable { + let total_hours: Double? + let total_revenue: Double? +} + +struct ProfitStats: Decodable { + let total_profit: Double? + let total_revenue: Double? + let total_expense: Double? +} + +struct CourtUtilizationStats: Decodable { + let utilization_percentage: Double? + let total_used_hours: Double? + let total_available_hours: Double? +} \ No newline at end of file diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ForgotPasswordResponse.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ForgotPasswordResponse.swift new file mode 100644 index 0000000..9dd8704 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ForgotPasswordResponse.swift @@ -0,0 +1,12 @@ +// +// ForgotPasswordResponse.swift +// Club Portal +// +// Created by Umer Tahir on 10/04/2025. +// + + +struct ForgotPasswordResponse: Codable { + let error: Bool + let message: String +} \ No newline at end of file diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/LoginResponse.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/LoginResponse.swift new file mode 100644 index 0000000..c254d80 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/LoginResponse.swift @@ -0,0 +1,31 @@ +// +// LoginResponse.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + +struct LoginResponse: Decodable { + let error: Bool + let role: String + let token: String + let expire_at: Int + let userId: Int + let first_name: String? + let last_name: String? + let photo: String? + let two_factor_enabled: Bool + + enum CodingKeys: String, CodingKey { + case error + case role + case token + case expire_at + case userId = "user_id" // Mapping JSON key "user_id" to "userId" + case first_name + case last_name + case photo + case two_factor_enabled + } +} diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ProfileRsp.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ProfileRsp.swift new file mode 100644 index 0000000..9137fe0 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ProfileRsp.swift @@ -0,0 +1,332 @@ +// +// Profile.swift +// Club Portal +// +// Created by Umer Tahir on 22/04/2025. +// + + +// This file was generated from JSON Schema using quicktype, do not modify it directly. +// To parse the JSON, add this file to your project and do: +// +// let profile = try? JSONDecoder().decode(Profile.self, from: jsonData) + +import Foundation + + +// MARK: - ProfileRsp +class ProfileRsp: Codable { + let error: Bool? + let model: ProfileModel? + + init(error: Bool?, model: ProfileModel?) { + self.error = error + self.model = model + } +} + +// MARK: - Model +class ProfileModel: Codable { + let user: User1? + let club: Club1? + let courts: [Court1]? + let sports: [Sport1]? + let pricing: [Pricing1]? + + init(user: User1?, club: Club1?, courts: [Court1]?, sports: [Sport1]?, pricing: [Pricing1]?) { + self.user = user + self.club = club + self.courts = courts + self.sports = sports + self.pricing = pricing + } +} + +// MARK: - Club +class Club1: Codable { + let id: Int? + let name, slug: String? + let title, description: String? + let userID, completed: Int? + let bio: String? + let openingTime, closingTime, times, splashScreen: String? + let showClinic, showNotification: Int? + let clubLogo, feeSettings, customFields, homeImage: String? + let showBuddy: Int? + let exceptions: String? + let clubLocation: String? + let showCoach, showGroups, showCourt: Int? + let buddyDescription, courtDescription, coachDescription, clinicDescription: String? + let lessonDescription: String? + let customRequestThreshold, clubFee, serviceFee, maxPlayers: Int? + let accountDetails: String? + let accountSettings, membershipSettings, dailyBreaks: String? + let daysOff, createAt, updateAt: String? + + enum CodingKeys: String, CodingKey { + case id, name, slug, title, description + case userID = "user_id" + case completed, bio + case openingTime = "opening_time" + case closingTime = "closing_time" + case times + case splashScreen = "splash_screen" + case showClinic = "show_clinic" + case showNotification = "show_notification" + case clubLogo = "club_logo" + case feeSettings = "fee_settings" + case customFields = "custom_fields" + case homeImage = "home_image" + case showBuddy = "show_buddy" + case exceptions + case clubLocation = "club_location" + case showCoach = "show_coach" + case showGroups = "show_groups" + case showCourt = "show_court" + case buddyDescription = "buddy_description" + case courtDescription = "court_description" + case coachDescription = "coach_description" + case clinicDescription = "clinic_description" + case lessonDescription = "lesson_description" + case customRequestThreshold = "custom_request_threshold" + case clubFee = "club_fee" + case serviceFee = "service_fee" + case maxPlayers = "max_players" + case accountDetails = "account_details" + case accountSettings = "account_settings" + case membershipSettings = "membership_settings" + case dailyBreaks = "daily_breaks" + case daysOff = "days_off" + case createAt = "create_at" + case updateAt = "update_at" + } + + init(id: Int?, name: String?, slug: String?, title: String?, description: String?, userID: Int?, completed: Int?, bio: String?, openingTime: String?, closingTime: String?, times: String?, splashScreen: String?, showClinic: Int?, showNotification: Int?, clubLogo: String?, feeSettings: String?, customFields: String?, homeImage: String?, showBuddy: Int?, exceptions: String?, clubLocation: String?, showCoach: Int?, showGroups: Int?, showCourt: Int?, buddyDescription: String?, courtDescription: String?, coachDescription: String?, clinicDescription: String?, lessonDescription: String?, customRequestThreshold: Int?, clubFee: Int?, serviceFee: Int?, maxPlayers: Int?, accountDetails: String?, accountSettings: String?, membershipSettings: String?, dailyBreaks: String?, daysOff: String?, createAt: String?, updateAt: String?) { + self.id = id + self.name = name + self.slug = slug + self.title = title + self.description = description + self.userID = userID + self.completed = completed + self.bio = bio + self.openingTime = openingTime + self.closingTime = closingTime + self.times = times + self.splashScreen = splashScreen + self.showClinic = showClinic + self.showNotification = showNotification + self.clubLogo = clubLogo + self.feeSettings = feeSettings + self.customFields = customFields + self.homeImage = homeImage + self.showBuddy = showBuddy + self.exceptions = exceptions + self.clubLocation = clubLocation + self.showCoach = showCoach + self.showGroups = showGroups + self.showCourt = showCourt + self.buddyDescription = buddyDescription + self.courtDescription = courtDescription + self.coachDescription = coachDescription + self.clinicDescription = clinicDescription + self.lessonDescription = lessonDescription + self.customRequestThreshold = customRequestThreshold + self.clubFee = clubFee + self.serviceFee = serviceFee + self.maxPlayers = maxPlayers + self.accountDetails = accountDetails + self.accountSettings = accountSettings + self.membershipSettings = membershipSettings + self.dailyBreaks = dailyBreaks + self.daysOff = daysOff + self.createAt = createAt + self.updateAt = updateAt + } +} + +// MARK: - Court +class Court1: Codable { + let id, clubID: Int? + let name: String? + let sportID: Int? + let type: String? + let surfaceID: Int? + let subType: String? + let availability: String? + let createAt, updateAt: String? + + enum CodingKeys: String, CodingKey { + case id + case clubID = "club_id" + case name + case sportID = "sport_id" + case type + case surfaceID = "surface_id" + case subType = "sub_type" + case availability + case createAt = "create_at" + case updateAt = "update_at" + } + + init(id: Int?, clubID: Int?, name: String?, sportID: Int?, type: String?, surfaceID: Int?, subType: String?, availability: String?, createAt: String?, updateAt: String?) { + self.id = id + self.clubID = clubID + self.name = name + self.sportID = sportID + self.type = type + self.surfaceID = surfaceID + self.subType = subType + self.availability = availability + self.createAt = createAt + self.updateAt = updateAt + } +} + +// MARK: - Pricing +class Pricing1: Codable { + let id: Int? + let surfaceID: String? + let clubID: Int? + let type, subtype: String? + let sportID, status, isGeneral, generalRate: Int? + let priceByHours, createAt, updateAt: String? + + enum CodingKeys: String, CodingKey { + case id + case surfaceID = "surface_id" + case clubID = "club_id" + case type, subtype + case sportID = "sport_id" + case status + case isGeneral = "is_general" + case generalRate = "general_rate" + case priceByHours = "price_by_hours" + case createAt = "create_at" + case updateAt = "update_at" + } + + init(id: Int?, surfaceID: String?, clubID: Int?, type: String?, subtype: String?, sportID: Int?, status: Int?, isGeneral: Int?, generalRate: Int?, priceByHours: String?, createAt: String?, updateAt: String?) { + self.id = id + self.surfaceID = surfaceID + self.clubID = clubID + self.type = type + self.subtype = subtype + self.sportID = sportID + self.status = status + self.isGeneral = isGeneral + self.generalRate = generalRate + self.priceByHours = priceByHours + self.createAt = createAt + self.updateAt = updateAt + } +} + +// MARK: - Sport +class Sport1: Codable { + let id, status: Int? + let name: String? + let clubID: Int? + let sportTypes: [SportType]? + + enum CodingKeys: String, CodingKey { + case id, status, name + case clubID = "club_id" + case sportTypes = "sport_types" + } + + init(id: Int?, status: Int?, name: String?, clubID: Int?, sportTypes: [SportType]?) { + self.id = id + self.status = status + self.name = name + self.clubID = clubID + self.sportTypes = sportTypes + } +} + +// MARK: - SportType +class SportType1: Codable { + let type: String? + let subtype: [String]? + let clubSportTypeID: String? + + enum CodingKeys: String, CodingKey { + case type, subtype + case clubSportTypeID = "club_sport_type_id" + } + + init(type: String?, subtype: [String]?, clubSportTypeID: String?) { + self.type = type + self.subtype = subtype + self.clubSportTypeID = clubSportTypeID + } +} + +// MARK: - User +class User1: Codable { + let id: Int? + let oauth: String? + let role, firstName, lastName, email: String? + let password: String? + let type: Int? + let alternativePhone, ageGroup, familyRole, defaultPaymentMethod: String? + let guardian, passwordLogin, verify: Int? + let clubID, phone, photo, bio: String? + let refer, stripeUid, paypalUid, twoFactorAuthentication: String? + let status: Int? + let createAt, updateAt: String? + + enum CodingKeys: String, CodingKey { + case id, oauth, role + case firstName = "first_name" + case lastName = "last_name" + case email, password, type + case alternativePhone = "alternative_phone" + case ageGroup = "age_group" + case familyRole = "family_role" + case defaultPaymentMethod = "default_payment_method" + case guardian + case passwordLogin = "password_login" + case verify + case clubID = "club_id" + case phone, photo, bio, refer + case stripeUid = "stripe_uid" + case paypalUid = "paypal_uid" + case twoFactorAuthentication = "two_factor_authentication" + case status + case createAt = "create_at" + case updateAt = "update_at" + } + + init(id: Int?, oauth: String?, role: String?, firstName: String?, lastName: String?, email: String?, password: String?, type: Int?, alternativePhone: String?, ageGroup: String?, familyRole: String?, defaultPaymentMethod: String?, guardian: Int?, passwordLogin: Int?, verify: Int?, clubID: String?, phone: String?, photo: String?, bio: String?, refer: String?, stripeUid: String?, paypalUid: String?, twoFactorAuthentication: String?, status: Int?, createAt: String?, updateAt: String?) { + self.id = id + self.oauth = oauth + self.role = role + self.firstName = firstName + self.lastName = lastName + self.email = email + self.password = password + self.type = type + self.alternativePhone = alternativePhone + self.ageGroup = ageGroup + self.familyRole = familyRole + self.defaultPaymentMethod = defaultPaymentMethod + self.guardian = guardian + self.passwordLogin = passwordLogin + self.verify = verify + self.clubID = clubID + self.phone = phone + self.photo = photo + self.bio = bio + self.refer = refer + self.stripeUid = stripeUid + self.paypalUid = paypalUid + self.twoFactorAuthentication = twoFactorAuthentication + self.status = status + self.createAt = createAt + self.updateAt = updateAt + } +} + +// MARK: - Encode/decode helpers + diff --git a/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ReservationResponse.swift b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ReservationResponse.swift new file mode 100644 index 0000000..f60f7ac --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/Api_Stuff/Response/ReservationResponse.swift @@ -0,0 +1,146 @@ +import Foundation + +// MARK: - Root +struct ReservationRsp: Codable { + let list: [Reservation] +} + +// MARK: - Reservation +struct Reservation: Codable { + let id: Int + let bookingID: Int + let buddyID: Int? + let spaceAssigned: String + let userID: Int + let clubID: Int + let reservationType: Int? + let status: Int + let checkIn: Int + let recurring: Int + let notes: String + let createAt: String + let updateAt: String + let booking: Booking + let user: User + let buddy: [String?] + let clubs: Club +} + +// MARK: - Booking +struct Booking: Codable { + let id: Int + let userID: Int + let sportID: Int + let type: String + let receiptID: String + let subtype: String + let status: Int + let clinicID: Int? + let minNtrp: Int + let maxNtrp: Int + let coachID: Int? + let reservationType: Int + let courtID: Int + let courtIDs: String + let customRequest: Int + let price: Double + let clubFee: Double + let serviceFee: Double + let clinicFee: Double + let last4: Int + let clubID: Int? + let coachFee: Double + let lessonID: Int? + let duration: Int + let paymentIntent: String? + let notes: String? + let spaceAssigned: String? + let playerIDs: String + let courtFee: Double? + let playerID: Int? + let date: String + let startTime: String + let endTime: String + let createAt: String + let updateAt: String + let coachIDs: String +} + +// MARK: - User +struct User: Codable { + let id: Int + let oauth: String? + let role: String + let firstName: String + let lastName: String + let email: String + let password: String + let type: Int + let alternativePhone: String? + let ageGroup: String? + let familyRole: String? + let defaultPaymentMethod: String? + let guardian: Int + let passwordLogin: String + let verify: Int + let clubID: Int? + let phone: String? + let photo: String? + let bio: String? + let refer: String? + let stripeUID: String? + let paypalUID: String? + let twoFactorAuthentication: String? + let status: Int + let createAt: String + let updateAt: String +} + +// MARK: - Buddy +struct Buddy: Codable { + // Add properties if needed +} + +// MARK: - Club +struct Club: Codable { + let id: Int + let name: String + let slug: String + let title: String + let description: String + let userID: Int + let completed: Int + let bio: String? + let openingTime: String + let closingTime: String + let times: String + let splashScreen: String + let showClinic: Int + let showNotification: Int + let clubLogo: String + let feeSettings: String + let customFields: String + let homeImage: String + let showBuddy: Int + let exceptions: String + let clubLocation: String + let showCoach: Int + let showGroups: Int + let showCourt: Int + let buddyDescription: String + let courtDescription: String + let coachDescription: String + let clinicDescription: String + let lessonDescription: String + let customRequestThreshold: Int + let clubFee: Double + let serviceFee: Double + let maxPlayers: Int + let accountDetails: String + let accountSettings: String + let membershipSettings: String + let dailyBreaks: String + let daysOff: String + let createAt: String + let updateAt: String +} diff --git a/Club_portal/Club Portal/Club Portal/Network/DataModels/AppleLoginRequest.swift b/Club_portal/Club Portal/Club Portal/Network/DataModels/AppleLoginRequest.swift new file mode 100644 index 0000000..07f6a6b --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Network/DataModels/AppleLoginRequest.swift @@ -0,0 +1,16 @@ +// +// AppleLoginRequest.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + +struct AppleLoginRequest: Encodable { + let first_name : String + let last_name : String + let identityToken : String + let apple_id : String + let role: String + +} diff --git a/Club_portal/Club Portal/Club Portal/Preview Content/Preview Assets.xcassets/Contents.json b/Club_portal/Club Portal/Club Portal/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Availbility/AvailabilityCardView.swift b/Club_portal/Club Portal/Club Portal/UI/Availbility/AvailabilityCardView.swift new file mode 100644 index 0000000..a9db5ba --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Availbility/AvailabilityCardView.swift @@ -0,0 +1,112 @@ +// +// AvailabilityCardView.swift +// Club Portal +// +// Created by Umer Tahir on 12/04/2025. +// +import SwiftUI + +struct AvailabilityCardView: View { + var tab: AvailabilityTab + var courts: [CourtsAvailable] = [] + var hours: [String]? + var coaches: [CoachesAvailable]? + var stsff: [StaffAvailable]? + @State var name : String = "" + @State var email : String = "" + @State var phone : String = "" + + @State var showSheet = false + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Label("Available \(getCount())", systemImage: "calendar") + Spacer() + Text("\(getCount())") + .font(.subheadline) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(Colorr.themeBlueColor) + .foregroundColor(.white) + .clipShape(Capsule()) + } + + switch tab { + case .courts: + ForEach(courts , id: \.self) { court in + HStack { + Text("Court") + Text("\(courts.count)") + .foregroundColor(.gray) + Spacer() + } + Divider() + } + case .hours: + ForEach(hours ?? [] , id: \.self) { hour in + Text("\(hour)") + .onTapGesture { + showSheet.toggle() + } + Divider() + } + case .coaches: + ForEach(coaches ?? [], id: \.self) { coach in + HStack { + ProfileImageView(imageUrl: coach.photo ?? "", size: 30) + + Text(coach.firstName ?? "") + Spacer() + Image("send") + .foregroundColor(.gray) + .onTapGesture { + name = coach.firstName ?? "" + email = coach.email ?? "" + phone = coach.phone ?? "" + showSheet.toggle() + + } + } + Divider() + } + case .staff: + ForEach(self.stsff ?? [], id: \.self) { coach in + HStack { + ProfileImageView(imageUrl: coach.photo ?? "", size: 30) + Text( coach.firstName ?? "") + Spacer() + Image("send") + .foregroundColor(.gray) + .onTapGesture { + name = coach.firstName ?? "" + email = coach.email ?? "" + phone = coach.phone ?? "" + showSheet.toggle() + } + } + Divider() + } + + } + } + .padding() + .background(Color.white) + .cornerRadius(14) + .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) + .padding(.horizontal) + .sheet(isPresented: $showSheet) { + ContactInfoBottomSheet(name: $name, email: $email, phone: $phone) + .presentationDetents([.height(250)]) + .presentationDragIndicator(.visible) + } + } + + func getCount() -> Int { + switch tab { + case .courts: return courts.count + case .hours: return hours?.count ?? 0 + case .coaches: return coaches?.count ?? 0 + case .staff: return stsff?.count ?? 0 + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Availbility/AvailabilityScreen.swift b/Club_portal/Club Portal/Club Portal/UI/Availbility/AvailabilityScreen.swift new file mode 100644 index 0000000..50766cb --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Availbility/AvailabilityScreen.swift @@ -0,0 +1,42 @@ +// +// AvailabilityScreen.swift +// Club Portal +// +// Created by Umer Tahir on 12/04/2025. +// + + +import SwiftUI + +struct AvailabilityScreen: View { + @State private var selectedTab: AvailabilityTab = .courts + @Binding var presentSideMenu: Bool // Optional for side menu + + @EnvironmentObject var viewModel : DashViewModel + + + var body: some View { + VStack(spacing: 16) { + HeaderView(presentSideMenu: $presentSideMenu) + + DateSelectorView() + + TabSelectorView(selectedTab: $selectedTab) + ScrollView { + AvailabilityCardView( + tab: selectedTab, + courts: viewModel.availRsp?.courts?.available ?? [] , + hours: viewModel.availRsp?.hours?.available ?? [] , + coaches: viewModel.availRsp?.coaches?.available ?? [], + stsff: viewModel.availRsp?.staff?.available ?? [] + ) + } + + Spacer() + } + .background(Color(.systemGray6).ignoresSafeArea()) + .onAppear(){ + viewModel.getAvailability() + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Availbility/AvailabilityTab.swift b/Club_portal/Club Portal/Club Portal/UI/Availbility/AvailabilityTab.swift new file mode 100644 index 0000000..3f12237 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Availbility/AvailabilityTab.swift @@ -0,0 +1,31 @@ +// +// AvailabilityTab.swift +// Club Portal +// +// Created by Umer Tahir on 12/04/2025. +// + +import SwiftUI +enum AvailabilityTab: String, CaseIterable { + case courts = "Courts" + case hours = "Hours" + case coaches = "Coaches" + case staff = "Staff" +} + +// +//struct Court: Identifiable { +// var id = UUID() +// var number: String +//} +// +//struct Hour: Identifiable { +// var id = UUID() +// var hour: String +//} +// +//struct Coach: Identifiable { +// var id = UUID() +// var name: String +// var image: String // image name in Assets +//} diff --git a/Club_portal/Club Portal/Club Portal/UI/Availbility/DateSelectorView.swift b/Club_portal/Club Portal/Club Portal/UI/Availbility/DateSelectorView.swift new file mode 100644 index 0000000..22b54ea --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Availbility/DateSelectorView.swift @@ -0,0 +1,58 @@ +// +// DateSelectorView.swift +// Club Portal +// +// Created by Umer Tahir on 12/04/2025. +// + +import SwiftUI + + +struct DateSelectorView: View { + @State private var startDate: Date = Date() + @State private var endDate: Date = Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date() + + @State private var startTime: Date = Date() + @State private var endTime: Date = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date() + + var body: some View { + VStack(spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + HStack { + Image(systemName: "calendar") + Text("Date Range") + .fontWeight(.semibold) + Spacer() + } + HStack { + DatePicker("From", selection: $startDate, displayedComponents: [.date]) + .labelsHidden() + DatePicker("Until", selection: $endDate, in: startDate..., displayedComponents: [.date]) + .labelsHidden() + } + } + .padding() + .background(Color(.white)) + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 4) { + HStack { + Image(systemName: "clock") + Text("Time Range") + .fontWeight(.semibold) + Spacer() + } + HStack { + DatePicker("From", selection: $startTime, displayedComponents: [.hourAndMinute]) + .labelsHidden() + DatePicker("Until", selection: $endTime, displayedComponents: [.hourAndMinute]) + .labelsHidden() + } + } + .padding() + .background(Color(.white)) + .cornerRadius(10) + } + .padding(.horizontal) + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Availbility/HeaderView.swift b/Club_portal/Club Portal/Club Portal/UI/Availbility/HeaderView.swift new file mode 100644 index 0000000..b9d0af6 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Availbility/HeaderView.swift @@ -0,0 +1,24 @@ +// +// HeaderView.swift +// Club Portal +// +// Created by Umer Tahir on 12/04/2025. +// + +import SwiftUI + +struct HeaderViewAv: View { + var body: some View { + HStack { + Image(systemName: "person.crop.circle.fill") + .resizable() + .frame(width: 30, height: 30) + Spacer() + Text("The Royal Club") + .font(.headline) + Spacer() + Image(systemName: "arrowshape.turn.up.backward") + } + .padding(.horizontal) + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Availbility/Sheet/ContactInfoBottomSheet.swift b/Club_portal/Club Portal/Club Portal/UI/Availbility/Sheet/ContactInfoBottomSheet.swift new file mode 100644 index 0000000..39e82b2 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Availbility/Sheet/ContactInfoBottomSheet.swift @@ -0,0 +1,68 @@ +// +// ContactInfoBottomSheet.swift +// Club Portal +// +// Created by Umer Tahir on 16/04/2025. +// + + +import SwiftUI + +struct ContactInfoBottomSheet: View { + @Binding var name : String + @Binding var email : String + @Binding var phone : String + + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + Text(name) + .font(.title3.bold()) + + VStack(alignment: .leading, spacing: 8) { + Text("EMAIL") + .font(.caption) + .foregroundColor(.gray) + + HStack { + Text(email) + .underline() + .lineLimit(1) + .truncationMode(.middle) + + Spacer() + + Button { + UIPasteboard.general.string = email + } label: { + Image(systemName: "doc.on.doc") + } + } + } + + VStack(alignment: .leading, spacing: 8) { + Text("PHONE") + .font(.caption) + .foregroundColor(.gray) + + HStack { + Text(phone) + .underline() + + Spacer() + + Button { + UIPasteboard.general.string = phone + } label: { + Image(systemName: "doc.on.doc") + } + } + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(20) + .padding() + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Availbility/TabSelectorView.swift b/Club_portal/Club Portal/Club Portal/UI/Availbility/TabSelectorView.swift new file mode 100644 index 0000000..b495840 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Availbility/TabSelectorView.swift @@ -0,0 +1,67 @@ +// +// TabSelectorView.swift +// Club Portal +// +// Created by Umer Tahir on 12/04/2025. +// + +import SwiftUI + +struct TabSelectorView: View { + @Binding var selectedTab: AvailabilityTab + + var body: some View { + HStack(spacing: 0) { + // ForEach(AvailabilityTab.allCases, id: \.self) { tab in + // Button(action: { + // selectedTab = tab + // }) { + // Text(tab.rawValue) + // .fontWeight(selectedTab == tab ? .bold : .regular) + // .foregroundColor(.black) + // .frame(maxWidth: .infinity) + // .padding(.vertical, 10) + // .background(selectedTab == tab ? Color.white : Color.clear) + // } + // } + // } + // .background(Color(.systemGray5)) + // .clipShape(Capsule()) + // .padding(.horizontal) + + Picker(selection: $selectedTab, label: Text("Options")) { + ForEach(AvailabilityTab.allCases, id: \.self) { index in + Text(index.rawValue).tag(index) + } + + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.horizontal) + .tint(.gray.opacity(0.5)) + + } + } +} + +//struct SegmentedControlView: View { +// @Binding var selectedTab : Int +// let options = ["Summary", "Analytics"] +// +// @EnvironmentObject var viewModel : DashViewModel +// +// var body: some View { +// Picker(selection: $selectedTab, label: Text("Options")) { +// ForEach(options.indices, id: \.self) { index in +// Text(self.options[index]).tag(index) +// } +// +// } +// .pickerStyle(SegmentedPickerStyle()) +// .padding(.horizontal) +// .tint(.gray.opacity(0.5)) +// +// Divider() +// +// +// } +//} diff --git a/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/CalendarKitView.swift b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/CalendarKitView.swift new file mode 100644 index 0000000..ebca9ab --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/CalendarKitView.swift @@ -0,0 +1,35 @@ +// +// CalendarKitView.swift +// Club Portal +// +// Created by Umer Tahir on 04/04/2025. +// + + +import SwiftUI +import CalendarKit + +//struct CalendarKitView: UIViewControllerRepresentable { +// func makeUIViewController(context: Context) -> DayViewController { +// let dayViewController = DayViewController() +// dayViewController.dataSource = context.coordinator +// return dayViewController +// } +// +// func updateUIViewController(_ uiViewController: DayViewController, context: Context) {} +// +// func makeCoordinator() -> Coordinator { +// return Coordinator() +// } +// +// class Coordinator: NSObject, EventDataSource { +// func eventsForDate(_ date: Date) -> [EventDescriptor] { +// let event = Event() +// // event.startDate = date +// // event.endDate = Calendar.current.date(byAdding: .hour, value: 1, to: date)! +// event.text = "Sample Event" +// event.color = .blue +// return [event] +// } +// } +//} diff --git a/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/CoachesTab.swift b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/CoachesTab.swift new file mode 100644 index 0000000..a2af383 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/CoachesTab.swift @@ -0,0 +1,36 @@ +// +// CoachesTab.swift +// Club Portal +// +// Created by Umer Tahir on 16/04/2025. +// + +import SwiftUI + +struct CoachesTab: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + ForEach(0..<3, id: \.self) { _ in + HStack(spacing: 12) { + ProfileImageView(imageUrl: "", size: 40) + + VStack(alignment: .leading, spacing: 4) { + Text("Steve Jobs") + .font(.headline) + Text("Hours playing: 9:00 AM – 10:00 AM") + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(10) + } + } + .padding() + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/DetailsTab.swift b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/DetailsTab.swift new file mode 100644 index 0000000..2c72330 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/DetailsTab.swift @@ -0,0 +1,52 @@ +// +// DetailsTab.swift +// Club Portal +// +// Created by Umer Tahir on 16/04/2025. +// + +import SwiftUI + +struct DetailRow: View { + let title: String + let value: String + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.subheadline) + .foregroundColor(.secondary) + Text(value) + .font(.body) + .foregroundColor(.primary) + Divider() + } + } +} + +struct DetailsTab: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Group { + DetailRow(title: "Reservation date", value: "16 April, 2024") + DetailRow(title: "Time", value: "9:00 AM – 10:00 AM") + DetailRow(title: "Event type", value: "Lesson") + DetailRow(title: "Sport/type", value: "Tennis • Indoors • Clay") + DetailRow(title: "Repeating", value: "Every week") + DetailRow(title: "Space assigned", value: "Court #5") + DetailRow(title: "Court fee", value: "$50.0") + DetailRow(title: "Court fee status", value: "Paid") + DetailRow(title: "Note", value: "{content.note}") + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(.systemBackground)) + .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) + ) + .padding() + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/EventCalendarView.swift b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/EventCalendarView.swift new file mode 100644 index 0000000..52b9003 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/EventCalendarView.swift @@ -0,0 +1,54 @@ +// +// EventCalendarView.swift +// Club Portal +// +// Created by Umer Tahir on 16/04/2025. +// + + +import SwiftUI +import CalendarKit + +//struct EventCalendarView: View { +// @Binding var selectedDate: Date +// @EnvironmentObject var viewModel: DashViewModel +// @StateObject private var eventStore = CalendarEventStore() +// +// var body: some View { +// CalendarView(eventStore: eventStore) +// .frame(height: 400) +// .cornerRadius(12) +// .onAppear { +// loadEvents() +// } +// .onChange(of: selectedDate) { _ in +// loadEvents() +// } +// } +// +// private func loadEvents() { +// guard let stats = viewModel.dailyReservations?.first else { return } +// eventStore.events.removeAll() +// +// func addEvent(title: String, duration: Double) { +// guard duration > 0 else { return } +// +// let start = Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: selectedDate)! +// let end = Calendar.current.date(byAdding: .minute, value: Int(duration * 60), to: start)! +// +// let event = Event() +// event.dateInterval = DateInterval(start: start, end: end) +// event.text = "\(title)" +// event.color = .blue.withAlphaComponent(0.6) +// event.lineBreakMode = .byTruncatingTail +// +// eventStore.events.append(event) +// } +// +// if let lesson = stats.booking { +// addEvent(title: "Lesson", duration: lesson.duration) +// } +// +// eventStore.objectWillChange.send() +// } +//} diff --git a/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/FilterView.swift b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/FilterView.swift new file mode 100644 index 0000000..a0e40bf --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/FilterView.swift @@ -0,0 +1,101 @@ +// +// FilterView.swift +// Club Portal +// +// Created by Umer Tahir on 04/04/2025. +// + + +import SwiftUI + +struct DailyFilterView: View { + @Environment(\.presentationMode) var presentationMode + @State private var selectedSport: String? = "Tennis" + @State private var isDropdownOpen = false + + let sports = ["Tennis", "Pickleball", "Sport 3", "Sport 4"] + + var body: some View { + VStack { + // Close and Title + HStack { + Button("Close") { + presentationMode.wrappedValue.dismiss() + } + Spacer() + Text("Filter") + .font(.headline) + Spacer() + } + .padding() + + // Dropdown Menu + VStack(alignment: .leading, spacing: 10) { + Button(action: { + withAnimation { + isDropdownOpen.toggle() + } + }) { + HStack { + Text("Sport") + .font(.subheadline) + Spacer() + Image(systemName: isDropdownOpen ? "chevron.up" : "chevron.down") + } + .padding() + } + .background(Color(.systemGray6)) + .cornerRadius(8) + + if isDropdownOpen { + VStack(spacing: 8) { + ForEach(sports, id: \..self) { sport in + HStack { + Text(sport) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + + if selectedSport == sport { + Image(systemName: "checkmark.square.fill") + .foregroundColor(.blue) + } else { + Image(systemName: "square") + } + } + .background(Color.white) + .cornerRadius(8) + .onTapGesture { + selectedSport = sport + } + } + } + .padding(.horizontal) + } + } + .padding() + + Spacer() + + // Apply Button + Button(action: { + presentationMode.wrappedValue.dismiss() + }) { + Text("Apply and close") + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + .padding() + } + .background(Color(.systemGray6).opacity(0.2)) + .edgesIgnoringSafeArea(.bottom) + } +} + +//struct FilterView_Previews: PreviewProvider { +// static var previews: some View { +// FilterView() +// } +//} diff --git a/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/FilterView1.swift b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/FilterView1.swift new file mode 100644 index 0000000..705da9b --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/FilterView1.swift @@ -0,0 +1,106 @@ +// +// FilterScreen.swift +// Club Portal +// +// Created by Umer Tahir on 15/04/2025. +// + + +import SwiftUI + + + +struct ScheduleFilterView: View { + @Environment(\.dismiss) var dismiss + @Binding var navigationPaths: [String] + + @State private var isClubExpanded = true + @State private var selectedClubs: Set = [] // For storing selected clubs + + @State var filterBlock: ((Int) -> Void)? + @EnvironmentObject var dasModel: DashViewModel + + var body: some View { + VStack(spacing: 0) { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + // Club Filter Section + DisclosureGroup(isExpanded: $isClubExpanded) { + VStack(spacing: 10) { + if let clubs = dasModel.club?.courts { + ForEach(clubs, id: \.self) { court in + if let clubName = court.name { + ClubCheckboxRow(clubName: clubName, isSelected: selectedClubs.contains(clubName)) { + if selectedClubs.contains(clubName) { + selectedClubs.remove(clubName) + } else { + selectedClubs.insert(clubName) + } + } + } + } + } + } + .padding(.top, 10) + } label: { + Text("Club") + .font(.headline) + } + .padding() + .background(RoundedRectangle(cornerRadius: 10).fill(Color(.systemGray6))) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color(.systemGray4), lineWidth: 1) + ) + .padding(.horizontal) + } + .padding(.top) + } + + Spacer() + + Button(action: { + // Handle apply logic + self.navigationPaths.removeLast() + }) { + Text("Apply and close") + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding() + .background(Colorr.themeBlueColor) + .foregroundColor(.white) + .cornerRadius(10) + .padding(.horizontal) + } + .padding(.vertical, 10) + .background(Color.white) + } + .navigationTitle("Filter") + .navigationBarTitleDisplayMode(.inline) + .background(Color(.systemGray6).ignoresSafeArea()) + } +} + +// MARK: - Club Checkbox Row +struct ClubCheckboxRow: View { + let clubName: String + let isSelected: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack { + Text(clubName) + .foregroundColor(.primary) + Spacer() + Image(systemName: isSelected ? "checkmark.square.fill" : "square") + .foregroundColor(isSelected ? Colorr.themeBlueColor : .gray) + } + .padding() + .background( + RoundedRectangle(cornerRadius: 8) + .stroke(isSelected ? Colorr.themeBlueColor : Color.gray.opacity(0.3), lineWidth: 1) + ) + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/PlayersTab.swift b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/PlayersTab.swift new file mode 100644 index 0000000..c180c3d --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/PlayersTab.swift @@ -0,0 +1,44 @@ +// +// PlayersTab.swift +// Club Portal +// +// Created by Umer Tahir on 16/04/2025. +// + +import SwiftUI + +struct PlayersTab: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + ForEach(0..<5, id: \.self) { index in + HStack(spacing: 12) { + ProfileImageView(imageUrl: "", size: 40) + + VStack(alignment: .leading, spacing: 2) { + Text("Steve Jobs") + .font(.headline) + + if index == 0 { + Text("Guardian sign-in: Steve Jobs") + .font(.caption) + .foregroundColor(.gray) + } + } + + Spacer() + + if index == 0 { + Image(systemName: "checkmark.seal.fill") + .foregroundColor(.green) + } + } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(10) + } + } + .padding() + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/ReservationDetailsView.swift b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/ReservationDetailsView.swift new file mode 100644 index 0000000..88b8a0a --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/ReservationDetailsView.swift @@ -0,0 +1,34 @@ +import SwiftUI + +struct ReservationDetailsView: View { + @State private var selectedTab = "Details" + let tabs = ["Details", "Players", "Coaches"] + + var body: some View { + VStack(spacing: 0) { + Picker("", selection: $selectedTab) { + ForEach(tabs, id: \.self) { tab in + Text(tab).tag(tab) + } + } + .pickerStyle(SegmentedPickerStyle()) + .padding() + + switch selectedTab { + case "Details": + DetailsTab() + case "Players": + PlayersTab() + case "Coaches": + CoachesTab() + default: + Spacer() + Text("Unknown Tab") + Spacer() + } + } + .navigationBarTitle("Reservation details", displayMode: .inline) + } +} + + diff --git a/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/ScheduleView.swift b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/ScheduleView.swift new file mode 100644 index 0000000..0ad0fa5 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/DailySecheduler/ScheduleView.swift @@ -0,0 +1,306 @@ +import SwiftUI + +struct Event: Identifiable { + let id = UUID() + let title: String + let startTime: Date + let endTime: Date + let color: Color +} + +struct ScheduleView: View { + @State var navigationPaths: [String] = [] + @State private var selectedDate = Date() + @State private var displayedMonth = Date() + @State private var events: [Event] = [] + @Binding var presentSideMenu: Bool // Optional for side menu + + let hours = Array(8...20) + let calendar = Calendar.current + + @EnvironmentObject var viewModel : DashViewModel + + var body: some View { + NavigationStack(path: $navigationPaths) { + + VStack(spacing: 0) { + // Top Header + HeaderView(presentSideMenu: $presentSideMenu) + Divider() + VStack { + // Search + Filter + searchBar + .padding(.top, 5) + + + // Month + Horizontal Dates + VStack(spacing: 10) { + Divider() + + monthHeader + Divider() + + dateScrollView + } + .padding(.horizontal) + .padding(.bottom) + + Divider() + + // Time Slots with Events + ScrollView { + VStack(spacing: 0) { + ForEach(hours, id: \.self) { hour in + timeSlotRow(hour: hour) + } + } + .padding(.horizontal) + .padding(.bottom, 20) + } + + } + } + .onAppear { + // loadDummyEvents() + viewModel.getDailySched(clubId: 10) + + if let club = self.viewModel.club { + print(club.courts?.count) + }else { + viewModel.getClubProfile() + } + } + .navigationDestination(for: String.self) { value in + if value == "DailyFilterView" { + ScheduleFilterView(navigationPaths: $navigationPaths) { filter in + viewModel.getDailySched(clubId: filter) + } + }else if value == "ReservationDetailsView" { + ReservationDetailsView() + } + + } + } + } + + // MARK: - Components + + var headerBar: some View { + HStack { + Image(systemName: "person.circle.fill") + .resizable() + .frame(width: 36, height: 36) + + Spacer() + + Text("The Royal Club") + .font(.headline) + + Spacer() + + Image(systemName: "arrow.right") + } + .padding() + } + + var searchBar: some View { + HStack { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + + TextField("search player", text: .constant("")) + .foregroundColor(.primary) + } + .padding(5) + .background(Color(.white)) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color(.systemGray4), lineWidth: 1) + ) + //.padding(.horizontal) + + Button(action: { + + navigationPaths.append("DailyFilterView") + }) { + HStack(spacing: 4) { + Image("filter 1") + + } + } + } + .padding(.horizontal) + } + + var monthHeader: some View { + HStack { + Button(action: { + displayedMonth = calendar.date(byAdding: .month, value: -1, to: displayedMonth) ?? displayedMonth + }) { + Image("left_arrow") + } + + Spacer() + + Text(formattedMonth(displayedMonth)) + .font(.subheadline) + + Spacer() + + Button(action: { + displayedMonth = calendar.date(byAdding: .month, value: 1, to: displayedMonth) ?? displayedMonth + }) { + Image("right_arrow") + } + } + .padding(.horizontal) + .background(Colorr.lightColor) + + } + + var dateScrollView: some View { + let days = getDatesForMonth(from: displayedMonth) + + return ScrollViewReader { scrollProxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(days, id: \.self) { date in + VStack(spacing: 4) { + Text(shortDayName(from: date)) + .font(.caption) + .foregroundColor(.gray) + + Text(dayNumber(from: date)) + .fontWeight(isSameDay(date, selectedDate) ? .bold : .regular) + .frame(width: 36, height: 36) + .background(isSameDay(date, selectedDate) ? Colorr.themeBlueColor : Color.clear) + .foregroundColor(isSameDay(date, selectedDate) ? .white : .primary) + .clipShape(RoundedCorner()) + } + .onTapGesture { + selectedDate = date + } + .id(calendar.startOfDay(for: date)) + } + } + .padding(.vertical, 4) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + scrollProxy.scrollTo(calendar.startOfDay(for: selectedDate), anchor: .center) + } + } + } + } + } + + func timeSlotRow(hour: Int) -> some View { + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .top) { + Text(timeLabel(for: hour)) + .font(.caption) + .frame(width: 50, alignment: .leading) + + ZStack(alignment: .topLeading) { + Rectangle() + .fill(Color(.systemGray6)) + .frame(height: 60) + eventOverlay(for: hour) + } + } + + Divider() + } + } + + func eventOverlay(for hour: Int) -> some View { + VStack(spacing: 4) { + ForEach(events.filter { + calendar.isDate($0.startTime, inSameDayAs: selectedDate) + && calendar.component(.hour, from: $0.startTime) == hour + }) { event in + VStack(alignment: .leading, spacing: 2) { + Text(event.title) + .font(.subheadline) + .bold() + Text("\(timeOnly(event.startTime)) - \(timeOnly(event.endTime))") + .font(.caption) + .foregroundColor(.gray) + } + .padding(8) + .background(event.color.opacity(0.2)) + .foregroundColor(event.color) + .cornerRadius(12) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(Color.clear) + .onTapGesture { + self.navigationPaths.append("ReservationDetailsView") + } + } + } + .padding(.top, 4) + .frame(maxWidth: .infinity) + } + + // MARK: - Utility + + func loadDummyEvents() { + let baseDate = calendar.startOfDay(for: Date()) + events = [ + Event( + title: "Court reserved", + startTime: calendar.date(bySettingHour: 9, minute: 0, second: 0, of: baseDate)!, + endTime: calendar.date(bySettingHour: 10, minute: 0, second: 0, of: baseDate)!, + color: .green + ), + Event( + title: "Clinic", + startTime: calendar.date(bySettingHour: 11, minute: 30, second: 0, of: baseDate)!, + endTime: calendar.date(bySettingHour: 13, minute: 0, second: 0, of: baseDate)!, + color: .purple + ) + ] + } + + func getDatesForMonth(from date: Date) -> [Date] { + let range = calendar.range(of: .day, in: .month, for: date)! + let start = calendar.date(from: calendar.dateComponents([.year, .month], from: date))! + return range.compactMap { calendar.date(byAdding: .day, value: $0 - 1, to: start) } + } + + func formattedMonth(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "MMMM yyyy" + return formatter.string(from: date) + } + + func dayNumber(from date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "dd" + return formatter.string(from: date) + } + + func shortDayName(from date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "EEE" + return formatter.string(from: date) + } + + func timeOnly(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "h:mm a" + return formatter.string(from: date) + } + + func timeLabel(for hour: Int) -> String { + let date = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: Date())! + let formatter = DateFormatter() + formatter.dateFormat = "h a" + return formatter.string(from: date) + } + + func isSameDay(_ a: Date, _ b: Date) -> Bool { + calendar.isDate(a, inSameDayAs: b) + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Dashboard/AnalyticsView.swift b/Club_portal/Club Portal/Club Portal/UI/Dashboard/AnalyticsView.swift new file mode 100644 index 0000000..031f095 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Dashboard/AnalyticsView.swift @@ -0,0 +1,241 @@ +// +// AnalyticsView.swift +// Club Portal +// +// Created by Umer Tahir on 04/04/2025. +// + + +import SwiftUI +import Charts + +struct AnalyticsView: View { + @State private var selectedTab = 1 + @EnvironmentObject var viewModel : DashViewModel + var body: some View { + NavigationView { + VStack { + //HeaderView() + // SegmentedControlView() + ScrollView { + VStack(spacing: 15) { +// PieChartCard(data: self.viewModel.statResponse?.revenueByModule ?? []) +// BarChartCard(data: self.viewModel.statResponse?.revenueHeatMap ?? []) + PieChartCard1() + BarChartCard1() + } + .padding(.horizontal) + } + } + .background(Color(.systemGray6)) + .navigationBarHidden(true) + .onAppear() { + if let data = self.viewModel.statResponse?.revenueHeatMap { + print(data.count) + } + } + } + } +} + +// MARK: - Pie Chart Card +struct PieChartCard1: View { + var pieData: [RevenueByModule]? + + let data: [(name: String, value: Double, color: Color)] = [ + ("Module A", 30, Colorr.pieBlue), + ("Module B", 70, Colorr.pieGreen) + ] + + var body: some View { + CardView(title: "% made through each module") { + VStack { + HStack { + LegendView(color: Colorr.pieBlue, text: "Module A") + LegendView(color: Colorr.pieGreen, text: "Module B") + } + .padding(.top, 10) + + Chart(data, id: \.name) { item in + SectorMark( + angle: .value("Value", item.value), + innerRadius: .ratio(0.5), + angularInset: 2 + ) + .foregroundStyle(item.color) + } + .chartLegend(.hidden) + .frame(height: 180) + + + } + } + .onAppear() { + if let d = self.pieData { + + d.forEach { r in + // let data = (r.module.) + } + } + } + } +} + +struct PieChartCard: View { + let data: [RevenueByModule] + + var body: some View { + let chartData = data.map { + (name: $0.module, value: $0.revenue ?? 0, color: randomColor()) + } + + CardView(title: "% made through each module") { + VStack { + HStack { + ForEach(chartData, id: \.name) { item in + LegendView(color: item.color, text: item.name) + } + .padding(.top, 5) + } + Chart(chartData, id: \.name) { item in + SectorMark( + angle: .value("Value", item.value), + innerRadius: .ratio(0.5), + angularInset: 2 + ) + .foregroundStyle(item.color) + } + .chartLegend(.hidden) + .frame(height: 180) + } + } + } + + func randomColor() -> Color { + let colors: [Color] = [Colorr.pieBlue, Colorr.pieGreen, .orange, .purple, .pink, .mint] + return colors.randomElement() ?? .blue + } +} + +struct BarChartCard1: View { + var data: [RevenueByModule]? + + let barData: [ModuleRevenue] = [ + ModuleRevenue(day: "Wed", module: "Module A", value: 200), + ModuleRevenue(day: "Wed", module: "Module B", value: 500), + ModuleRevenue(day: "Thu", module: "Module A", value: 50), + ModuleRevenue(day: "Thu", module: "Module B", value: 300), + ModuleRevenue(day: "Fri", module: "Module A", value: 30), + ModuleRevenue(day: "Fri", module: "Module B", value: 200), + ModuleRevenue(day: "Sat", module: "Module A", value: 400), + ModuleRevenue(day: "Sat", module: "Module B", value: 250), + ModuleRevenue(day: "Sun", module: "Module A", value: 180), + ModuleRevenue(day: "Sun", module: "Module B", value: 600) + ] + + var body: some View { + CardView(title: "Revenue from each module") { + Chart(barData) { data in + BarMark( + x: .value("Day", data.day), + y: .value("Revenue", data.value) + ) + .foregroundStyle(data.module == "Module A" ? Colorr.pieBlue : Colorr.pieGreen) + .position(by: .value("Module", data.module)) + } + .chartYAxis { + AxisMarks(position: .leading) + } + .frame(height: 200) + } + } +} + +struct BarChartCard: View { + let data: [RevenueHeatMap] + + var body: some View { + let barData: [ModuleRevenue] = data.map { + ModuleRevenue(day: $0.date.toWeekday()!, module: $0.date.toWeekday()!, value: $0.total_revenue ) + } + + CardView(title: "Revenue from each module") { + Chart(barData) { data in + BarMark( + x: .value("Day", data.day), + y: .value("Revenue", data.value) + ) + .foregroundStyle( + data.module == "Module A" ? Colorr.pieBlue : Colorr.pieGreen + ) + } + .chartYAxis { + AxisMarks(position: .leading) + } + .frame(height: 200) + } + + } +} + +// MARK: - Common Components + +// Reusable card for charts +struct CardView: View { + let title: String + @ViewBuilder let content: Content + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(title) + .font(.subheadline) + .bold() + Spacer() + // Image(systemName: "arrow.up.right.square") + } + .padding(.bottom, 5) + + content + } + .padding() + .background(Color.white) + .cornerRadius(10) + .shadow(color: .gray.opacity(0.2), radius: 5) + } +} + +// Legend for pie chart +struct LegendView: View { + let color: Color + let text: String + + var body: some View { + HStack { + Circle() + .fill(color) + .frame(width: 12, height: 12) + Text(text) + .font(.footnote) + } + } +} + + + + + +// MARK: - Preview +struct AnalyticsView_Previews: PreviewProvider { + static var previews: some View { + AnalyticsView() + } +} + +// MARK: - Model +struct ModuleRevenue: Identifiable, Hashable { + var id = UUID() + var day: String + var module: String + var value: Double +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Dashboard/DashboardView.swift b/Club_portal/Club Portal/Club Portal/UI/Dashboard/DashboardView.swift new file mode 100644 index 0000000..5fa6771 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Dashboard/DashboardView.swift @@ -0,0 +1,280 @@ +// +// SummaryView.swift +// Club Portal +// +// Created by Umer Tahir on 04/04/2025. +// + + +import SwiftUI + +struct SummaryView: View { + //@Binding var selectedTab: Int + @State var navigationPaths: [String] = [] + @State var selectedTab = 0 + @EnvironmentObject var viewModel : DashViewModel + @Binding var presentSideMenu: Bool // Optional for side menu + @State var filterCount = 0 + var body: some View { + NavigationStack(path: $navigationPaths) { + VStack { + VStack { + HeaderView(presentSideMenu: $presentSideMenu) + SegmentedControlView(selectedTab: $selectedTab) + } + .background(Color(.white)) + + if selectedTab == 0 { + FilterView(navigationPaths: $navigationPaths, filterCount: $filterCount) + CourtUtilizationView() + + ScrollView { + LazyVStack(spacing: 10) { + + ReservationCard(title: "Court reservations", hours: "\(self.viewModel.statResponse?.coachReservations.first?.total_hours ?? 0 )", revenue: "$\(self.viewModel.statResponse?.coachReservations.first?.total_revenue ?? 0 )") + ReservationCard(title: "Clinic reservations", hours: "\(self.viewModel.statResponse?.clinicReservations.first?.total_hours ?? 0 )", revenue: "$\(self.viewModel.statResponse?.clinicReservations.first?.total_revenue ?? 0 )") + ReservationCard(title: "Find a Buddy", hours: "\(self.viewModel.statResponse?.buddyStatistics.first?.total_hours ?? 0 )", revenue: "$\(self.viewModel.statResponse?.buddyStatistics.first?.total_revenue ?? 0 )") + ReservationCard(title: "Lesson reservation", hours: "\(self.viewModel.statResponse?.lessonReservations.first?.total_hours ?? 0 )", revenue: "$\(self.viewModel.statResponse?.lessonReservations.first?.total_revenue ?? 0 )") + + ReservationCard(title: "Total expenses", hours: "\(self.viewModel.statResponse?.totalExpenses.first?.total_hours ?? 0 )", revenue:"$\(self.viewModel.statResponse?.totalExpenses.first?.total_expense ?? 0 )") + ReservationCard(title: "Total profit", hours: "\(self.viewModel.statResponse?.totalProfit.first?.total_profit ?? 0 )", revenue: "$\(self.viewModel.statResponse?.totalProfit.first?.total_revenue ?? 0 )") + ReservationCard(title: "Total revenue", hours: "\(self.viewModel.statResponse?.totalRevenue.first?.total_hours ?? 0 )", revenue: "$\(self.viewModel.statResponse?.totalRevenue.first?.total_revenue ?? 0 )") + + + } + .padding(.horizontal) + } + }else { + AnalyticsView() + + } + // .background(Color(UIColor.systemGroupedBackground)) + } + .background(Color(.systemGray6)) + .navigationBarHidden(true) + .navigationDestination(for: String.self) { value in + if value == "FilterScreen" { + FilterScreen(navigationPaths: $navigationPaths) { filter, filterCount in + self.filterCount = filterCount + viewModel.getStats(completion: {}) + } + } + } + .onAppear(){ + viewModel.getStats { + if viewModel.sessionExpired { + AppSettings.token = "" + presentSideMenu = false + } + } + // viewModel.getClubProfile() + // viewModel.getProfile() + + } + + } + } +} + +// MARK: - Header +struct HeaderView: View { + @Binding var presentSideMenu: Bool + @State private var showLogoutConfirmation = false + + var body: some View { + HStack { + Image(systemName: "person.circle.fill") + .resizable() + .frame(width: 32, height: 32) + .onTapGesture { + presentSideMenu.toggle() + } + + Spacer() + + Text("The Royal Club") + .font(.headline) + + Spacer() + + Image("logout") + .onTapGesture { + showLogoutConfirmation = true + } + } + .padding() + .alert("Are you sure you want to logout?", isPresented: $showLogoutConfirmation) { + Button("Logout", role: .destructive) { + // Your logout logic here + print("User logged out") + AppSettings.token = "" + + } + Button("Cancel", role: .cancel) { } + } + } +} + +// MARK: - Segmented Control +struct SegmentedControlView: View { + @Binding var selectedTab : Int + let options = ["Summary", "Analytics"] + + @EnvironmentObject var viewModel : DashViewModel + + var body: some View { + Picker(selection: $selectedTab, label: Text("Options")) { + ForEach(options.indices, id: \.self) { index in + Text(self.options[index]).tag(index) + } + + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.horizontal) + .tint(.gray.opacity(0.5)) + + Divider() + + + } +} + + + +// MARK: - Filter View +struct FilterView: View { + @Binding var navigationPaths: [String] + @State var isFilterScreenPresented = false + @Binding var filterCount : Int + @EnvironmentObject var viewModel : DashViewModel + + var body: some View { + + VStack(alignment: .leading) { + HStack { + Spacer() + .frame(width: 20) + Button(action: { isFilterScreenPresented = true }) { + HStack(spacing: 5) { + Image("filter") + Text("Filter") + .font(.footnote) + .foregroundColor(.gray) + + Text("\(self.filterCount ) ") + .font(.caption) + .foregroundColor(.white) + .padding(.horizontal, 8) // Horizontal padding for better width + .padding(.vertical, 4) // Vertical padding for height + .background( + Capsule() + .fill(Colorr.themeBlueColor) // Use your custom color + ) + } + .padding(8) // Padding inside the button + .background(Color.white) + .cornerRadius(10) + .shadow(radius: 2) + .onTapGesture { + self.navigationPaths.append("FilterScreen") + } + + + } + Text("Clear all") + .onTapGesture { + filterCount = 0 + viewModel.dashboardfilters = "" + } + + Spacer() + + } + + .padding(.bottom, 5) + } + Divider() + } +} + +// MARK: - Court Utilization +struct CourtUtilizationView: View { + var body: some View { + HStack { + Text("Court utilization") + .font(.subheadline) + .bold() + Spacer() + Text("66%") + .font(.subheadline) + .bold() + } + .padding() + .background(Color.white) + .cornerRadius(10) + .padding(.horizontal) + } +} + +// MARK: - Reservation Card +struct ReservationCard: View { + let title: String + let hours: String + let revenue: String + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(title) + .font(.body) + // .padding(.top) + + HStack { + Spacer() + InfoView(icon: "clock", label: "Hours", value: hours) + .padding(.leading) + Spacer() + .frame(width: 150) + InfoView(icon: "cash", label: "Revenue", value: revenue) + .padding(.trailing) + Spacer() + + + } + .padding(.bottom) + } + .padding() + .background(Color.white) + .cornerRadius(10) + .shadow(color: .gray.opacity(0.2), radius: 5) + } +} + +// MARK: - Info View +struct InfoView: View { + let icon: String + let label: String + let value: String + + var body: some View { + VStack { + Image( icon) + .font(.title2) + .foregroundColor(.blue) + + Text(label) + .font(.footnote) + .foregroundColor(.gray) + + Text(value) + .font(.headline) + .bold() + } + } +} + +// MARK: - Preview +//struct SummaryView_Previews: PreviewProvider { +// static var previews: some View { +// SummaryView() +// } +//} diff --git a/Club_portal/Club Portal/Club Portal/UI/Dashboard/FilterScreen.swift b/Club_portal/Club Portal/Club Portal/UI/Dashboard/FilterScreen.swift new file mode 100644 index 0000000..52fb97b --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Dashboard/FilterScreen.swift @@ -0,0 +1,95 @@ +// +// FilterScreen.swift +// Club Portal +// +// Created by Umer Tahir on 14/04/2025. +// + + +import SwiftUI + +struct FilterScreen: View { + @Binding var navigationPaths: [String] + @State private var startTime: Date = Date() + @State private var endTime: Date = Date() + @State private var startDate: Date = Date() + @State private var endDate: Date = Date() + + @State private var selectedDuration: String = "This Day" + let durationOptions = ["This Day", "This Week", "This Month", "This Year"] + + @State var filterBlock : ((String, Int) ->Void)? + + var body: some View { + NavigationView { + Form { + Section(header: Text("Time Range")) { + DatePicker("Start Time", selection: $startTime, displayedComponents: .hourAndMinute) + DatePicker("End Time", selection: $endTime, displayedComponents: .hourAndMinute) + } + + Section(header: Text("Date Range")) { + DatePicker("Start Date", selection: $startDate, displayedComponents: .date) + DatePicker("End Date", selection: $endDate, displayedComponents: .date) + } + + Section(header: Text("Duration")) { + Picker("Duration", selection: $selectedDuration) { + ForEach(durationOptions, id: \.self) { duration in + Text(duration) + } + } + .pickerStyle(MenuPickerStyle()) + } + + Section { + Button(action: { + applyFilter() + }) { + HStack { + Spacer() + Text("Apply") + .fontWeight(.bold) + Spacer() + } + } + } + } + .navigationTitle("Filter") + } + } + + private func applyFilter() { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + + let timeFormatter = DateFormatter() + timeFormatter.dateFormat = "HH:mm:ss" + + let startDateStr = dateFormatter.string(from: startDate) + let endDateStr = dateFormatter.string(from: endDate) + let startTimeStr = timeFormatter.string(from: startTime) + let endTimeStr = timeFormatter.string(from: endTime) + + let query = """ + start_date=\(startDateStr)&\ + end_date=\(endDateStr)&\ + start_time=\(startTimeStr)&\ + end_time=\(endTimeStr) + """ + + // 🔢 Count how many filters are applied + var filterCount = 0 + if !Calendar.current.isDateInToday(startDate) { filterCount += 1 } + if !Calendar.current.isDateInToday(endDate) { filterCount += 1 } + if !Calendar.current.isDate(startTime, equalTo: Date(), toGranularity: .minute) { filterCount += 1 } + if !Calendar.current.isDate(endTime, equalTo: Date(), toGranularity: .minute) { filterCount += 1 } + if selectedDuration != "This Day" { filterCount += 1 } + + print("Generated Filter Query: \(query)") + print("Filter count: \(filterCount)") + + self.filterBlock?(query, filterCount) + self.navigationPaths.removeLast() + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Main/Club_PortalApp.swift b/Club_portal/Club Portal/Club Portal/UI/Main/Club_PortalApp.swift new file mode 100644 index 0000000..e7de98a --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Main/Club_PortalApp.swift @@ -0,0 +1,23 @@ +// +// Club_PortalApp.swift +// Club Portal +// +// Created by Umer Tahir on 04/04/2025. +// + +import SwiftUI + +@main +struct Club_PortalApp: App { + + var body: some Scene { + WindowGroup { + WelcomeView() + .environmentObject(AuthViewModel()) + .environmentObject(DashViewModel()) + .preferredColorScheme(.light) // or `.light or` `nil` to use the current scheme + + } + + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/Main/ContentView.swift b/Club_portal/Club Portal/Club Portal/UI/Main/ContentView.swift new file mode 100644 index 0000000..b378abb --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Main/ContentView.swift @@ -0,0 +1,24 @@ +// +// ContentView.swift +// Club Portal +// +// Created by Umer Tahir on 04/04/2025. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/Club_portal/Club Portal/Club Portal/UI/OnBoarding/ForgotPasswordView.swift b/Club_portal/Club Portal/Club Portal/UI/OnBoarding/ForgotPasswordView.swift new file mode 100644 index 0000000..a51752b --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/OnBoarding/ForgotPasswordView.swift @@ -0,0 +1,105 @@ +// +// ForgotPasswordView.swift +// Club Portal +// +// Created by Umer Tahir on 10/04/2025. +// + + +import SwiftUI + +struct ForgotPasswordView: View { + @Binding var navigationPaths: [String] + + @State private var showAlert = false + @Environment(\.presentationMode) var presentationMode + @EnvironmentObject var viewModel: AuthViewModel + @State private var navigateToOtp = false + @State var showError = false + + var body: some View { + // NavigationStack { // ✅ Change NavigationView to NavigationStack + HStack(spacing: 10) { + Image(systemName: "arrow.left") + .foregroundColor(.black) + .onTapGesture { + presentationMode.wrappedValue.dismiss() + } + Spacer() + Text("Forgot password") + .bold() + Spacer() + } + .padding() + ScrollView { + VStack(spacing: 20) { + // Title + Image("lock") + .resizable() + .frame(width: 80, height: 80) + .foregroundColor(.green) + .padding(.top, 40) + + // Phone Number Input + VStack(alignment: .leading) { + Text("Email") + .foregroundColor(.black) + .padding(.leading, 10) + + TextField("Email", text: $viewModel.email) + .keyboardType(.emailAddress) + .autocapitalization(.none) + .padding(12) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(viewModel.email.isEmpty || viewModel.isEmailValid ? Color.gray : Color.red, lineWidth: 1) + ) + .cornerRadius(8) + } + + // Description Text + Text("Enter the email associated with your account and we will send you a code to reset your password.") + .font(.footnote) + .foregroundColor(.gray) + .padding(.top, 5) + } + .padding(.horizontal, 10) + + // Request Password Reset Code Button + Button(action: { + viewModel.forgotPassword { isSuccess in + if isSuccess { + self.navigationPaths.append("OtpView") + } else { + showError = true + } + } + }) { + Text("Request password reset code") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color("green")) + .cornerRadius(8) + } + .disabled(!viewModel.isEmailValid) + .opacity(viewModel.isEmailValid ? 1 : 0.5) + + Spacer() + } + .padding(.horizontal, 20) + .toast(message: viewModel.toastMessage, isShowing: $viewModel.showToast) + .overlay( + Group { + if viewModel.isLoading { + LoadingView() + } + } + ) + .navigationBarBackButtonHidden(true) + } + + + } + diff --git a/Club_portal/Club Portal/Club Portal/UI/OnBoarding/OtpView.swift b/Club_portal/Club Portal/Club Portal/UI/OnBoarding/OtpView.swift new file mode 100644 index 0000000..9824588 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/OnBoarding/OtpView.swift @@ -0,0 +1,159 @@ +// +// OtpView.swift +// Club Portal +// +// Created by Umer Tahir on 10/04/2025. +// + + + + +import SwiftUI + +struct OtpView: View { + @EnvironmentObject var viewModel : AuthViewModel + @Binding var navPaths : [String] + @State private var code = ["", "", "", "", "",""] // Array to hold each digit of the OTP + // Accept phone number as a parameter + @State private var showAlert = false + @State private var navigateToNext = false // State to control navigation + @Environment(\.presentationMode) var presentationMode + // @State var otpCode = "" + @FocusState private var focusedField: Int? + + var body: some View { + HStack(spacing: 10) { + Image(systemName: "arrow.left") + .foregroundColor(.black) + .onTapGesture { + presentationMode.wrappedValue.dismiss() + } + Spacer() + Text("Verify security code") + .bold() + Spacer() + } + .padding() + ScrollView { + VStack(spacing: 20) { + // Title + Image("otp") + .resizable() + .frame(width: 80, height: 80) + .foregroundColor(.green) + .padding(.top, 40) + + // Security Code Description + VStack(alignment: .leading) { + Text("Security code") + .foregroundColor(.black) + .padding(.leading, 10) + + Text("We’ve sent a verification code to: \(viewModel.email)") + .font(.footnote) + .foregroundColor(.gray) + .padding(.top, 5) + } + .padding(.horizontal, 20) +// if viewModel.isFetching { +// ProgressView("Login...") +// } + // OTP Input Fields + HStack(spacing: 10) { + ForEach(0..<6) { index in + TextField("", text: $code[index]) + .keyboardType(.numberPad) + .frame(width: 50, height: 50) + //.background(Color.gray.opacity(0.2)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray, lineWidth: 1) + ) + .cornerRadius(8) + .multilineTextAlignment(.center) + .focused($focusedField, equals: index) // Bind focus state + .onChange(of: code[index]) { newValue in + if newValue.count > 1 { + code[index] = String(newValue.prefix(1)) // Limit to one character + } + + if !newValue.isEmpty && index < 5 { + // Move to the next field if not the last one + focusedField = index + 1 + } else if newValue.isEmpty && index > 0 { + // Move focus back if the user deletes the input + focusedField = index - 1 + } + } + } + } + .padding(.horizontal, 20) + .onAppear { + // Set initial focus to the first text field + focusedField = 0 + } + + // Resend Code Link + HStack { + Text("Didn’t get the text?") + .foregroundColor(.gray) + + Button(action: { + // Handle resend code action +// viewModel.forgotUsingPhoneNumber { isSuccess in +// if isSuccess { +// DispatchQueue.main.async { +// // navigateToOtp = true +// // viewModel.phoneNumber = "\(viewModel.countryCode) \(viewModel.phone)" +// } +// }else { +// } +// } + }) { + Text("Resend") + .foregroundColor(Color("green")) + .underline() // Underline the text + } + } + .padding(.leading, 10) + + // Continue Button + Button(action: { + let otpCode = code.joined() + viewModel.otpCode = otpCode + navPaths.append("SetNewPasswordView") + }) { + Text("Continue") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color("green")) + .cornerRadius(8) + } + .disabled(code.joined().count != 6) + .opacity(code.joined().count == 6 ? 1 : 0.5) + .padding( 10) + .alert(isPresented: $showAlert) { + Alert(title: Text("Incomplete OTP"), + message: Text("Please enter all digits of the OTP."), + dismissButton: .default(Text("OK"))) + } + + Spacer() + } + .padding(.horizontal, 20) + } + .navigationTitle("") + // .toast(isPresented: $viewModel.isError, message: viewModel.errorMessage) + .navigationBarBackButtonHidden(true) + } + func verifyCode(){ +// viewModel.reset { +// if viewModel.isError == false { +// navPaths.append("SetNewPasswordView") +// } +// } + } +} + diff --git a/Club_portal/Club Portal/Club Portal/UI/OnBoarding/SetNewPasswordView.swift b/Club_portal/Club Portal/Club Portal/UI/OnBoarding/SetNewPasswordView.swift new file mode 100644 index 0000000..b0406e1 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/OnBoarding/SetNewPasswordView.swift @@ -0,0 +1,167 @@ +// +// SetNewPasswordView.swift +// Club Portal +// +// Created by Umer Tahir on 10/04/2025. +// + + + + +import SwiftUI + +struct SetNewPasswordView: View { + @Binding var navigationPaths: [String] + + + @State private var showNewPassword = false + @State private var showConfirmPassword = false + @Environment(\.presentationMode) var presentationMode + @EnvironmentObject var viewModel: AuthViewModel + @State private var showAlert = false + @State private var navigateToSignIn = false // State to control navigation to SignInView + + + + var body: some View { + HStack(spacing: 10) { + Image(systemName: "arrow.left") + .foregroundColor(.black) + .onTapGesture { + presentationMode.wrappedValue.dismiss() + } + Spacer() + Text("Set new password") + .bold() + Spacer() + } + .padding() + ScrollView { + VStack(spacing: 20) { + // Title + Image("lock") + .resizable() + .frame(width: 80, height: 80) + .foregroundColor(.green) + .padding(.top, 40) + + // New Password Field + VStack(alignment: .leading) { + Text("Set new password") + .foregroundColor(.black) + .padding(.leading, 10) + + ZStack { + if showNewPassword { + TextField("New Password", text: $viewModel.newPassword) + } else { + SecureField("New Password", text: $viewModel.newPassword) + } + } + .padding(12) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray, lineWidth: 1) + ) + .cornerRadius(8) + .overlay( + Button(action: { + showNewPassword.toggle() + }) { + Image(systemName: showNewPassword ? "eye.slash" : "eye") + .foregroundColor(.gray) + .padding(.trailing, 10) + }, alignment: .trailing + ) + } + .padding(.horizontal, 20) + + // Confirm Password Field + VStack(alignment: .leading) { + Text("Confirm new password") + .foregroundColor(.black) + .padding(.leading, 10) + + ZStack { + if showConfirmPassword { + TextField("Confirm Password", text: $viewModel.confirmPassword) + + } else { + SecureField("Confirm Password", text: $viewModel.confirmPassword) + + } + } + .padding(12) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray, lineWidth: 1) + ) + .cornerRadius(8) + .overlay( + Button(action: { + showConfirmPassword.toggle() + }) { + Image(systemName: showConfirmPassword ? "eye.slash" : "eye") + .foregroundColor(.gray) + .padding(.trailing, 10) + }, alignment: .trailing + ) + } + .padding(.horizontal, 20) + + Button(action: { + viewModel.resetPassword { success in + if success { + self.navigationPaths.removeAll() + } + } + }) { + Text("Set new password") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color("green")) + .cornerRadius(8) + } + .disabled(!viewModel.canSubmitNewPassword) + .opacity(viewModel.canSubmitNewPassword ? 1 : 0.5) + + .padding(.vertical, 10) + + Spacer() + } + .padding(.horizontal, 20) +// if viewModel.isFetching { +// ProgressView("Loading...") +// } + } + .navigationTitle("") + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) // Hide the back button + .alert(isPresented: $showAlert) { + Alert( + title: Text("Alert"), + message: Text(viewModel.showToast ? viewModel.toastMessage : viewModel.toastMessage), + dismissButton: .default(Text("OK")) { + if !viewModel.showToast { + // navigateToSignIn = true // Navigate to SignInView on success + self.navigationPaths.removeAll() + } + } + ) + } + .toast(message: viewModel.toastMessage, isShowing: $viewModel.showToast) + .overlay( + Group { + if viewModel.isLoading { + LoadingView() + } + } + ) + } +} + +//#Preview { +// SetNewPasswordView() +//} diff --git a/Club_portal/Club Portal/Club Portal/UI/OnBoarding/SigninView.swift b/Club_portal/Club Portal/Club Portal/UI/OnBoarding/SigninView.swift new file mode 100644 index 0000000..932a4db --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/OnBoarding/SigninView.swift @@ -0,0 +1,164 @@ +// +// SigninView.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + + +import SwiftUI +import LocalAuthentication + +struct SigninView: View { + @State var navigationPaths: [String] = [] + + @State private var keepLoggedIn = false + @State private var showPassword = false + @EnvironmentObject var viewModel: AuthViewModel + + var body: some View { + NavigationStack(path: $navigationPaths) { + ScrollView { + + + VStack(spacing: 20) { + // Welcome Text + Image("ball") + .resizable() + .frame(width: 80, height: 80) + .foregroundColor(.green) + .padding(.top, 40) + + + // Email and Password Fields + CustomTextField(placeholder: "Email", text: $viewModel.email, keyboardType: .emailAddress) + + VStack(alignment: .leading) { + Text("Password") + .foregroundColor(.black) + .padding(.leading, 10) + + + + ZStack { + if showPassword { + TextField("Password", text: $viewModel.password) + + } else { + SecureField("Password", text: $viewModel.password) + + } + } + .padding(12) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray, lineWidth: 1) + ) + .cornerRadius(8) + + .overlay( + Button(action: { + showPassword.toggle() + }) { + Image(systemName: showPassword ? "eye.slash" : "eye") + .foregroundColor(.gray) + .padding(.trailing, 10) + }, alignment: .trailing + ) + } + .padding(0) + + // Forgot Password Link + HStack { + + Button(action: { + // Push SignUp destination onto the navigation path + navigationPaths.append("ForgotPassword") + }) { + Text("Forgot Password") + .foregroundColor(.black) + .underline() + } + + Spacer() + } + + // Keep me logged in Checkbox + HStack { + Button(action: { + keepLoggedIn.toggle() + viewModel.isRefresh.toggle() + AppSettings.keepLoginIn.toggle() + }) { + Image(systemName: AppSettings.keepLoginIn ? "checkmark.square" : "square") + .foregroundColor(Color("blue")) + } + Text("Keep me logged in for 30 days") + .foregroundColor(.black) + .onTapGesture { + viewModel.isRefresh.toggle() + + } + } + + // Log In Button + Button(action: { + viewModel.login {_ in + // Navigate to the Dashboard on successful login + navigationPaths.removeAll() + navigationPaths.append("Dashboard") + } + }) { + Text("Log in") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(viewModel.canLogin ? Color("green") : Color.gray) + .cornerRadius(8) + } + .disabled(!viewModel.canLogin) + .padding(.vertical, 10) + + // Sign Up Link + + } + .padding(.horizontal, 20) + .onAppear(){ + + } + } + .navigationTitle("Welcome back") + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .navigationBarBackButtonHidden(true) + .navigationDestination(for: String.self) { value in + if value == "ForgotPassword" { + ForgotPasswordView(navigationPaths: $navigationPaths) + } else if value == "OtpView" { + OtpView(navPaths: $navigationPaths) + } else if value == "SetNewPasswordView" { + SetNewPasswordView(navigationPaths: $navigationPaths) + } else if value == "Dashboard" { + WelcomeView() // Navigate to the Dashboard screen + } + } + .toast(message: viewModel.toastMessage, isShowing: $viewModel.showToast) + .overlay( + Group { + if viewModel.isLoading { + LoadingView() + } + } + ) + } + } + + +} + + +#Preview { + SigninView() +} diff --git a/Club_portal/Club Portal/Club Portal/UI/SideMenu/SideMenu.swift b/Club_portal/Club Portal/Club Portal/UI/SideMenu/SideMenu.swift new file mode 100644 index 0000000..578fbbc --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/SideMenu/SideMenu.swift @@ -0,0 +1,32 @@ +// +// SideMenu.swift +// +// + +import SwiftUI + +struct SideMenu: View { + @Binding var isShowing: Bool + var content: AnyView + var edgeTransition: AnyTransition = .move(edge: .leading) + var body: some View { + ZStack(alignment: .bottom) { + if (isShowing) { + Color.black + .opacity(0.3) + .ignoresSafeArea() + .onTapGesture { + isShowing.toggle() + } + content + .transition(edgeTransition) + .background( + Color.clear + ) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + .ignoresSafeArea() + .animation(.easeInOut, value: isShowing) + } +} diff --git a/Club_portal/Club Portal/Club Portal/UI/SideMenu/SideMenuView.swift b/Club_portal/Club Portal/Club Portal/UI/SideMenu/SideMenuView.swift new file mode 100644 index 0000000..51d8e77 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/SideMenu/SideMenuView.swift @@ -0,0 +1,206 @@ +// +// SideMenuView.swift +// +// + +import SwiftUI + +enum SideMenuRowType: Int, CaseIterable { + case logout + + + var title: String { + switch self { + case .logout: + return "Logout" + + } + } + + var iconName: String { + switch self { + case .logout: + return "logout" + + } + } +} + +struct SideMenuView: View { + @Binding var selectedSideMenuTab: Int? + @Binding var presentSideMenu: Bool + @EnvironmentObject var userAuth: AuthViewModel + @State private var isActive = false // Track active navigation state + @State var nav = [String]() + @State var isUpgrde = false + + var body: some View { + HStack { + ZStack { + Rectangle() + .fill(.white) + .frame(width: 320) + .shadow(color: .purple.opacity(0.1), radius: 5, x: 0, y: 3) + + VStack(alignment: .leading, spacing: 0) { + HStack { + ProfileHead() + .frame(height: 50) + .padding(.bottom, 30) + + } + + Spacer() + .frame(height: 40) + ForEach(SideMenuRowType.allCases, id: \.self) { row in + + RowView(isSelected: selectedSideMenuTab == row.rawValue, imageName: row.iconName, title: row.title, action: { + + if row.title == "Logout" { + // userAuth.logout() + AppSettings.token = "" + presentSideMenu = false + } + }) + + } + + Spacer() + + HStack{ + Button(action: + { + ConstantData.openLink(urlString: ConstantData.termsLink) + }) { + Text("Terms and Conditions") + .font(.footnote) + .foregroundColor(.gray.opacity(0.7)) + .padding(.leading) + } + + Spacer() + + Button(action: { + ConstantData.openLink(urlString: ConstantData.privacyLink) + }) { + Text("Privacy Policy") + .font(.footnote) + .foregroundColor(.gray.opacity(0.7)) + .padding(.trailing) + } + + } + Text("v.\(appVersion)") + .font(.footnote) + .padding(.leading) + .foregroundColor(.gray.opacity(0.7)) + Spacer() + .frame(height: 50) + } + .padding(.top, 75) + .frame(width: 320) + .background(Color.white) + .background( + NavigationLink( + destination: self.handleMenuSelection(for: selectedSideMenuTab ?? 0), + isActive: $isActive // Bind to the state + ) { + EmptyView() + } + ) + + } + + Spacer() + } + .background(.clear) + } + private var appVersion: String { + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "N/A" + let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "N/A" + return "\(version) (\(build))" + } + + + func ProfileHead() -> some View { + VStack(alignment: .leading) { + HStack { + ProfileImageView(imageUrl: AppSettings.photo, size: 50) + .padding(.leading) + + Spacer() + Image("menuLeft") + .padding(.trailing) + .onTapGesture { + presentSideMenu.toggle() + + } + + } + + Text("John Smith") + .padding(.leading) + .font(.system(size: 18, weight: .bold)) + .foregroundColor(.black) + + Text("John@gmail.com") + .padding(.leading) + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.gray) + } + .padding(.leading) + } + + func RowView(isSelected: Bool, imageName: String, title: String, action: @escaping (() -> ())) -> some View { + Button { + action() + } label: { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 20) { + Rectangle() + .fill(isSelected ? .gray : .white) + .frame(width: 5) + + ZStack { + Image(imageName) + .resizable() + .renderingMode(.template) + .foregroundColor(Colorr.lableColor) + .frame(width: 26, height: 26) + } + .frame(width: 30, height: 30) + + Text(title) + .font(.system(size: 14, weight: .regular)) + .foregroundColor(.black) + + // Spacer() + } + .frame(height: 40) + + // Move Divider outside HStack to be a full-width line + if title.lowercased() == "billing" { + Divider() + .background(Colorr.lightColor) + .padding(.horizontal) + .padding(.vertical) + + } + } + } + } + + private func handleMenuSelection(for tab: Int) -> some View { + switch tab { + case 0: + print("Navigate to ProfileView") + return AnyView(EmptyView()) + + default: + return AnyView(EmptyView()) + } + } + + +} + diff --git a/Club_portal/Club Portal/Club Portal/UI/Tabbar/WelcomeView.swift b/Club_portal/Club Portal/Club Portal/UI/Tabbar/WelcomeView.swift new file mode 100644 index 0000000..534d08b --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/UI/Tabbar/WelcomeView.swift @@ -0,0 +1,107 @@ +// +// WelcomeView.swift +// Club Portal +// +// Created by Umer Tahir on 11/04/2025. +// + + + +import SwiftUI + +struct WelcomeView: View { + + @EnvironmentObject var userAuth: AuthViewModel + + @State private var isAccountViewPresented = false + @State private var selectedTab = 0 // Track the selected tab + @State private var presentSideMenu = false + @State private var selectedSideMenuTab: Int? = nil // Optional for side menu + + @AppStorage("token") var token: String = "" + + var body: some View { + // NavigationView { + + ZStack(alignment: .topTrailing) { + if token != "" { + TabView(selection: $selectedTab) { + SummaryView(presentSideMenu: $presentSideMenu) + .tabItem { + Image(selectedTab == 0 ? "tab1_" : "tab1") + Text("Home") + } + .tag(0) + //.environmentObject(treeqlViewModel) + + + ScheduleView(presentSideMenu: $presentSideMenu) + .tabItem { + Image(selectedTab == 1 ? "tab2_" : "tab2") + Text("Daily scheduler") + } + .tag(1) + //.environmentObject(treeqlViewModel) + + AvailabilityScreen(presentSideMenu: $presentSideMenu) + .tabItem { + Image(selectedTab == 2 ? "tab3_" : "tab3") + Text("Availability") + } + .tag(2) + // .environmentObject(treeqlViewModel) + + + + } + .accentColor(Color(hex: "#162664")) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + withAnimation { + presentSideMenu.toggle() + } + } label: { + Image("menu") + .resizable() + .frame(width: 32, height: 32) + } + } + } + .background(Color.red) + .ignoresSafeArea(.all) + + SideMenu(isShowing: $presentSideMenu, content: AnyView( + SideMenuView(selectedSideMenuTab: $selectedSideMenuTab, presentSideMenu: $presentSideMenu) + )) + + + } else { + SigninView() + .environmentObject(userAuth) + } + } + .navigationBarBackButtonHidden() + .navigationBarBackButtonHidden(true) + .onChange(of: token) { newVal in + print("Token changed: \(newVal)") + } + // } + .onAppear { + // treeqlViewModel.getSportsList() + print(AppSettings.token) + } + + .navigationBarBackButtonHidden(true) + .navigationBarBackButtonHidden() + + } + +} + +struct WelcomeView_Previews: PreviewProvider { + static var previews: some View { + WelcomeView() + .environmentObject(AuthViewModel()) + } +} diff --git a/Club_portal/Club Portal/Club Portal/Utliz/AppSettings.swift b/Club_portal/Club Portal/Club Portal/Utliz/AppSettings.swift new file mode 100644 index 0000000..e601324 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Utliz/AppSettings.swift @@ -0,0 +1,346 @@ +// +// AppSettings.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + + +import Foundation +import SwiftUI + +final class AppSettings { + enum SettingKey: String { + case userID + case email + case fName + case lastName + case role + case token + case photo + case password + case clubId + case ntrp + case keepLoginIn + case clubLogo + case clubName + case clubFee + case serviceFee + case clubPlans + case activePlanID + + case plan_name + case price + case allow_clinic + case allow_buddy + case allow_coach + case allow_groups + case allow_court + case lastReservationID + case lastReserveTime + case lastReservationTotal + case isLoggedin + + + + + } + static var lastReservationID: Int { + get { + return USERDEFAULTS_GET_INT_KEY(key: SettingKey.lastReservationID.rawValue) + } + set { + USERDEFAULTS_SET_INT_KEY(object: newValue, key: SettingKey.lastReservationID.rawValue) + } + } + + static var lastReserveTime: Double { + get { + return USERDEFAULTS_GET_DOUBLE_KEY(key: SettingKey.lastReserveTime.rawValue) + } + set { + USERDEFAULTS_SET_DOUBLE_KEY(object: newValue, key: SettingKey.lastReserveTime.rawValue) + } + } + + static var lastReservationTotal: Double { + get { + return USERDEFAULTS_GET_DOUBLE_KEY(key: SettingKey.lastReservationTotal.rawValue) + } + set { + USERDEFAULTS_SET_DOUBLE_KEY(object: newValue, key: SettingKey.lastReservationTotal.rawValue) + } + } + + static var plan_name: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.plan_name.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.plan_name.rawValue) + } + } + static var price: Double { + get { + return USERDEFAULTS_GET_DOUBLE_KEY(key: SettingKey.price.rawValue) + } + set { + USERDEFAULTS_SET_DOUBLE_KEY(object: newValue, key: SettingKey.price.rawValue) + } + } + + static var allow_clinic: Bool { + get { + return USERDEFAULTS_GET_BOOL_KEY(key: SettingKey.allow_clinic.rawValue) + } + set { + USERDEFAULTS_SET_BOOL_KEY(object: newValue, key: SettingKey.allow_clinic.rawValue) + } + } + static var allow_buddy: Bool { + get { + return USERDEFAULTS_GET_BOOL_KEY(key: SettingKey.allow_buddy.rawValue) + } + set { + USERDEFAULTS_SET_BOOL_KEY(object: newValue, key: SettingKey.allow_buddy.rawValue) + } + } + static var allow_coach: Bool { + get { + return USERDEFAULTS_GET_BOOL_KEY(key: SettingKey.allow_coach.rawValue) + } + set { + USERDEFAULTS_SET_BOOL_KEY(object: newValue, key: SettingKey.allow_coach.rawValue) + } + } + static var allow_groups: Bool { + get { + return USERDEFAULTS_GET_BOOL_KEY(key: SettingKey.allow_groups.rawValue) + } + set { + USERDEFAULTS_SET_BOOL_KEY(object: newValue, key: SettingKey.allow_groups.rawValue) + } + } + static var allow_court: Bool { + get { + return USERDEFAULTS_GET_BOOL_KEY(key: SettingKey.allow_court.rawValue) + } + set { + USERDEFAULTS_SET_BOOL_KEY(object: newValue, key: SettingKey.allow_court.rawValue) + } + } + + + static var clubFee: Double { + get { + return USERDEFAULTS_GET_DOUBLE_KEY(key: SettingKey.clubFee.rawValue) + } + set { + USERDEFAULTS_SET_DOUBLE_KEY(object: newValue, key: SettingKey.clubFee.rawValue) + } + } + static var serviceFee: Double { + get { + return USERDEFAULTS_GET_DOUBLE_KEY(key: SettingKey.serviceFee.rawValue) + } + set { + USERDEFAULTS_SET_DOUBLE_KEY(object: newValue, key: SettingKey.serviceFee.rawValue) + } + } + + + static var keepLoginIn: Bool { + get { + return USERDEFAULTS_GET_BOOL_KEY(key: SettingKey.keepLoginIn.rawValue) + } + set { + USERDEFAULTS_SET_BOOL_KEY(object: newValue, key: SettingKey.keepLoginIn.rawValue) + } + } + static var clubLogo: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.clubLogo.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.clubLogo.rawValue) + } + } + static var clubPlans: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.clubPlans.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.clubPlans.rawValue) + } + } + static var clubName: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.clubName.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.clubName.rawValue) + } + } + + static var photo: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.photo.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.photo.rawValue) + } + } + + static var password: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.password.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.password.rawValue) + } + } + + + static var lastName: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.lastName.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.lastName.rawValue) + } + } + + + static var email: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.email.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.email.rawValue) + } + } + + + static var fName: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.fName.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.fName.rawValue) + } + } + + + static var role: String { + get { + return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.role.rawValue) + } + set { + USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.role.rawValue) + } + } + + @AppStorage("token") static var token: String = "" + +// static var token: String { +// get { +// return USERDEFAULTS_GET_STRING_KEY(key: SettingKey.token.rawValue) +// } +// set { +// USERDEFAULTS_SET_STRING_KEY(object: newValue, key: SettingKey.token.rawValue) +// } +// } + + static var userID: Int { + get { + return USERDEFAULTS_GET_INT_KEY(key: SettingKey.userID.rawValue) + } + set { + USERDEFAULTS_SET_INT_KEY(object: newValue, key: SettingKey.userID.rawValue) + } + } + + static var clubId: Int { + get { + return USERDEFAULTS_GET_INT_KEY(key: SettingKey.clubId.rawValue) + } + set { + USERDEFAULTS_SET_INT_KEY(object: newValue, key: SettingKey.clubId.rawValue) + } + } + + static var activePlanID: Int { + get { + return USERDEFAULTS_GET_INT_KEY(key: SettingKey.activePlanID.rawValue) + } + set { + USERDEFAULTS_SET_INT_KEY(object: newValue, key: SettingKey.activePlanID.rawValue) + } + } + + static var ntrp: Int { + get { + return USERDEFAULTS_GET_INT_KEY(key: SettingKey.ntrp.rawValue) + } + set { + USERDEFAULTS_SET_INT_KEY(object: newValue, key: SettingKey.ntrp.rawValue) + } + } + +} +enum USERDEFAULTS_KEYS: String { + case user + +} + +let USERDEFAULTS = UserDefaults.standard + +func USERDEFAULTS_SET_STRING_KEY(object:String, key:String) -> Void { + USERDEFAULTS .set(object, forKey: key) + USERDEFAULTS.synchronize() +} +func USERDEFAULTS_GET_STRING_KEY(key:String) -> String { + return USERDEFAULTS.object(forKey: key) as? String == nil ? "" : USERDEFAULTS.object(forKey: key) as! String +} +func USERDEFAULTS_SET_BOOL_KEY(object:Bool, key:String) -> Void { + USERDEFAULTS .set(object, forKey: key) + USERDEFAULTS.synchronize() +} +func USERDEFAULTS_GET_BOOL_KEY(key:String) -> Bool { + return USERDEFAULTS.object(forKey: key) as? Bool == nil ? false : USERDEFAULTS.object(forKey: key) as! Bool + +} +func USERDEFAULTS_SET_INT_KEY(object:Int, key:String) -> Void { + USERDEFAULTS .set(object, forKey: key) + USERDEFAULTS.synchronize() +} +func USERDEFAULTS_GET_INT_KEY(key:String) -> Int { + return USERDEFAULTS.object(forKey: key) as? Int == nil ? 0 : USERDEFAULTS.object(forKey: key) as! Int +} +func USERDEFAULTS_SET_DOUBLE_KEY(object:Double, key:String) -> Void { + USERDEFAULTS .set(object, forKey: key) + USERDEFAULTS.synchronize() +} +func USERDEFAULTS_GET_DOUBLE_KEY(key:String) -> Double { + return USERDEFAULTS.object(forKey: key) as? Double == nil ? 0.0 : USERDEFAULTS.object(forKey: key) as! Double +} +//func USERDEFAULTS_SET_USER_OBJECT(object:User) -> Void { +// USERDEFAULTS .set(try? PropertyListEncoder().encode(object), forKey: USERDEFAULTS_KEYS.user.rawValue) +// USERDEFAULTS.synchronize() +//} +//func USERDEFAULTS_GET_USER_OBJECT() -> User? { +// +// guard let data = USERDEFAULTS.object(forKey: USERDEFAULTS_KEYS.user.rawValue) as? Data else{ +// return nil +// } +// let user = try? PropertyListDecoder().decode(User.self, from: data) +// return user +//} +func USERDEFAULTS_REMOVE_OBJECT(key:String){ + USERDEFAULTS.removeObject(forKey: key) + USERDEFAULTS.synchronize() +} + + + diff --git a/Club_portal/Club Portal/Club Portal/Utliz/Colorr.swift b/Club_portal/Club Portal/Club Portal/Utliz/Colorr.swift new file mode 100644 index 0000000..14e4d5b --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Utliz/Colorr.swift @@ -0,0 +1,57 @@ +// +// Colorr.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + +// +// extension.swift +// courtmatch +// +// Created by Umer Tahir on 13/11/2024. +// + + +import SwiftUI + +struct Colorr { + static let themeBlueColor = Color(hex: "112a8c") + static let reservationBackgroundColor = Color(hex: "C2D6FF").opacity(0.1) + static let requestSentBg = Color( hex: "38C793") + static let requestJoinAlertOrangeBg = Color(hex: "F17B2C") + static let greenColor = Color(hex: "176448") + static let lableColor = Color(hex: "525866") + static let lightColor = Color(hex: "F6F8FA") + static let findBuddyColor = Color(hex: "CBF5E5") + static let redColor = Color(hex: "DF1C41") + static let pieGreen = Color(hex: "2D9F75") + static let pieBlue = Color(hex: "253EA7") + + +} +struct ConstantData { + + static let privacyLink = "https://courtmatchup.manaknightdigital.com/privacy-policy" + static let termsLink = "https://courtmatchup.manaknightdigital.com/terms-and-conditions" + + static let stripeTestKey = "pk_test_51Ll5ukBgOlWo0lDUrBhA2W7EX2MwUH9AR5Y3KQoujf7PTQagZAJylWP1UOFbtH4UwxoufZbInwehQppWAq53kmNC00UIKSmebO" + static let appleMarchantId = "merchant.com.courtmatch.app" + static func openLink(urlString: String) { + guard let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) else { + print("Invalid URL") + return + } + UIApplication.shared.open(url) + } + + static let NTRPVALUES = ["not set", "8.0","7.5", "7.0", "6.5", "6.0", "5.5", "5.0", "4.5", "4.0", "3.5", "3.0", "2.5", "2.0"] + static let genders = ["Male", "Female", "Other"] + + static var isCustomRequest = 0 +} + + + + diff --git a/Club_portal/Club Portal/Club Portal/Utliz/CustomTextField.swift b/Club_portal/Club Portal/Club Portal/Utliz/CustomTextField.swift new file mode 100644 index 0000000..11cdc9e --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Utliz/CustomTextField.swift @@ -0,0 +1,32 @@ +// +// CustomTextField.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + +import SwiftUI +// Custom TextField Component +struct CustomTextField: View { + let placeholder: String + @Binding var text: String + var keyboardType: UIKeyboardType = .default + + var body: some View { + VStack(alignment: .leading) { + Text(placeholder) + .foregroundColor(.black) + .padding(.leading, 10) + + TextField( placeholder == "Date of birth" ? "MM/DD/YYY" : placeholder, text: $text) + .keyboardType(keyboardType) + .padding(12) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray, lineWidth: 1) + ) + .cornerRadius(8) + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/Utliz/Extension.swift b/Club_portal/Club Portal/Club Portal/Utliz/Extension.swift new file mode 100644 index 0000000..48f82c7 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Utliz/Extension.swift @@ -0,0 +1,18 @@ +// +// Extension.swift +// Club Portal +// +// Created by Umer Tahir on 10/04/2025. +// + +import Foundation + +extension String { + var isValidEmail: Bool { + let emailPredicate = NSPredicate( + format: "SELF MATCHES %@", + "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + ) + return emailPredicate.evaluate(with: self) + } +} diff --git a/Club_portal/Club Portal/Club Portal/Utliz/Extensions.swift b/Club_portal/Club Portal/Club Portal/Utliz/Extensions.swift new file mode 100644 index 0000000..1611911 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Utliz/Extensions.swift @@ -0,0 +1,355 @@ + + + + import Foundation + import SwiftUI + + +extension Color { + init(hex: String) { + let scanner = Scanner(string: hex) + var rgbValue: UInt64 = 0 + + scanner.scanHexInt64(&rgbValue) + + let r = Double((rgbValue & 0xff0000) >> 16) / 255.0 + let g = Double((rgbValue & 0xff00) >> 8) / 255.0 + let b = Double(rgbValue & 0xff) / 255.0 + + self.init(red: r, green: g, blue: b) + } +} + +extension Data { + mutating func append(_ string: String) { + if let data = string.data(using: .utf8) { + append(data) + } + } + + +} +import Foundation + +extension String { + /// Converts a date string from one format to another. + /// - Parameters: + /// - inputFormat: The format of the input date string. + /// - outputFormat: The desired format for the output date string. + /// - Returns: A formatted date string, or `nil` if conversion fails. + func convertDateFormat(from inputFormat: String? = "yyyy-MM-dd", to outputFormat: String? = "EEEE, MMM d") -> String? { + let inputFormatter = DateFormatter() + inputFormatter.dateFormat = inputFormat + + guard let date = inputFormatter.date(from: self) else { + return nil + } + + let outputFormatter = DateFormatter() + outputFormatter.dateFormat = outputFormat + return outputFormatter.string(from: date) + } + + func convertDateForReq(from inputFormat: String? = "yyyy-MM-dd", to outputFormat: String? = "EEEE, MMM d") -> String? { + let inputFormatter = DateFormatter() + inputFormatter.dateFormat = inputFormat + inputFormatter.locale = Locale.current + inputFormatter.timeZone = TimeZone.current + + guard let date = inputFormatter.date(from: self) else { + return nil + } + + let calendar = Calendar.current + let now = Date() + + let components = calendar.dateComponents([.day], from: calendar.startOfDay(for: now), to: calendar.startOfDay(for: date)) + + let dayFormatter = DateFormatter() + dayFormatter.dateFormat = "EEEE, MMM d" + + let formattedDate = dayFormatter.string(from: date) + + switch components.day { + case 0: + return "Today (\(formattedDate))" + case 1: + return "Tomorrow (\(formattedDate))" + default: + let outputFormatter = DateFormatter() + outputFormatter.dateFormat = outputFormat + return outputFormatter.string(from: date) + } + } + + func isDateForTmr() ->Bool { + let inputFormatter = DateFormatter() + inputFormatter.dateFormat = "yyyy-MM-dd" + inputFormatter.locale = Locale.current + inputFormatter.timeZone = TimeZone.current + + guard let date = inputFormatter.date(from: self) else { + return false + } + + let calendar = Calendar.current + let now = Date() + + let components = calendar.dateComponents([.day], from: calendar.startOfDay(for: now), to: calendar.startOfDay(for: date)) + + let dayFormatter = DateFormatter() + dayFormatter.dateFormat = "EEEE, MMM d" + + + if components.day == 1 { + return true + } + + return false + } + + + func getStringArray() -> [String] { + if let data = self.data(using: .utf8) { + do { + let stringArray = try JSONDecoder().decode([String].self, from: data) + print(stringArray) // Output: ["Indoor", "Outdoor"] + return stringArray + } catch { + print("Decoding error: \(error)") + } + } + return [] + } +} +extension Double { + /// Rounds the double to 2 decimal places + var roundedTo2Decimals: Double { + var decimalValue = Decimal(self) + var roundedValue = Decimal() + NSDecimalRound(&roundedValue, &decimalValue, 2, .plain) // Rounds to 2 decimal places + return (roundedValue as NSDecimalNumber).doubleValue + } + + func formattedString(decimalPlaces: Int) -> String { + return String(format: "%.\(decimalPlaces)f", self) + } + + +} + +extension String { + + func fromTimeIntervaldDate(format: String = "MMM dd, yyyy") -> String? { + guard let timestamp = TimeInterval(self) else { return nil } + let date = Date(timeIntervalSince1970: timestamp) + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = format + dateFormatter.timeZone = TimeZone.current // Adjust to device's timezone + let s = dateFormatter.string(from: date) + return s + } + + /// Converts a time string from 24-hour format to 12-hour format with AM/PM + func to12HourFormat1() -> String? { + let inputDateFormatter = DateFormatter() + inputDateFormatter.dateFormat = "HH:mm:ss" // Input format + + let outputDateFormatter = DateFormatter() + outputDateFormatter.dateFormat = "hh:mm a" // Output format + + // Convert the string to a `Date` object + if let date = inputDateFormatter.date(from: self) { + return outputDateFormatter.string(from: date) // Convert back to string in desired format + } else { + return nil // Return nil if conversion fails + } + } + + func to12HourFormat() -> String? { + let inputDateFormatter = DateFormatter() + inputDateFormatter.dateFormat = "HH:mm:ss" // Input format (24-hour) + + let outputDateFormatter = DateFormatter() + outputDateFormatter.dateFormat = "hh:mm a" // Output format (12-hour with AM/PM) + + // Split the range into start and end times + let timeComponents = self.components(separatedBy: "-") + + if timeComponents.count == 2 { + let startTime = inputDateFormatter.date(from: timeComponents[0]) + let endTime = inputDateFormatter.date(from: timeComponents[1]) + + let start12Hour = outputDateFormatter.string(from: startTime!) + let end12Hour = outputDateFormatter.string(from: endTime!) + + return "\(start12Hour) - \(end12Hour)" + }else { + let startTime = inputDateFormatter.date(from: self) + let start12Hour = outputDateFormatter.string(from: startTime!) + + return start12Hour + } + + // Return nil if conversion fails + } + + + + + func convertTo24HourFormat() -> String? { + // Initialize date formatter + let inputFormatter = DateFormatter() + inputFormatter.dateFormat = "hh:mm a" // Input format (12-hour with AM/PM) + + // Convert string to date + if let date = inputFormatter.date(from: self) { + // Initialize another date formatter for 24-hour format + let outputFormatter = DateFormatter() + outputFormatter.dateFormat = "HH:mm" // 24-hour format + + // Convert date to 24-hour string + return outputFormatter.string(from: date) + } + return nil + } + + + func timeAgo() -> String? { + // Set up the date formatter + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + dateFormatter.locale = Locale.current + dateFormatter.timeZone = TimeZone.current + + // Convert the string into a Date object + guard let date = dateFormatter.date(from: self) else { return nil } + + // Get the current date and calculate the time interval + let calendar = Calendar.current + let now = Date() + let components = calendar.dateComponents([.year, .month, .day], from: date, to: now) + + // Check the components to return the correct "time ago" string + if let years = components.year, years > 0 { + return "\(years) year\(years > 1 ? "s" : "") ago" + } else if let months = components.month, months > 0 { + return "\(months) month\(months > 1 ? "s" : "") ago" + } else if let days = components.day, days > 0 { + return "\(days) day\(days > 1 ? "s" : "") ago" + } else { + return "Just now" + } + } + + + +} +extension Date { + func dayOfWeek( ) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE" // Full day name + return dateFormatter.string(from: self) + } + + func getStringDate() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" // Specify the desired format + return dateFormatter.string(from: self) + } + + func currentDateOfMonth() ->Int { + let day = Calendar.current.component(.day, from: Date()) + return day + } +} + +extension UIImage { + func resize(to targetSize: CGSize) -> UIImage? { + let size = self.size + + let widthRatio = targetSize.width / size.width + let heightRatio = targetSize.height / size.height + + // Figure out what our orientation is, and use that to form the rectangle + var newSize: CGSize + if(widthRatio > heightRatio) { + newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio) + } else { + newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio) + } + + // This is the rect that we've calculated out and this is what is actually used below + let rect = CGRect(origin: .zero, size: newSize) + + // Actually do the resizing to the rect using the ImageContext stuff + UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) + self.draw(in: rect) + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return newImage + } +} + +struct RoundedCorner: Shape { + + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + return Path(path.cgPath) + } +} + +extension View { + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape( RoundedCorner(radius: radius, corners: corners) ) + } +} + +extension String { + func toIntArray() -> [Int] { + // Remove brackets and split the string by commas + let trimmed = self.trimmingCharacters(in: CharacterSet(charactersIn: "[]")) + let stringArray = trimmed.split(separator: ",") + + // Convert each substring to an integer + return stringArray.compactMap { Int($0) } + } + func removeFlag() -> String { + let filteredString = self.filter { !$0.isFlag } + return filteredString.trimmingCharacters(in: .whitespaces) + } + +} + +extension Character { + // Check if a character is a flag emoji + var isFlag: Bool { + return unicodeScalars.allSatisfy { $0.properties.isEmoji && ($0.value >= 0x1F1E6 && $0.value <= 0x1F1FF) } + } +} + +extension Double { + func formatNTRP () -> String { + return String(format: "%.1f", self ) + } +} + +extension String { + func toWeekday() -> String? { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + formatter.locale = Locale(identifier: "en_US_POSIX") + + guard let date = formatter.date(from: self) else { + return nil + } + + formatter.dateFormat = "E" // Short weekday like Mon, Tue, etc. + return formatter.string(from: date) + } +} diff --git a/Club_portal/Club Portal/Club Portal/Utliz/LoadingView.swift b/Club_portal/Club Portal/Club Portal/Utliz/LoadingView.swift new file mode 100644 index 0000000..36dd6be --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Utliz/LoadingView.swift @@ -0,0 +1,21 @@ +// +// LoadingView.swift +// Club Portal +// +// Created by Umer Tahir on 10/04/2025. +// + + +import SwiftUI + +struct LoadingView: View { + var body: some View { + ZStack { + Color.black.opacity(0.3).ignoresSafeArea() + ProgressView("Loading...") + .padding() + .background(Color.white) + .cornerRadius(12) + } + } +} \ No newline at end of file diff --git a/Club_portal/Club Portal/Club Portal/Utliz/ProfileImageView.swift b/Club_portal/Club Portal/Club Portal/Utliz/ProfileImageView.swift new file mode 100644 index 0000000..e8d7b03 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Utliz/ProfileImageView.swift @@ -0,0 +1,36 @@ +// +// ProfileImageView.swift +// Club Portal +// +// Created by Umer Tahir on 15/04/2025. +// + + + +import SwiftUI + +struct ProfileImageView: View { + let imageUrl: String? + let size: CGFloat + + var body: some View { + if let urlString = imageUrl, let url = URL(string: urlString) { + AsyncImage(url: url) { image in + image + .resizable() + .scaledToFill() + .frame(width: size, height: size) + .clipShape(Circle()) + } placeholder: { + Circle() + .fill(Color.gray) + .frame(width: size, height: size) + } + } else { + Circle() + .fill(Color.gray) + .frame(width: size, height: size) + + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/Utliz/SignInButton.swift b/Club_portal/Club Portal/Club Portal/Utliz/SignInButton.swift new file mode 100644 index 0000000..6aa2ab4 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Utliz/SignInButton.swift @@ -0,0 +1,40 @@ +// +// SignInButton.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + + +import SwiftUI + +// Social Sign-In Button Component +struct SignInButton: View { + let title: String + let iconName: String + var action: (() -> ())? + + var body: some View { + Button(action: { + // Handle sign-in + action?() + }) { + HStack { + Image(iconName) + .font(.headline) + Text(title) + .font(.headline) + } + .foregroundColor(.black) + .frame(maxWidth: .infinity) + .padding() + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray.opacity(0.5), lineWidth: 1) + ) + .cornerRadius(8) + } + } +} + diff --git a/Club_portal/Club Portal/Club Portal/Utliz/ToastModifier.swift b/Club_portal/Club Portal/Club Portal/Utliz/ToastModifier.swift new file mode 100644 index 0000000..6e96b35 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/Utliz/ToastModifier.swift @@ -0,0 +1,48 @@ +// +// ToastModifier.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + +import SwiftUI + +struct ToastModifier: ViewModifier { + let message: String + let backgroundColor: Color + + @Binding var isShowing: Bool + + func body(content: Content) -> some View { + ZStack { + content + + if isShowing { + VStack { + Spacer() + Text(message) + .padding() + .background(backgroundColor) + .foregroundColor(.white) + .cornerRadius(10) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .padding(.bottom, 40) + } + .animation(.easeInOut, value: isShowing) + .onAppear { + // Hide toast automatically after 3 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + isShowing = false + } + } + } + } + } +} + +extension View { + func toast(message: String, isShowing: Binding, backgroundColor: Color = .green) -> some View { + self.modifier(ToastModifier(message: message, backgroundColor: backgroundColor, isShowing: isShowing)) + } +} diff --git a/Club_portal/Club Portal/Club Portal/ViewModels/AuthViewModel1.swift b/Club_portal/Club Portal/Club Portal/ViewModels/AuthViewModel1.swift new file mode 100644 index 0000000..ec53ffb --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/ViewModels/AuthViewModel1.swift @@ -0,0 +1,154 @@ +// +// AuthViewModel.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + +import Foundation +import LocalAuthentication + +@MainActor +class AuthViewModel: ObservableObject { + @Published var isLoggedIn = false + @Published var showToast = false + @Published var toastMessage = "" + @Published var email = "" + @Published var password = "" + @Published var newPassword = "" + @Published var confirmPassword = "" + @Published var otpCode = "" // Code received via email + @Published var isLoading = false + @Published var isRefresh: Bool = false + + var isEmailValid: Bool { + email.isValidEmail + } + + var isNewPasswordValid: Bool { + let pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])[A-Za-z\\d!@#$%^&*]{6,}$" + return NSPredicate(format: "SELF MATCHES %@", pattern).evaluate(with: newPassword) + } + + var isConfirmPasswordMatching: Bool { + return !confirmPassword.isEmpty && confirmPassword == newPassword + } + + var canSubmitNewPassword: Bool { + return isNewPasswordValid && isConfirmPasswordMatching + } + var isPasswordValid: Bool { + return !password.isEmpty && password.count >= 6 + } + + var canLogin: Bool { + return isEmailValid && isPasswordValid + } + + func login(completion: @escaping (Bool) -> Void) { + guard canLogin else { + toastMessage = "Please enter a valid email and password." + showToast = true + return + } + + isLoading = true + + let endpoint = LoginEndpoint(email: email, password: password, isRefresh: AppSettings.keepLoginIn) + print("endpoint: ", endpoint) + + Task { + do { + let response: LoginResponse = try await APIService.shared.request(endpoint, responseType: LoginResponse.self) + // Handle the successful login response + print("response: ", response ) + + if response.error { + toastMessage = "Login failed. Please try again." + showToast = true + } else { + // Save the token and user ID for the session + AppSettings.token = response.token + AppSettings.userID = response.userId + AppSettings.lastName + // Update login state + self.isLoggedIn = true + completion(true) // Proceed to Dashboard + } + } catch { + toastMessage = "Error: \(error.localizedDescription)" + showToast = true + } + + isLoading = false + } + } + func forgotPassword(role: String = "club", completion: @escaping (Bool) -> Void) { + self.isLoading = true + + Task { + let endpoint = ForgotPasswordEndpoint(email: email, role: role) + print("endpoint: ", endpoint) + do { + let response: ForgotPasswordResponse = try await APIService.shared.request(endpoint, responseType: ForgotPasswordResponse.self) + toastMessage = response.message + print("response: ", response ) + showToast = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.showToast = false + self.isLoading = false + + completion(true) + } + } catch { + toastMessage = error.localizedDescription + showToast = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.showToast = false + self.isLoading = false + + completion(false) + } + } + } + } + + + func resetPassword(completion: @escaping (Bool) -> Void) { + isLoading = true + + Task { + let endpoint = ResetPasswordEndpoint( + email: email, + code: otpCode, + password: newPassword + ) + print("endpoint: ", endpoint) + + do { + let response: ForgotPasswordResponse = try await APIService.shared.request(endpoint, responseType: ForgotPasswordResponse.self) + toastMessage = response.message + print("response: ", response ) + showToast = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.showToast = false + self.isLoading = false + + completion(true) + } + } catch { + toastMessage = error.localizedDescription + showToast = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.showToast = false + self.isLoading = false + completion(false) + } + } + } + } + + + +} diff --git a/Club_portal/Club Portal/Club Portal/ViewModels/CalendarEventStore.swift b/Club_portal/Club Portal/Club Portal/ViewModels/CalendarEventStore.swift new file mode 100644 index 0000000..5eac224 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/ViewModels/CalendarEventStore.swift @@ -0,0 +1,28 @@ +// +// CalendarEventStore.swift +// Club Portal +// +// Created by Umer Tahir on 04/04/2025. +// + +import SwiftUI +import CalendarKit + +//class CalendarEventStore: ObservableObject, EventDataSource { +// @Published var events: [EventDescriptor] = [] +// +// func eventsForDate(_ date: Date) -> [EventDescriptor] { +// return events +// } +// +// func addDummyEvent() { +// let event = Event() +// let startDate = Date() +// let endDate = Calendar.current.date(byAdding: .hour, value: 1, to: startDate)! +// event.dateInterval = DateInterval(start: startDate, end: endDate) +// event.text = "Dummy Event" +// event.color = .blue +// events.append(event) +// } +//} +// diff --git a/Club_portal/Club Portal/Club Portal/ViewModels/DashViewModel.swift b/Club_portal/Club Portal/Club Portal/ViewModels/DashViewModel.swift new file mode 100644 index 0000000..de63924 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/ViewModels/DashViewModel.swift @@ -0,0 +1,104 @@ +// +// DashViewModel.swift +// Club Portal +// +// Created by Umer Tahir on 11/04/2025. +// + +import SwiftUI + +@MainActor +class DashViewModel: ObservableObject { + + @Published var dashboardfilters = "" + @Published var statResponse : ClubStatisticsModel? + @Published var dailyReservations : [Reservation]? + @Published var availRsp : AvailabliltyRsp? + @Published var club : ClubDetail? + @Published var clubDetail : ClubDetailsResponse? + @Published var profile : ProfileRsp? + @Published var sessionExpired = false + + func getStats(completion: @escaping () -> ()) { + let endpoint = ClubStatisticsEndpoint( + startDate: "2024-10-25", + endDate: "2024-11-25", + startTime: "17:30:20", + endTime: "19:20:30" + ) + + Task { + do { + let response: ClubStatisticsResponse = try await APIService.shared.request(endpoint, responseType: ClubStatisticsResponse.self) + print("Statistics fetched: \(response.model)") + if !response.error { + self.statResponse = response.model + } + else { + + } + } catch { + print("Error fetching stats: \(error)") + // if error.localizedDescription == "Token expired" { + sessionExpired = true + completion() + // } + } + } + } + + func getDailySched(clubId: Int){ + let endpoint = ReservationListEndpoint(clubID: clubId) + Task { + do { + let result = try await APIService.shared.request(endpoint, responseType: ReservationRsp.self) + dailyReservations = result.list + print("Reservations:", result.list) + } catch { + print("Error fetching reservations:", error) + } + } + } + + func getAvailability(){ + let endpoint = AvailabilityListEndpoint() + Task { + do { + let result = try await APIService.shared.request(endpoint, responseType: AvailabliltyRsp.self) + availRsp = result + print("avail:", result) + } catch { + print("Error fetching reservations:", error) + } + } + } + + func getProfile(){ + let endpoint = GetprofileEndpoint() + Task { + do { + + let result = try await APIService.shared.request(endpoint, responseType: ProfileRsp.self) + profile = result + AppSettings.clubId = result.model?.club?.id ?? 0 + AppSettings.clubName = result.model?.club?.name ?? "" + AppSettings.clubName = result.model?.user?.email ?? "" + + } catch { + print("Error fetching reservations:", error) + } + } + } + + func getClubProfile(){ + let endpoint = GetClubEndpoint(clubId: "10") + Task { + do { + let result = try await APIService.shared.request(endpoint, responseType: ClubDetail.self) + club = result + } catch { + print("Error fetching reservations:", error) + } + } + } +} diff --git a/Club_portal/Club Portal/Club Portal/ViewModels/LoginViewModel.swift b/Club_portal/Club Portal/Club Portal/ViewModels/LoginViewModel.swift new file mode 100644 index 0000000..7907e09 --- /dev/null +++ b/Club_portal/Club Portal/Club Portal/ViewModels/LoginViewModel.swift @@ -0,0 +1,79 @@ +// +// LoginViewModel.swift +// Club Portal +// +// Created by Umer Tahir on 09/04/2025. +// + + +// +// LoginViewModel.swift +// courtmatch +// +// Created by Umer Tahir on 03/12/2024. +// + +import AuthenticationServices +import SwiftUI + +class LoginViewModel: NSObject { + + var appleRes : ((AppleLoginRequest?, String?)->Void)? + @Published var appleLoginRequest: AppleLoginRequest? = nil + +} + +extension LoginViewModel: ASAuthorizationControllerDelegate { + func authorizationController(controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization) { + if let appleIdCredential = authorization.credential as? ASAuthorizationAppleIDCredential { + guard let token = appleIdCredential.identityToken?.base64EncodedString() else { + return + } + + // MARK: TODO + /// 1. Set token here + /// 2. Perform tasks to do after login + var firstName = "" + var lastName = "" + + + if appleIdCredential.authorizedScopes.contains(.fullName) { + print(appleIdCredential.fullName?.givenName ?? "No given name") + firstName = appleIdCredential.fullName?.givenName ?? "" + lastName = appleIdCredential.fullName?.givenName ?? "" + + } + + if appleIdCredential.authorizedScopes.contains(.email) { + print(appleIdCredential.email ?? "No email") + } + + let userIdentifier = appleIdCredential.user + let identityTokenData = appleIdCredential.identityToken + let authCode = appleIdCredential.authorizationCode + let realUserStatus = appleIdCredential.realUserStatus + let identityTokenString = String(data: identityTokenData!, encoding: .utf8) ?? "" + + + let request = AppleLoginRequest(first_name: firstName, last_name: lastName, identityToken: identityTokenString, apple_id: userIdentifier, role: "user") + + appleRes?(request, nil) + + } + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + print(error) + } + + func performAppleSignIn(comppletion : @escaping ((AppleLoginRequest?, String?)->Void)) { + let provider = ASAuthorizationAppleIDProvider() + let request = provider.createRequest() + request.requestedScopes = [.fullName, .email] + let controller = ASAuthorizationController(authorizationRequests: [request]) + controller.delegate = self + self.appleRes = comppletion + controller.performRequests() + } +}